Skip to content

feature(compiler): add AoT compilation + lazy route bundling #2315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/webpack/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@angular-cli/webpack",
"version": "1.0.0",
"description": "",
"main": "lib/index.js",
"scripts": {
"build:main": "tsc",
"test": "echo \"Error: no test specified\" && exit 0"
},
"author": "Rob Wormald <[email protected]>",
"license": "MIT",
"devDependencies": {
"@types/node": "^6.0.39",
"node-sass": "^3.8.0",
"sass-loader": "^4.0.1",
"typescript": "2.0.2",
"webpack": "^2.1.0-beta.21",
"@angular/compiler-cli": "^0.6.0",
"@angular/core": "^2.0.0"
}
}
52 changes: 52 additions & 0 deletions packages/webpack/src/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {Observable} from 'rxjs/Rx'
import * as ts from 'typescript'
import * as ngCompiler from '@angular/compiler-cli'
import * as tscWrapped from '@angular/tsc-wrapped'
import * as tsc from '@angular/tsc-wrapped/src/tsc'
import * as path from 'path'
import * as fs from 'fs'

export interface CodeGenOptions {
program: ts.Program;
ngcOptions: any;
i18nOptions: any;
resourceLoader?:any; //impl of ResourceLoader
compilerHost: any;
}

function _readConfig(tsConfigPath){
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
if(error){
throw error;
}
return config;
}

export function createCodeGenerator({ngcOptions, i18nOptions, resourceLoader, compilerHost}){

return program => new Observable<{fileName:string, sourceText: string}>(codegenOutput => {
//emit files from observable monkeypatch
const writeFile = compilerHost.writeFile;

compilerHost.writeFile = (fileName, sourceText) => {
writeFile(fileName, sourceText);
codegenOutput.next({fileName, sourceText});
};
const codeGenerator = ngCompiler.CodeGenerator.create(
ngcOptions,
i18nOptions,
program,
compilerHost,
undefined, //TODO: hook in reflector host context
resourceLoader
);

codeGenerator
.codegen().then(
() => {
codegenOutput.complete();
},
err => codegenOutput.error(err)
);
});
}
17 changes: 17 additions & 0 deletions packages/webpack/src/compiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as ngCompiler from '@angular/compiler-cli'
import * as tscWrapped from '@angular/tsc-wrapped/src/compiler_host'
import * as ts from 'typescript'


export class NgcWebpackCompilerHost extends tscWrapped.DelegatingHost {
fileCache:Map<string,string> = new Map<string, string> ()
constructor(delegate: ts.CompilerHost){
super(delegate);

}
}

export function createCompilerHost(tsConfig){
const delegateHost = ts.createCompilerHost(tsConfig.compilerOptions);
return new NgcWebpackCompilerHost(delegateHost);
}
3 changes: 3 additions & 0 deletions packages/webpack/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './codegen'
export * from './plugin'
export {ngcLoader as default} from './loader'
9 changes: 9 additions & 0 deletions packages/webpack/src/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//super simple TS transpiler loader for testing / isolated usage. does not type check!
import * as path from 'path'
import * as fs from 'fs'
import * as ts from 'typescript'

export function ngcLoader(sourceFile){

return ts.transpileModule(sourceFile, {compilerOptions: {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.ES2015}}).outputText;
}
262 changes: 262 additions & 0 deletions packages/webpack/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
//@angular/webpack plugin main
import 'reflect-metadata';
import { ReflectiveInjector, OpaqueToken, NgModule } from '@angular/core'
import * as ts from 'typescript'
import * as ngCompiler from '@angular/compiler-cli'
import * as tscWrapped from '@angular/tsc-wrapped'
import * as tsc from '@angular/tsc-wrapped/src/tsc'
import * as path from 'path'
import * as fs from 'fs'

import { WebpackResourceLoader } from './resource_loader'
import { createCodeGenerator } from './codegen'
import { createCompilerHost } from './compiler'
import { createResolveDependenciesFromContextMap } from './utils'

