Skip to content

Commit 3938863

Browse files
committed
feat(@schematics/angular): add migration to migrate from @nguniversal to @angular/ssr
This commit adds a migration to migrate usages of `@nguniversal` to `@angular/ssr`.
1 parent 1635fdc commit 3938863

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"factory": "./update-17/replace-nguniversal-builders",
66
"description": "Replace usages of '@nguniversal/builders' with '@angular-devkit/build-angular'."
77
},
8+
"replace-nguniversal-engines": {
9+
"version": "17.0.0",
10+
"factory": "./update-17/replace-nguniversal-engines",
11+
"description": "Replace usages of '@nguniversal/' packages with '@angular/ssr'."
12+
},
813
"update-workspace-config": {
914
"version": "17.0.0",
1015
"factory": "./update-17/update-workspace-config",
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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.io/license
7+
*/
8+
9+
import { DirEntry, Rule, chain } from '@angular-devkit/schematics';
10+
import { addDependency } from '../../utility';
11+
import { removePackageJsonDependency } from '../../utility/dependencies';
12+
import { latestVersions } from '../../utility/latest-versions';
13+
import { allTargetOptions, getWorkspace } from '../../utility/workspace';
14+
import { Builders, ProjectType } from '../../utility/workspace-models';
15+
16+
function* visit(directory: DirEntry): IterableIterator<[fileName: string, contents: string]> {
17+
for (const path of directory.subfiles) {
18+
if (path.endsWith('.ts') && !path.endsWith('.d.ts')) {
19+
const entry = directory.file(path);
20+
if (entry) {
21+
const content = entry.content;
22+
if (content.includes('@nguniversal/')) {
23+
// Only need to rename the import so we can just string replacements.
24+
yield [entry.path, content.toString()];
25+
}
26+
}
27+
}
28+
}
29+
30+
for (const path of directory.subdirs) {
31+
if (path === 'node_modules' || path.startsWith('.')) {
32+
continue;
33+
}
34+
35+
yield* visit(directory.dir(path));
36+
}
37+
}
38+
39+
/**
40+
* Regexp to match Universal packages.
41+
* @nguniversal/common/engine
42+
* @nguniversal/common
43+
* @nguniversal/express-engine
44+
**/
45+
const NGUNIVERSAL_PACKAGE_REGEXP = /@nguniversal\/(common(\/engine)?|express-engine)/g;
46+
47+
export default function (): Rule {
48+
return chain([
49+
async (tree) => {
50+
// Replace server file.
51+
const workspace = await getWorkspace(tree);
52+
for (const [, project] of workspace.projects) {
53+
if (project.extensions.projectType !== ProjectType.Application) {
54+
continue;
55+
}
56+
57+
const serverMainFiles = new Map<string /** Main Path */, string /** Output Path */>();
58+
for (const [, target] of project.targets) {
59+
if (target.builder !== Builders.Server) {
60+
continue;
61+
}
62+
63+
const outputPath = project.targets.get('build')?.options?.outputPath;
64+
65+
for (const [, { main }] of allTargetOptions(target, false)) {
66+
if (
67+
typeof main === 'string' &&
68+
typeof outputPath === 'string' &&
69+
tree.readText(main).includes('ngExpressEngine')
70+
) {
71+
serverMainFiles.set(main, outputPath);
72+
}
73+
}
74+
}
75+
76+
// Replace server file
77+
for (const [path, outputPath] of serverMainFiles.entries()) {
78+
tree.rename(path, path + '.bak');
79+
tree.create(path, getServerFileContents(outputPath));
80+
}
81+
}
82+
83+
// Replace all import specifiers in all files.
84+
for (const file of visit(tree.root)) {
85+
const [path, content] = file;
86+
tree.overwrite(path, content.replaceAll(NGUNIVERSAL_PACKAGE_REGEXP, '@angular/ssr'));
87+
}
88+
89+
// Remove universal packages from deps.
90+
removePackageJsonDependency(tree, '@nguniversal/express-engine');
91+
removePackageJsonDependency(tree, '@nguniversal/common');
92+
},
93+
addDependency('@angular/ssr', latestVersions.Angular),
94+
]);
95+
}
96+
97+
function getServerFileContents(outputPath: string): string {
98+
return `
99+
import 'zone.js/node';
100+
101+
import { APP_BASE_HREF } from '@angular/common';
102+
import { CommonEngine } from '@angular/ssr';
103+
import * as express from 'express';
104+
import { existsSync } from 'node:fs';
105+
import { join } from 'node:path';
106+
import bootstrap from './src/main.server';
107+
108+
// The Express app is exported so that it can be used by serverless Functions.
109+
export function app(): express.Express {
110+
const server = express();
111+
const distFolder = join(process.cwd(), '${outputPath}');
112+
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
113+
? join(distFolder, 'index.original.html')
114+
: join(distFolder, 'index.html');
115+
116+
const commonEngine = new CommonEngine();
117+
118+
server.set('view engine', 'html');
119+
server.set('views', distFolder);
120+
121+
// Example Express Rest API endpoints
122+
// server.get('/api/**', (req, res) => { });
123+
// Serve static files from /browser
124+
server.get('*.*', express.static(distFolder, {
125+
maxAge: '1y'
126+
}));
127+
128+
// All regular routes use the Angular engine
129+
server.get('*', (req, res, next) => {
130+
commonEngine
131+
.render({
132+
bootstrap,
133+
documentFilePath: indexHtml,
134+
url: req.originalUrl,
135+
publicPath: distFolder,
136+
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],
137+
})
138+
.then((html) => res.send(html))
139+
.catch((err) => next(err));
140+
});
141+
142+
return server;
143+
}
144+
145+
function run(): void {
146+
const port = process.env['PORT'] || 4000;
147+
148+
// Start up the Node server
149+
const server = app();
150+
server.listen(port, () => {
151+
console.log(\`Node Express server listening on http://localhost:\${port}\`);
152+
});
153+
}
154+
155+
// Webpack will replace 'require' with '__webpack_require__'
156+
// '__non_webpack_require__' is a proxy to Node 'require'
157+
// The below code is to ensure that the server is run only when not requiring the bundle.
158+
declare const __non_webpack_require__: NodeRequire;
159+
const mainModule = __non_webpack_require__.main;
160+
const moduleFilename = mainModule && mainModule.filename || '';
161+
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
162+
run();
163+
}
164+
165+
export default bootstrap;
166+
`;
167+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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.io/license
7+
*/
8+
9+
import { EmptyTree } from '@angular-devkit/schematics';
10+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
11+
import { Builders, ProjectType, WorkspaceSchema } from '../../utility/workspace-models';
12+
13+
function createWorkSpaceConfig(tree: UnitTestTree) {
14+
const angularConfig: WorkspaceSchema = {
15+
version: 1,
16+
projects: {
17+
app: {
18+
root: '',
19+
sourceRoot: '/src',
20+
projectType: ProjectType.Application,
21+
prefix: 'app',
22+
architect: {
23+
build: {
24+
builder: Builders.Browser,
25+
options: {
26+
tsConfig: 'tsconfig.json',
27+
main: 'main.ts',
28+
polyfills: '',
29+
outputPath: 'dist/browser',
30+
},
31+
},
32+
server: {
33+
builder: Builders.Server,
34+
options: {
35+
tsConfig: 'tsconfig.json',
36+
main: 'server.ts',
37+
outputPath: 'dist/server',
38+
},
39+
configurations: {
40+
production: {
41+
main: 'server.ts',
42+
},
43+
},
44+
},
45+
},
46+
},
47+
},
48+
};
49+
50+
tree.create('/angular.json', JSON.stringify(angularConfig, undefined, 2));
51+
}
52+
53+
describe(`Migration to replace usages of '@nguniversal/' packages with '@angular/ssr'.`, () => {
54+
const schematicName = 'replace-nguniversal-engines';
55+
const schematicRunner = new SchematicTestRunner(
56+
'migrations',
57+
require.resolve('../migration-collection.json'),
58+
);
59+
60+
let tree: UnitTestTree;
61+
beforeEach(() => {
62+
tree = new UnitTestTree(new EmptyTree());
63+
64+
createWorkSpaceConfig(tree);
65+
tree.create(
66+
'/package.json',
67+
JSON.stringify(
68+
{
69+
dependencies: {
70+
'@nguniversal/common': '0.0.0',
71+
'@nguniversal/express-engine': '0.0.0',
72+
},
73+
},
74+
undefined,
75+
2,
76+
),
77+
);
78+
79+
tree.create(
80+
'server.ts',
81+
`
82+
import 'zone.js/node';
83+
84+
import { APP_BASE_HREF } from '@angular/common';
85+
import { ngExpressEngine } from '@nguniversal/express-engine';
86+
import * as express from 'express';
87+
import { existsSync } from 'fs';
88+
import { join } from 'path';
89+
90+
import { AppServerModule } from './src/main.server';
91+
92+
// The Express app is exported so that it can be used by serverless Functions.
93+
export function app(): express.Express {
94+
const server = express();
95+
const distFolder = join(process.cwd(), 'dist/browser');
96+
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
97+
? 'index.original.html'
98+
: 'index';
99+
100+
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
101+
server.engine(
102+
'html',
103+
ngExpressEngine({
104+
bootstrap: AppServerModule,
105+
inlineCriticalCss: true,
106+
}),
107+
);
108+
109+
server.set('view engine', 'html');
110+
server.set('views', distFolder);
111+
112+
// Example Express Rest API endpoints
113+
// server.get('/api/**', (req, res) => { });
114+
// Serve static files from /browser
115+
server.get(
116+
'*.*',
117+
express.static(distFolder, {
118+
maxAge: '1y',
119+
}),
120+
);
121+
122+
// All regular routes use the Universal engine
123+
server.get('*', (req, res) => {
124+
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
125+
});
126+
127+
return server;
128+
}
129+
130+
function run() {
131+
const port = process.env.PORT || 4000;
132+
133+
// Start up the Node server
134+
const server = app();
135+
server.listen(port);
136+
}
137+
138+
// Webpack will replace 'require' with '__webpack_require__'
139+
// '__non_webpack_require__' is a proxy to Node 'require'
140+
// The below code is to ensure that the server is run only when not requiring the bundle.
141+
declare const __non_webpack_require__: NodeRequire;
142+
const mainModule = __non_webpack_require__.main;
143+
const moduleFilename = (mainModule && mainModule.filename) || '';
144+
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
145+
run();
146+
} `,
147+
);
148+
});
149+
150+
it(`should remove all '@nguniversal/' from dependencies`, async () => {
151+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
152+
const { dependencies } = JSON.parse(newTree.readContent('/package.json'));
153+
expect(dependencies['@nguniversal/common']).toBeUndefined();
154+
expect(dependencies['@nguniversal/express-engine']).toBeUndefined();
155+
});
156+
157+
it(`should add '@angular/ssr' as a dependencies`, async () => {
158+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
159+
const { dependencies } = JSON.parse(newTree.readContent('/package.json'));
160+
expect(dependencies['@angular/ssr']).toBeDefined();
161+
});
162+
163+
it(`should replace imports from '@nguniversal/common' to '@angular/ssr'`, async () => {
164+
tree.create(
165+
'file.ts',
166+
`
167+
import { CommonEngine } from '@nguniversal/common';
168+
import { Component } from '@angular/core';
169+
`,
170+
);
171+
172+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
173+
expect(newTree.readContent('/file.ts')).toContain(
174+
`import { CommonEngine } from '@angular/ssr';`,
175+
);
176+
});
177+
178+
it(`should replace anf backup 'server.ts' file`, async () => {
179+
const newTree = await schematicRunner.runSchematic(schematicName, {}, tree);
180+
expect(newTree.readContent('server.ts.bak')).toContain(
181+
`import { ngExpressEngine } from '@nguniversal/express-engine';`,
182+
);
183+
184+
const newServerFile = newTree.readContent('server.ts');
185+
expect(newServerFile).toContain(`import { CommonEngine } from '@angular/ssr';`);
186+
expect(newServerFile).toContain(`const distFolder = join(process.cwd(), 'dist/browser');`);
187+
});
188+
});

0 commit comments

Comments
 (0)