Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ const loaders = {
'.cjs': loadScriptOrModule,
'.mjs': loadScriptOrModule,
'.js': loadScriptOrModule,
'.cts': loadScriptOrModule,
'.mts': loadScriptOrModule,
'.ts': loadScriptOrModule,
Copy link
Member

@wooorm wooorm Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not come before .js? Or is it intentional that built JS is preferred over TS

Next to docs, tests are missing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think js configs should be preferred because they're supported natively and this is just old behavior, what means if a user have a .remarkrc.ts compiled into .remarkrx.js manually, this will continue work even on unsupported Node versions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, probably!

'.yaml': loadYaml,
'.yml': loadYaml
}
Expand Down
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1223,7 +1223,11 @@ engine(

This example processes `readme.md` and allows configuration from `.remarkrc`,
`.remarkrc.json`, `.remarkrc.yml`, `.remarkrc.yaml`, `.remarkrc.js`,
`.remarkrc.cjs`, and `.remarkrc.mjs` files.
`.remarkrc.cjs`, `.remarkrc.mjs`, `.remarkrc.ts`, `.remarkrc.cts`, and
`.remarkrc.mts` files.

Note: TypeScript support relies on Node.js itself, so it requires Node.js 22.7+
with `--experimental-transform-types` flag enabled or 23.6+ by default.

```js
import {remark} from 'remark'
Expand Down
45 changes: 45 additions & 0 deletions test/configuration-plugins.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict'
import path from 'node:path'
import process from 'node:process'
import test from 'node:test'
import {engine} from 'unified-engine'
import {cleanError} from './util/clean-error.js'
Expand Down Expand Up @@ -71,6 +72,50 @@ test('configuration (plugins)', async function (t) {
}
)

if (process.features.typescript) {
await t.test(
'should support an ESM plugin w/ an `.mts` extname',
async function () {
const stderr = spy()

globalThis.unifiedEngineTestCalls = 0

const result = await engine({
cwd: new URL('config-plugins-esm-mts/', fixtures),
files: ['one.txt'],
processor: noop(),
rcName: '.foorc',
streamError: stderr.stream
})

assert.equal(result.code, 0)
assert.equal(stderr(), 'one.txt: no issues found\n')
assert.equal(globalThis.unifiedEngineTestCalls, 1)
}
)

await t.test(
'should support an ESM plugin w/ a `.ts` extname',
async function () {
const stderr = spy()

globalThis.unifiedEngineTestCalls = 0

const result = await engine({
cwd: new URL('config-plugins-esm-ts/', fixtures),
files: ['one.txt'],
processor: noop(),
rcName: '.foorc',
streamError: stderr.stream
})

assert.equal(result.code, 0)
assert.equal(stderr(), 'one.txt: no issues found\n')
assert.equal(globalThis.unifiedEngineTestCalls, 1)
}
)
}

await t.test('should handle failing plugins', async function () {
const stderr = spy()

Expand Down
81 changes: 81 additions & 0 deletions test/configuration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'node:assert/strict'
import path from 'node:path'
import process from 'node:process'
import test from 'node:test'
import {engine} from 'unified-engine'
import {cleanError} from './util/clean-error.js'
Expand Down Expand Up @@ -202,6 +203,86 @@ test('configuration', async function (t) {
)
})

if (process.features.typescript) {
await t.test(
'should prefer `.rc.js` scripts over `.rc.ts`',
async function () {
const stderr = spy()
let calls = 0

const result = await engine({
cwd: new URL('mixed-rc-script/', fixtures),
extensions: ['txt'],
files: ['.'],
plugins: [
function () {
assert.deepEqual(this.data('settings'), {})
calls++
}
],
processor: noop,
rcName: '.foorc',
streamError: stderr.stream
})

assert.deepEqual(
[result.code, calls, stderr()],
[0, 1, 'one.txt: no issues found\n']
)
}
)

await t.test('should support `.rc.mts` module', async function () {
const stderr = spy()
let calls = 0

const result = await engine({
cwd: new URL('rc-module-mts/', fixtures),
extensions: ['txt'],
files: ['.'],
plugins: [
function () {
assert.deepEqual(this.data('settings'), {foo: 'bar'})
calls++
}
],
processor: noop,
rcName: '.foorc',
streamError: stderr.stream
})

assert.deepEqual(
[result.code, calls, stderr()],
[0, 1, 'one.txt: no issues found\n']
)
})

await t.test('should support `.rc.cts` module', async function () {
const stderr = spy()
let calls = 0

const result = await engine({
cwd: new URL('rc-module-cts/', fixtures),
extensions: ['txt'],
files: ['.'],
plugins: [
function () {
assert.deepEqual(this.data('settings'), {foo: 'bar'})
calls++
}
],
processor: noop,
rcName: '.foorc',
streamError: stderr.stream
})

assert.deepEqual(
[result.code, calls, stderr()],
[0, 1, 'one.txt: no issues found\n']
)
})
}

await t.test('should support `.rc.yaml` config files', async function () {
const stderr = spy()

Expand Down
3 changes: 3 additions & 0 deletions test/fixtures/config-plugins-esm-mts/.foorc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["./test.mts"]
}
Empty file.
6 changes: 6 additions & 0 deletions test/fixtures/config-plugins-esm-mts/test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import assert from 'node:assert/strict'

export default function test() {
assert(typeof globalThis.unifiedEngineTestCalls === 'number')
globalThis.unifiedEngineTestCalls++
}
3 changes: 3 additions & 0 deletions test/fixtures/config-plugins-esm-ts/.foorc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"plugins": ["./test.ts"]
}
Empty file.
3 changes: 3 additions & 0 deletions test/fixtures/config-plugins-esm-ts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
6 changes: 6 additions & 0 deletions test/fixtures/config-plugins-esm-ts/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import assert from 'node:assert/strict'

export default function test() {
assert(typeof globalThis.unifiedEngineTestCalls === 'number')
globalThis.unifiedEngineTestCalls++
}
8 changes: 8 additions & 0 deletions test/fixtures/mixed-rc-script/.foorc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @import {Preset} from 'unified-engine'
*/

/** @type {Preset} */
const config = {settings: {}}

export default config
9 changes: 9 additions & 0 deletions test/fixtures/mixed-rc-script/.foorc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type {Preset} from 'unified-engine'

const config: Preset = {
settings: {
foo: 'bar'
}
}

export default config
Empty file.
5 changes: 5 additions & 0 deletions test/fixtures/rc-module-cts/.foorc.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {Settings} from 'unified'

exports.settings = {
foo: 'bar'
} satisfies Settings
Comment on lines +1 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript doesn’t understand the exports object inside TypeScript files. Instead, you are supposed to either use the special export = syntax

Suggested change
import type {Settings} from 'unified'
exports.settings = {
foo: 'bar'
} satisfies Settings
import type {PresetSupportingSpecifiers} from 'unified'
const config: PresetSupportingSpecifiers = {
settings: {
foo: 'bar'
}
}
export = config

or use an ESM style export (doesn’t work with verbatimModuleSyntax)

Suggested change
import type {Settings} from 'unified'
exports.settings = {
foo: 'bar'
} satisfies Settings
import type {Settings} from 'unified'
export const settings: Settings = {
foo: 'bar'
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not all features are supported by Node type stripping.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, though I don’t know entirely for sure which features are or aren’t supported. If neither of these syntaxes is supported by Node.js type stripping, then it doesn’t support .cts.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import type {Settings} from 'unified' work well in .cts, and .mts also doesn't support all TypeScript features, both of them are partial supported, I treat .cts same as .mts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider a module format supported, if there’s a way to import and export things in a way that TypeScript understands. In CJS, for imports means at least one of:

import module = require('module')
import {member} from 'module'

And for exports that means at least one of:

export = {}
export const member = {}

If there’s no (proper) way to import or export from a .cts file, then it’s not properly supported. In that case IMO we should not support it either, as it promotes writing incorrect TypeScript code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case is still a valid .cts usage, import for typings, exports for runtime codes.

We're talking about Node type stripping, and it's how .cts works in Node itself which is partial support.

Using .cts doesn't mean all module features are supported.

So IMO, .cts is supported by Node itself this way, then we could support how it works.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s not valid .cts usage. The use of the module, exports, or require variables in .cts is a hacky workaround. It only works, because the file isn’t imported by user code.

We shouldn’t promote this anti-pattern, especially considering this is already a topic of confusion in the TypeScript community.

Empty file.
15 changes: 15 additions & 0 deletions test/fixtures/rc-module-mts/.foorc.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type {Preset} from 'unified-engine'

declare module 'unified' {
interface Settings {
foo: string
}
}

const config: Preset = {
settings: {
foo: 'bar'
}
}

export default config
Empty file.