From ae9d4224845d63c359698e0aef50611fbbc812df Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:57:04 -0400 Subject: [PATCH] refactor(@schematics/angular): use oxc-parser in trust-proxy-headers migration Replaces the TypeScript AST API (`ts.createSourceFile`, `ts.Node`, and manual child traversal) in the `trust-proxy-headers` migration with `oxc-parser`. This introduces `oxc-parser` as a dependency to `@schematics/angular` and refactors the AST inspection in `trust-proxy-headers` to use `parseSync` and the `Visitor` class. Node ranges and AST structure continue to drive text insertions via the Schematics `UpdateRecorder`. --- packages/schematics/angular/BUILD.bazel | 2 + .../trust-proxy-headers/migration.ts | 93 +++++++++++-------- .../trust-proxy-headers/migration_spec.ts | 60 ++++++++++++ packages/schematics/angular/package.json | 4 + pnpm-lock.yaml | 7 ++ 5 files changed, 126 insertions(+), 40 deletions(-) diff --git a/packages/schematics/angular/BUILD.bazel b/packages/schematics/angular/BUILD.bazel index 34d730f2aa6d..8ddce91295f1 100644 --- a/packages/schematics/angular/BUILD.bazel +++ b/packages/schematics/angular/BUILD.bazel @@ -101,7 +101,9 @@ ts_project( deps = [ ":node_modules/@angular-devkit/core", ":node_modules/@angular-devkit/schematics", + ":node_modules/@oxc-project/types", ":node_modules/jsonc-parser", + ":node_modules/oxc-parser", ":node_modules/typescript", "//:node_modules/@types/node", ], diff --git a/packages/schematics/angular/migrations/trust-proxy-headers/migration.ts b/packages/schematics/angular/migrations/trust-proxy-headers/migration.ts index 9be44b03039b..695992901b66 100644 --- a/packages/schematics/angular/migrations/trust-proxy-headers/migration.ts +++ b/packages/schematics/angular/migrations/trust-proxy-headers/migration.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Rule } from '@angular-devkit/schematics'; -import ts from 'typescript'; +import type { Rule } from '@angular-devkit/schematics'; +import { Visitor, parseSync } from 'oxc-parser'; import { allTargetOptions, allWorkspaceTargets, getWorkspace } from '../../utility/workspace'; const TODO_COMMENT = @@ -45,52 +45,65 @@ export default function (): Rule { continue; } - const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); + const parseResult = parseSync(path, content, { + sourceType: 'module', + }); + + if (parseResult.errors.length > 0) { + continue; + } + const recorder = tree.beginUpdate(path); - function visit(node: ts.Node) { - if ( - ts.isNewExpression(node) && - ts.isIdentifier(node.expression) && - (node.expression.text === 'AngularNodeAppEngine' || - node.expression.text === 'AngularAppEngine') - ) { - // Check arguments - if (!node.arguments || node.arguments.length === 0) { - // Case 1: No arguments passed - const insertPos = node.end - 1; // right before ) - recorder.insertRight( - insertPos, - `{\n ${TODO_COMMENT}\n ` + - `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}`, - ); - } else if (node.arguments.length > 0) { - const firstArg = node.arguments[0]; - if (ts.isObjectLiteralExpression(firstArg)) { - // Check if trustProxyHeaders is already present - const hasTrustProxyHeaders = firstArg.properties.some( - (prop: ts.ObjectLiteralElementLike) => - ts.isPropertyAssignment(prop) && - (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) && - prop.name.text === 'trustProxyHeaders', + const visitor = new Visitor({ + NewExpression(node) { + if ( + node.callee.type === 'Identifier' && + (node.callee.name === 'AngularNodeAppEngine' || node.callee.name === 'AngularAppEngine') + ) { + // Check arguments + if (!node.arguments || node.arguments.length === 0) { + // Case 1: No arguments passed + const hasParens = content[node.end - 1] === ')'; + const insertPos = hasParens ? node.end - 1 : node.end; + recorder.insertRight( + insertPos, + hasParens + ? `{\n ${TODO_COMMENT}\n ` + + `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}` + : `({\n ${TODO_COMMENT}\n ` + + `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n})`, ); - - if (!hasTrustProxyHeaders) { - // Insert right after the opening brace - const insertPos = firstArg.getStart() + 1; - recorder.insertRight( - insertPos, - `\n ${TODO_COMMENT}\n ` + - `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`, + } else if (node.arguments.length > 0) { + const firstArg = node.arguments[0]; + if (firstArg.type === 'ObjectExpression') { + // Check if trustProxyHeaders is already present + const hasTrustProxyHeaders = firstArg.properties.some( + (prop) => + prop.type === 'Property' && + ((!prop.computed && + prop.key.type === 'Identifier' && + prop.key.name === 'trustProxyHeaders') || + (prop.key.type === 'Literal' && prop.key.value === 'trustProxyHeaders')), ); + + if (!hasTrustProxyHeaders) { + // Insert right after the opening brace + const insertPos = firstArg.start + 1; + recorder.insertRight( + insertPos, + `\n ${TODO_COMMENT}\n ` + + `trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`, + ); + } } } } - } - ts.forEachChild(node, visit); - } + }, + }); + + visitor.visit(parseResult.program); - visit(sourceFile); tree.commitUpdate(recorder); } }; diff --git a/packages/schematics/angular/migrations/trust-proxy-headers/migration_spec.ts b/packages/schematics/angular/migrations/trust-proxy-headers/migration_spec.ts index 3e6b7ab613ea..5e997d74c1e9 100644 --- a/packages/schematics/angular/migrations/trust-proxy-headers/migration_spec.ts +++ b/packages/schematics/angular/migrations/trust-proxy-headers/migration_spec.ts @@ -98,4 +98,64 @@ describe(`Migration to add trustProxyHeaders to server.ts`, () => { const content = newTree.readText('/server.ts'); expect(content).toBe(originalContent); }); + + it(`should not add trustProxyHeaders if it already exists as a string literal`, async () => { + const originalContent = + `import { AngularAppEngine } from '@angular/ssr';\n` + + `const angularApp = new AngularAppEngine({\n 'trustProxyHeaders': true\n});`; + tree.create('/server.ts', originalContent); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const content = newTree.readText('/server.ts'); + expect(content).toBe(originalContent); + }); + + it(`should not add trustProxyHeaders if it already exists as a shorthand property`, async () => { + const originalContent = + `import { AngularAppEngine } from '@angular/ssr';\n` + + `const trustProxyHeaders = true;\n` + + `const angularApp = new AngularAppEngine({\n trustProxyHeaders\n});`; + tree.create('/server.ts', originalContent); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const content = newTree.readText('/server.ts'); + expect(content).toBe(originalContent); + }); + + it(`should add trustProxyHeaders to AngularAppEngine without parentheses`, async () => { + tree.create( + '/server.ts', + `import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine;`, + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const content = newTree.readText('/server.ts'); + expect(content).toContain(`const angularApp = new AngularAppEngine({`); + expect(content).toContain(TODO_COMMENT); + expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n})`); + }); + + it(`should add trustProxyHeaders when a computed property with variable name trustProxyHeaders exists`, async () => { + tree.create( + '/server.ts', + `import { AngularAppEngine } from '@angular/ssr';\n` + + `const trustProxyHeaders = 'customHeader';\n` + + `const angularApp = new AngularAppEngine({\n [trustProxyHeaders]: true\n});`, + ); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const content = newTree.readText('/server.ts'); + expect(content).toContain(TODO_COMMENT); + expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`); + expect(content).toContain(`[trustProxyHeaders]: true`); + }); + + it(`should skip files with parse errors without throwing`, async () => { + const malformedContent = `import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine(;`; + tree.create('/server.ts', malformedContent); + + const newTree = await schematicRunner.runSchematic(schematicName, {}, tree); + const content = newTree.readText('/server.ts'); + expect(content).toBe(malformedContent); + }); }); diff --git a/packages/schematics/angular/package.json b/packages/schematics/angular/package.json index 17b55e384252..f06ea50a4d1f 100644 --- a/packages/schematics/angular/package.json +++ b/packages/schematics/angular/package.json @@ -20,6 +20,10 @@ "@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER", "@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER", "jsonc-parser": "3.3.1", + "oxc-parser": "0.147.0", "typescript": "6.0.3" + }, + "devDependencies": { + "@oxc-project/types": "0.147.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24e2297ff748..678141885167 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -833,9 +833,16 @@ importers: jsonc-parser: specifier: 3.3.1 version: 3.3.1 + oxc-parser: + specifier: 0.147.0 + version: 0.147.0 typescript: specifier: 6.0.3 version: 6.0.3 + devDependencies: + '@oxc-project/types': + specifier: 0.147.0 + version: 0.147.0 tests: devDependencies: