Skip to content

Commit 2db94b8

Browse files
committed
fixup! feat(@angular/ssr): add modulepreload for lazy-loaded routes
(cherry picked from commit c2a01afc3a1bf7681382994e74931ab7b08caa74)
1 parent 18d0d35 commit 2db94b8

File tree

9 files changed

+321
-119
lines changed

9 files changed

+321
-119
lines changed

packages/angular/build/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ ts_library(
127127
"@npm//@angular/compiler-cli",
128128
"@npm//@babel/core",
129129
"@npm//prettier",
130+
"@npm//typescript",
130131
],
131132
)
132133

packages/angular/build/src/tools/angular/compilation/aot-compilation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
ensureSourceFileVersions,
1818
} from '../angular-host';
1919
import { replaceBootstrap } from '../transformers/jit-bootstrap-transformer';
20+
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
2021
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
2122
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
2223

@@ -287,6 +288,7 @@ export class AotCompilation extends AngularCompilation {
287288
transformers.before ??= [];
288289
transformers.before.push(
289290
replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker()),
291+
lazyRoutesTransformer(typeScriptProgram.getProgram(), compilerHost),
290292
);
291293
transformers.before.push(webWorkerTransform);
292294

packages/angular/build/src/tools/angular/compilation/jit-compilation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { loadEsmModule } from '../../../utils/load-esm';
1313
import { profileSync } from '../../esbuild/profiling';
1414
import { AngularHostOptions, createAngularCompilerHost } from '../angular-host';
1515
import { createJitResourceTransformer } from '../transformers/jit-resource-transformer';
16+
import { lazyRoutesTransformer } from '../transformers/lazy-routes-transformer';
1617
import { createWorkerTransformer } from '../transformers/web-worker-transformer';
1718
import { AngularCompilation, DiagnosticModes, EmitFileResult } from './angular-compilation';
1819

