Skip to content

Commit fdfd59a

Browse files
authored
feat: allow passing custom swc configuration to swcPlugin (#1313)
1 parent 769aa49 commit fdfd59a

File tree

7 files changed

+177
-4
lines changed

7 files changed

+177
-4
lines changed

docs/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,64 @@ tsup --tsconfig tsconfig.prod.json
609609

610610
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.
611611

612+
### Using custom Swc configuration
613+
614+
When you use legacy TypeScript decorator by enabling `emitDecoratorMetadata` in your tsconfig, tsup will automatically use [SWC](https://swc.rs) to transpile
615+
decorators. In this case, you can give extra swc configuration in the `tsup.config.ts` file.
616+
617+
For example, if you have to define `useDefineForClassFields`, you can do that as follows:
618+
```ts
619+
import { defineConfig } from 'tsup'
620+
621+
export default defineConfig({
622+
entry: ['src/index.ts'],
623+
splitting: false,
624+
sourcemap: true,
625+
clean: true,
626+
swc: {
627+
jsc: {
628+
transform: {
629+
useDefineForClassFields: true
630+
}
631+
}
632+
}
633+
})
634+
```
635+
636+
Note: some SWC options cannot be configured:
637+
638+
```json
639+
{
640+
"parser": {
641+
"syntax": "typescript",
642+
"decorators": true
643+
},
644+
"transform": {
645+
"legacyDecorator": true,
646+
"decoratorMetadata": true
647+
},
648+
"keepClassNames": true,
649+
"target": "es2022"
650+
}
651+
```
652+
653+
You can also define a custom `.swcrc` configuration file. Just set `swcrc` to `true`
654+
in `tsup.config.ts` to allow SWC plugin to discover automatically your custom swc config file.
655+
656+
```ts
657+
import { defineConfig } from 'tsup'
658+
659+
export default defineConfig({
660+
entry: ['src/index.ts'],
661+
splitting: false,
662+
sourcemap: true,
663+
clean: true,
664+
swc: {
665+
swcrc: true
666+
}
667+
})
668+
```
669+
612670
## Troubleshooting
613671

614672
### error: No matching export in "xxx.ts" for import "xxx"

schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@
181181
}
182182
]
183183
},
184+
"swc": {
185+
"type": "object"
186+
},
184187
"globalName": {
185188
"type": "string"
186189
},

