diff --git a/docs/README.md b/docs/README.md index 46449dd8..e9816cec 100644 --- a/docs/README.md +++ b/docs/README.md @@ -609,6 +609,64 @@ tsup --tsconfig tsconfig.prod.json By default, tsup try to find the `tsconfig.json` file in the current directory, if it's not found, it will use the default tsup config. +### Using custom Swc configuration + +When you use legacy TypeScript decorator by enabling `emitDecoratorMetadata` in your tsconfig, tsup will automatically use [SWC](https://swc.rs) to transpile +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' + +export default defineConfig({ + entry: ['src/index.ts'], + splitting: false, + sourcemap: true, + clean: true, + swc: { + jsc: { + transform: { + useDefineForClassFields: true + } + } + } +}) +``` + +Note: some SWC options cannot be configured: + +```json +{ + "parser": { + "syntax": "typescript", + "decorators": true + }, + "transform": { + "legacyDecorator": true, + "decoratorMetadata": true + }, + "keepClassNames": true, + "target": "es2022" +} + ``` + +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 +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + splitting: false, + sourcemap: true, + clean: true, + swc: { + swcrc: true + } +}) +``` + ## Troubleshooting ### error: No matching export in "xxx.ts" for import "xxx" diff --git a/schema.json b/schema.json index cb1b135f..4dd3008b 100644 --- a/schema.json +++ b/schema.json @@ -181,6 +181,9 @@ } ] }, + "swc": { + "type": "object" + }, "globalName": { "type": "string" }, diff --git a/src/esbuild/index.ts b/src/esbuild/index.ts index 7a39a654..9766a746 100644 --- a/src/esbuild/index.ts +++ b/src/esbuild/index.ts @@ -138,7 +138,7 @@ export async function runEsbuild( skipNodeModulesBundle: options.skipNodeModulesBundle, tsconfigResolvePaths: options.tsconfigResolvePaths, }), - options.tsconfigDecoratorMetadata && swcPlugin({ logger }), + options.tsconfigDecoratorMetadata && swcPlugin({ ...options.swc, logger }), nativeNodeModulesPlugin(), postcssPlugin({ css, diff --git a/src/esbuild/swc.test.ts b/src/esbuild/swc.test.ts new file mode 100644 index 00000000..6ee59ed2 --- /dev/null +++ b/src/esbuild/swc.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, test, vi } from 'vitest' +import { swcPlugin, type SwcPluginConfig } from './swc' +import { localRequire } from '../utils' + +vi.mock('../utils') + +const getFixture = async (opts: Partial = {}) => { + const swc = { + transformFile: vi.fn().mockResolvedValue({ + code: 'source-code', + map: JSON.stringify({ + sources: ['file:///path/to/file.ts'], + }), + }), + } + + const logger = { + warn: vi.fn(), + error: vi.fn(), + info: vi.fn(), + } + + const build = { + initialOptions: { + keepNames: true, + }, + onLoad: vi.fn(), + } + + vi.mocked(localRequire).mockReturnValue(swc) + + const plugin = swcPlugin({ + ...opts, + logger: logger as never, + }) + + await plugin.setup(build as never) + + const onLoad = build.onLoad.mock.calls[0][1] as Function + + return { swc, onLoad, logger, build } +} +describe('swcPlugin', () => { + test('swcPlugin transforms TypeScript code with decorators and default plugin swc option', async () => { + const { swc, onLoad } = await getFixture() + + await onLoad({ + path: 'file.ts', + }) + + expect(swc.transformFile).toHaveBeenCalledWith('file.ts', { + configFile: false, + jsc: { + keepClassNames: true, + parser: { + decorators: true, + syntax: 'typescript', + }, + target: 'es2022', + transform: { + decoratorMetadata: true, + legacyDecorator: true, + }, + }, + sourceMaps: true, + swcrc: false, + }) + }) + test('swcPlugin transforms TypeScript code and use given plugin swc option', async () => { + const { swc, onLoad } = await getFixture({ + jsc: { + transform: { + useDefineForClassFields: true, + }, + }, + }) + + await onLoad({ + path: 'file.ts', + }) + + expect(swc.transformFile).toHaveBeenCalledWith('file.ts', { + configFile: false, + jsc: { + keepClassNames: true, + parser: { + decorators: true, + syntax: 'typescript', + }, + target: 'es2022', + transform: { + decoratorMetadata: true, + legacyDecorator: true, + useDefineForClassFields: true, + }, + }, + sourceMaps: true, + swcrc: false, + }) + }) +}) diff --git a/src/esbuild/swc.ts b/src/esbuild/swc.ts index 8d9d28ea..755009d3 100644 --- a/src/esbuild/swc.ts +++ b/src/esbuild/swc.ts @@ -3,11 +3,13 @@ */ import path from 'node:path' import { localRequire } from '../utils' -import type { JscConfig } from '@swc/core' +import type { JscConfig, Options } from '@swc/core' import type { Plugin } from 'esbuild' import type { Logger } from '../log' -export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => { +export type SwcPluginConfig = { logger: Logger } & Options + +export const swcPlugin = ({ logger, ...swcOptions }: SwcPluginConfig): Plugin => { return { name: 'swc', @@ -29,11 +31,14 @@ export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => { const isTs = /\.tsx?$/.test(args.path) const jsc: JscConfig = { + ...swcOptions.jsc, parser: { + ...swcOptions.jsc?.parser, syntax: isTs ? 'typescript' : 'ecmascript', decorators: true, }, transform: { + ...swcOptions.jsc?.transform, legacyDecorator: true, decoratorMetadata: true, }, @@ -42,10 +47,11 @@ export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => { } const result = await swc.transformFile(args.path, { + ...swcOptions, jsc, sourceMaps: true, configFile: false, - swcrc: false, + swcrc: swcOptions.swcrc ?? false, }) let code = result.code diff --git a/src/options.ts b/src/options.ts index 827790d4..6bbee23b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -4,6 +4,7 @@ import type { MinifyOptions } from 'terser' import type { MarkRequired } from 'ts-essentials' import type { Plugin } from './plugin' import type { TreeshakingStrategy } from './plugins/tree-shaking' +import type { SwcPluginConfig } from './esbuild/swc.js' export type KILL_SIGNAL = 'SIGKILL' | 'SIGTERM' @@ -256,6 +257,8 @@ export type Options = { * @default true */ removeNodeProtocol?: boolean + + swc?: SwcPluginConfig; } export interface NormalizedExperimentalDtsConfig { @@ -272,4 +275,5 @@ export type NormalizedOptions = Omit< tsconfigResolvePaths: Record tsconfigDecoratorMetadata?: boolean format: Format[] + swc?: SwcPluginConfig } diff --git a/vitest.config.mts b/vitest.config.mts index c650e84a..8f6d3ac6 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -4,5 +4,6 @@ export default defineConfig({ test: { testTimeout: 50000, globalSetup: 'vitest-global.ts', + include: ["test/*.test.ts", "src/**/*.test.ts"] }, })