diff --git a/docs/README.md b/docs/README.md index e9816cec..e095f33f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -615,6 +615,7 @@ When you use legacy TypeScript decorator by enabling `emitDecoratorMetadata` in decorators. In this case, you can give extra swc configuration in the `tsup.config.ts` file. For example, if you have to define `useDefineForClassFields`, you can do that as follows: + ```ts import { defineConfig } from 'tsup' @@ -626,10 +627,10 @@ export default defineConfig({ swc: { jsc: { transform: { - useDefineForClassFields: true - } - } - } + useDefineForClassFields: true, + }, + }, + }, }) ``` @@ -648,9 +649,9 @@ Note: some SWC options cannot be configured: "keepClassNames": true, "target": "es2022" } - ``` +``` -You can also define a custom `.swcrc` configuration file. Just set `swcrc` to `true` +You can also define a custom `.swcrc` configuration file. Just set `swcrc` to `true` in `tsup.config.ts` to allow SWC plugin to discover automatically your custom swc config file. ```ts @@ -662,8 +663,8 @@ export default defineConfig({ sourcemap: true, clean: true, swc: { - swcrc: true - } + swcrc: true, + }, }) ``` diff --git a/src/api-extractor.ts b/src/api-extractor.ts index 495d153b..02254abf 100644 --- a/src/api-extractor.ts +++ b/src/api-extractor.ts @@ -98,7 +98,11 @@ 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/esbuild/swc.ts b/src/esbuild/swc.ts index 755009d3..ee12c1dc 100644 --- a/src/esbuild/swc.ts +++ b/src/esbuild/swc.ts @@ -9,7 +9,10 @@ import type { Logger } from '../log' export type SwcPluginConfig = { logger: Logger } & Options -export const swcPlugin = ({ logger, ...swcOptions }: SwcPluginConfig): Plugin => { +export const swcPlugin = ({ + logger, + ...swcOptions +}: SwcPluginConfig): Plugin => { return { name: 'swc', diff --git a/src/index.ts b/src/index.ts index 64a1d690..cc583f0c 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 6bbee23b..354d38cd 100644 --- a/src/options.ts +++ b/src/options.ts @@ -257,8 +257,8 @@ export type Options = { * @default true */ removeNodeProtocol?: boolean - - swc?: SwcPluginConfig; + + swc?: SwcPluginConfig } export interface NormalizedExperimentalDtsConfig { @@ -275,5 +275,14 @@ export type NormalizedOptions = Omit< tsconfigResolvePaths: Record tsconfigDecoratorMetadata?: boolean format: Format[] + + /** + * Custom file extension per each + * {@linkcode Format | module format}. + * + * @since 8.6.0 + */ + outputExtensionMap: Map + swc?: SwcPluginConfig } diff --git a/src/rollup.ts b/src/rollup.ts index 90aef3be..bde2f47d 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -138,7 +138,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 5480f0f6..6b8af174 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.js' import type { Entry, Format, @@ -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 941db49d..51193bac 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 825afbbf..be681f47 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 5edcd079..f3c47cb5 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 }; diff --git a/vitest.config.mts b/vitest.config.mts index 8f6d3ac6..7e032eda 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -4,6 +4,6 @@ export default defineConfig({ test: { testTimeout: 50000, globalSetup: 'vitest-global.ts', - include: ["test/*.test.ts", "src/**/*.test.ts"] + include: ['test/*.test.ts', 'src/**/*.test.ts'], }, })