diff --git a/src/api-extractor.ts b/src/api-extractor.ts index 495d153bd..5ecd1a263 100644 --- a/src/api-extractor.ts +++ b/src/api-extractor.ts @@ -98,7 +98,9 @@ async function rollupDtsFiles( const declarationDir = ensureTempDeclarationDir() const outDir = options.outDir || 'dist' const pkg = await loadPkg(process.cwd()) - const dtsExtension = defaultOutExtension({ format, pkgType: pkg.type }).dts + const dtsExtension = + options.outputExtensionMap.get(format)?.dts || + defaultOutExtension({ format, pkgType: pkg.type }).dts const tsconfig = options.tsconfig || 'tsconfig.json' let dtsInputFilePath = path.join( diff --git a/src/index.ts b/src/index.ts index 85a6ea041..66e9f1110 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import { removeFiles, resolveExperimentalDtsConfig, resolveInitialExperimentalDtsConfig, + resolveOutputExtensionMap, slash, } from './utils' import { createLogger, setSilent } from './log' @@ -77,14 +78,17 @@ const normalizeOptions = async ( ...optionsOverride, } + const formats = + typeof _options.format === 'string' + ? [_options.format] + : _options.format || ['cjs'] + const options: Partial = { outDir: 'dist', removeNodeProtocol: true, ..._options, - format: - typeof _options.format === 'string' - ? [_options.format as Format] - : _options.format || ['cjs'], + format: formats, + dts: typeof _options.dts === 'boolean' ? _options.dts @@ -161,6 +165,10 @@ const normalizeOptions = async ( options.target = 'node16' } + options.outputExtensionMap = await resolveOutputExtensionMap( + options as NormalizedOptions, + ) + return options as NormalizedOptions } diff --git a/src/options.ts b/src/options.ts index 827790d43..e0f669b3d 100644 --- a/src/options.ts +++ b/src/options.ts @@ -272,4 +272,11 @@ export type NormalizedOptions = Omit< tsconfigResolvePaths: Record tsconfigDecoratorMetadata?: boolean format: Format[] + /** + * Custom file extension per each + * {@linkcode Format | module format}. + * + * @since 8.4.0 + */ + outputExtensionMap: Map } diff --git a/src/rollup.ts b/src/rollup.ts index 37fe23583..cc080b1ea 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -156,7 +156,7 @@ const getRollupConfig = async ( }, outputConfig: options.format.map((format): OutputOptions => { const outputExtension = - options.outExtension?.({ format, options, pkgType: pkg.type }).dts || + options.outputExtensionMap.get(format)?.dts || defaultOutExtension({ format, pkgType: pkg.type }).dts return { dir: options.outDir || 'dist', diff --git a/src/utils.ts b/src/utils.ts index e69cd3ad7..4fc28cbda 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,6 +4,7 @@ import resolveFrom from 'resolve-from' import type { InputOption } from 'rollup' import strip from 'strip-json-comments' import { glob } from 'tinyglobby' +import { loadPkg } from './load' import type { Entry, Format, @@ -322,7 +323,7 @@ const convertArrayEntriesToObjectEntries = (arrayOfEntries: string[]) => { /** * Resolves and standardizes entry paths into an object format. If the provided * entry is a string or an array of strings, it resolves any potential glob - * patterns amd converts the result into an entry object. If the input is + * patterns and converts the result into an entry object. If the input is * already an object, it is returned as-is. * * @example @@ -422,3 +423,40 @@ export const resolveInitialExperimentalDtsConfig = async ( : await resolveEntryPaths(experimentalDts.entry), } } + +/** + * Resolves the + * {@linkcode NormalizedOptions.outputExtensionMap | output extension map} + * for each specified {@linkcode Format | format} + * in the provided {@linkcode options}. + * + * @param options - The normalized options containing format and output extension details. + * @returns A {@linkcode Promise | promise} that resolves to a {@linkcode Map}, where each key is a {@linkcode Format | format} and each value is an object containing the resolved output extensions for both `js` and `dts` files. + * + * @internal + */ +export const resolveOutputExtensionMap = async ( + options: NormalizedOptions, +): Promise => { + const pkg = await loadPkg(process.cwd()) + + const formatOutExtension = new Map( + options.format.map((format) => { + const outputExtensions = options.outExtension?.({ + format, + options, + pkgType: pkg.type, + }) + + return [ + format, + { + ...defaultOutExtension({ format, pkgType: pkg.type }), + ...(outputExtensions || {}), + }, + ] as const + }), + ) + + return formatOutExtension +} diff --git a/test/dts.test.ts b/test/dts.test.ts index 941db49d8..51193bac5 100644 --- a/test/dts.test.ts +++ b/test/dts.test.ts @@ -480,3 +480,58 @@ test('declaration files with multiple entrypoints #316', async () => { 'dist/bar/index.d.ts', ).toMatchSnapshot() }) + +test('custom dts output extension', async ({ expect, task }) => { + const { outFiles } = await run( + getTestName(), + { + 'src/types.ts': `export type Person = { name: string }`, + 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types'`, + 'tsup.config.ts': `export default { + name: '${task.name}', + entry: { index: 'src/index.ts' }, + dts: true, + format: ['esm', 'cjs'], + outExtension({ format }) { + return { + js: format === 'esm' ? '.cjs' : '.mjs', + dts: format === 'esm' ? '.d.cts' : '.d.mts', + } + }, + }`, + 'package.json': JSON.stringify( + { + name: 'custom-dts-output-extension', + description: task.name, + type: 'module', + }, + null, + 2, + ), + 'tsconfig.json': JSON.stringify( + { + compilerOptions: { + outDir: './dist', + rootDir: './src', + moduleResolution: 'Bundler', + module: 'ESNext', + strict: true, + skipLibCheck: true, + }, + include: ['src'], + }, + null, + 2, + ), + }, + { + entry: [], + }, + ) + expect(outFiles).toStrictEqual([ + 'index.cjs', + 'index.d.cts', + 'index.d.mts', + 'index.mjs', + ]) +}) diff --git a/test/experimental-dts.test.ts b/test/experimental-dts.test.ts index 825afbbfc..be681f47b 100644 --- a/test/experimental-dts.test.ts +++ b/test/experimental-dts.test.ts @@ -604,3 +604,64 @@ test('experimentalDts.entry can be a string of glob pattern', async ({ ), ) }) + +test('custom outExtension works with experimentalDts', async ({ + expect, + task, +}) => { + const { outFiles } = await run( + getTestName(), + { + 'src/types.ts': `export type Person = { name: string }`, + 'src/index.ts': `export const foo = [1, 2, 3]\nexport type { Person } from './types'`, + 'tsup.config.ts': `export default { + name: '${task.name}', + entry: { index: 'src/index.ts' }, + format: ['esm', 'cjs'], + experimentalDts: true, + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : '.mjs', + dts: format === 'cjs' ? '.d.cts' : '.d.mts', + } + }, + }`, + 'package.json': JSON.stringify( + { + name: 'custom-dts-output-extension-with-experimental-dts', + description: task.name, + type: 'module', + }, + null, + 2, + ), + 'tsconfig.json': JSON.stringify( + { + compilerOptions: { + outDir: './dist', + rootDir: './src', + moduleResolution: 'Bundler', + module: 'ESNext', + strict: true, + skipLibCheck: true, + }, + include: ['src'], + }, + null, + 2, + ), + }, + { + entry: [], + }, + ) + + expect(outFiles).toStrictEqual([ + '_tsup-dts-rollup.d.cts', + '_tsup-dts-rollup.d.mts', + 'index.cjs', + 'index.d.cts', + 'index.d.mts', + 'index.mjs', + ]) +}) diff --git a/test/index.test.ts b/test/index.test.ts index 5b42572e2..3da659eb6 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -217,7 +217,7 @@ test('onSuccess: use a function from config file', async () => { await new Promise((resolve) => { setTimeout(() => { console.log('world') - resolve('') + resolve('') }, 1_000) }) } @@ -601,7 +601,7 @@ test('use rollup for treeshaking --format cjs', async () => { }`, 'input.tsx': ` import ReactSelect from 'react-select' - + export const Component = (props: {}) => { return };