Skip to content

Commit ef40c62

Browse files
clydindgp1130
authored andcommitted
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`.
1 parent 26a59de commit ef40c62

5 files changed

Lines changed: 126 additions & 40 deletions

File tree

packages/schematics/angular/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ ts_project(
101101
deps = [
102102
":node_modules/@angular-devkit/core",
103103
":node_modules/@angular-devkit/schematics",
104+
":node_modules/@oxc-project/types",
104105
":node_modules/jsonc-parser",
106+
":node_modules/oxc-parser",
105107
":node_modules/typescript",
106108
"//:node_modules/@types/node",
107109
],

packages/schematics/angular/migrations/trust-proxy-headers/migration.ts

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { Rule } from '@angular-devkit/schematics';
10-
import ts from 'typescript';
9+
import type { Rule } from '@angular-devkit/schematics';
10+
import { Visitor, parseSync } from 'oxc-parser';
1111
import { allTargetOptions, allWorkspaceTargets, getWorkspace } from '../../utility/workspace';
1212

1313
const TODO_COMMENT =
@@ -45,52 +45,65 @@ export default function (): Rule {
4545
continue;
4646
}
4747

48-
const sourceFile = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true);
48+
const parseResult = parseSync(path, content, {
49+
sourceType: 'module',
50+
});
51+
52+
if (parseResult.errors.length > 0) {
53+
continue;
54+
}
55+
4956
const recorder = tree.beginUpdate(path);
5057

51-
function visit(node: ts.Node) {
52-
if (
53-
ts.isNewExpression(node) &&
54-
ts.isIdentifier(node.expression) &&
55-
(node.expression.text === 'AngularNodeAppEngine' ||
56-
node.expression.text === 'AngularAppEngine')
57-
) {
58-
// Check arguments
59-
if (!node.arguments || node.arguments.length === 0) {
60-
// Case 1: No arguments passed
61-
const insertPos = node.end - 1; // right before )
62-
recorder.insertRight(
63-
insertPos,
64-
`{\n ${TODO_COMMENT}\n ` +
65-
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}`,
66-
);
67-
} else if (node.arguments.length > 0) {
68-
const firstArg = node.arguments[0];
69-
if (ts.isObjectLiteralExpression(firstArg)) {
70-
// Check if trustProxyHeaders is already present
71-
const hasTrustProxyHeaders = firstArg.properties.some(
72-
(prop: ts.ObjectLiteralElementLike) =>
73-
ts.isPropertyAssignment(prop) &&
74-
(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
75-
prop.name.text === 'trustProxyHeaders',
58+
const visitor = new Visitor({
59+
NewExpression(node) {
60+
if (
61+
node.callee.type === 'Identifier' &&
62+
(node.callee.name === 'AngularNodeAppEngine' || node.callee.name === 'AngularAppEngine')
63+
) {
64+
// Check arguments
65+
if (!node.arguments || node.arguments.length === 0) {
66+
// Case 1: No arguments passed
67+
const hasParens = content[node.end - 1] === ')';
68+
const insertPos = hasParens ? node.end - 1 : node.end;
69+
recorder.insertRight(
70+
insertPos,
71+
hasParens
72+
? `{\n ${TODO_COMMENT}\n ` +
73+
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n}`
74+
: `({\n ${TODO_COMMENT}\n ` +
75+
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n})`,
7676
);
77-
78-
if (!hasTrustProxyHeaders) {
79-
// Insert right after the opening brace
80-
const insertPos = firstArg.getStart() + 1;
81-
recorder.insertRight(
82-
insertPos,
83-
`\n ${TODO_COMMENT}\n ` +
84-
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`,
77+
} else if (node.arguments.length > 0) {
78+
const firstArg = node.arguments[0];
79+
if (firstArg.type === 'ObjectExpression') {
80+
// Check if trustProxyHeaders is already present
81+
const hasTrustProxyHeaders = firstArg.properties.some(
82+
(prop) =>
83+
prop.type === 'Property' &&
84+
((!prop.computed &&
85+
prop.key.type === 'Identifier' &&
86+
prop.key.name === 'trustProxyHeaders') ||
87+
(prop.key.type === 'Literal' && prop.key.value === 'trustProxyHeaders')),
8588
);
89+
90+
if (!hasTrustProxyHeaders) {
91+
// Insert right after the opening brace
92+
const insertPos = firstArg.start + 1;
93+
recorder.insertRight(
94+
insertPos,
95+
`\n ${TODO_COMMENT}\n ` +
96+
`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`,
97+
);
98+
}
8699
}
87100
}
88101
}
89-
}
90-
ts.forEachChild(node, visit);
91-
}
102+
},
103+
});
104+
105+
visitor.visit(parseResult.program);
92106

93-
visit(sourceFile);
94107
tree.commitUpdate(recorder);
95108
}
96109
};

packages/schematics/angular/migrations/trust-proxy-headers/migration_spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,64 @@ describe(`Migration to add trustProxyHeaders to server.ts`, () => {
9898
const content = newTree.readText('/server.ts');
9999
expect(content).toBe(originalContent);
100100
});
101+
102+
it(`should not add trustProxyHeaders if it already exists as a string literal`, async () => {
103+
const originalContent =
104+
`import { AngularAppEngine } from '@angular/ssr';\n` +
105+
`const angularApp = new AngularAppEngine({\n 'trustProxyHeaders': true\n});`;
106+
tree.create('/server.ts', originalContent);
107+
108+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
109+
const content = newTree.readText('/server.ts');
110+
expect(content).toBe(originalContent);
111+
});
112+
113+
it(`should not add trustProxyHeaders if it already exists as a shorthand property`, async () => {
114+
const originalContent =
115+
`import { AngularAppEngine } from '@angular/ssr';\n` +
116+
`const trustProxyHeaders = true;\n` +
117+
`const angularApp = new AngularAppEngine({\n trustProxyHeaders\n});`;
118+
tree.create('/server.ts', originalContent);
119+
120+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
121+
const content = newTree.readText('/server.ts');
122+
expect(content).toBe(originalContent);
123+
});
124+
125+
it(`should add trustProxyHeaders to AngularAppEngine without parentheses`, async () => {
126+
tree.create(
127+
'/server.ts',
128+
`import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine;`,
129+
);
130+
131+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
132+
const content = newTree.readText('/server.ts');
133+
expect(content).toContain(`const angularApp = new AngularAppEngine({`);
134+
expect(content).toContain(TODO_COMMENT);
135+
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],\n})`);
136+
});
137+
138+
it(`should add trustProxyHeaders when a computed property with variable name trustProxyHeaders exists`, async () => {
139+
tree.create(
140+
'/server.ts',
141+
`import { AngularAppEngine } from '@angular/ssr';\n` +
142+
`const trustProxyHeaders = 'customHeader';\n` +
143+
`const angularApp = new AngularAppEngine({\n [trustProxyHeaders]: true\n});`,
144+
);
145+
146+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
147+
const content = newTree.readText('/server.ts');
148+
expect(content).toContain(TODO_COMMENT);
149+
expect(content).toContain(`trustProxyHeaders: ['x-forwarded-host', 'x-forwarded-proto'],`);
150+
expect(content).toContain(`[trustProxyHeaders]: true`);
151+
});
152+
153+
it(`should skip files with parse errors without throwing`, async () => {
154+
const malformedContent = `import { AngularAppEngine } from '@angular/ssr';\nconst angularApp = new AngularAppEngine(;`;
155+
tree.create('/server.ts', malformedContent);
156+
157+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
158+
const content = newTree.readText('/server.ts');
159+
expect(content).toBe(malformedContent);
160+
});
101161
});

packages/schematics/angular/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
"@angular-devkit/core": "workspace:0.0.0-PLACEHOLDER",
2121
"@angular-devkit/schematics": "workspace:0.0.0-PLACEHOLDER",
2222
"jsonc-parser": "3.3.1",
23+
"oxc-parser": "0.147.0",
2324
"typescript": "6.0.3"
25+
},
26+
"devDependencies": {
27+
"@oxc-project/types": "0.147.0"
2428
}
2529
}

pnpm-lock.yaml

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)