diff --git a/docs/README.md b/docs/README.md index c6fd6f4a..9641ef0c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -145,16 +145,12 @@ Provide the following configuration in your `.vscode/settings.json` (or global) "json.schemas": [ { "url": "https://cdn.jsdelivr.net/npm/tsup/schema.json", - "fileMatch": [ - "package.json", - "tsup.config.json" - ] + "fileMatch": ["package.json", "tsup.config.json"] } ] } ``` - ### Multiple entrypoints Beside using positional arguments `tsup [...files]` to specify multiple entrypoints, you can also use the cli flag `--entry`: @@ -164,7 +160,7 @@ Beside using positional arguments `tsup [...files]` to specify multiple entrypoi tsup --entry src/a.ts --entry src/b.ts ``` -The associated output file names can be defined as follows: +The associated output file names can be defined as follows: ```bash # Outputs `dist/foo.js` and `dist/bar.js`. @@ -350,6 +346,14 @@ tsup src/index.ts --env.NODE_ENV production When an entry file like `src/cli.ts` contains hashbang like `#!/bin/env node` tsup will automatically make the output file executable, so you don't have to run `chmod +x dist/cli.js`. +### Interop with CommonJS + +By default, esbuild will transform `export default x` to `module.exports.default = x` in CommonJS, but you can change this behavior by using the `--cjsInterop` flag: If there are only default exports and no named exports, it will be transformed to `module.exports = x` instead. + +```bash +tsup src/index.ts --cjsInterop +``` + ### Watch mode ```bash diff --git a/src/cli-main.ts b/src/cli-main.ts index b0d453f4..b3338b6c 100644 --- a/src/cli-main.ts +++ b/src/cli-main.ts @@ -95,6 +95,7 @@ export async function main(options: Options = {}) { '--killSignal ', 'Signal to kill child process, "SIGTERM" or "SIGKILL"' ) + .option('--cjsInterop', 'Enable cjs interop') .action(async (files: string[], flags) => { const { build } = await import('.') Object.assign(options, { diff --git a/src/index.ts b/src/index.ts index bbe4f767..0eecc990 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ import { sizeReporter } from './plugins/size-reporter' import { treeShakingPlugin } from './plugins/tree-shaking' import { copyPublicDir, isInPublicDir } from './lib/public-dir' import { terserPlugin } from './plugins/terser' +import { cjsInterop } from './plugins/cjs-interop' export type { Format, Options, NormalizedOptions } @@ -254,6 +255,7 @@ export async function build(_options: Options) { silent: options.silent, }), cjsSplitting(), + cjsInterop(), es5(), sizeReporter(), terserPlugin({ diff --git a/src/options.ts b/src/options.ts index e671cfe2..cee1b491 100644 --- a/src/options.ts +++ b/src/options.ts @@ -224,6 +224,11 @@ export type Options = { */ publicDir?: string | boolean killSignal?: KILL_SIGNAL + /** + * Interop default within `module.exports` in cjs + * @default false + */ + cjsInterop?: boolean } export type NormalizedOptions = Omit< diff --git a/src/plugins/cjs-interop.ts b/src/plugins/cjs-interop.ts new file mode 100644 index 00000000..56c8289f --- /dev/null +++ b/src/plugins/cjs-interop.ts @@ -0,0 +1,26 @@ +import { Plugin } from '../plugin' + +export const cjsInterop = (): Plugin => { + return { + name: 'cjs-interop', + + async renderChunk(code, info) { + if ( + !this.options.cjsInterop || + this.format !== 'cjs' || + info.type !== 'chunk' || + !/\.(js|cjs)$/.test(info.path) || + !info.entryPoint || + info.exports?.length !== 1 || + info.exports[0] !== 'default' + ) { + return + } + + return { + code: code + '\nmodule.exports = exports.default', + map: info.map, + } + }, + } +} diff --git a/src/rollup.ts b/src/rollup.ts index fa9a2f50..28672fb2 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -132,6 +132,25 @@ const getRollupConfig = async ( }, } + const fixCjsExport: Plugin = { + name: 'tsup:fix-cjs-export', + renderChunk(code, info) { + if ( + info.type !== 'chunk' || + !/\.(ts|cts)$/.test(info.fileName) || + !info.isEntry || + info.exports?.length !== 1 || + info.exports[0] !== 'default' + ) + return + + return code.replace( + /(?<=(?<=[;}]|^)\s*export\s*){\s*([\w$]+)\s*as\s+default\s*}/, + `= $1` + ) + }, + } + return { inputConfig: { input: dtsOptions.entry, @@ -179,7 +198,7 @@ const getRollupConfig = async ( ...(options.external || []), ], }, - outputConfig: options.format.map((format) => { + outputConfig: options.format.map((format): OutputOptions => { const outputExtension = options.outExtension?.({ format, options, pkgType: pkg.type }).dts || defaultOutExtension({ format, pkgType: pkg.type }).dts @@ -190,6 +209,9 @@ const getRollupConfig = async ( banner: dtsOptions.banner, footer: dtsOptions.footer, entryFileNames: `[name]${outputExtension}`, + plugins: [ + format === 'cjs' && options.cjsInterop && fixCjsExport, + ].filter(Boolean), } }), }