Skip to content

Commit 49562d4

Browse files
authored
fix(webpack): enable traversal of child nodes for NativeClass detection (#11137)
1 parent 81dd859 commit 49562d4

File tree

2 files changed

+220
-2
lines changed

2 files changed

+220
-2
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import ts from 'typescript';
2+
import nativeClassTransformer from '../../src/transformers/NativeClass';
3+
4+
function transform(input: string): string {
5+
return ts.transpileModule(input, {
6+
compilerOptions: {
7+
module: ts.ModuleKind.ESNext,
8+
target: ts.ScriptTarget.ES2022,
9+
experimentalDecorators: true,
10+
emitDecoratorMetadata: false,
11+
useDefineForClassFields: false,
12+
},
13+
transformers: {
14+
before: [nativeClassTransformer as ts.TransformerFactory<ts.SourceFile>],
15+
},
16+
}).outputText;
17+
}
18+
19+
function countClassDeclarations(sourceText: string): number {
20+
const sourceFile = ts.createSourceFile(
21+
'/transformed.js',
22+
sourceText,
23+
ts.ScriptTarget.Latest,
24+
true,
25+
ts.ScriptKind.JS,
26+
);
27+
28+
let count = 0;
29+
const walk = (node: ts.Node) => {
30+
if (ts.isClassDeclaration(node)) {
31+
count++;
32+
}
33+
ts.forEachChild(node, walk);
34+
};
35+
36+
walk(sourceFile);
37+
return count;
38+
}
39+
40+
describe('NativeClass transformer', () => {
41+
describe('top-level classes', () => {
42+
it('downlevels @NativeClass (no parentheses) to ES5 IIFE', () => {
43+
const output = transform(`
44+
@NativeClass
45+
class Foo extends NSObject {}
46+
`);
47+
48+
expect(output).toContain('var Foo =');
49+
expect(output).toContain('__extends(Foo, _super)');
50+
expect(output).toContain('function (_super)');
51+
expect(output).not.toContain('@NativeClass');
52+
expect(countClassDeclarations(output)).toBe(0);
53+
});
54+
55+
it('downlevels @NativeClass() (with parentheses) to ES5 IIFE', () => {
56+
const output = transform(`
57+
@NativeClass()
58+
class Foo extends NSObject {}
59+
`);
60+
61+
expect(output).toContain('var Foo =');
62+
expect(output).toContain('__extends(Foo, _super)');
63+
expect(output).toContain('function (_super)');
64+
expect(output).not.toContain('@NativeClass');
65+
expect(countClassDeclarations(output)).toBe(0);
66+
});
67+
68+
it('downlevels exported @NativeClass() class and preserves export', () => {
69+
const output = transform(`
70+
@NativeClass()
71+
export class Bar extends NSObject {}
72+
`);
73+
74+
expect(output).toContain('var Bar =');
75+
expect(output).toContain('__extends(Bar, _super)');
76+
expect(output).toContain('export { Bar }');
77+
expect(output).not.toContain('@NativeClass');
78+
expect(countClassDeclarations(output)).toBe(0);
79+
});
80+
81+
it('preserves prototype methods on the downleveled class', () => {
82+
const output = transform(`
83+
@NativeClass()
84+
class Baz extends UIView {
85+
doWork() { return 1; }
86+
}
87+
`);
88+
89+
expect(output).toContain('var Baz =');
90+
expect(output).toContain('__extends(Baz, _super)');
91+
expect(output).toContain('Baz.prototype.doWork = function ()');
92+
expect(output).not.toContain('@NativeClass');
93+
});
94+
95+
it('downlevels multiple @NativeClass() classes in the same file', () => {
96+
const output = transform(`
97+
@NativeClass()
98+
class A extends NSObject {}
99+
@NativeClass()
100+
class B extends NSObject {}
101+
`);
102+
103+
expect(output).toContain('var A =');
104+
expect(output).toContain('__extends(A, _super)');
105+
expect(output).toContain('var B =');
106+
expect(output).toContain('__extends(B, _super)');
107+
expect(output).not.toContain('@NativeClass');
108+
expect(countClassDeclarations(output)).toBe(0);
109+
});
110+
111+
it('does NOT downlevel classes without @NativeClass', () => {
112+
const output = transform(`
113+
class Plain extends Base {
114+
method() {}
115+
}
116+
`);
117+
118+
expect(output).toContain('class Plain extends Base');
119+
expect(output).not.toContain('var Plain =');
120+
expect(output).not.toContain('__extends');
121+
expect(countClassDeclarations(output)).toBe(1);
122+
});
123+
124+
it('downlevels only the @NativeClass class when mixed with a plain class', () => {
125+
const output = transform(`
126+
class Regular {}
127+
@NativeClass()
128+
class Native extends NSObject {}
129+
`);
130+
131+
expect(output).toContain('class Regular');
132+
expect(output).toContain('var Native =');
133+
expect(output).toContain('__extends(Native, _super)');
134+
// Regular class stays as modern class declaration
135+
expect(countClassDeclarations(output)).toBe(1);
136+
});
137+
});
138+
139+
describe('strip-loader marker', () => {
140+
it('downlevels a class preceded by the /*__NativeClass__*/ marker', () => {
141+
const output = transform(`
142+
/*__NativeClass__*/
143+
class Marked extends NSObject {}
144+
`);
145+
146+
expect(output).toContain('var Marked =');
147+
expect(output).toContain('__extends(Marked, _super)');
148+
expect(output).not.toContain('/*__NativeClass__*/');
149+
expect(countClassDeclarations(output)).toBe(0);
150+
});
151+
});
152+
153+
describe('import cleanup', () => {
154+
it('removes NativeClass from a named import while keeping other imports', () => {
155+
const output = transform(`
156+
import { NativeClass, Observable } from '@nativescript/core';
157+
class Foo {}
158+
`);
159+
160+
expect(output).toContain("from '@nativescript/core'");
161+
expect(output).toContain('Observable');
162+
expect(output).not.toContain('NativeClass');
163+
});
164+
165+
it('removes the entire import statement when NativeClass is the only import', () => {
166+
const output = transform(`
167+
import { NativeClass } from '@nativescript/core';
168+
@NativeClass()
169+
class X extends NSObject {}
170+
`);
171+
172+
expect(output).not.toContain('NativeClass');
173+
expect(output).toContain('var X =');
174+
expect(output).toContain('__extends(X, _super)');
175+
});
176+
});
177+
178+
describe('nested scopes', () => {
179+
it('downlevels @NativeClass() class declared inside a function body', () => {
180+
const output = transform(`
181+
function test() {
182+
@NativeClass()
183+
class Test extends UIView {}
184+
}
185+
`);
186+
187+
expect(output).toContain('function test()');
188+
expect(output).toContain('var Test =');
189+
expect(output).toContain('__extends(Test, _super)');
190+
expect(output).toContain('function (_super)');
191+
expect(output).not.toContain('@NativeClass');
192+
expect(countClassDeclarations(output)).toBe(0);
193+
});
194+
195+
it('downlevels @NativeClass() class declared inside an arrow function body', () => {
196+
const output = transform(`
197+
const test2 = () => {
198+
@NativeClass()
199+
class Test extends UIView {}
200+
};
201+
`);
202+
203+
expect(output).toContain('const test2 = () =>');
204+
expect(output).toContain('var Test =');
205+
expect(output).toContain('__extends(Test, _super)');
206+
expect(output).toContain('function (_super)');
207+
expect(output).not.toContain('@NativeClass');
208+
expect(countClassDeclarations(output)).toBe(0);
209+
});
210+
});
211+
});

packages/webpack5/src/transformers/NativeClass/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,15 @@ export default function (context: ts.TransformationContext, ...args) {
279279
result.push(updated);
280280
continue;
281281
}
282-
// No deep traversal for unrelated nodes
283-
result.push(statement);
282+
// iterate over children as there might be a NativeClass inside of functions, blocks, etc
283+
const visited = ts.visitEachChild(statement, visitNode, context);
284+
if (visited !== statement) {
285+
mutated = true;
286+
changed = true;
287+
}
288+
if (visited) {
289+
result.push(visited);
290+
}
284291
}
285292
return [changed ? factory.createNodeArray(result) : statements, changed];
286293
}

0 commit comments

Comments
 (0)