src/esbuild/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export async function runEsbuild(
138138
skipNodeModulesBundle: options.skipNodeModulesBundle,
139139
tsconfigResolvePaths: options.tsconfigResolvePaths,
140140
}),
141-
options.tsconfigDecoratorMetadata && swcPlugin({ logger }),
141+
options.tsconfigDecoratorMetadata && swcPlugin({ ...options.swc, logger }),
142142
nativeNodeModulesPlugin(),
143143
postcssPlugin({
144144
css,

src/esbuild/swc.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { describe, expect, test, vi } from 'vitest'
2+
import { swcPlugin, type SwcPluginConfig } from './swc'
3+
import { localRequire } from '../utils'
4+
5+
vi.mock('../utils')
6+
7+
const getFixture = async (opts: Partial<SwcPluginConfig> = {}) => {
8+
const swc = {
9+
transformFile: vi.fn().mockResolvedValue({
10+
code: 'source-code',
11+
map: JSON.stringify({
12+
sources: ['file:///path/to/file.ts'],
13+
}),
14+
}),
15+
}
16+
17+
const logger = {
18+
warn: vi.fn(),
19+
error: vi.fn(),
20+
info: vi.fn(),
21+
}
22+
23+
const build = {
24+
initialOptions: {
25+
keepNames: true,
26+
},
27+
onLoad: vi.fn(),
28+
}
29+
30+
vi.mocked(localRequire).mockReturnValue(swc)
31+
32+
const plugin = swcPlugin({
33+
...opts,
34+
logger: logger as never,
35+
})
36+
37+
await plugin.setup(build as never)
38+
39+
const onLoad = build.onLoad.mock.calls[0][1] as Function
40+
41+
return { swc, onLoad, logger, build }
42+
}
43+
describe('swcPlugin', () => {
44+
test('swcPlugin transforms TypeScript code with decorators and default plugin swc option', async () => {
45+
const { swc, onLoad } = await getFixture()
46+
47+
await onLoad({
48+
path: 'file.ts',
49+
})
50+
51+
expect(swc.transformFile).toHaveBeenCalledWith('file.ts', {
52+
configFile: false,
53+
jsc: {
54+
keepClassNames: true,
55+
parser: {
56+
decorators: true,
57+
syntax: 'typescript',
58+
},
59+
target: 'es2022',
60+
transform: {
61+
decoratorMetadata: true,
62+
legacyDecorator: true,
63+
},
64+
},
65+
sourceMaps: true,
66+
swcrc: false,
67+
})
68+
})
69+
test('swcPlugin transforms TypeScript code and use given plugin swc option', async () => {
70+
const { swc, onLoad } = await getFixture({
71+
jsc: {
72+
transform: {
73+
useDefineForClassFields: true,
74+
},
75+
},
76+
})
77+
78+
await onLoad({
79+
path: 'file.ts',
80+
})
81+
82+
expect(swc.transformFile).toHaveBeenCalledWith('file.ts', {
83+
configFile: false,
84+
jsc: {
85+
keepClassNames: true,
86+
parser: {
87+
decorators: true,
88+
syntax: 'typescript',
89+
},
90+
target: 'es2022',
91+
transform: {
92+
decoratorMetadata: true,
93+
legacyDecorator: true,
94+
useDefineForClassFields: true,
95+
},
96+
},
97+
sourceMaps: true,
98+
swcrc: false,
99+
})
100+
})
101+
})

src/esbuild/swc.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
*/
44
import path from 'node:path'
55
import { localRequire } from '../utils'
6-
import type { JscConfig } from '@swc/core'
6+
import type { JscConfig, Options } from '@swc/core'
77
import type { Plugin } from 'esbuild'
88
import type { Logger } from '../log'
99

10-
export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => {
10+
export type SwcPluginConfig = { logger: Logger } & Options
11+
12+
export const swcPlugin = ({ logger, ...swcOptions }: SwcPluginConfig): Plugin => {
1113
return {
1214
name: 'swc',
1315

@@ -29,11 +31,14 @@ export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => {
2931
const isTs = /\.tsx?$/.test(args.path)
3032

3133
const jsc: JscConfig = {
34+
...swcOptions.jsc,
3235
parser: {
36+
...swcOptions.jsc?.parser,
3337
syntax: isTs ? 'typescript' : 'ecmascript',
3438
decorators: true,
3539
},
3640
transform: {
41+
...swcOptions.jsc?.transform,
3742
legacyDecorator: true,
3843
decoratorMetadata: true,
3944
},
@@ -42,10 +47,11 @@ export const swcPlugin = ({ logger }: { logger: Logger }): Plugin => {
4247
}
4348

4449
const result = await swc.transformFile(args.path, {
50+
...swcOptions,
4551
jsc,
4652
sourceMaps: true,
4753
configFile: false,
48-
swcrc: false,
54+
swcrc: swcOptions.swcrc ?? false,
4955
})
5056

5157
let code = result.code

src/options.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { MinifyOptions } from 'terser'
44
import type { MarkRequired } from 'ts-essentials'
55
import type { Plugin } from './plugin'
66
import type { TreeshakingStrategy } from './plugins/tree-shaking'
7+
import type { SwcPluginConfig } from './esbuild/swc.js'
78

89
export type KILL_SIGNAL = 'SIGKILL' | 'SIGTERM'
910

@@ -256,6 +257,8 @@ export type Options = {
256257
* @default true
257258
*/
258259
removeNodeProtocol?: boolean
260+
261+
swc?: SwcPluginConfig;
259262
}
260263

261264
export interface NormalizedExperimentalDtsConfig {
@@ -272,4 +275,5 @@ export type NormalizedOptions = Omit<
272275
tsconfigResolvePaths: Record<string, string[]>
273276
tsconfigDecoratorMetadata?: boolean
274277
format: Format[]
278+
swc?: SwcPluginConfig
275279
}

vitest.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export default defineConfig({
44
test: {
55
testTimeout: 50000,
66
globalSetup: 'vitest-global.ts',
7+
include: ["test/*.test.ts", "src/**/*.test.ts"]
78
},
89
})

0 commit comments

Comments
 (0)