Skip to content

Commit 26fd4ea

Browse files
committedMar 18, 2025·
feat(@schematics/angular): add migrations for server rendering updates
- Migrate imports of `provideServerRendering` from `@angular/platform-server` to `@angular/ssr`. - Update `provideServerRendering` to use `withRoutes` and remove `provideServerRouting` from `@angular/ssr`.
1 parent 33b9de3 commit 26fd4ea

File tree

5 files changed

+398
-0
lines changed

5 files changed

+398
-0
lines changed
 

‎packages/schematics/angular/migrations/migration-collection.json

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
{
22
"schematics": {
3+
"replace-provide-server-rendering-import": {
4+
"version": "20.0.0",
5+
"factory": "./replace-provide-server-rendering-import/migration",
6+
"description": "Migrate imports of 'provideServerRendering' from '@angular/platform-server' to '@angular/ssr'."
7+
},
8+
"replace-provide-server-routing": {
9+
"version": "20.0.0",
10+
"factory": "./replace-provide-server-routing/migration",
11+
"description": "Migrate 'provideServerRendering' to use 'withRoutes' and remove 'provideServerRouting' from '@angular/ssr'."
12+
},
313
"use-application-builder": {
414
"version": "20.0.0",
515
"factory": "./use-application-builder/migration",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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 { DirEntry, Rule } from '@angular-devkit/schematics';
10+
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
11+
import { NodeDependencyType, addPackageJsonDependency } from '../../utility/dependencies';
12+
import { latestVersions } from '../../utility/latest-versions';
13+
14+
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> {
15+
for (const path of directory.subfiles) {
16+
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
17+
const entry = directory.file(path);
18+
if (entry) {
19+
const content = entry.content;
20+
if (
21+
content.includes('provideServerRendering') &&
22+
content.includes('@angular/platform-server')
23+
) {
24+
// Only need to rename the import so we can just string replacements.
25+
yield [entry.path, content.toString()];
26+
}
27+
}
28+
}
29+
}
30+
31+
for (const path of directory.subdirs) {
32+
if (path === 'node_modules' || path.startsWith('.')) {
33+
continue;
34+
}
35+
36+
yield* visit(directory.dir(path));
37+
}
38+
}
39+
40+
export default function (): Rule {
41+
return async (tree) => {
42+
addPackageJsonDependency(tree, {
43+
name: '@angular/ssr',
44+
version: latestVersions.AngularSSR,
45+
type: NodeDependencyType.Default,
46+
overwrite: false,
47+
});
48+
49+
for (const [filePath, content] of visit(tree.root)) {
50+
let updatedContent = content;
51+
const ssrImports = new Set<string>();
52+
const platformServerImports = new Set<string>();
53+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
54+
55+
sourceFile.forEachChild((node) => {
56+
if (ts.isImportDeclaration(node)) {
57+
const moduleSpecifier = node.moduleSpecifier.getText(sourceFile);
58+
if (moduleSpecifier.includes('@angular/platform-server')) {
59+
const importClause = node.importClause;
60+
if (
61+
importClause &&
62+
importClause.namedBindings &&
63+
ts.isNamedImports(importClause.namedBindings)
64+
) {
65+
const namedImports = importClause.namedBindings.elements.map((e) =>
66+
e.getText(sourceFile),
67+
);
68+
namedImports.forEach((importName) => {
69+
if (importName === 'provideServerRendering') {
70+
ssrImports.add(importName);
71+
} else {
72+
platformServerImports.add(importName);
73+
}
74+
});
75+
}
76+
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
77+
} else if (moduleSpecifier.includes('@angular/ssr')) {
78+
const importClause = node.importClause;
79+
if (
80+
importClause &&
81+
importClause.namedBindings &&
82+
ts.isNamedImports(importClause.namedBindings)
83+
) {
84+
importClause.namedBindings.elements.forEach((e) => {
85+
ssrImports.add(e.getText(sourceFile));
86+
});
87+
}
88+
updatedContent = updatedContent.replace(node.getFullText(sourceFile), '');
89+
}
90+
}
91+
});
92+
93+
if (platformServerImports.size > 0) {
94+
updatedContent =
95+
`import { ${Array.from(platformServerImports).sort().join(', ')} } from '@angular/platform-server';\n` +
96+
updatedContent;
97+
}
98+
99+
if (ssrImports.size > 0) {
100+
updatedContent =
101+
`import { ${Array.from(ssrImports).sort().join(', ')} } from '@angular/ssr';\n` +
102+
updatedContent;
103+
}
104+
105+
if (content !== updatedContent) {
106+
tree.overwrite(filePath, updatedContent);
107+
}
108+
}
109+
};
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
12+
describe(`Migration to use the 'provideServerRendering' from '@angular/ssr'`, () => {
13+
const schematicRunner = new SchematicTestRunner(
14+
'migrations',
15+
require.resolve('../migration-collection.json'),
16+
);
17+
18+
let tree: UnitTestTree;
19+
const schematicName = 'replace-provide-server-rendering-import';
20+
21+
beforeEach(() => {
22+
tree = new UnitTestTree(new EmptyTree());
23+
tree.create(
24+
'/package.json',
25+
JSON.stringify({
26+
dependencies: {
27+
'@angular/ssr': '0.0.0',
28+
},
29+
}),
30+
);
31+
});
32+
33+
it('should replace provideServerRendering with @angular/ssr and keep other imports', async () => {
34+
tree.create(
35+
'test.ts',
36+
`import { provideServerRendering, otherFunction } from '@angular/platform-server';`,
37+
);
38+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
39+
const content = newTree.readContent('test.ts');
40+
expect(content).toContain("import { provideServerRendering } from '@angular/ssr';");
41+
expect(content).toContain("import { otherFunction } from '@angular/platform-server';");
42+
});
43+
44+
it('should not replace provideServerRendering that is imported from @angular/ssr', async () => {
45+
tree.create(
46+
'test.ts',
47+
`
48+
import { otherFunction } from '@angular/platform-server';
49+
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
50+
`,
51+
);
52+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
53+
const content = newTree.readContent('test.ts');
54+
expect(content).toContain(
55+
"import { provideServerRendering, provideServerRouting } from '@angular/ssr';",
56+
);
57+
expect(content).toContain("import { otherFunction } from '@angular/platform-server';");
58+
});
59+
60+
it('should merge with existing @angular/ssr imports', async () => {
61+
tree.create(
62+
'test.ts',
63+
`
64+
import { provideServerRouting } from '@angular/ssr';
65+
import { provideServerRendering } from '@angular/platform-server';
66+
`,
67+
);
68+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
69+
const content = newTree.readContent('test.ts');
70+
expect(content).toContain(
71+
"import { provideServerRendering, provideServerRouting } from '@angular/ssr';",
72+
);
73+
expect(content.match(/@angular\/ssr/g) || []).toHaveSize(1);
74+
});
75+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 { DirEntry, Rule } from '@angular-devkit/schematics';
10+
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
11+
import { getPackageJsonDependency } from '../../utility/dependencies';
12+
13+
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> {
14+
for (const path of directory.subfiles) {
15+
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
16+
const entry = directory.file(path);
17+
if (entry) {
18+
const content = entry.content;
19+
if (content.includes('provideServerRouting') && content.includes('@angular/ssr')) {
20+
// Only need to rename the import so we can just string replacements.
21+
yield [entry.path, content.toString()];
22+
}
23+
}
24+
}
25+
}
26+
27+
for (const path of directory.subdirs) {
28+
if (path === 'node_modules' || path.startsWith('.')) {
29+
continue;
30+
}
31+
32+
yield* visit(directory.dir(path));
33+
}
34+
}
35+
36+
export default function (): Rule {
37+
return async (tree) => {
38+
if (!getPackageJsonDependency(tree, '@angular/ssr')) {
39+
return;
40+
}
41+
42+
for (const [filePath, content] of visit(tree.root)) {
43+
const recorder = tree.beginUpdate(filePath);
44+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
45+
46+
function visit(node: ts.Node) {
47+
if (
48+
ts.isPropertyAssignment(node) &&
49+
ts.isIdentifier(node.name) &&
50+
node.name.text === 'providers' &&
51+
ts.isArrayLiteralExpression(node.initializer)
52+
) {
53+
const providersArray = node.initializer;
54+
const newProviders = providersArray.elements
55+
.filter((el) => {
56+
return !(
57+
ts.isCallExpression(el) &&
58+
ts.isIdentifier(el.expression) &&
59+
el.expression.text === 'provideServerRendering'
60+
);
61+
})
62+
.map((el) => {
63+
if (
64+
ts.isCallExpression(el) &&
65+
ts.isIdentifier(el.expression) &&
66+
el.expression.text === 'provideServerRouting'
67+
) {
68+
const [withRouteVal, ...others] = el.arguments.map((arg) => arg.getText());
69+
70+
return `provideServerRendering(withRoutes(${withRouteVal})${others.length ? ', ' + others.join(', ') : ''})`;
71+
}
72+
73+
return el.getText();
74+
});
75+
76+
// Update the 'providers' array in the source file
77+
recorder.remove(providersArray.getStart(), providersArray.getWidth());
78+
recorder.insertRight(providersArray.getStart(), `[${newProviders.join(', ')}]`);
79+
}
80+
81+
ts.forEachChild(node, visit);
82+
}
83+
84+
// Visit all nodes to update 'providers'
85+
visit(sourceFile);
86+
87+
// Update imports by removing 'provideServerRouting'
88+
const importDecl = sourceFile.statements.find(
89+
(stmt) =>
90+
ts.isImportDeclaration(stmt) &&
91+
ts.isStringLiteral(stmt.moduleSpecifier) &&
92+
stmt.moduleSpecifier.text === '@angular/ssr',
93+
) as ts.ImportDeclaration | undefined;
94+
95+
if (importDecl?.importClause?.namedBindings) {
96+
const namedBindings = importDecl?.importClause.namedBindings;
97+
98+
if (ts.isNamedImports(namedBindings)) {
99+
const elements = namedBindings.elements;
100+
const updatedElements = elements
101+
.map((el) => el.getText())
102+
.filter((x) => x !== 'provideServerRouting');
103+
104+
updatedElements.push('withRoutes');
105+
106+
recorder.remove(namedBindings.getStart(), namedBindings.getWidth());
107+
recorder.insertLeft(namedBindings.getStart(), `{ ${updatedElements.sort().join(', ')} }`);
108+
}
109+
}
110+
111+
tree.commitUpdate(recorder);
112+
}
113+
};
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
12+
describe(`Migration to replace 'provideServerRouting' with 'provideServerRendering' from '@angular/ssr'`, () => {
13+
const schematicRunner = new SchematicTestRunner(
14+
'migrations',
15+
require.resolve('../migration-collection.json'),
16+
);
17+
18+
const schematicName = 'replace-provide-server-routing';
19+
let tree: UnitTestTree;
20+
21+
beforeEach(async () => {
22+
tree = new UnitTestTree(new EmptyTree());
23+
tree.create(
24+
'/package.json',
25+
JSON.stringify({
26+
dependencies: {
27+
'@angular/ssr': '0.0.0',
28+
},
29+
}),
30+
);
31+
32+
tree.create(
33+
'src/app/app.config.ts',
34+
`
35+
import { ApplicationConfig } from '@angular/core';
36+
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
37+
import { serverRoutes } from './app.routes';
38+
39+
const serverConfig: ApplicationConfig = {
40+
providers: [
41+
provideServerRendering(),
42+
provideServerRouting(serverRoutes)
43+
]
44+
};
45+
`,
46+
);
47+
});
48+
49+
it('should add "withRoutes" to the import statement', async () => {
50+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
51+
const content = newTree.readContent('src/app/app.config.ts');
52+
53+
expect(content).toContain(`import { provideServerRendering, withRoutes } from '@angular/ssr';`);
54+
});
55+
56+
it('should remove "provideServerRouting" and update "provideServerRendering"', async () => {
57+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
58+
const content = newTree.readContent('src/app/app.config.ts');
59+
60+
expect(content).toContain(`providers: [provideServerRendering(withRoutes(serverRoutes))]`);
61+
expect(content).not.toContain(`provideServerRouting(serverRoutes)`);
62+
});
63+
64+
it('should correctly handle provideServerRouting with extra arguments', async () => {
65+
tree.overwrite(
66+
'src/app/app.config.ts',
67+
`
68+
import { ApplicationConfig } from '@angular/core';
69+
import { provideServerRendering, provideServerRouting } from '@angular/ssr';
70+
import { serverRoutes } from './app.routes';
71+
72+
const serverConfig: ApplicationConfig = {
73+
providers: [
74+
provideServerRendering(),
75+
provideServerRouting(serverRoutes, withAppShell(AppShellComponent))
76+
]
77+
};
78+
`,
79+
);
80+
81+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
82+
const content = newTree.readContent('src/app/app.config.ts');
83+
84+
expect(content).toContain(
85+
`providers: [provideServerRendering(withRoutes(serverRoutes), withAppShell(AppShellComponent))]`,
86+
);
87+
expect(content).not.toContain(`provideServerRouting(serverRoutes)`);
88+
});
89+
});

0 commit comments

Comments
 (0)
Please sign in to comment.