From fcfdb271c52050c3c292e7a05e3574a4ef29b778 Mon Sep 17 00:00:00 2001 From: SinhSinh An Date: Fri, 10 Apr 2026 02:09:15 -0500 Subject: [PATCH 1/4] feat(exports): add `jsExtension` option for subpath export keys Add a new `jsExtension` boolean option to `ExportsOptions` that appends `.js` extensions to all subpath export keys (except the root `"."`). This follows the Node.js recommendation for subpath exports: https://nodejs.org/api/packages.html#extensions-in-subpaths Closes #898 --- src/features/pkg/exports.test.ts | 154 +++++++++++++++++++++++++++++++ src/features/pkg/exports.ts | 18 ++++ 2 files changed, 172 insertions(+) diff --git a/src/features/pkg/exports.test.ts b/src/features/pkg/exports.test.ts index c9841dd11..404fd0b36 100644 --- a/src/features/pkg/exports.test.ts +++ b/src/features/pkg/exports.test.ts @@ -866,6 +866,160 @@ describe('generateExports', () => { }) }) + test('jsExtension adds .js to subpath export keys', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('foo.js'), genChunk('bar.js')], + }, + { + exports: { jsExtension: true }, + }, + ) + await expect(results).resolves.toMatchInlineSnapshot(` + { + "bin": undefined, + "exports": { + ".": "./index.js", + "./bar.js": "./bar.js", + "./foo.js": "./foo.js", + "./package.json": "./package.json", + }, + "inlinedDependencies": undefined, + "main": undefined, + "module": undefined, + "publishExports": undefined, + "types": undefined, + } + `) + }) + + test('jsExtension with directory index entries', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('foo/index.js')], + }, + { + exports: { jsExtension: true }, + }, + ) + await expect(results).resolves.toMatchInlineSnapshot(` + { + "bin": undefined, + "exports": { + ".": "./index.js", + "./foo.js": "./foo/index.js", + "./package.json": "./package.json", + }, + "inlinedDependencies": undefined, + "main": undefined, + "module": undefined, + "publishExports": undefined, + "types": undefined, + } + `) + }) + + test('jsExtension with dual formats', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('utils.js')], + cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], + }, + { + exports: { jsExtension: true }, + }, + ) + await expect(results).resolves.toMatchInlineSnapshot(` + { + "bin": undefined, + "exports": { + ".": { + "import": "./index.js", + "require": "./index.cjs", + }, + "./package.json": "./package.json", + "./utils.js": { + "import": "./utils.js", + "require": "./utils.cjs", + }, + }, + "inlinedDependencies": undefined, + "main": "./index.cjs", + "module": "./index.js", + "publishExports": undefined, + "types": undefined, + } + `) + }) + + test('jsExtension does not affect root export', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('main.js')], + }, + { + exports: { jsExtension: true }, + }, + ) + await expect(results).resolves.toMatchInlineSnapshot(` + { + "bin": undefined, + "exports": { + ".": "./main.js", + "./package.json": "./package.json", + }, + "inlinedDependencies": undefined, + "main": undefined, + "module": undefined, + "publishExports": undefined, + "types": undefined, + } + `) + }) + + test('jsExtension with devExports', async ({ expect }) => { + const results = await generateExports( + { + es: [genChunk('index.js'), genChunk('utils.js')], + cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], + }, + { + exports: { jsExtension: true, devExports: '@my-org/source' }, + }, + ) + // key order matters + expect(JSON.stringify(results, undefined, 2)).toMatchInlineSnapshot(` + "{ + "main": "./index.cjs", + "module": "./index.js", + "exports": { + ".": { + "@my-org/source": "./SRC/index.js", + "import": "./index.js", + "require": "./index.cjs" + }, + "./utils.js": { + "@my-org/source": "./SRC/utils.js", + "import": "./utils.js", + "require": "./utils.cjs" + }, + "./package.json": "./package.json" + }, + "publishExports": { + ".": { + "import": "./index.js", + "require": "./index.cjs" + }, + "./utils.js": { + "import": "./utils.js", + "require": "./utils.cjs" + }, + "./package.json": "./package.json" + } + }" + `) + }) + test('generate css publish exports', async ({ expect }) => { const results = generateExports( { es: [genChunk('index.js'), genAsset('style.css')] }, diff --git a/src/features/pkg/exports.ts b/src/features/pkg/exports.ts index 08da58600..d082107aa 100644 --- a/src/features/pkg/exports.ts +++ b/src/features/pkg/exports.ts @@ -96,6 +96,19 @@ export interface ExportsOptions { */ inlinedDependencies?: boolean + /** + * Add `.js` extension to subpath export keys. + * + * When enabled, all subpath exports (except the root `"."`) will include + * a `.js` extension in the key (e.g., `"./utils.js"` instead of `"./utils"`). + * + * This follows the Node.js recommendation for subpath exports: + * @see {@link https://nodejs.org/api/packages.html#extensions-in-subpaths} + * + * @default false + */ + jsExtension?: boolean + /** * Auto-generate the `bin` field in package.json. * @@ -185,6 +198,7 @@ export async function generateExports( exclude, customExports, legacy, + jsExtension, inlinedDependencies: emitInlinedDeps = true, bin, }, @@ -268,6 +282,10 @@ export async function generateExports( name = `./${name}` } + if (jsExtension && name !== '.') { + name = `${name}.js` + } + let subExport = exportsMap.get(name) if (!subExport) { subExport = {} From 189dfd46730bfec1aec4a669131f1c2021051b1e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:11:01 +0000 Subject: [PATCH 2/4] [autofix.ci] apply automated fixes --- dts.snapshot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dts.snapshot.json b/dts.snapshot.json index 3f72b9e53..19464fc27 100644 --- a/dts.snapshot.json +++ b/dts.snapshot.json @@ -143,7 +143,7 @@ "DevtoolsOptions": "interface DevtoolsOptions extends NonNullable {\n ui?: boolean | Partial\n clean?: boolean\n}", "DtsOptions": "interface DtsOptions extends Options$1 {\n cjsReexport?: boolean\n}", "ExeOptions": "interface ExeOptions extends ExeExtensionOptions {\n seaConfig?: Omit\n fileName?: string | ((_: RolldownChunk) => string)\n outDir?: string\n}", - "ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record | ((_: Record, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable>)\n inlinedDependencies?: boolean\n bin?: boolean | string | Record\n}", + "ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record | ((_: Record, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable>)\n inlinedDependencies?: boolean\n jsExtension?: boolean\n bin?: boolean | string | Record\n}", "Format": "type Format = ModuleFormat", "InlineConfig": "interface InlineConfig extends UserConfig {\n config?: boolean | string\n configLoader?: 'auto' | 'native' | 'unrun'\n filter?: RegExp | Arrayable\n}", "NoExternalFn": "type NoExternalFn = (_: string, _: string | undefined) => boolean | null | undefined | void", From 311a2e2f6ae7db7bae0ff539e3dfe89898ab18a6 Mon Sep 17 00:00:00 2001 From: SinhSinh An Date: Fri, 10 Apr 2026 09:43:59 -0500 Subject: [PATCH 3/4] refactor: rename jsExtension to extensions per review feedback Rename the option from `jsExtension` to `extensions` and update the JSDoc description to be more general, as requested by the maintainer. --- src/features/pkg/exports.test.ts | 20 ++++++++++---------- src/features/pkg/exports.ts | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/features/pkg/exports.test.ts b/src/features/pkg/exports.test.ts index 404fd0b36..d4903adc1 100644 --- a/src/features/pkg/exports.test.ts +++ b/src/features/pkg/exports.test.ts @@ -866,13 +866,13 @@ describe('generateExports', () => { }) }) - test('jsExtension adds .js to subpath export keys', async ({ expect }) => { + test('extensions adds .js to subpath export keys', async ({ expect }) => { const results = generateExports( { es: [genChunk('index.js'), genChunk('foo.js'), genChunk('bar.js')], }, { - exports: { jsExtension: true }, + exports: { extensions: true }, }, ) await expect(results).resolves.toMatchInlineSnapshot(` @@ -893,13 +893,13 @@ describe('generateExports', () => { `) }) - test('jsExtension with directory index entries', async ({ expect }) => { + test('extensions with directory index entries', async ({ expect }) => { const results = generateExports( { es: [genChunk('index.js'), genChunk('foo/index.js')], }, { - exports: { jsExtension: true }, + exports: { extensions: true }, }, ) await expect(results).resolves.toMatchInlineSnapshot(` @@ -919,14 +919,14 @@ describe('generateExports', () => { `) }) - test('jsExtension with dual formats', async ({ expect }) => { + test('extensions with dual formats', async ({ expect }) => { const results = generateExports( { es: [genChunk('index.js'), genChunk('utils.js')], cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], }, { - exports: { jsExtension: true }, + exports: { extensions: true }, }, ) await expect(results).resolves.toMatchInlineSnapshot(` @@ -952,13 +952,13 @@ describe('generateExports', () => { `) }) - test('jsExtension does not affect root export', async ({ expect }) => { + test('extensions does not affect root export', async ({ expect }) => { const results = generateExports( { es: [genChunk('main.js')], }, { - exports: { jsExtension: true }, + exports: { extensions: true }, }, ) await expect(results).resolves.toMatchInlineSnapshot(` @@ -977,14 +977,14 @@ describe('generateExports', () => { `) }) - test('jsExtension with devExports', async ({ expect }) => { + test('extensions with devExports', async ({ expect }) => { const results = await generateExports( { es: [genChunk('index.js'), genChunk('utils.js')], cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], }, { - exports: { jsExtension: true, devExports: '@my-org/source' }, + exports: { extensions: true, devExports: '@my-org/source' }, }, ) // key order matters diff --git a/src/features/pkg/exports.ts b/src/features/pkg/exports.ts index d082107aa..a3a94cbc7 100644 --- a/src/features/pkg/exports.ts +++ b/src/features/pkg/exports.ts @@ -97,7 +97,7 @@ export interface ExportsOptions { inlinedDependencies?: boolean /** - * Add `.js` extension to subpath export keys. + * Add file extensions to subpath export keys. * * When enabled, all subpath exports (except the root `"."`) will include * a `.js` extension in the key (e.g., `"./utils.js"` instead of `"./utils"`). @@ -107,7 +107,7 @@ export interface ExportsOptions { * * @default false */ - jsExtension?: boolean + extensions?: boolean /** * Auto-generate the `bin` field in package.json. @@ -198,7 +198,7 @@ export async function generateExports( exclude, customExports, legacy, - jsExtension, + extensions, inlinedDependencies: emitInlinedDeps = true, bin, }, @@ -282,7 +282,7 @@ export async function generateExports( name = `./${name}` } - if (jsExtension && name !== '.') { + if (extensions && name !== '.') { name = `${name}.js` } From 8f24e927249c45bb971352397c779226306d2418 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:46:20 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- dts.snapshot.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dts.snapshot.json b/dts.snapshot.json index 19464fc27..fccf564bc 100644 --- a/dts.snapshot.json +++ b/dts.snapshot.json @@ -143,7 +143,7 @@ "DevtoolsOptions": "interface DevtoolsOptions extends NonNullable {\n ui?: boolean | Partial\n clean?: boolean\n}", "DtsOptions": "interface DtsOptions extends Options$1 {\n cjsReexport?: boolean\n}", "ExeOptions": "interface ExeOptions extends ExeExtensionOptions {\n seaConfig?: Omit\n fileName?: string | ((_: RolldownChunk) => string)\n outDir?: string\n}", - "ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record | ((_: Record, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable>)\n inlinedDependencies?: boolean\n jsExtension?: boolean\n bin?: boolean | string | Record\n}", + "ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record | ((_: Record, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable>)\n inlinedDependencies?: boolean\n extensions?: boolean\n bin?: boolean | string | Record\n}", "Format": "type Format = ModuleFormat", "InlineConfig": "interface InlineConfig extends UserConfig {\n config?: boolean | string\n configLoader?: 'auto' | 'native' | 'unrun'\n filter?: RegExp | Arrayable\n}", "NoExternalFn": "type NoExternalFn = (_: string, _: string | undefined) => boolean | null | undefined | void",