diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json
index ccd71182dbeb..7a8b2c08e4c5 100644
--- a/packages/@angular/cli/lib/config/schema.json
+++ b/packages/@angular/cli/lib/config/schema.json
@@ -83,6 +83,12 @@
"type": "string",
"description": "Base url for the application being built."
},
+ "platform": {
+ "type": "string",
+ "enum": ["browser", "server"],
+ "default": "browser",
+ "description": "The runtime platform of the app."
+ },
"index": {
"type": "string",
"default": "index.html",
diff --git a/packages/@angular/cli/models/webpack-config.ts b/packages/@angular/cli/models/webpack-config.ts
index 8d084801e48c..4e7e259cf868 100644
--- a/packages/@angular/cli/models/webpack-config.ts
+++ b/packages/@angular/cli/models/webpack-config.ts
@@ -7,6 +7,7 @@ import {
getDevConfig,
getProdConfig,
getStylesConfig,
+ getServerConfig,
getNonAotConfig,
getAotConfig
} from './webpack-configs';
@@ -37,9 +38,12 @@ export class NgCliWebpackConfig {
}
public buildConfig() {
+ const platformConfig = this.wco.appConfig.platform === 'server' ?
+ getServerConfig(this.wco) : getBrowserConfig(this.wco);
+
let webpackConfigs = [
getCommonConfig(this.wco),
- getBrowserConfig(this.wco),
+ platformConfig,
getStylesConfig(this.wco),
this.getTargetConfig(this.wco)
];
diff --git a/packages/@angular/cli/models/webpack-configs/index.ts b/packages/@angular/cli/models/webpack-configs/index.ts
index 7c985a1e093d..70560367f79f 100644
--- a/packages/@angular/cli/models/webpack-configs/index.ts
+++ b/packages/@angular/cli/models/webpack-configs/index.ts
@@ -2,6 +2,7 @@ export * from './browser';
export * from './common';
export * from './development';
export * from './production';
+export * from './server';
export * from './styles';
export * from './test';
export * from './typescript';
diff --git a/packages/@angular/cli/models/webpack-configs/server.ts b/packages/@angular/cli/models/webpack-configs/server.ts
new file mode 100644
index 000000000000..262849877de2
--- /dev/null
+++ b/packages/@angular/cli/models/webpack-configs/server.ts
@@ -0,0 +1,38 @@
+import { WebpackConfigOptions } from '../webpack-config';
+
+/**
+ * Returns a partial specific to creating a bundle for node
+ * @param _wco Options which are include the build options and app config
+ */
+export const getServerConfig = function (_wco: WebpackConfigOptions) {
+ return {
+ target: 'node',
+ output: {
+ libraryTarget: 'commonjs'
+ },
+ externals: [
+ /^@angular/,
+ function (_: any, request: any, callback: (error?: any, result?: any) => void) {
+ // Absolute & Relative paths are not externals
+ if (request.match(/^\.{0,2}\//)) {
+ return callback();
+ }
+
+ try {
+ // Attempt to resolve the module via Node
+ const e = require.resolve(request);
+ if (/node_modules/.test(e)) {
+ // It's a node_module
+ callback(null, request);
+ } else {
+ // It's a system thing (.ie util, fs...)
+ callback();
+ }
+ } catch (e) {
+ // Node couldn't find it, so it must be user-aliased
+ callback();
+ }
+ }
+ ]
+ };
+};
diff --git a/packages/@angular/cli/models/webpack-configs/typescript.ts b/packages/@angular/cli/models/webpack-configs/typescript.ts
index ba58d224ba64..39c7a7295687 100644
--- a/packages/@angular/cli/models/webpack-configs/typescript.ts
+++ b/packages/@angular/cli/models/webpack-configs/typescript.ts
@@ -68,6 +68,7 @@ function _createAotPlugin(wco: WebpackConfigOptions, options: any) {
i18nFile: buildOptions.i18nFile,
i18nFormat: buildOptions.i18nFormat,
locale: buildOptions.locale,
+ replaceExport: appConfig.platform === 'server',
hostReplacementPaths,
// If we don't explicitely list excludes, it will default to `['**/*.spec.ts']`.
exclude: []
diff --git a/packages/@angular/cli/tasks/e2e.ts b/packages/@angular/cli/tasks/e2e.ts
index 3b2cec98991e..40228aafeb83 100644
--- a/packages/@angular/cli/tasks/e2e.ts
+++ b/packages/@angular/cli/tasks/e2e.ts
@@ -4,6 +4,7 @@ import { stripIndents } from 'common-tags';
import { E2eTaskOptions } from '../commands/e2e';
import { CliConfig } from '../models/config';
import { requireProjectModule } from '../utilities/require-project-module';
+import { getAppFromConfig } from '../utilities/app-utils';
const Task = require('../ember-cli/lib/models/task');
const SilentError = require('silent-error');
@@ -14,10 +15,14 @@ export const E2eTask = Task.extend({
const projectConfig = CliConfig.fromProject().config;
const projectRoot = this.project.root;
const protractorLauncher = requireProjectModule(projectRoot, 'protractor/built/launcher');
+ const appConfig = getAppFromConfig(e2eTaskOptions.app);
if (projectConfig.project && projectConfig.project.ejected) {
throw new SilentError('An ejected project cannot use the build command anymore.');
}
+ if (appConfig.platform === 'server') {
+ throw new SilentError('ng test for platform server applications is coming soon!');
+ }
return new Promise(function () {
let promise = Promise.resolve();
diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts
index c9f6cdce8f5b..8be7ae7a3f0b 100644
--- a/packages/@angular/cli/tasks/eject.ts
+++ b/packages/@angular/cli/tasks/eject.ts
@@ -436,6 +436,9 @@ export default Task.extend({
if (project.root === path.resolve(outputPath)) {
throw new SilentError ('Output path MUST not be project root directory!');
}
+ if (appConfig.platform === 'server') {
+ throw new SilentError('ng eject for platform server applications is coming soon!');
+ }
const webpackConfig = new NgCliWebpackConfig(runTaskOptions, appConfig).buildConfig();
const serializer = new JsonWebpackSerializer(process.cwd(), outputPath, appConfig.root);
diff --git a/packages/@angular/cli/tasks/serve.ts b/packages/@angular/cli/tasks/serve.ts
index 750c3c66e5db..618ea14419ba 100644
--- a/packages/@angular/cli/tasks/serve.ts
+++ b/packages/@angular/cli/tasks/serve.ts
@@ -31,6 +31,9 @@ export default Task.extend({
if (projectConfig.project && projectConfig.project.ejected) {
throw new SilentError('An ejected project cannot use the build command anymore.');
}
+ if (appConfig.platform === 'server') {
+ throw new SilentError('ng serve for platform server applications is coming soon!');
+ }
if (serveTaskOptions.deleteOutputPath) {
fs.removeSync(path.resolve(this.project.root, outputPath));
}
diff --git a/packages/@angular/cli/tasks/test.ts b/packages/@angular/cli/tasks/test.ts
index 9b1fca73b009..a41ec55ac4f5 100644
--- a/packages/@angular/cli/tasks/test.ts
+++ b/packages/@angular/cli/tasks/test.ts
@@ -3,6 +3,7 @@ import * as path from 'path';
import { TestOptions } from '../commands/test';
import { CliConfig } from '../models/config';
import { requireProjectModule } from '../utilities/require-project-module';
+import { getAppFromConfig } from '../utilities/app-utils';
const Task = require('../ember-cli/lib/models/task');
const SilentError = require('silent-error');
@@ -12,10 +13,14 @@ export default Task.extend({
run: function (options: TestOptions) {
const projectConfig = CliConfig.fromProject().config;
const projectRoot = this.project.root;
+ const appConfig = getAppFromConfig(options.app);
if (projectConfig.project && projectConfig.project.ejected) {
throw new SilentError('An ejected project cannot use the build command anymore.');
}
+ if (appConfig.platform === 'server') {
+ throw new SilentError('ng test for platform server applications is coming soon!');
+ }
return new Promise((resolve) => {
const karma = requireProjectModule(projectRoot, 'karma');
diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts
index bd1a3ff3b8a2..60124c1dbee5 100644
--- a/packages/@ngtools/webpack/src/loader.ts
+++ b/packages/@ngtools/webpack/src/loader.ts
@@ -435,6 +435,83 @@ function _diagnoseDeps(reasons: ModuleReason[], plugin: AotPlugin, checked: Set<
}
+export function _getModuleExports(plugin: AotPlugin,
+ refactor: TypeScriptFileRefactor): ts.Identifier[] {
+ const exports = refactor
+ .findAstNodes(refactor.sourceFile, ts.SyntaxKind.ExportDeclaration, true);
+
+ return exports
+ .filter(node => {
+
+ const identifiers = refactor.findAstNodes(node, ts.SyntaxKind.Identifier, false);
+
+ identifiers
+ .filter(node => node.getText() === plugin.entryModule.className);
+
+ return identifiers.length > 0;
+ }) as ts.Identifier[];
+}
+
+
+export function _replaceExport(plugin: AotPlugin, refactor: TypeScriptFileRefactor) {
+ if (!plugin.replaceExport) {
+ return;
+ }
+ _getModuleExports(plugin, refactor)
+ .forEach(node => {
+ const factoryPath = _getNgFactoryPath(plugin, refactor);
+ const factoryClassName = plugin.entryModule.className + 'NgFactory';
+ const exportStatement = `export \{ ${factoryClassName} \} from '${factoryPath}'`;
+ refactor.appendAfter(node, exportStatement);
+ });
+}
+
+
+export function _exportModuleMap(plugin: AotPlugin, refactor: TypeScriptFileRefactor) {
+ if (!plugin.replaceExport) {
+ return;
+ }
+
+ const dirName = path.normalize(path.dirname(refactor.fileName));
+ const classNameAppend = plugin.skipCodeGeneration ? '' : 'NgFactory';
+ const modulePathAppend = plugin.skipCodeGeneration ? '' : '.ngfactory';
+
+ _getModuleExports(plugin, refactor)
+ .forEach(node => {
+ const modules = Object.keys(plugin.discoveredLazyRoutes)
+ .map((loadChildrenString) => {
+ let [lazyRouteKey, moduleName] = loadChildrenString.split('#');
+
+ if (!lazyRouteKey || !moduleName) {
+ throw new Error(`${loadChildrenString} was not a proper loadChildren string`);
+ }
+
+ moduleName += classNameAppend;
+ lazyRouteKey += modulePathAppend;
+ const modulePath = plugin.lazyRoutes[lazyRouteKey];
+
+ return {
+ modulePath,
+ moduleName,
+ loadChildrenString
+ };
+ });
+
+ modules.forEach((module, index) => {
+ const relativePath = path.relative(dirName, module.modulePath).replace(/\\/g, '/');
+ refactor.prependBefore(node, `import * as __lazy_${index}__ from './${relativePath}'`);
+ });
+
+ const jsonContent: string = modules
+ .map((module, index) =>
+ `"${module.loadChildrenString}": __lazy_${index}__.${module.moduleName}`)
+ .join();
+
+ refactor.appendAfter(node, `export const LAZY_MODULE_MAP = {${jsonContent}};`);
+ });
+}
+
+
// Super simple TS transpiler loader for testing / isolated usage. does not type check!
export function ngcLoader(this: LoaderContext & { _compilation: any }, source: string | null) {
const cb = this.async();
@@ -464,11 +541,14 @@ export function ngcLoader(this: LoaderContext & { _compilation: any }, source: s
if (!plugin.skipCodeGeneration) {
return Promise.resolve()
.then(() => _removeDecorators(refactor))
- .then(() => _refactorBootstrap(plugin, refactor));
+ .then(() => _refactorBootstrap(plugin, refactor))
+ .then(() => _replaceExport(plugin, refactor))
+ .then(() => _exportModuleMap(plugin, refactor));
} else {
return Promise.resolve()
.then(() => _replaceResources(refactor))
- .then(() => _removeModuleId(refactor));
+ .then(() => _removeModuleId(refactor))
+ .then(() => _exportModuleMap(plugin, refactor));
}
})
.then(() => {
diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts
index 16aa9542dd49..0535aa3a9aff 100644
--- a/packages/@ngtools/webpack/src/plugin.ts
+++ b/packages/@ngtools/webpack/src/plugin.ts
@@ -25,6 +25,7 @@ export interface AotPluginOptions {
mainPath?: string;
typeChecking?: boolean;
skipCodeGeneration?: boolean;
+ replaceExport?: boolean;
hostOverrideFileSystem?: { [path: string]: string };
hostReplacementPaths?: { [path: string]: string };
i18nFile?: string;
@@ -49,6 +50,7 @@ export class AotPlugin implements Tapable {
private _rootFilePath: string[];
private _compilerHost: WebpackCompilerHost;
private _resourceLoader: WebpackResourceLoader;
+ private _discoveredLazyRoutes: LazyRouteMap;
private _lazyRoutes: LazyRouteMap = Object.create(null);
private _tsConfigPath: string;
private _entryModule: string;
@@ -59,6 +61,7 @@ export class AotPlugin implements Tapable {
private _typeCheck = true;
private _skipCodeGeneration = false;
+ private _replaceExport = false;
private _basePath: string;
private _genDir: string;
@@ -89,11 +92,14 @@ export class AotPlugin implements Tapable {
get genDir() { return this._genDir; }
get program() { return this._program; }
get skipCodeGeneration() { return this._skipCodeGeneration; }
+ get replaceExport() { return this._replaceExport; }
get typeCheck() { return this._typeCheck; }
get i18nFile() { return this._i18nFile; }
get i18nFormat() { return this._i18nFormat; }
get locale() { return this._locale; }
get firstRun() { return this._firstRun; }
+ get lazyRoutes() { return this._lazyRoutes; }
+ get discoveredLazyRoutes() { return this._discoveredLazyRoutes; }
private _setupOptions(options: AotPluginOptions) {
// Fill in the missing options.
@@ -232,6 +238,9 @@ export class AotPlugin implements Tapable {
if (options.hasOwnProperty('locale')) {
this._locale = options.locale;
}
+ if (options.hasOwnProperty('replaceExport')) {
+ this._replaceExport = options.replaceExport || this._replaceExport;
+ }
}
private _findLazyRoutesInAst(): LazyRouteMap {
@@ -510,14 +519,14 @@ export class AotPlugin implements Tapable {
.then(() => {
// We need to run the `listLazyRoutes` the first time because it also navigates libraries
// and other things that we might miss using the findLazyRoutesInAst.
- let discoveredLazyRoutes: LazyRouteMap = this.firstRun
+ this._discoveredLazyRoutes = this.firstRun
? this._getLazyRoutesFromNgtools()
: this._findLazyRoutesInAst();
// Process the lazy routes discovered.
- Object.keys(discoveredLazyRoutes)
+ Object.keys(this.discoveredLazyRoutes)
.forEach(k => {
- const lazyRoute = discoveredLazyRoutes[k];
+ const lazyRoute = this.discoveredLazyRoutes[k];
k = k.split('#')[0];
if (lazyRoute === null) {
return;
diff --git a/tests/e2e/assets/webpack/test-server-app/app/app.module.ts b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts
index 7ef819b3a46e..7c8a0c296448 100644
--- a/tests/e2e/assets/webpack/test-server-app/app/app.module.ts
+++ b/tests/e2e/assets/webpack/test-server-app/app/app.module.ts
@@ -1,7 +1,10 @@
import { NgModule, Component } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
+import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
+
import { AppComponent } from './app.component';
+import { MyInjectable } from './injectable';
@Component({
selector: 'home-view',
@@ -16,12 +19,16 @@ export class HomeView {}
HomeView
],
imports: [
+ BrowserModule.withServerTransition({
+ appId: 'app'
+ }),
ServerModule,
RouterModule.forRoot([
{path: 'lazy', loadChildren: './lazy.module#LazyModule'},
{path: '', component: HomeView}
])
],
+ providers: [MyInjectable],
bootstrap: [AppComponent]
})
export class AppModule {
diff --git a/tests/e2e/assets/webpack/test-server-app/app/injectable.ts b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts
index 04d8486586c4..b357678ae77a 100644
--- a/tests/e2e/assets/webpack/test-server-app/app/injectable.ts
+++ b/tests/e2e/assets/webpack/test-server-app/app/injectable.ts
@@ -4,5 +4,5 @@ import {DOCUMENT} from '@angular/platform-browser';
@Injectable()
export class MyInjectable {
- constructor(public viewContainer: ViewContainerRef, @Inject(DOCUMENT) public doc) {}
+ constructor(@Inject(DOCUMENT) public doc) {}
}
diff --git a/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts b/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts
new file mode 100644
index 000000000000..ce26d93a11de
--- /dev/null
+++ b/tests/e2e/assets/webpack/test-server-app/app/main.commonjs.ts
@@ -0,0 +1 @@
+export { AppModule } from './app.module';
diff --git a/tests/e2e/assets/webpack/test-server-app/index.js b/tests/e2e/assets/webpack/test-server-app/index.js
new file mode 100644
index 000000000000..bdfb2e792acd
--- /dev/null
+++ b/tests/e2e/assets/webpack/test-server-app/index.js
@@ -0,0 +1,12 @@
+const fs = require('fs');
+const { AppModuleNgFactory } = require('./dist/app.main');
+const { renderModuleFactory } = require('@angular/platform-server');
+
+require('zone.js/dist/zone-node');
+
+renderModuleFactory(AppModuleNgFactory, {
+ url: '/',
+ document: '