function debug(...args) {
console.log.apply(console, ['ngc:', ...args]);
}

/**
* Option Constants
*/
export type NGC_COMPILER_MODE = 'aot' | 'jit'

export interface AngularWebpackPluginOptions {
tsconfigPath?: string;
compilerMode?: NGC_COMPILER_MODE;
providers?: any[];
entryModule: string;
}

const noTransformExtensions = ['.html', '.css']

export class NgcWebpackPlugin {
projectPath: string;
rootModule: string;
rootModuleName: string;
fileCache: any;
codeGeneratorFactory: any;
reflector: ngCompiler.StaticReflector;
reflectorHost: ngCompiler.ReflectorHost;
program: ts.Program;
private injector: ReflectiveInjector;
compilerHost: ts.CompilerHost;
compilerOptions: ts.CompilerOptions;
angularCompilerOptions: any;
files: any[];
contextRegex = /.*/;
lazyRoutes: any;

constructor(public options: any = {}) {
const tsConfig = tsc.tsc.readConfiguration(options.project, options.baseDir);
this.compilerOptions = tsConfig.parsed.options;
this.files = tsConfig.parsed.fileNames;
this.angularCompilerOptions = tsConfig.ngOptions;
this.angularCompilerOptions.basePath = options.baseDir || process.cwd();

if (!this.angularCompilerOptions) {
//TODO:robwormald more validation here
throw new Error(`"angularCompilerOptions" is not set in your tsconfig file!`);
}
const [rootModule, rootNgModule] = this.angularCompilerOptions.entryModule.split('#');

this.projectPath = options.project;
this.rootModule = rootModule;
this.rootModuleName = rootNgModule;

this.compilerHost = ts.createCompilerHost(this.compilerOptions, true);
this.program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost);

//TODO: pick this up from ngOptions
const i18nOptions = {
i18nFile: undefined,
i18nFormat: undefined,
locale: undefined,
basePath: options.baseDir
}

this.reflectorHost = new ngCompiler.ReflectorHost(this.program, this.compilerHost, tsConfig.ngOptions);
this.reflector = new ngCompiler.StaticReflector(this.reflectorHost);
this.codeGeneratorFactory = createCodeGenerator({
ngcOptions: tsConfig.ngOptions,
i18nOptions,
compilerHost: this.compilerHost,
resourceLoader: undefined //TODO: handle styles/templatess here.
});
}

//registration hook for webpack plugin
apply(compiler) {
compiler.plugin('context-module-factory', (cmf) => this._resolveImports(cmf));
compiler.plugin('make', (compiler, cb) => this._make(compiler, cb));

}

private _resolveImports(contextModuleFactory){
const plugin = this;
contextModuleFactory.plugin('before-resolve',(request, callback) => plugin._beforeResolveImports(request, callback));
contextModuleFactory.plugin('after-resolve', (request, callback) => plugin._afterResolveImports(request, callback));
return contextModuleFactory;
}

private _beforeResolveImports(result, callback){
if(!result) return callback();
if(this.contextRegex.test(result.request)){
result.request = path.resolve(process.cwd(), 'app/ngfactory');
result.recursive = true;
result.dependencies.forEach(d => d.critical = false);

}
return callback(null, result);
}

private _afterResolveImports(result, callback){
if(!result) return callback();
if(this.contextRegex.test(result.resource)) {
result.resource = path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app');
result.recursive = true;
result.dependencies.forEach(d => d.critical = false);
result.resolveDependencies = createResolveDependenciesFromContextMap((fs, cb) => cb(null, this.lazyRoutes));
}
return callback(null, result);
}

private _make(compilation, cb) {

const rootModulePath = this.rootModule + '.ts';
const rootModuleName = this.rootModuleName;

//process the lazy routes
const lazyModules = this._processNgModule("./" + rootModulePath, rootModuleName, "./" + rootModulePath).map(moduleKey => {
return moduleKey.split('#')[0];
})
const program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost)

