Skip to content

Commit 6f23636

Browse files
FrozenPandazhansl
authored andcommitted
feat(@angular/cli): add ability to build bundle for node and export lazy route map
1 parent 7d8f54a commit 6f23636

File tree

20 files changed

+317
-10
lines changed

20 files changed

+317
-10
lines changed

packages/@angular/cli/lib/config/schema.json

+6
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@
8383
"type": "string",
8484
"description": "Base url for the application being built."
8585
},
86+
"platform": {
87+
"type": "string",
88+
"enum": ["browser", "server"],
89+
"default": "browser",
90+
"description": "The runtime platform of the app."
91+
},
8692
"index": {
8793
"type": "string",
8894
"default": "index.html",

packages/@angular/cli/models/webpack-config.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
getDevConfig,
88
getProdConfig,
99
getStylesConfig,
10+
getServerConfig,
1011
getNonAotConfig,
1112
getAotConfig
1213
} from './webpack-configs';
@@ -37,9 +38,12 @@ export class NgCliWebpackConfig {
3738
}
3839

3940
public buildConfig() {
41+
const platformConfig = this.wco.appConfig.platform === 'server' ?
42+
getServerConfig(this.wco) : getBrowserConfig(this.wco);
43+
4044
let webpackConfigs = [
4145
getCommonConfig(this.wco),
42-
getBrowserConfig(this.wco),
46+
platformConfig,
4347
getStylesConfig(this.wco),
4448
this.getTargetConfig(this.wco)
4549
];

packages/@angular/cli/models/webpack-configs/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './browser';
22
export * from './common';
33
export * from './development';
44
export * from './production';
5+
export * from './server';
56
export * from './styles';
67
export * from './test';
78
export * from './typescript';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { WebpackConfigOptions } from '../webpack-config';
2+
3+
/**
4+
* Returns a partial specific to creating a bundle for node
5+
* @param _wco Options which are include the build options and app config
6+
*/
7+
export const getServerConfig = function (_wco: WebpackConfigOptions) {
8+
return {
9+
target: 'node',
10+
output: {
11+
libraryTarget: 'commonjs'
12+
},
13+
externals: [
14+
/^@angular/,
15+
function (_: any, request: any, callback: (error?: any, result?: any) => void) {
16+
// Absolute & Relative paths are not externals
17+
if (request.match(/^\.{0,2}\//)) {
18+
return callback();
19+
}
20+
21+
try {
22+
// Attempt to resolve the module via Node
23+
const e = require.resolve(request);
24+
if (/node_modules/.test(e)) {
25+
// It's a node_module
26+
callback(null, request);
27+
} else {
28+
// It's a system thing (.ie util, fs...)
29+
callback();
30+
}
31+
} catch (e) {
32+
// Node couldn't find it, so it must be user-aliased
33+
callback();
34+
}
35+
}
36+
]
37+
};
38+
};

packages/@angular/cli/models/webpack-configs/typescript.ts

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
6868
i18nFile: buildOptions.i18nFile,
6969
i18nFormat: buildOptions.i18nFormat,
7070
locale: buildOptions.locale,
71+
replaceExport: appConfig.platform === 'server',
7172
hostReplacementPaths,
7273
// If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`.
7374
exclude: []

packages/@angular/cli/tasks/e2e.ts

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { stripIndents } from 'common-tags';
44
import { E2eTaskOptions } from '../commands/e2e';
55
import { CliConfig } from '../models/config';
66
import { requireProjectModule } from '../utilities/require-project-module';
7+
import { getAppFromConfig } from '../utilities/app-utils';
78

89
const Task = require('../ember-cli/lib/models/task');
910
const SilentError = require('silent-error');
@@ -14,10 +15,14 @@ export const E2eTask = Task.extend({
1415
const projectConfig = CliConfig.fromProject().config;
1516
const projectRoot = this.project.root;
1617
const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher');
18+
const appConfig = getAppFromConfig(e2eTaskOptions.app);
1719

1820
if (projectConfig.project && projectConfig.project.ejected) {
1921
throw new SilentError('An ejected project cannot use the build command anymore.');
2022
}
23+
if (appConfig.platform === 'server') {
24+
throw new SilentError('ng test for platform server applications is coming soon!');
25+
}
2126

2227
return new Promise(function () {
2328
let promise = Promise.resolve();

packages/@angular/cli/tasks/eject.ts

+3
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,9 @@ export default Task.extend({
436436
if (project.root === path.resolve(outputPath)) {
437437
throw new SilentError ('Output path MUST not be project root directory!');
438438
}
439+
if (appConfig.platform === 'server') {
440+
throw new SilentError('ng eject for platform server applications is coming soon!');
441+
}
439442

440443
const webpackConfig = new NgCliWebpackConfig(runTaskOptions, appConfig).buildConfig();
441444
const serializer = new JsonWebpackSerializer(process.cwd(), outputPath, appConfig.root);

packages/@angular/cli/tasks/serve.ts

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ export default Task.extend({
3131
if (projectConfig.project && projectConfig.project.ejected) {
3232
throw new SilentError('An ejected project cannot use the build command anymore.');
3333
}
34+
if (appConfig.platform === 'server') {
35+
throw new SilentError('ng serve for platform server applications is coming soon!');
36+
}
3437
if (serveTaskOptions.deleteOutputPath) {
3538
fs.removeSync(path.resolve(this.project.root, outputPath));
3639
}

packages/@angular/cli/tasks/test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as path from 'path';
33
import { TestOptions } from '../commands/test';
44
import { CliConfig } from '../models/config';
55
import { requireProjectModule } from '../utilities/require-project-module';
6+
import { getAppFromConfig } from '../utilities/app-utils';
67

78
const Task = require('../ember-cli/lib/models/task');
89
const SilentError = require('silent-error');
@@ -12,10 +13,14 @@ export default Task.extend({
1213
run: function (options: TestOptions) {
1314
const projectConfig = CliConfig.fromProject().config;
1415
const projectRoot = this.project.root;
16+
const appConfig = getAppFromConfig(options.app);
1517

1618
if (projectConfig.project && projectConfig.project.ejected) {
1719
throw new SilentError('An ejected project cannot use the build command anymore.');
1820
}
21+
if (appConfig.platform === 'server') {
22+
throw new SilentError('ng test for platform server applications is coming soon!');
23+
}
1924

2025
return new Promise((resolve) => {
2126
const karma = requireProjectModule(projectRoot, 'karma');

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

+82-2
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,83 @@ function _diagnoseDeps(reasons: ModuleReason[], plugin: AotPlugin, checked: Set<
435435
}
436436

437437

438+
export function _getModuleExports(plugin: AotPlugin,
439+
refactor: TypeScriptFileRefactor): ts.Identifier[] {
440+
const exports = refactor
441+
.findAstNodes(refactor.sourceFile, ts.SyntaxKind.ExportDeclaration, true);
442+
443+
return exports
444+
.filter(node => {
445+
446+
const identifiers = refactor.findAstNodes(node, ts.SyntaxKind.Identifier, false);
447+
448+
identifiers
449+
.filter(node => node.getText() === plugin.entryModule.className);
450+
451+
return identifiers.length > 0;
452+
}) as ts.Identifier[];
453+
}
454+
455+
456+
export function _replaceExport(plugin: AotPlugin, refactor: TypeScriptFileRefactor) {
457+
if (!plugin.replaceExport) {
458+
return;
459+
}
460+
_getModuleExports(plugin, refactor)
461+
.forEach(node => {
462+
const factoryPath = _getNgFactoryPath(plugin, refactor);
463+
const factoryClassName = plugin.entryModule.className + 'NgFactory';
464+
const exportStatement = `export \{ ${factoryClassName} \} from '${factoryPath}'`;
465+
refactor.appendAfter(node, exportStatement);
466+
});
467+
}
468+
469+
470+
export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) {
471+
if (!plugin.replaceExport) {
472+
return;
473+
}
474+
475+
const dirName = path.normalize(path.dirname(refactor.fileName));
476+
const classNameAppend = plugin.skipCodeGeneration ? '' : 'NgFactory';
477+
const modulePathAppend = plugin.skipCodeGeneration ? '' : '.ngfactory';
478+
479+
_getModuleExports(plugin, refactor)
480+
.forEach(node => {
481+
const modules = Object.keys(plugin.discoveredLazyRoutes)
482+
.map((loadChildrenString) => {
483+
let [lazyRouteKey, moduleName] = loadChildrenString.split('#');
484+
485+
if (!lazyRouteKey || !moduleName) {
486+
throw new Error(`${loadChildrenString} was not a proper loadChildren string`);
487+
}
488+
489+
moduleName += classNameAppend;
490+
lazyRouteKey += modulePathAppend;
491+
const modulePath = plugin.lazyRoutes[lazyRouteKey];
492+
493+
return {
494+
modulePath,
495+
moduleName,
496+
loadChildrenString
497+
};
498+
});
499+
500+
modules.forEach((module, index) => {
501+
const relativePath = path.relative(dirName, module.modulePath).replace(/\\/g, '/');
502+
refactor.prependBefore(node, `import * as __lazy_${index}__ from './${relativePath}'`);
503+
});
504+
505+
const jsonContent: string = modules
506+
.map((module, index) =>
507+
`"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`)
508+
.join();
509+
510+
refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`);
511+
});
512+
}
513+
514+
438515
// Super simple TS transpiler loader for testing / isolated usage. does not type check!
439516
export function ngcLoader(this: LoaderContext & { _compilation: any }, source: string | null) {
440517
const cb = this.async();
@@ -464,11 +541,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
464541
if (!plugin.skipCodeGeneration) {
465542
return Promise.resolve()
466543
.then(() => _removeDecorators(refactor))
467-
.then(() => _refactorBootstrap(plugin, refactor));
544+
.then(() => _refactorBootstrap(plugin, refactor))
545+
.then(() => _replaceExport(plugin, refactor))
546+
.then(() => _exportModuleMap(plugin, refactor));
468547
} else {
469548
return Promise.resolve()
470549
.then(() => _replaceResources(refactor))
471-
.then(() => _removeModuleId(refactor));
550+
.then(() => _removeModuleId(refactor))
551+
.then(() => _exportModuleMap(plugin, refactor));
472552
}
473553
})
474554
.then(() => {

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

+12-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface AotPluginOptions {
2525
mainPath?: string;
2626
typeChecking?: boolean;
2727
skipCodeGeneration?: boolean;
28+
replaceExport?: boolean;
2829
hostOverrideFileSystem?: { [path: string]: string };
2930
hostReplacementPaths?: { [path: string]: string };
3031
i18nFile?: string;
@@ -49,6 +50,7 @@ export class AotPlugin implements Tapable {
4950
private _rootFilePath: string[];
5051
private _compilerHost: WebpackCompilerHost;
5152
private _resourceLoader: WebpackResourceLoader;
53+
private _discoveredLazyRoutes: LazyRouteMap;
5254
private _lazyRoutes: LazyRouteMap = Object.create(null);
5355
private _tsConfigPath: string;
5456
private _entryModule: string;
@@ -59,6 +61,7 @@ export class AotPlugin implements Tapable {
5961

6062
private _typeCheck = true;
6163
private _skipCodeGeneration = false;
64+
private _replaceExport = false;
6265
private _basePath: string;
6366
private _genDir: string;
6467

@@ -89,11 +92,14 @@ export class AotPlugin implements Tapable {
8992
get genDir() { return this._genDir; }
9093
get program() { return this._program; }
9194
get skipCodeGeneration() { return this._skipCodeGeneration; }
95+
get replaceExport() { return this._replaceExport; }
9296
get typeCheck() { return this._typeCheck; }
9397
get i18nFile() { return this._i18nFile; }
9498
get i18nFormat() { return this._i18nFormat; }
9599
get locale() { return this._locale; }
96100
get firstRun() { return this._firstRun; }
101+
get lazyRoutes() { return this._lazyRoutes; }
102+
get discoveredLazyRoutes() { return this._discoveredLazyRoutes; }
97103

98104
private _setupOptions(options: AotPluginOptions) {
99105
// Fill in the missing options.
@@ -232,6 +238,9 @@ export class AotPlugin implements Tapable {
232238
if (options.hasOwnProperty('locale')) {
233239
this._locale = options.locale;
234240
}
241+
if (options.hasOwnProperty('replaceExport')) {
242+
this._replaceExport = options.replaceExport || this._replaceExport;
243+
}
235244
}
236245

237246
private _findLazyRoutesInAst(): LazyRouteMap {
@@ -510,14 +519,14 @@ export class AotPlugin implements Tapable {
510519
.then(() => {
511520
// We need to run the `listLazyRoutes` the first time because it also navigates libraries
512521
// and other things that we might miss using the findLazyRoutesInAst.
513-
let discoveredLazyRoutes: LazyRouteMap = this.firstRun
522+
this._discoveredLazyRoutes = this.firstRun
514523
? this._getLazyRoutesFromNgtools()
515524
: this._findLazyRoutesInAst();
516525

517526
// Process the lazy routes discovered.
518-
Object.keys(discoveredLazyRoutes)
527+
Object.keys(this.discoveredLazyRoutes)
519528
.forEach(k => {
520-
const lazyRoute = discoveredLazyRoutes[k];
529+
const lazyRoute = this.discoveredLazyRoutes[k];
521530
k = k.split('#')[0];
522531
if (lazyRoute === null) {
523532
return;

tests/e2e/assets/webpack/test-server-app/app/app.module.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { NgModule, Component } from '@angular/core';
22
import { ServerModule } from '@angular/platform-server';
3+
import { BrowserModule } from '@angular/platform-browser';
34
import { RouterModule } from '@angular/router';
5+
46
import { AppComponent } from './app.component';
7+
import { MyInjectable } from './injectable';
58

69
@Component({
710
selector: 'home-view',
@@ -16,12 +19,16 @@ export class HomeView {}
1619
HomeView
1720
],
1821
imports: [
22+
BrowserModule.withServerTransition({
23+
appId: 'app'
24+
}),
1925
ServerModule,
2026
RouterModule.forRoot([
2127
{path: 'lazy', loadChildren: './lazy.module#LazyModule'},
2228
{path: '', component: HomeView}
2329
])
2430
],
31+
providers: [MyInjectable],
2532
bootstrap: [AppComponent]
2633
})
2734
export class AppModule {

tests/e2e/assets/webpack/test-server-app/app/injectable.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import {DOCUMENT} from '@angular/platform-browser';
44

55
@Injectable()
66
export class MyInjectable {
7-
constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {}
7+
constructor(@Inject(DOCUMENT) public doc) {}
88
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { AppModule } from './app.module';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const fs = require('fs');
2+
const { AppModuleNgFactory } = require('./dist/app.main');
3+
const { renderModuleFactory } = require('@angular/platform-server');
4+
5+
require('zone.js/dist/zone-node');
6+
7+
renderModuleFactory(AppModuleNgFactory, {
8+
url: '/',
9+
document: '<app-root></app-root>'
10+
}).then(html => {
11+
fs.writeFileSync('dist/index.html', html);
12+
})

0 commit comments

Comments
 (0)