@@ -137,6 +138,7 @@ export class JitCompilation extends AngularCompilation {
137138
replaceResourcesTransform,
138139
constructorParametersDownlevelTransform,
139140
webWorkerTransform,
141+
lazyRoutesTransformer(typeScriptProgram.getProgram(), compilerHost),
140142
],
141143
};
142144

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import assert from 'node:assert';
10+
import { relative } from 'node:path/posix';
11+
import ts from 'typescript';
12+
13+
export function lazyRoutesTransformer(
14+
program: ts.Program,
15+
compilerHost: ts.CompilerHost,
16+
): ts.TransformerFactory<ts.SourceFile> {
17+
const compilerOptions = program.getCompilerOptions();
18+
const moduleResolutionCache = compilerHost.getModuleResolutionCache?.();
19+
assert(
20+
typeof compilerOptions.basePath === 'string',
21+
'compilerOptions.basePath should be a string.',
22+
);
23+
const basePath = compilerOptions.basePath;
24+
25+
return (context: ts.TransformationContext) => {
26+
const factory = context.factory;
27+
28+
const visitor = (node: ts.Node): ts.Node => {
29+
if (!ts.isObjectLiteralExpression(node)) {
30+
return ts.visitEachChild(node, visitor, context);
31+
}
32+
33+
let hasPathProperty = false;
34+
let loadComponentOrChildrenProperty: ts.PropertyAssignment | undefined;
35+
36+
for (const prop of node.properties) {
37+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
38+
continue;
39+
}
40+
41+
if (prop.name.text === 'path') {
42+
hasPathProperty = true;
43+
} else if (prop.name.text === 'loadChildren' || prop.name.text === 'loadComponent') {
44+
loadComponentOrChildrenProperty = prop;
45+
}
46+
47+
if (hasPathProperty && loadComponentOrChildrenProperty) {
48+
break;
49+
}
50+
}
51+
52+
const initializer = loadComponentOrChildrenProperty?.initializer;
53+
if (
54+
!hasPathProperty ||
55+
!initializer ||
56+
!ts.isArrowFunction(initializer) ||
57+
!ts.isCallExpression(initializer.body) ||
58+
!ts.isPropertyAccessExpression(initializer.body.expression) ||
59+
initializer.body.expression.name.text !== 'then' ||
60+
!ts.isCallExpression(initializer.body.expression.expression) ||
61+
initializer.body.expression.expression.expression.kind !== ts.SyntaxKind.ImportKeyword
62+
) {
63+
return ts.visitEachChild(node, visitor, context);
64+
}
65+
66+
const callExpressionArgument = initializer.body.expression.expression.arguments[0];
67+
if (
68+
!ts.isStringLiteral(callExpressionArgument) &&
69+
!ts.isNoSubstitutionTemplateLiteral(callExpressionArgument)
70+
) {
71+
return ts.visitEachChild(node, visitor, context);
72+
}
73+
74+
const resolvedPath = ts.resolveModuleName(
75+
callExpressionArgument.text,
76+
node.getSourceFile().fileName,
77+
compilerOptions,
78+
compilerHost,
79+
moduleResolutionCache,
80+
)?.resolvedModule?.resolvedFileName;
81+
82+
if (!resolvedPath) {
83+
return ts.visitEachChild(node, visitor, context);
84+
}
85+
86+
const resolvedRelativePath = relative(basePath, resolvedPath);
87+
88+
// Create the new property
89+
// Exmaple: ɵentryName: typeof ngServerMode !== 'undefined' && ngServerMode ? 'src/app/home.ts' : undefined
90+
const importPathProp = factory.createPropertyAssignment(
91+
factory.createIdentifier('ɵentryName'),
92+
factory.createConditionalExpression(
93+
factory.createBinaryExpression(
94+
factory.createBinaryExpression(
95+
factory.createTypeOfExpression(factory.createIdentifier('ngServerMode')),
96+
factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken),
97+
factory.createStringLiteral('undefined'),
98+
),
99+
factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
100+
factory.createIdentifier('ngServerMode'),
101+
),
102+
factory.createToken(ts.SyntaxKind.QuestionToken),
103+
factory.createStringLiteral(resolvedRelativePath),
104+
factory.createToken(ts.SyntaxKind.ColonToken),
105+
factory.createIdentifier('undefined'),
106+
),
107+
);
108+
109+
return factory.updateObjectLiteralExpression(node, [...node.properties, importPathProp]);
110+
};
111+
112+
return (sourceFile) => {
113+
const text = sourceFile.text;
114+
if (!text.includes('loadComponent') && !text.includes('loadChildren')) {
115+
// Fast check
116+
return sourceFile;
117+
}
118+
119+
return ts.visitEachChild(sourceFile, visitor, context);
120+
};
121+
};
122+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import { lazyRoutesTransformer } from './lazy-routes-transformer';
11+
12+
describe('lazyRoutesTransformer', () => {
13+
let program: ts.Program;
14+
let compilerHost: ts.CompilerHost;
15+
16+
beforeEach(() => {
17+
// Mock a basic TypeScript program and compilerHost
18+
program = ts.createProgram(['/project/src/dummy.ts'], { basePath: '/project/' });
19+
compilerHost = {
20+
getNewLine: () => '\n',
21+
fileExists: () => true,
22+
readFile: () => '',
23+
writeFile: () => undefined,
24+
getCanonicalFileName: (fileName: string) => fileName,
25+
getCurrentDirectory: () => '/project',
26+
getDefaultLibFileName: () => 'lib.d.ts',
27+
getSourceFile: () => undefined,
28+
useCaseSensitiveFileNames: () => true,
29+
resolveModuleNames: (moduleNames, containingFile) =>
30+
moduleNames.map(
31+
(name) =>
32+
({
33+
resolvedFileName: `/project/src/${name}.ts`,
34+
}) as ts.ResolvedModule,
35+
),
36+
};
37+
});
38+
39+
const transformSourceFile = (sourceCode: string): ts.SourceFile => {
40+
const sourceFile = ts.createSourceFile(
41+
'/project/src/dummy.ts',
42+
sourceCode,
43+
ts.ScriptTarget.ESNext,
44+
true,
45+
ts.ScriptKind.TS,
46+
);
47+
48+
const transformer = lazyRoutesTransformer(program, compilerHost);
49+
const result = ts.transform(sourceFile, [transformer]);
50+
51+
return result.transformed[0];
52+
};
53+
54+
it('should add ɵentryName property to object with loadComponent and path', () => {
55+
const source = `
56+
const routes = [
57+
{
58+
path: 'home',
59+
loadComponent: () => import('./home').then(m => m.HomeComponent)
60+
}
61+
];
62+
`;
63+
64+
const transformedSourceFile = transformSourceFile(source);
65+
const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
66+
67+
expect(transformedCode).toContain(
68+
`ɵentryName: typeof ngServerMode !== "undefined" && ngServerMode ? "src/home.ts" : undefined`,
69+
);
70+
});
71+
72+
it('should not modify unrelated object literals', () => {
73+
const source = `
74+
const routes = [
75+
{
76+
path: 'home',
77+
component: HomeComponent
78+
}
79+
];
80+
`;
81+
82+
const transformedSourceFile = transformSourceFile(source);
83+
const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
84+
85+
expect(transformedCode).not.toContain(`ɵentryName`);
86+
});
87+
88+
it('should ignore loadComponent without a valid import call', () => {
89+
const source = `
90+
const routes = [
91+
{
92+
path: 'home',
93+
loadComponent: () => someFunction()
94+
}
95+
];
96+
`;
97+
98+
const transformedSourceFile = transformSourceFile(source);
99+
const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
100+
101+
expect(transformedCode).not.toContain(`ɵentryName`);
102+
});
103+
104+
it('should resolve paths relative to basePath', () => {
105+
const source = `
106+
const routes = [
107+
{
108+
path: 'about',
109+
loadChildren: () => import('./features/about').then(m => m.AboutModule)
110+
}
111+
];
112+
`;
113+
114+
const transformedSourceFile = transformSourceFile(source);
115+
const transformedCode = ts.createPrinter().printFile(transformedSourceFile);
116+
117+
expect(transformedCode).toContain(
118+
`typeof ngServerMode !== "undefined" && ngServerMode ? "src/features/about.ts" : undefined`,
119+
);
120+
});
121+
});

packages/angular/build/src/tools/esbuild/application-code-bundle.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,10 @@ export function createServerMainCodeBundleOptions(
274274
...getEsBuildServerCommonOptions(options),
275275
target,
276276
banner: {
277-
js: `import './polyfills.server.mjs';`,
277+
js: [
278+
`import './polyfills.server.mjs';`,
279+
`export const \u0275\u0275__import_meta_url__ = import.meta.url;`,
280+
].join(''),
278281
},
279282
entryPoints,
280283
supported: getFeatureSupport(target, zoneless),

0 commit comments

Comments
 (0)