diff --git a/__snapshots__/tsnapi/index.snapshot.d.ts b/__snapshots__/tsnapi/index.snapshot.d.ts index c9d35b2ad..d39844f54 100644 --- a/__snapshots__/tsnapi/index.snapshot.d.ts +++ b/__snapshots__/tsnapi/index.snapshot.d.ts @@ -54,6 +54,7 @@ export interface ExportsOptions { isPublish: boolean; }) => Awaitable>); inlinedDependencies?: boolean; + extensions?: boolean; bin?: boolean | string | Record; } export interface InlineConfig extends UserConfig { diff --git a/src/features/pkg/exports.test.ts b/src/features/pkg/exports.test.ts index c9841dd11..d4903adc1 100644 --- a/src/features/pkg/exports.test.ts +++ b/src/features/pkg/exports.test.ts @@ -866,6 +866,160 @@ describe('generateExports', () => { }) }) + test('extensions adds .js to subpath export keys', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('foo.js'), genChunk('bar.js')], + }, + { + exports: { extensions: 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('extensions with directory index entries', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('foo/index.js')], + }, + { + exports: { extensions: 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('extensions with dual formats', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('index.js'), genChunk('utils.js')], + cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], + }, + { + exports: { extensions: 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('extensions does not affect root export', async ({ expect }) => { + const results = generateExports( + { + es: [genChunk('main.js')], + }, + { + exports: { extensions: 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('extensions with devExports', async ({ expect }) => { + const results = await generateExports( + { + es: [genChunk('index.js'), genChunk('utils.js')], + cjs: [genChunk('index.cjs'), genChunk('utils.cjs')], + }, + { + exports: { extensions: 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..a3a94cbc7 100644 --- a/src/features/pkg/exports.ts +++ b/src/features/pkg/exports.ts @@ -96,6 +96,19 @@ export interface ExportsOptions { */ inlinedDependencies?: boolean + /** + * 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"`). + * + * This follows the Node.js recommendation for subpath exports: + * @see {@link https://nodejs.org/api/packages.html#extensions-in-subpaths} + * + * @default false + */ + extensions?: boolean + /** * Auto-generate the `bin` field in package.json. * @@ -185,6 +198,7 @@ export async function generateExports( exclude, customExports, legacy, + extensions, inlinedDependencies: emitInlinedDeps = true, bin, }, @@ -268,6 +282,10 @@ export async function generateExports( name = `./${name}` } + if (extensions && name !== '.') { + name = `${name}.js` + } + let subExport = exportsMap.get(name) if (!subExport) { subExport = {}