feat: allow passing custom swc configuration to swcPlugin (#1313)

This commit is contained in:
Romain Lenzotti 2025-05-16 15:17:29 +02:00 committed by GitHub
parent 769aa49cae
commit fdfd59afb2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 177 additions and 4 deletions

View File

@ -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"

View File

@ -181,6 +181,9 @@
}
]
},
"swc": {
"type": "object"
},
"globalName": {
"type": "string"
},

View File

@ -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,

101
src/esbuild/swc.test.ts Normal file
View File

@ -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<SwcPluginConfig> = {}) => {
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,
})
})
})

View File

@ -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

View File

@ -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<string, string[]>
tsconfigDecoratorMetadata?: boolean
format: Format[]
swc?: SwcPluginConfig
}

View File

@ -4,5 +4,6 @@ export default defineConfig({
test: {
testTimeout: 50000,
globalSetup: 'vitest-global.ts',
include: ["test/*.test.ts", "src/**/*.test.ts"]
},
})