this.codeGeneratorFactory(program)
.reduce((files, generatedFile) => files.concat(generatedFile), [])
.do(files => debug(`generated ${files.length} files`))
.map(allGeneratedCode => {
return lazyModules.reduce((lazyRoutes, lazyModule) => {
const lazyPath = lazyModule + '.ngfactory';
lazyRoutes[lazyPath] = path.join(path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app'), lazyModule + '.ngfactory.ts');
return lazyRoutes;
}, {});
})
.do(lazyRouteConfig => this.lazyRoutes = lazyRouteConfig)
.forEach(v => console.log('codegen complete'))
.then(
_ => cb(),
err => cb(err)
);
}

private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] {
const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);

const moduleChildren = loadChildren.reduce((res, lc) => {
const [childMoudle, childNgModule] = lc.split('#');

//TODO calculate a different containingFile for relative paths

const children = this._processNgModule(childMoudle, childNgModule, containingFile);
return res.concat(children);
}, loadChildren);

return moduleChildren;
}

private _convertToModule(s: string): string {
// TODO. Currently we assume that the string is the same as the import
return s;
}

private _resolve(compiler, resolver, requestObject, cb) {
cb()
}


private _run(compiler, cb) {
cb()
}

private _watch(watcher, cb) {
this._make(watcher.compiler, cb);
}

private _readConfig(tsConfigPath): any {
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
if (error) {
throw error;
}
return ts.parseJsonConfigFileContent(config, new ParseConfigHost(), "");
}

private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
if (ngModules.length === 0) {
throw new Error(`${staticSymbol.name} is not an NgModule`);
}
return ngModules[0];
}

private extractLoadChildren(ngModuleDecorator: any): any[] {
const routes = ngModuleDecorator.imports.reduce((mem, m) => {
return mem.concat(this.collectRoutes(m.providers));
}, this.collectRoutes(ngModuleDecorator.providers));
return this.collectLoadChildren(routes);
}

private collectRoutes(providers: any[]): any[] {
if (!providers) return [];
const ROUTES = this.reflectorHost.findDeclaration("@angular/router/src/router_config_loader", "ROUTES", undefined);
return providers.reduce((m, p) => {
if (p.provide === ROUTES) {
return m.concat(p.useValue);

} else if (Array.isArray(p)) {
return m.concat(this.collectRoutes(p));

} else {
return m;
}
}, []);
}

private collectLoadChildren(routes: any[]): any[] {
if (!routes) return [];
return routes.reduce((m, r) => {
if (r.loadChildren) {
return m.concat([r.loadChildren]);

} else if (Array.isArray(r)) {
return m.concat(this.collectLoadChildren(r));

} else if (r.children) {
return m.concat(this.collectLoadChildren(r.children));

} else {
return m;
}
}, []);
}
}

class ParseConfigHost implements ts.ParseConfigHost {
useCaseSensitiveFileNames: boolean = true;

readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[] {
return ts.sys.readDirectory(rootDir, extensions, excludes, includes);
}
/**
* Gets a value indicating whether the specified path exists and is a file.
* @param path The path to test.
*/
fileExists(path: string): boolean {
return ts.sys.fileExists(path);
}
}


17 changes: 17 additions & 0 deletions packages/webpack/src/resource_loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//stub for ng2 compiler's ResourceLoader, responsible for fetching HTML and CSS files into the AoT compiler
//TODO: integrate this with webpack loaders for less/sass

import { ResourceLoader } from '@angular/compiler'
import * as fs from 'fs'

export class WebpackResourceLoader implements ResourceLoader {
constructor(private compiler){}
//called by AOT compiler to retrieve files from disk
get(filePath){
return Promise.resolve(fs.readFileSync(filePath, 'utf-8'))
.then(resource => this.transform(resource));
}
transform(resource:string):string {
return resource;
}
}
Loading