From 2101c73bba104c84e5b5ef6dc138bfbb474b3c66 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:13:57 +0000 Subject: [PATCH] fix(@angular/build): prevent IIFE wrapper interleaving for adjacent classes in minified files In 22.1.0, the Babel-based advancedOptimizations pass was replaced with oxc-parser + magic-string. When wrapping a class declaration in a pure IIFE, appendLeft was used for the opener and appendRight for the closer. In minified bundles where sibling top-level class declarations are adjacent without whitespace (classA.end === classB.start), magic-string emits appendLeft content before appendRight content at a shared index. This caused class B's IIFE opener to be emitted ahead of class A's IIFE closer, interleaving the wrappers and resulting in duplicate symbol declarations in subsequent build phases. This change swaps the insertion methods to use appendRight for the opening fragment and appendLeft for the closing fragment (and when splitting default exports), ensuring correct ordering at shared boundaries. closes #33699 --- .../adjust-static-class-members_oxc_spec.ts | 102 ++++++++++++++++++ .../src/tools/babel/plugins/oxc-transform.ts | 18 ++-- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts index af28ab66b264..7fe8c68030fc 100644 --- a/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts +++ b/packages/angular/build/src/tools/babel/plugins/adjust-static-class-members_oxc_spec.ts @@ -998,4 +998,106 @@ describe('adjust-static-class-members oxc-transform implementation', () => { `, }), ); + + it( + 'wraps adjacent class declarations without interleaving', + testCase({ + input: 'class A{static s=[1]}class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent exported class declarations without interleaving', + testCase({ + input: 'export class A{static s=[1]}export class B{static s=[2]}', + expected: ` + export let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + export let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent default export and class declarations without interleaving', + testCase({ + input: 'export default class A{static s=[1]}class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + class A { + static s = [1] + } + return A; + })(); + export { A as default }; + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'splits default export without interleaving with adjacent class declaration', + testCase({ + input: 'export default class A{}class B{static s=[2]}', + expected: ` + class A {} + export { A as default }; + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); + + it( + 'wraps adjacent variable class declarations without interleaving', + testCase({ + input: 'let A=class A{static s=[1]};class B{static s=[2]}', + expected: ` + let A = /*#__PURE__*/ (() => { + let A = class A { + static s = [1] + }; + return A; + })(); + let B = /*#__PURE__*/ (() => { + class B { + static s = [2] + } + return B; + })(); + `, + }), + ); }); diff --git a/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts index 2c69d98cdf1a..70989a8bc08d 100644 --- a/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts +++ b/packages/angular/build/src/tools/babel/plugins/oxc-transform.ts @@ -584,25 +584,25 @@ export function transform(filename: string, code: string, options: OxcTransformO // 1. Remove `export default ` s.overwrite(statement.start, classNode.start, ''); // 2. Wrap in IIFE - s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); - s.appendRight( + s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + s.appendLeft( lastStatement.end, `\nreturn ${classIdName};\n})();\nexport { ${classIdName} as default };`, ); } else if (isExportNamed) { // 1. Export is kept, turn `class` into `let ClassName = IIFE` - s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); - s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`); + s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + s.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`); } else if (isVariableClass) { // Wrap class inside init: `/*#__PURE__*/ (() => { let ClassName = class ClassName {}; return ClassName; })()` - s.appendLeft(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `); + s.appendRight(classNode.start, `/*#__PURE__*/ (() => {\nlet ${classIdName} = `); const terminator = activeWrapPaths.length === 0 ? ';' : ''; const iifeClosing = activeWrapPaths.length === 0 ? '})()' : '})();'; - s.appendRight(lastStatement.end, `${terminator}\nreturn ${classIdName};\n${iifeClosing}`); + s.appendLeft(lastStatement.end, `${terminator}\nreturn ${classIdName};\n${iifeClosing}`); } else { // Standard ClassDeclaration - s.appendLeft(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); - s.appendRight(lastStatement.end, `\nreturn ${classIdName};\n})();`); + s.appendRight(classNode.start, `let ${classIdName} = /*#__PURE__*/ (() => {\n`); + s.appendLeft(lastStatement.end, `\nreturn ${classIdName};\n})();`); } markEdited(statement.start, lastStatement.end); @@ -612,7 +612,7 @@ export function transform(filename: string, code: string, options: OxcTransformO } else if (isExportDefault && !hasPotentialSideEffects) { // Splitting default export even when not wrapped s.overwrite(statement.start, classNode.start, ''); - s.appendRight(classNode.end, `\nexport { ${classIdName} as default };`); + s.appendLeft(classNode.end, `\nexport { ${classIdName} as default };`); markEdited(statement.start, classNode.end); } }