Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/schematics/angular/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Comment thread
clydin marked this conversation as resolved.

visit(sourceFile);
tree.commitUpdate(recorder);
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 4 additions & 0 deletions packages/schematics/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading