Skip to content

Commit 600e5d0

Browse files
alan-agius4Keen Yee Liau
authored andcommitted
feat(@schematics/angular): add migration to add missing exports in main server file
Update the `main.server.ts` file by adding exports to `renderModule` and `renderModuleFactory` which are now required for Universal and App-Shell for Ivy and `bundleDependencies`.
1 parent 6292c73 commit 600e5d0

File tree

3 files changed

+253
-0
lines changed

3 files changed

+253
-0
lines changed

packages/schematics/angular/migrations/update-9/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { updateLibraries } from './ivy-libraries';
1212
import { updateNGSWConfig } from './ngsw-config';
1313
import { updateApplicationTsConfigs } from './update-app-tsconfigs';
1414
import { updateDependencies } from './update-dependencies';
15+
import { updateServerMainFile } from './update-server-main-file';
1516
import { updateWorkspaceConfig } from './update-workspace-config';
1617

1718
export default function(): Rule {
@@ -22,6 +23,7 @@ export default function(): Rule {
2223
updateNGSWConfig(),
2324
updateApplicationTsConfigs(),
2425
updateDependencies(),
26+
updateServerMainFile(),
2527
(tree, context) => {
2628
const packageChanges = tree.actions.some(a => a.path.endsWith('/package.json'));
2729
if (packageChanges) {
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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+
import { Rule } from '@angular-devkit/schematics';
9+
import * as ts from '../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
10+
import { findNodes } from '../../utility/ast-utils';
11+
import { findPropertyInAstObject } from '../../utility/json-utils';
12+
import { Builders } from '../../utility/workspace-models';
13+
import { getTargets, getWorkspace } from './utils';
14+
15+
/**
16+
* Update the `main.server.ts` file by adding exports to `renderModule` and `renderModuleFactory` which are
17+
* now required for Universal and App-Shell for Ivy and `bundleDependencies`.
18+
*/
19+
export function updateServerMainFile(): Rule {
20+
return tree => {
21+
const workspace = getWorkspace(tree);
22+
23+
for (const { target } of getTargets(workspace, 'server', Builders.Server)) {
24+
const options = findPropertyInAstObject(target, 'options');
25+
if (!options || options.kind !== 'object') {
26+
continue;
27+
}
28+
29+
// find the main server file
30+
const mainFile = findPropertyInAstObject(options, 'main');
31+
if (!mainFile || typeof mainFile.value !== 'string') {
32+
continue;
33+
}
34+
35+
const mainFilePath = mainFile.value;
36+
37+
const content = tree.read(mainFilePath);
38+
if (!content) {
39+
continue;
40+
}
41+
42+
const source = ts.createSourceFile(
43+
mainFilePath,
44+
content.toString().replace(/^\uFEFF/, ''),
45+
ts.ScriptTarget.Latest,
46+
true,
47+
);
48+
49+
// find exports in main server file
50+
const exportDeclarations = findNodes(source, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration[];
51+
52+
const platformServerExports = exportDeclarations.filter(({ moduleSpecifier }) => (
53+
moduleSpecifier && ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@angular/platform-server'
54+
));
55+
56+
let hasRenderModule = false;
57+
let hasRenderModuleFactory = false;
58+
59+
// find exports of renderModule or renderModuleFactory
60+
for (const { exportClause } of platformServerExports) {
61+
if (exportClause && ts.isNamedExports(exportClause)) {
62+
if (!hasRenderModuleFactory) {
63+
hasRenderModuleFactory = exportClause.elements.some(({ name }) => name.text === 'renderModuleFactory');
64+
}
65+
66+
if (!hasRenderModule) {
67+
hasRenderModule = exportClause.elements.some(({ name }) => name.text === 'renderModule');
68+
}
69+
}
70+
}
71+
72+
if (hasRenderModule && hasRenderModuleFactory) {
73+
// We have both required exports
74+
continue;
75+
}
76+
77+
let exportSpecifiers: ts.ExportSpecifier[] = [];
78+
let updateExisting = false;
79+
80+
// Add missing exports
81+
if (platformServerExports.length) {
82+
const { exportClause } = platformServerExports[0] as ts.ExportDeclaration;
83+
if (!exportClause) {
84+
continue;
85+
}
86+
87+
exportSpecifiers = [...exportClause.elements];
88+
updateExisting = true;
89+
}
90+
91+
if (!hasRenderModule) {
92+
exportSpecifiers.push(ts.createExportSpecifier(
93+
undefined,
94+
ts.createIdentifier('renderModule'),
95+
));
96+
}
97+
98+
if (!hasRenderModuleFactory) {
99+
exportSpecifiers.push(ts.createExportSpecifier(
100+
undefined,
101+
ts.createIdentifier('renderModuleFactory'),
102+
));
103+
}
104+
105+
// Create a TS printer to get the text of the export node
106+
const printer = ts.createPrinter();
107+
108+
const moduleSpecifier = ts.createStringLiteral('@angular/platform-server');
109+
110+
// TypeScript will emit the Node with double quotes.
111+
// In schematics we usually write code with a single quotes
112+
// tslint:disable-next-line: no-any
113+
(moduleSpecifier as any).singleQuote = true;
114+
115+
const newExportDeclarationText = printer.printNode(
116+
ts.EmitHint.Unspecified,
117+
ts.createExportDeclaration(
118+
undefined,
119+
undefined,
120+
ts.createNamedExports(exportSpecifiers),
121+
moduleSpecifier,
122+
),
123+
source,
124+
);
125+
126+
const recorder = tree.beginUpdate(mainFilePath);
127+
if (updateExisting) {
128+
const start = platformServerExports[0].getStart();
129+
const width = platformServerExports[0].getWidth();
130+
131+
recorder
132+
.remove(start, width)
133+
.insertLeft(start, newExportDeclarationText);
134+
} else {
135+
recorder.insertLeft(source.getWidth(), '\n' + newExportDeclarationText);
136+
}
137+
138+
tree.commitUpdate(recorder);
139+
}
140+
141+
return tree;
142+
};
143+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. 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 { tags } from '@angular-devkit/core';
10+
import { EmptyTree } from '@angular-devkit/schematics';
11+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
12+
13+
const mainServerContent = tags.stripIndents`
14+
import { enableProdMode } from '@angular/core';
15+
16+
import { environment } from './environments/environment';
17+
18+
if (environment.production) {
19+
enableProdMode();
20+
}
21+
22+
export { AppServerModule } from './app/app.server.module';
23+
`;
24+
25+
const mainServerFile = 'src/main.server.ts';
26+
27+
describe('Migration to version 9', () => {
28+
describe('Migrate Server Main File', () => {
29+
const schematicRunner = new SchematicTestRunner(
30+
'migrations',
31+
require.resolve('../migration-collection.json'),
32+
);
33+
34+
let tree: UnitTestTree;
35+
36+
beforeEach(async () => {
37+
tree = new UnitTestTree(new EmptyTree());
38+
tree = await schematicRunner
39+
.runExternalSchematicAsync(
40+
require.resolve('../../collection.json'),
41+
'ng-new',
42+
{
43+
name: 'migration-test',
44+
version: '1.2.3',
45+
directory: '.',
46+
},
47+
tree,
48+
)
49+
.toPromise();
50+
tree = await schematicRunner
51+
.runExternalSchematicAsync(
52+
require.resolve('../../collection.json'),
53+
'universal',
54+
{
55+
clientProject: 'migration-test',
56+
},
57+
tree,
58+
)
59+
.toPromise();
60+
});
61+
62+
it(`should add exports from '@angular/platform-server'`, async () => {
63+
tree.overwrite(mainServerFile, mainServerContent);
64+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
65+
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
66+
export { AppServerModule } from './app/app.server.module';
67+
export { renderModule, renderModuleFactory } from '@angular/platform-server';
68+
`);
69+
});
70+
71+
it(`should add 'renderModule' and 'renderModuleFactory' to existing '@angular/platform-server' export`, async () => {
72+
tree.overwrite(mainServerFile, tags.stripIndents`
73+
${mainServerContent}
74+
export { platformDynamicServer } from '@angular/platform-server';
75+
export { PlatformConfig } from '@angular/platform-server';
76+
`);
77+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
78+
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
79+
export { AppServerModule } from './app/app.server.module';
80+
export { platformDynamicServer, renderModule, renderModuleFactory } from '@angular/platform-server';
81+
export { PlatformConfig } from '@angular/platform-server';
82+
`);
83+
});
84+
85+
it(`should add 'renderModule' to existing '@angular/platform-server' export`, async () => {
86+
tree.overwrite(mainServerFile, tags.stripIndents`
87+
${mainServerContent}
88+
export { platformDynamicServer, renderModuleFactory } from '@angular/platform-server';
89+
`);
90+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
91+
expect(tree2.readContent(mainServerFile)).toContain(tags.stripIndents`
92+
export { AppServerModule } from './app/app.server.module';
93+
export { platformDynamicServer, renderModuleFactory, renderModule } from '@angular/platform-server';
94+
`);
95+
});
96+
97+
it(`should not update exports when 'renderModule' and 'renderModuleFactory' are already exported`, async () => {
98+
const input = tags.stripIndents`
99+
${mainServerContent}
100+
export { renderModule, renderModuleFactory } from '@angular/platform-server';
101+
`;
102+
103+
tree.overwrite(mainServerFile, input);
104+
const tree2 = await schematicRunner.runSchematicAsync('migration-09', {}, tree.branch()).toPromise();
105+
expect(tree2.readContent(mainServerFile)).toBe(input);
106+
});
107+
});
108+
});

0 commit comments

Comments
 (0)