Skip to content

Commit aa90e42

Browse files
FrozenPandazclydin
authored andcommitted
feat(@ngtools/webpack): replace bootstrap code for server apps
1 parent 9b85c07 commit aa90e42

20 files changed

+635
-1
lines changed

packages/@ngtools/webpack/src/angular_compiler_plugin.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { resolveEntryModuleFromMain } from './entry_resolver';
2020
import {
2121
replaceBootstrap,
22+
replaceServerBootstrap,
2223
exportNgFactory,
2324
exportLazyModuleMap,
2425
removeDecorators,
@@ -697,7 +698,9 @@ export class AngularCompilerPlugin implements Tapable {
697698
} else if (this._platform === PLATFORM.Server) {
698699
this._transformers.push(exportLazyModuleMap(isMainPath, getLazyRoutes));
699700
if (!this._JitMode) {
700-
this._transformers.push(exportNgFactory(isMainPath, getEntryModule));
701+
this._transformers.push(
702+
exportNgFactory(isMainPath, getEntryModule),
703+
replaceServerBootstrap(isMainPath, getEntryModule, getTypeChecker));
701704
}
702705
}
703706
}

packages/@ngtools/webpack/src/transformers/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './make_transform';
44
export * from './insert_import';
55
export * from './elide_imports';
66
export * from './replace_bootstrap';
7+
export * from './replace_server_bootstrap';
78
export * from './export_ngfactory';
89
export * from './export_lazy_module_map';
910
export * from './register_locale_data';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import { oneLine, stripIndent } from 'common-tags';
2+
import { createTypescriptContext, transformTypescript } from './ast_helpers';
3+
import { replaceServerBootstrap } from './replace_server_bootstrap';
4+
5+
describe('@ngtools/webpack transformers', () => {
6+
describe('replace_server_bootstrap', () => {
7+
it('should replace bootstrap', () => {
8+
const input = stripIndent`
9+
import { enableProdMode } from '@angular/core';
10+
import { platformDynamicServer } from '@angular/platform-server';
11+
12+
import { AppModule } from './app/app.module';
13+
import { environment } from './environments/environment';
14+
15+
if (environment.production) {
16+
enableProdMode();
17+
}
18+
19+
platformDynamicServer().bootstrapModule(AppModule);
20+
`;
21+
22+
// tslint:disable:max-line-length
23+
const output = stripIndent`
24+
import { enableProdMode } from '@angular/core';
25+
import { environment } from './environments/environment';
26+
27+
import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
28+
import * as __NgCli_bootstrap_2 from "@angular/platform-server";
29+
30+
if (environment.production) {
31+
enableProdMode();
32+
}
33+
__NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory);
34+
`;
35+
// tslint:enable:max-line-length
36+
37+
const { program, compilerHost } = createTypescriptContext(input);
38+
const transformer = replaceServerBootstrap(
39+
() => true,
40+
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
41+
() => program.getTypeChecker(),
42+
);
43+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
44+
45+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
46+
});
47+
48+
it('should replace renderModule', () => {
49+
const input = stripIndent`
50+
import { enableProdMode } from '@angular/core';
51+
import { renderModule } from '@angular/platform-server';
52+
53+
import { AppModule } from './app/app.module';
54+
import { environment } from './environments/environment';
55+
56+
if (environment.production) {
57+
enableProdMode();
58+
}
59+
60+
renderModule(AppModule, {
61+
document: '<app-root></app-root>',
62+
url: '/'
63+
});
64+
`;
65+
66+
// tslint:disable:max-line-length
67+
const output = stripIndent`
68+
import { enableProdMode } from '@angular/core';
69+
import { environment } from './environments/environment';
70+
71+
import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
72+
import * as __NgCli_bootstrap_2 from "@angular/platform-server";
73+
74+
if (environment.production) {
75+
enableProdMode();
76+
}
77+
__NgCli_bootstrap_2.renderModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory, {
78+
document: '<app-root></app-root>',
79+
url: '/'
80+
});
81+
`;
82+
// tslint:enable:max-line-length
83+
84+
const { program, compilerHost } = createTypescriptContext(input);
85+
const transformer = replaceServerBootstrap(
86+
() => true,
87+
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
88+
() => program.getTypeChecker(),
89+
);
90+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
91+
92+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
93+
});
94+
95+
it('should replace when the module is used in a config object', () => {
96+
const input = stripIndent`
97+
import * as express from 'express';
98+
99+
import { enableProdMode } from '@angular/core';
100+
import { ngExpressEngine } from '@nguniversal/express-engine';
101+
102+
import { AppModule } from './app/app.module';
103+
import { environment } from './environments/environment';
104+
105+
if (environment.production) {
106+
enableProdMode();
107+
}
108+
109+
const server = express();
110+
server.engine('html', ngExpressEngine({
111+
bootstrap: AppModule
112+
}));
113+
`;
114+
115+
// tslint:disable:max-line-length
116+
const output = stripIndent`
117+
import * as express from 'express';
118+
119+
import { enableProdMode } from '@angular/core';
120+
import { ngExpressEngine } from '@nguniversal/express-engine';
121+
122+
import { environment } from './environments/environment';
123+
124+
import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
125+
126+
if (environment.production) {
127+
enableProdMode();
128+
}
129+
130+
const server = express();
131+
server.engine('html', ngExpressEngine({
132+
bootstrap: __NgCli_bootstrap_1.AppModuleNgFactory
133+
}));
134+
`;
135+
// tslint:enable:max-line-length
136+
137+
const { program, compilerHost } = createTypescriptContext(input);
138+
const transformer = replaceServerBootstrap(
139+
() => true,
140+
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
141+
() => program.getTypeChecker(),
142+
);
143+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
144+
145+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
146+
});
147+
148+
it('should replace bootstrap when barrel files are used', () => {
149+
const input = stripIndent`
150+
import { enableProdMode } from '@angular/core';
151+
import { platformDynamicServer } from '@angular/platform-browser-dynamic';
152+
153+
import { AppModule } from './app';
154+
import { environment } from './environments/environment';
155+
156+
if (environment.production) {
157+
enableProdMode();
158+
}
159+
160+
platformDynamicServer().bootstrapModule(AppModule);
161+
`;
162+
163+
// tslint:disable:max-line-length
164+
const output = stripIndent`
165+
import { enableProdMode } from '@angular/core';
166+
import { environment } from './environments/environment';
167+
168+
import * as __NgCli_bootstrap_1 from "./app/app.module.ngfactory";
169+
import * as __NgCli_bootstrap_2 from "@angular/platform-server";
170+
171+
if (environment.production) {
172+
enableProdMode();
173+
}
174+
__NgCli_bootstrap_2.platformServer().bootstrapModuleFactory(__NgCli_bootstrap_1.AppModuleNgFactory);
175+
`;
176+
// tslint:enable:max-line-length
177+
178+
const { program, compilerHost } = createTypescriptContext(input);
179+
const transformer = replaceServerBootstrap(
180+
() => true,
181+
() => ({ path: '/project/src/app/app.module', className: 'AppModule' }),
182+
() => program.getTypeChecker(),
183+
);
184+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
185+
186+
expect(oneLine`${result}`).toEqual(oneLine`${output}`);
187+
});
188+
189+
it('should not replace bootstrap when there is no entry module', () => {
190+
const input = stripIndent`
191+
import { enableProdMode } from '@angular/core';
192+
import { platformDynamicServer } from '@angular/platform-browser-dynamic';
193+
194+
import { AppModule } from './app/app.module';
195+
import { environment } from './environments/environment';
196+
197+
if (environment.production) {
198+
enableProdMode();
199+
}
200+
201+
platformDynamicServer().bootstrapModule(AppModule);
202+
`;
203+
204+
const { program, compilerHost } = createTypescriptContext(input);
205+
const transformer = replaceServerBootstrap(
206+
() => true,
207+
() => undefined,
208+
() => program.getTypeChecker(),
209+
);
210+
const result = transformTypescript(undefined, [transformer], program, compilerHost);
211+
212+
expect(oneLine`${result}`).toEqual(oneLine`${input}`);
213+
});
214+
});
215+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// @ignoreDep typescript
2+
import * as ts from 'typescript';
3+
import { relative, dirname } from 'path';
4+
5+
import { collectDeepNodes } from './ast_helpers';
6+
import { insertStarImport } from './insert_import';
7+
import { StandardTransform, ReplaceNodeOperation, TransformOperation } from './interfaces';
8+
import { makeTransform } from './make_transform';
9+
10+
export function replaceServerBootstrap(
11+
shouldTransform: (fileName: string) => boolean,
12+
getEntryModule: () => { path: string, className: string },
13+
getTypeChecker: () => ts.TypeChecker,
14+
): ts.TransformerFactory<ts.SourceFile> {
15+
16+
const standardTransform: StandardTransform = function (sourceFile: ts.SourceFile) {
17+
const ops: TransformOperation[] = [];
18+
19+
const entryModule = getEntryModule();
20+
21+
if (!shouldTransform(sourceFile.fileName) || !entryModule) {
22+
return ops;
23+
}
24+
25+
// Find all identifiers.
26+
const entryModuleIdentifiers = collectDeepNodes<ts.Identifier>(sourceFile,
27+
ts.SyntaxKind.Identifier)
28+
.filter(identifier => identifier.text === entryModule.className);
29+
30+
if (entryModuleIdentifiers.length === 0) {
31+
return [];
32+
}
33+
34+
const relativeEntryModulePath = relative(dirname(sourceFile.fileName), entryModule.path);
35+
const normalizedEntryModulePath = `./${relativeEntryModulePath}`.replace(/\\/g, '/');
36+
const factoryClassName = entryModule.className + 'NgFactory';
37+
const factoryModulePath = normalizedEntryModulePath + '.ngfactory';
38+
39+
// Find the bootstrap calls.
40+
entryModuleIdentifiers.forEach(entryModuleIdentifier => {
41+
if (!entryModuleIdentifier.parent) {
42+
return;
43+
}
44+
45+
if (entryModuleIdentifier.parent.kind !== ts.SyntaxKind.CallExpression &&
46+
entryModuleIdentifier.parent.kind !== ts.SyntaxKind.PropertyAssignment) {
47+
return;
48+
}
49+
50+
if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.CallExpression) {
51+
// Figure out if it's a `platformDynamicServer().bootstrapModule(AppModule)` call.
52+
53+
const callExpr = entryModuleIdentifier.parent as ts.CallExpression;
54+
55+
if (callExpr.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
56+
57+
const propAccessExpr = callExpr.expression as ts.PropertyAccessExpression;
58+
59+
if (!(propAccessExpr.name.text === 'bootstrapModule'
60+
&& propAccessExpr.expression.kind === ts.SyntaxKind.CallExpression)) {
61+
return;
62+
}
63+
64+
const bootstrapModuleIdentifier = propAccessExpr.name;
65+
const innerCallExpr = propAccessExpr.expression as ts.CallExpression;
66+
67+
if (!(
68+
innerCallExpr.expression.kind === ts.SyntaxKind.Identifier
69+
&& (innerCallExpr.expression as ts.Identifier).text === 'platformDynamicServer'
70+
)) {
71+
return;
72+
}
73+
74+
const platformDynamicServerIdentifier = innerCallExpr.expression as ts.Identifier;
75+
76+
const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_');
77+
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');
78+
79+
// Add the transform operations.
80+
ops.push(
81+
// Replace the entry module import.
82+
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
83+
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
84+
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))),
85+
// Replace the platformBrowserDynamic import.
86+
...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'),
87+
new ReplaceNodeOperation(sourceFile, platformDynamicServerIdentifier,
88+
ts.createPropertyAccess(idPlatformServer, 'platformServer')),
89+
new ReplaceNodeOperation(sourceFile, bootstrapModuleIdentifier,
90+
ts.createIdentifier('bootstrapModuleFactory')),
91+
);
92+
} else if (callExpr.expression.kind === ts.SyntaxKind.Identifier) {
93+
// Figure out if it is renderModule
94+
95+
const identifierExpr = callExpr.expression as ts.Identifier;
96+
97+
if (identifierExpr.text !== 'renderModule') {
98+
return;
99+
}
100+
101+
const renderModuleIdentifier = identifierExpr as ts.Identifier;
102+
103+
const idPlatformServer = ts.createUniqueName('__NgCli_bootstrap_');
104+
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');
105+
106+
ops.push(
107+
// Replace the entry module import.
108+
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
109+
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
110+
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName))),
111+
// Replace the renderModule import.
112+
...insertStarImport(sourceFile, idPlatformServer, '@angular/platform-server'),
113+
new ReplaceNodeOperation(sourceFile, renderModuleIdentifier,
114+
ts.createPropertyAccess(idPlatformServer, 'renderModuleFactory')),
115+
);
116+
}
117+
} else if (entryModuleIdentifier.parent.kind === ts.SyntaxKind.PropertyAssignment) {
118+
// This is for things that accept a module as a property in a config object
119+
// .ie the express engine
120+
121+
const idNgFactory = ts.createUniqueName('__NgCli_bootstrap_');
122+
123+
ops.push(
124+
...insertStarImport(sourceFile, idNgFactory, factoryModulePath),
125+
new ReplaceNodeOperation(sourceFile, entryModuleIdentifier,
126+
ts.createPropertyAccess(idNgFactory, ts.createIdentifier(factoryClassName)))
127+
);
128+
}
129+
});
130+
131+
return ops;
132+
};
133+
134+
return makeTransform(standardTransform, getTypeChecker);
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div>
2+
<h1>hello world</h1>
3+
<a [routerLink]="['lazy']">lazy</a>
4+
<router-outlet></router-outlet>
5+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:host {
2+
background-color: blue;
3+
}

0 commit comments

Comments
 (0)