From 1e9f7c0e39d295a2757802abdb918e07b46ce244 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:06:23 +0000 Subject: [PATCH 1/4] fix(@angular/build): prevent syntax corruption in linker and oxc transform This commit addresses two sources of JavaScript syntax corruption: 1. StringAstFactory: Parenthesize object literal expression bodies in arrow functions to prevent them from being parsed as BlockStatements where property keys are treated as statement labels. 2. oxc-transform: Preserve parentheses when unwrapping enum IIFE argument assignments so expressions like Area || (exports.Area = {}) maintain correct operator precedence. --- .../angular/linker/string-ast-factory.ts | 6 +++++- .../oxc/adjust-typescript-enums_oxc_spec.ts | 20 +++++++++++++++++++ .../build/src/tools/oxc/oxc-transform.ts | 11 ++++++---- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts index 906056bec1c7..55f4489a3f9f 100644 --- a/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts +++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts @@ -177,8 +177,12 @@ export class StringAstFactory implements AstFactory { createArrowFunctionExpression(parameters: Parameter[], body: unknown): string { const params = parameters.map((p) => p.name).join(', '); + const renderedBody = this.render(body); + const isObjectLiteral = + renderedBody.startsWith('{') && renderedBody.endsWith('}') && !renderedBody.includes(';'); + const formattedBody = isObjectLiteral ? `(${renderedBody})` : renderedBody; - return `(${params}) => ${this.render(body)}`; + return `(${params}) => ${formattedBody}`; } createDynamicImport(url: unknown): string { diff --git a/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts b/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts index fc01eecf991a..c074f315d35f 100644 --- a/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts +++ b/packages/angular/build/src/tools/oxc/adjust-typescript-enums_oxc_spec.ts @@ -280,4 +280,24 @@ describe('adjust-typescript-enums oxc-transform implementation', () => { `, }), ); + + it( + 'handles TypeScript enums with chained exports assignment (angular-split / shared-docs pattern)', + testCase({ + input: ` + var Area; + (function (a1) { + a1[a1["areaAfter"] = 0] = "areaAfter"; + a1[a1["preserveOtherCategoryOrder"] = 1] = "preserveOtherCategoryOrder"; + })(Area || (Area = exports.Area = {})); + `, + expected: ` + var Area = /*#__PURE__*/ (function (a1) { + a1[(a1["areaAfter"] = 0)] = "areaAfter"; + a1[(a1["preserveOtherCategoryOrder"] = 1)] = "preserveOtherCategoryOrder"; + return a1; + })(Area || (exports.Area = {})); + `, + }), + ); }); diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts index 1c00bb072ddf..ade63197d434 100644 --- a/packages/angular/build/src/tools/oxc/oxc-transform.ts +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -421,11 +421,14 @@ export function transform(filename: string, code: string, options: OxcTransformO // 3. Remove `Name = ` assignment in arguments if it's a simple identifier if (rightCallArgument.left.type === 'Identifier') { - s.overwrite( - arg.right.start, - arg.right.end, - code.substring(rightCallArgument.right.start, rightCallArgument.right.end), + let replacement = code.substring( + rightCallArgument.right.start, + rightCallArgument.right.end, ); + if (rightCallArgument.right.type === 'AssignmentExpression') { + replacement = `(${replacement})`; + } + s.overwrite(arg.right.start, arg.right.end, replacement); markEdited(arg.right.start, arg.right.end); } From 3a79292b2d75f924b3f9d91ad9a26287c3f206d5 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:12:37 +0000 Subject: [PATCH 2/4] refactor(@angular/build): use unwrapParentheses for oxc AST assignment check --- packages/angular/build/src/tools/oxc/oxc-transform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/oxc/oxc-transform.ts b/packages/angular/build/src/tools/oxc/oxc-transform.ts index ade63197d434..64a5aa74062e 100644 --- a/packages/angular/build/src/tools/oxc/oxc-transform.ts +++ b/packages/angular/build/src/tools/oxc/oxc-transform.ts @@ -425,7 +425,7 @@ export function transform(filename: string, code: string, options: OxcTransformO rightCallArgument.right.start, rightCallArgument.right.end, ); - if (rightCallArgument.right.type === 'AssignmentExpression') { + if (unwrapParentheses(rightCallArgument.right).type === 'AssignmentExpression') { replacement = `(${replacement})`; } s.overwrite(arg.right.start, arg.right.end, replacement); From 72ad9d014b7224ef53dfb5aea6d992922db54582 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 5 Aug 2026 10:22:49 +0000 Subject: [PATCH 3/4] refactor(@angular/build): use prefix tagging for object literal detection in StringAstFactory --- .../angular/linker/string-ast-factory.ts | 23 ++++++++++++------- .../angular/linker/string-ast-factory_spec.ts | 19 +++++++++++++-- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts index 55f4489a3f9f..a703856c39bd 100644 --- a/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts +++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory.ts @@ -21,6 +21,8 @@ import type { Parameter, } from '@angular/compiler-cli/src/ngtsc/translator/src/api/ast_factory'; +const OBJECT_LITERAL_PREFIX = '\0obj_'; + /** * An implementation of `AstFactory` that generates JavaScript code strings directly. */ @@ -28,10 +30,10 @@ export class StringAstFactory implements AstFactory { constructor(private readonly sourceCode: string = '') {} private render(expr: unknown): string { + let rendered: string; if (typeof expr === 'string') { - return expr; - } - if ( + rendered = expr; + } else if ( typeof expr === 'object' && expr !== null && typeof (expr as { start?: number; end?: number }).start === 'number' && @@ -39,10 +41,16 @@ export class StringAstFactory implements AstFactory { ) { const { start, end } = expr as { start: number; end: number }; - return this.sourceCode.slice(start, end); + rendered = this.sourceCode.slice(start, end); + } else { + rendered = String(expr); } - return String(expr); + if (rendered.startsWith(OBJECT_LITERAL_PREFIX)) { + return rendered.slice(OBJECT_LITERAL_PREFIX.length); + } + + return rendered; } /** @@ -177,9 +185,8 @@ export class StringAstFactory implements AstFactory { createArrowFunctionExpression(parameters: Parameter[], body: unknown): string { const params = parameters.map((p) => p.name).join(', '); + const isObjectLiteral = typeof body === 'string' && body.startsWith(OBJECT_LITERAL_PREFIX); const renderedBody = this.render(body); - const isObjectLiteral = - renderedBody.startsWith('{') && renderedBody.endsWith('}') && !renderedBody.includes(';'); const formattedBody = isObjectLiteral ? `(${renderedBody})` : renderedBody; return `(${params}) => ${formattedBody}`; @@ -222,7 +229,7 @@ export class StringAstFactory implements AstFactory { return `${key}: ${this.render(p.value)}`; }); - return `{\n${props.join(',\n')}\n}`; + return `${OBJECT_LITERAL_PREFIX}{\n${props.join(',\n')}\n}`; } createParenthesizedExpression(expression: unknown): string { diff --git a/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts b/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts index c47de303977f..eb62a358dfcd 100644 --- a/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts +++ b/packages/angular/build/src/tools/angular/linker/string-ast-factory_spec.ts @@ -63,7 +63,7 @@ describe('StringAstFactory', () => { { kind: 'property' as const, propertyName: 'foo', value: '1', quoted: false }, { kind: 'property' as const, propertyName: 'foo-bar', value: '2', quoted: true }, ]; - expect(factory.createObjectLiteral(props)).toBe('{\nfoo: 1,\n"foo-bar": 2\n}'); + expect(factory.createObjectLiteral(props)).toContain('{\nfoo: 1,\n"foo-bar": 2\n}'); }); it('should correctly format object literals with spread properties', () => { @@ -71,7 +71,7 @@ describe('StringAstFactory', () => { { kind: 'property' as const, propertyName: 'foo', value: '1', quoted: false }, { kind: 'spread' as const, expression: 'bar' }, ]; - expect(factory.createObjectLiteral(props)).toBe('{\nfoo: 1,\n...bar\n}'); + expect(factory.createObjectLiteral(props)).toContain('{\nfoo: 1,\n...bar\n}'); }); }); @@ -165,6 +165,21 @@ describe('StringAstFactory', () => { expect(factory.createArrowFunctionExpression(params, 'a + b')).toBe('(a, b) => a + b'); }); + it('should parenthesize object literal expression bodies in arrow functions even when containing semicolons', () => { + const params = [{ name: 'a', type: null }]; + const obj = factory.createObjectLiteral([ + { + kind: 'property', + propertyName: 'factory', + value: '() => { return new Service(); }', + quoted: false, + }, + ]); + expect(factory.createArrowFunctionExpression(params, obj)).toBe( + '(a) => ({\nfactory: () => { return new Service(); }\n})', + ); + }); + it('should format statements', () => { expect(factory.createBlock(['foo();', 'bar();'])).toBe('{\nfoo();\nbar();\n}'); expect(factory.createIfStatement('cond', 'foo();', 'bar();')).toBe( From 7231815c271f2712323588be5aa82aa9dad5314f Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 5 Aug 2026 13:17:26 +0000 Subject: [PATCH 4/4] fix(@angular/build): disable linker internal source mapping in OXC linker The OXC linker handles source maps externally via MagicString and remapping. Disabling the linker's internal sourceMapping option prevents the LinkerEnvironment from attempting to read source files through the noopFileSystem. --- packages/angular/build/src/tools/angular/linker/oxc-linker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts index ddadb9b766f4..467a1332cd97 100644 --- a/packages/angular/build/src/tools/angular/linker/oxc-linker.ts +++ b/packages/angular/build/src/tools/angular/linker/oxc-linker.ts @@ -135,7 +135,7 @@ export function linkWithOxc(filename: string, code: string, options: OxcLinkerOp SHARED_LOGGER, SHARED_AST_HOST, astFactory, - { linkerJitMode: options.jit ?? false }, + { linkerJitMode: options.jit ?? false, sourceMapping: false }, ); const fileLinker = new FileLinker(linkerEnvironment, filename as AbsoluteFsPath, code);