diff --git a/packages/webpack5/__tests__/transformers/native-class.spec.ts b/packages/webpack5/__tests__/transformers/native-class.spec.ts index af0f6a0168..2f528380a5 100644 --- a/packages/webpack5/__tests__/transformers/native-class.spec.ts +++ b/packages/webpack5/__tests__/transformers/native-class.spec.ts @@ -175,6 +175,48 @@ class X extends NSObject {} }); }); + describe('after a preceding transformer updates the source file', () => { + // Simulates transformers like Angular's Ivy transform, which rebuild the + // source file via factory.updateSourceFile() on files containing Angular + // decorators. The updated SourceFile node is flagged Synthesized; the + // NativeClass transformer must still process its original statements. + const updateSourceFileTransformer: ts.TransformerFactory = + (context) => (sourceFile) => + context.factory.updateSourceFile(sourceFile, [ + ...sourceFile.statements, + ]); + + function transformAfterUpdate(input: string): string { + return ts.transpileModule(input, { + compilerOptions: { + module: ts.ModuleKind.ESNext, + target: ts.ScriptTarget.ES2022, + experimentalDecorators: true, + emitDecoratorMetadata: false, + useDefineForClassFields: false, + }, + transformers: { + before: [ + updateSourceFileTransformer, + nativeClassTransformer as ts.TransformerFactory, + ], + }, + }).outputText; + } + + it('downlevels @NativeClass() on a synthesized (updated) source file', () => { + const output = transformAfterUpdate(` +@NativeClass() +class Foo extends NSObject {} +`); + + expect(output).toContain('var Foo ='); + expect(output).toContain('__extends(Foo, _super)'); + expect(output).not.toContain('NativeClass'); + expect(countClassDeclarations(output)).toBe(0); + }); + }); + describe('nested scopes', () => { it('downlevels @NativeClass() class declared inside a function body', () => { const output = transform(` diff --git a/packages/webpack5/src/transformers/NativeClass/index.ts b/packages/webpack5/src/transformers/NativeClass/index.ts index 597ade9840..bb9cf75194 100644 --- a/packages/webpack5/src/transformers/NativeClass/index.ts +++ b/packages/webpack5/src/transformers/NativeClass/index.ts @@ -182,14 +182,20 @@ export default function (context: ts.TransformationContext, ...args) { } function visitNode(node: ts.Node): ts.Node { - // Do not traverse synthesized helper trees; leave them intact - if (((node as MutableNode).flags ?? 0) & ts.NodeFlags.Synthesized) { - return node; - } + // Handle the source file before the Synthesized bail-out below: a source + // file updated by an earlier transformer (e.g. Angular's Ivy transform on + // files containing Angular-decorated classes) is itself flagged + // Synthesized, but its original statements still need processing. The + // per-statement Synthesized check in transformStatements protects any + // generated statements. if (ts.isSourceFile(node)) { const [stmts, changed] = transformStatements(node.statements, true); return changed ? factory.updateSourceFile(node, stmts) : node; } + // Do not traverse synthesized helper trees; leave them intact + if (((node as MutableNode).flags ?? 0) & ts.NodeFlags.Synthesized) { + return node; + } if (ts.isBlock(node)) { const [stmts, changed] = transformStatements(node.statements, false); return changed ? factory.updateBlock(node, stmts) : node;