Skip to content

Commit d0058e2

Browse files
authored
fix: swc transform compatibility in babel-swc-loader (#1364)
1 parent a7a5834 commit d0058e2

7 files changed

Lines changed: 236 additions & 37 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack": patch
3+
---
4+
5+
Fix `babel-swc-loader` compatibility for loose class fields, private methods, and destructuring by adding the complementary SWC transforms SWC expects to run alongside them.

packages/repack/src/loaders/babelSwcLoader/__tests__/__snapshots__/babelSwcLoader.test.ts.snap

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`partitionTransforms ignores unsupported and disabled transforms (snapshot): unsupported and disabled 1`] = `
3+
exports[`partitionTransforms ignores unsupported transforms (snapshot): unsupported only 1`] = `
44
{
5-
"includedSwcTransforms": [],
6-
"supportedSwcTransforms": [],
5+
"includedSwcTransforms": [
6+
"transform-class-static-block",
7+
"transform-class-properties",
8+
"transform-private-methods",
9+
],
10+
"supportedSwcTransforms": [
11+
"transform-class-static-block",
12+
"transform-class-properties",
13+
"transform-private-methods",
14+
],
715
"swcConfig": {
816
"jsc": {
17+
"assumptions": {
18+
"privateFieldsAsProperties": true,
19+
"setPublicClassFields": true,
20+
},
921
"parser": {
1022
"jsx": true,
1123
"syntax": "ecmascript",

packages/repack/src/loaders/babelSwcLoader/__tests__/__snapshots__/swc.test.ts.snap

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,6 @@ exports[`swc transforms support detection getSupportedSwcConfigurableTransforms
1919
}
2020
`;
2121

22-
exports[`swc transforms support detection getSupportedSwcConfigurableTransforms updates both privateFieldsAsProperties and setPublicClassFields for private-property-in-object but does not override explicit true (snapshot): private-property-in-object assumptions and names 1`] = `
23-
{
24-
"assumptions": {
25-
"privateFieldsAsProperties": true,
26-
"setPublicClassFields": true,
27-
},
28-
"transformNames": [
29-
"transform-private-property-in-object",
30-
],
31-
}
32-
`;
33-
3422
exports[`swc transforms support detection getSupportedSwcCustomTransforms configures modules commonjs options based on provided config (snapshot): modules commonjs config 1`] = `
3523
{
3624
"allowTopLevelThis": false,

packages/repack/src/loaders/babelSwcLoader/__tests__/babelSwcLoader.test.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ describe('partitionTransforms', () => {
88
['transform-block-scoping', {}],
99
['transform-react-jsx', {}],
1010
['transform-object-rest-spread', { loose: true }],
11-
['transform-class-properties', { loose: true }], // disabled configurable
12-
['transform-private-methods', { loose: true }], // disabled configurable
11+
['transform-class-properties', { loose: true }],
12+
['transform-private-methods', { loose: true }],
1313
['unknown-plugin', {}],
1414
['transform-modules-commonjs', {}],
1515
['transform-classes', {}],
@@ -22,30 +22,36 @@ describe('partitionTransforms', () => {
2222
expect(includedSwcTransforms).toEqual([
2323
'transform-block-scoping',
2424
'transform-classes',
25+
'transform-class-static-block',
2526
'transform-object-rest-spread',
27+
'transform-class-properties',
28+
'transform-private-methods',
2629
'transform-for-of',
2730
]);
2831

2932
expect(supportedSwcTransforms).toEqual([
3033
'transform-block-scoping',
3134
'transform-classes',
35+
'transform-class-static-block',
3236
'transform-object-rest-spread',
37+
'transform-class-properties',
38+
'transform-private-methods',
3339
'transform-for-of',
3440
'transform-react-jsx',
3541
'transform-modules-commonjs',
3642
]);
3743
});
3844

39-
it('ignores unsupported and disabled transforms (snapshot)', () => {
45+
it('ignores unsupported transforms (snapshot)', () => {
4046
const transforms: TransformEntry[] = [
41-
['transform-class-properties', { loose: true }], // disabled
42-
['transform-private-methods', { loose: true }], // disabled
47+
['transform-class-properties', { loose: true }],
48+
['transform-private-methods', { loose: true }],
4349
['unknown-plugin', {}],
4450
];
4551

4652
const result = partitionTransforms('/virtual/file.js', transforms);
4753

48-
expect(result).toMatchSnapshot('unsupported and disabled');
54+
expect(result).toMatchSnapshot('unsupported only');
4955
});
5056

5157
it('only custom transforms are excluded from included set but present in supported set (snapshot)', () => {
@@ -65,6 +71,46 @@ describe('partitionTransforms', () => {
6571

6672
expect(result).toMatchSnapshot('empty arrays');
6773
});
74+
75+
it('adds transform-object-rest-spread when transform-destructuring is present', () => {
76+
const transforms: TransformEntry[] = [
77+
['transform-destructuring', {}],
78+
['transform-react-jsx', {}],
79+
];
80+
81+
const { includedSwcTransforms, supportedSwcTransforms } =
82+
partitionTransforms('/virtual/file.js', transforms);
83+
84+
expect(includedSwcTransforms).toEqual([
85+
'transform-destructuring',
86+
'transform-object-rest-spread',
87+
]);
88+
expect(supportedSwcTransforms).toEqual([
89+
'transform-destructuring',
90+
'transform-object-rest-spread',
91+
'transform-react-jsx',
92+
]);
93+
});
94+
95+
it('adds transform-class-static-block when transform-class-properties is present', () => {
96+
const transforms: TransformEntry[] = [
97+
['transform-class-properties', { loose: true }],
98+
['transform-react-jsx', {}],
99+
];
100+
101+
const { includedSwcTransforms, supportedSwcTransforms } =
102+
partitionTransforms('/virtual/file.js', transforms);
103+
104+
expect(includedSwcTransforms).toEqual([
105+
'transform-class-static-block',
106+
'transform-class-properties',
107+
]);
108+
expect(supportedSwcTransforms).toEqual([
109+
'transform-class-static-block',
110+
'transform-class-properties',
111+
'transform-react-jsx',
112+
]);
113+
});
68114
});
69115

70116
describe('buildFinalSwcConfig', () => {

packages/repack/src/loaders/babelSwcLoader/__tests__/swc.test.ts

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import type { SwcLoaderOptions } from '@rspack/core';
1+
import { type SwcLoaderOptions, experiments } from '@rspack/core';
2+
import { buildFinalSwcConfig, partitionTransforms } from '../babelSwcLoader.js';
23
import {
4+
addSwcComplementaryTransforms,
35
getSupportedSwcConfigurableTransforms,
46
getSupportedSwcCustomTransforms,
57
getSupportedSwcNormalTransforms,
@@ -8,13 +10,52 @@ import {
810
type TransformEntry = [string, Record<string, any> | undefined];
911

1012
describe('swc transforms support detection', () => {
13+
describe('addSwcComplementaryTransforms', () => {
14+
it('adds transform-class-static-block when transform-class-properties is present', () => {
15+
const transforms: TransformEntry[] = [
16+
['transform-class-properties', { loose: true }],
17+
];
18+
19+
const result = addSwcComplementaryTransforms(transforms);
20+
21+
expect(result).toEqual([
22+
['transform-class-properties', { loose: true }],
23+
['transform-class-static-block', undefined],
24+
]);
25+
});
26+
27+
it('adds transform-object-rest-spread when transform-destructuring is present', () => {
28+
const transforms: TransformEntry[] = [['transform-destructuring', {}]];
29+
30+
const result = addSwcComplementaryTransforms(transforms);
31+
32+
expect(result).toEqual([
33+
['transform-destructuring', {}],
34+
['transform-object-rest-spread', undefined],
35+
]);
36+
});
37+
38+
it('does not duplicate already included complementary transforms', () => {
39+
const transforms: TransformEntry[] = [
40+
['transform-class-properties', { loose: true }],
41+
['transform-class-static-block', {}],
42+
['transform-destructuring', {}],
43+
['transform-object-rest-spread', { loose: true }],
44+
];
45+
46+
const result = addSwcComplementaryTransforms(transforms);
47+
48+
expect(result).toEqual(transforms);
49+
});
50+
});
51+
1152
describe('getSupportedSwcNormalTransforms', () => {
1253
it('returns only supported normal transform names preserving order', () => {
1354
const transforms: TransformEntry[] = [
1455
['transform-block-scoping', undefined],
1556
['transform-classes', {}],
1657
['transform-react-jsx', undefined], // custom
17-
['transform-private-methods', undefined], // configurable (disabled)
58+
['transform-private-methods', undefined], // configurable
1859
['unknown-plugin', undefined],
1960
['transform-object-rest-spread', undefined], // configurable
2061
];
@@ -29,8 +70,8 @@ describe('swc transforms support detection', () => {
2970
it('returns only supported configurable transform names preserving order', () => {
3071
const baseSwcConfig = {};
3172
const transforms: TransformEntry[] = [
32-
['transform-class-properties', { loose: true }], // disabled
33-
['transform-private-methods', { loose: true }], // disabled
73+
['transform-class-properties', { loose: true }],
74+
['transform-private-methods', { loose: true }],
3475
['transform-private-property-in-object', {}],
3576
['transform-object-rest-spread', {}],
3677
['transform-optional-chaining', {}],
@@ -47,6 +88,8 @@ describe('swc transforms support detection', () => {
4788
);
4889

4990
expect(transformNames).toEqual([
91+
'transform-class-properties',
92+
'transform-private-methods',
5093
'transform-private-property-in-object',
5194
'transform-object-rest-spread',
5295
'transform-optional-chaining',
@@ -55,6 +98,21 @@ describe('swc transforms support detection', () => {
5598
]);
5699
});
57100

101+
it('sets class fields and private methods assumptions explicitly in loose mode', () => {
102+
const { swcConfig } = getSupportedSwcConfigurableTransforms(
103+
[
104+
['transform-class-properties', { loose: true }],
105+
['transform-private-methods', { loose: true }],
106+
],
107+
{} as SwcLoaderOptions
108+
);
109+
110+
expect(swcConfig.jsc?.assumptions).toEqual({
111+
setPublicClassFields: true,
112+
privateFieldsAsProperties: true,
113+
});
114+
});
115+
58116
it('applies loose mode to setSpreadProperties when not defined; preserves explicit true (snapshot)', () => {
59117
const baseUndefined = {} as SwcLoaderOptions;
60118
const { swcConfig: cfg1 } = getSupportedSwcConfigurableTransforms(
@@ -98,7 +156,7 @@ describe('swc transforms support detection', () => {
98156
);
99157
});
100158

101-
it('updates both privateFieldsAsProperties and setPublicClassFields for private-property-in-object but does not override explicit true (snapshot)', () => {
159+
it('updates both privateFieldsAsProperties and setPublicClassFields for private-property-in-object but does not override explicit true', () => {
102160
const base: SwcLoaderOptions = {
103161
jsc: {
104162
assumptions: {
@@ -114,10 +172,11 @@ describe('swc transforms support detection', () => {
114172
base
115173
);
116174

117-
expect({
118-
transformNames,
119-
assumptions: swcConfig.jsc?.assumptions,
120-
}).toMatchSnapshot('private-property-in-object assumptions and names');
175+
expect(transformNames).toEqual(['transform-private-property-in-object']);
176+
expect(swcConfig.jsc?.assumptions).toEqual({
177+
privateFieldsAsProperties: true,
178+
setPublicClassFields: true,
179+
});
121180
});
122181
});
123182

@@ -305,4 +364,55 @@ describe('swc transforms support detection', () => {
305364
);
306365
});
307366
});
367+
368+
describe('real swc transform regression', () => {
369+
it('compiles loose class properties and private methods with complementary static block support', () => {
370+
const source = `
371+
class Example {
372+
static value = 1;
373+
static {
374+
this.value += 1;
375+
}
376+
377+
#count = 0;
378+
379+
#increment() {
380+
this.#count += 1;
381+
}
382+
383+
run() {
384+
this.#increment();
385+
return this.#count;
386+
}
387+
}
388+
389+
export default new Example().run();
390+
`;
391+
392+
const { includedSwcTransforms, swcConfig } = partitionTransforms(
393+
'/virtual/file.js',
394+
[
395+
['transform-class-properties', { loose: true }],
396+
['transform-private-methods', { loose: true }],
397+
]
398+
);
399+
const finalSwcConfig = buildFinalSwcConfig({
400+
swcConfig,
401+
includedSwcTransforms,
402+
lazyImports: false,
403+
sourceType: 'module',
404+
});
405+
406+
const result = experiments.swc.transformSync(source, {
407+
...finalSwcConfig,
408+
filename: '/virtual/file.js',
409+
configFile: false,
410+
swcrc: false,
411+
sourceMaps: false,
412+
});
413+
414+
expect(result.code).not.toContain('static {');
415+
expect(result.code).not.toContain('#count');
416+
});
417+
});
308418
});

packages/repack/src/loaders/babelSwcLoader/babelSwcLoader.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { LoaderContext, SwcLoaderOptions } from '@rspack/core';
33
import { transform } from '../babelLoader/babelLoader.js';
44
import type { BabelSwcLoaderOptions } from './options.js';
55
import {
6+
addSwcComplementaryTransforms,
67
getSupportedSwcConfigurableTransforms,
78
getSupportedSwcCustomTransforms,
89
getSupportedSwcNormalTransforms,
@@ -36,11 +37,13 @@ export function partitionTransforms(
3637
},
3738
};
3839

39-
normalTransforms = getSupportedSwcNormalTransforms(babelTransforms);
40+
const swcTransforms = addSwcComplementaryTransforms(babelTransforms);
41+
42+
normalTransforms = getSupportedSwcNormalTransforms(swcTransforms);
4043
({ swcConfig, transformNames: configurableTransforms } =
41-
getSupportedSwcConfigurableTransforms(babelTransforms, swcConfig));
44+
getSupportedSwcConfigurableTransforms(swcTransforms, swcConfig));
4245
({ swcConfig, transformNames: customTransforms } =
43-
getSupportedSwcCustomTransforms(babelTransforms, swcConfig));
46+
getSupportedSwcCustomTransforms(swcTransforms, swcConfig));
4447

4548
const includedSwcTransforms: string[] = [
4649
...normalTransforms,

0 commit comments

Comments
 (0)