Skip to content

Commit

Permalink
feat(@angular/cli): add ability to build bundle for node and export l…
Browse files Browse the repository at this point in the history
…azy route map
  • Loading branch information
FrozenPandaz authored and hansl committed Jul 20, 2017
1 parent 7d8f54a commit 6f23636
Show file tree
Hide file tree
Showing 20 changed files with 317 additions and 10 deletions.
6 changes: 6 additions & 0 deletions packages/@angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion packages/@angular/cli/models/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getDevConfig,
getProdConfig,
getStylesConfig,
getServerConfig,
getNonAotConfig,
getAotConfig
} from './webpack-configs';
Expand Down Expand Up @@ -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)
];
Expand Down
1 change: 1 addition & 0 deletions packages/@angular/cli/models/webpack-configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
38 changes: 38 additions & 0 deletions packages/@angular/cli/models/webpack-configs/server.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
]
};
};
1 change: 1 addition & 0 deletions packages/@angular/cli/models/webpack-configs/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand Down
5 changes: 5 additions & 0 deletions packages/@angular/cli/tasks/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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();
Expand Down
3 changes: 3 additions & 0 deletions packages/@angular/cli/tasks/eject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions packages/@angular/cli/tasks/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
5 changes: 5 additions & 0 deletions packages/@angular/cli/tasks/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
84 changes: 82 additions & 2 deletions packages/@ngtools/webpack/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(() => {
Expand Down
15 changes: 12 additions & 3 deletions packages/@ngtools/webpack/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -59,6 +61,7 @@ export class AotPlugin implements Tapable {

private _typeCheck = true;
private _skipCodeGeneration = false;
private _replaceExport = false;
private _basePath: string;
private _genDir: string;

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e/assets/webpack/test-server-app/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/assets/webpack/test-server-app/app/injectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AppModule } from './app.module';
12 changes: 12 additions & 0 deletions tests/e2e/assets/webpack/test-server-app/index.js
Original file line number Diff line number Diff line change
@@ -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: '<app-root></app-root>'
}).then(html => {
fs.writeFileSync('dist/index.html', html);
})
Loading

0 comments on commit 6f23636

Please sign in to comment.