Skip to content

Commit e48b648

Browse files
committed
feature(compiler): add ngc compiler host impl
1 parent c73f53b commit e48b648

21 files changed

+618
-0
lines changed

packages/webpack/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@angular-cli/webpack",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "lib/index.js",
6+
"scripts": {
7+
"build:main": "tsc",
8+
"test": "echo \"Error: no test specified\" && exit 0"
9+
},
10+
"author": "Rob Wormald <[email protected]>",
11+
"license": "MIT",
12+
"devDependencies": {
13+
"@types/node": "^6.0.39",
14+
"node-sass": "^3.8.0",
15+
"sass-loader": "^4.0.1",
16+
"typescript": "2.0.2",
17+
"webpack": "^2.1.0-beta.21",
18+
"@angular/compiler-cli": "^0.6.0",
19+
"@angular/core": "^2.0.0"
20+
}
21+
}

packages/webpack/src/codegen.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {Observable} from 'rxjs/Rx'
2+
import * as ts from 'typescript'
3+
import * as ngCompiler from '@angular/compiler-cli'
4+
import * as tscWrapped from '@angular/tsc-wrapped'
5+
import * as tsc from '@angular/tsc-wrapped/src/tsc'
6+
import * as path from 'path'
7+
import * as fs from 'fs'
8+
9+
export interface CodeGenOptions {
10+
program: ts.Program;
11+
ngcOptions: any;
12+
i18nOptions: any;
13+
resourceLoader?:any; //impl of ResourceLoader
14+
compilerHost: any;
15+
}
16+
17+
function _readConfig(tsConfigPath){
18+
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
19+
if(error){
20+
throw error;
21+
}
22+
return config;
23+
}
24+
25+
export function createCodeGenerator({ngcOptions, i18nOptions, resourceLoader, compilerHost}){
26+
27+
return program => new Observable<{fileName:string, sourceText: string}>(codegenOutput => {
28+
//emit files from observable monkeypatch
29+
const writeFile = compilerHost.writeFile;
30+
31+
compilerHost.writeFile = (fileName, sourceText) => {
32+
writeFile(fileName, sourceText);
33+
codegenOutput.next({fileName, sourceText});
34+
};
35+
const codeGenerator = ngCompiler.CodeGenerator.create(
36+
ngcOptions,
37+
i18nOptions,
38+
program,
39+
compilerHost,
40+
undefined, //TODO: hook in reflector host context
41+
resourceLoader
42+
);
43+
44+
codeGenerator
45+
.codegen().then(
46+
() => {
47+
codegenOutput.complete();
48+
},
49+
err => codegenOutput.error(err)
50+
);
51+
});
52+
}

packages/webpack/src/compiler.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as ngCompiler from '@angular/compiler-cli'
2+
import * as tscWrapped from '@angular/tsc-wrapped/src/compiler_host'
3+
import * as ts from 'typescript'
4+
5+
6+
export class NgcWebpackCompilerHost extends tscWrapped.DelegatingHost {
7+
fileCache:Map<string,string> = new Map<string, string> ()
8+
constructor(delegate: ts.CompilerHost){
9+
super(delegate);
10+
11+
}
12+
}
13+
14+
export function createCompilerHost(tsConfig){
15+
const delegateHost = ts.createCompilerHost(tsConfig.compilerOptions);
16+
return new NgcWebpackCompilerHost(delegateHost);
17+
}

packages/webpack/src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './codegen'
2+
export * from './plugin'
3+
export {ngcLoader as default} from './loader'

packages/webpack/src/loader.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//super simple TS transpiler loader for testing / isolated usage. does not type check!
2+
import * as path from 'path'
3+
import * as fs from 'fs'
4+
import * as ts from 'typescript'
5+
6+
export function ngcLoader(sourceFile){
7+
8+
return ts.transpileModule(sourceFile, {compilerOptions: {target: ts.ScriptTarget.ES5, module: ts.ModuleKind.ES2015}}).outputText;
9+
}

packages/webpack/src/plugin.ts

+261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
//@angular/webpack plugin main
2+
import 'reflect-metadata';
3+
import { ReflectiveInjector, OpaqueToken, NgModule } from '@angular/core'
4+
import * as ts from 'typescript'
5+
import * as ngCompiler from '@angular/compiler-cli'
6+
import * as tscWrapped from '@angular/tsc-wrapped'
7+
import * as tsc from '@angular/tsc-wrapped/src/tsc'
8+
import * as path from 'path'
9+
import * as fs from 'fs'
10+
11+
import { WebpackResourceLoader } from './resource_loader'
12+
import { createCodeGenerator } from './codegen'
13+
import { createCompilerHost } from './compiler'
14+
import { createResolveDependenciesFromContextMap } from './utils'
15+
16+
function debug(...args) {
17+
console.log.apply(console, ['ngc:', ...args]);
18+
}
19+
20+
/**
21+
* Option Constants
22+
*/
23+
export type NGC_COMPILER_MODE = 'aot' | 'jit'
24+
25+
export interface AngularWebpackPluginOptions {
26+
tsconfigPath?: string;
27+
compilerMode?: NGC_COMPILER_MODE;
28+
providers?: any[];
29+
entryModule: string;
30+
}
31+
32+
const noTransformExtensions = ['.html', '.css']
33+
34+
export class NgcWebpackPlugin {
35+
projectPath: string;
36+
rootModule: string;
37+
rootModuleName: string;
38+
fileCache: any;
39+
codeGeneratorFactory: any;
40+
reflector: ngCompiler.StaticReflector;
41+
reflectorHost: ngCompiler.ReflectorHost;
42+
program: ts.Program;
43+
private injector: ReflectiveInjector;
44+
compilerHost: ts.CompilerHost;
45+
compilerOptions: ts.CompilerOptions;
46+
angularCompilerOptions: any;
47+
files: any[];
48+
contextRegex = /.*/;
49+
lazyRoutes: any;
50+
51+
constructor(public options: any = {}) {
52+
const tsConfig = tsc.tsc.readConfiguration(options.project, options.baseDir);
53+
this.compilerOptions = tsConfig.parsed.options;
54+
this.files = tsConfig.parsed.fileNames;
55+
this.angularCompilerOptions = tsConfig.ngOptions;
56+
this.angularCompilerOptions.basePath = options.baseDir || process.cwd();
57+
58+
if (!this.angularCompilerOptions) {
59+
//TODO:robwormald more validation here
60+
throw new Error(`"angularCompilerOptions" is not set in your tsconfig file!`);
61+
}
62+
const [rootModule, rootNgModule] = this.angularCompilerOptions.entryModule.split('#');
63+
64+
this.projectPath = options.project;
65+
this.rootModule = rootModule;
66+
this.rootModuleName = rootNgModule;
67+
68+
this.compilerHost = ts.createCompilerHost(this.compilerOptions, true);
69+
this.program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost);
70+
71+
//TODO: pick this up from ngOptions
72+
const i18nOptions = {
73+
i18nFile: undefined,
74+
i18nFormat: undefined,
75+
locale: undefined,
76+
basePath: options.baseDir
77+
}
78+
79+
this.reflectorHost = new ngCompiler.ReflectorHost(this.program, this.compilerHost, tsConfig.ngOptions);
80+
this.reflector = new ngCompiler.StaticReflector(this.reflectorHost);
81+
this.codeGeneratorFactory = createCodeGenerator({ ngcOptions: tsConfig.ngOptions, i18nOptions, compilerHost: this.compilerHost, resourceLoader: undefined });
82+
}
83+
84+
_configureCompiler(compiler){
85+
86+
}
87+
88+
//registration hook for webpack plugin
89+
apply(compiler) {
90+
compiler.plugin('context-module-factory', (cmf) => this._resolveImports(cmf));
91+
compiler.plugin('make', (compiler, cb) => this._make(compiler, cb));
92+
93+
}
94+
95+
private _resolveImports(contextModuleFactory){
96+
const plugin = this;
97+
contextModuleFactory.plugin('before-resolve',(request, callback) => plugin._beforeResolveImports(request, callback));
98+
contextModuleFactory.plugin('after-resolve', (request, callback) => plugin._afterResolveImports(request, callback));
99+
return contextModuleFactory;
100+
}
101+
102+
private _beforeResolveImports(result, callback){
103+
if(!result) return callback();
104+
if(this.contextRegex.test(result.request)){
105+
result.request = path.resolve(process.cwd(), 'app/ngfactory');
106+
result.recursive = true;
107+
result.dependencies.forEach(d => d.critical = false);
108+
109+
}
110+
return callback(null, result);
111+
}
112+
113+
private _afterResolveImports(result, callback){
114+
if(!result) return callback();
115+
if(this.contextRegex.test(result.resource)) {
116+
result.resource = path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app');
117+
result.recursive = true;
118+
result.dependencies.forEach(d => d.critical = false);
119+
result.resolveDependencies = createResolveDependenciesFromContextMap((fs, cb) => cb(null, this.lazyRoutes));
120+
}
121+
return callback(null, result);
122+
}
123+
124+
private _make(compilation, cb) {
125+
126+
const rootModulePath = this.rootModule + '.ts';
127+
const rootModuleName = this.rootModuleName;
128+
129+
//process the lazy routes
130+
const lazyModules = this._processNgModule("./" + rootModulePath, rootModuleName, "./" + rootModulePath).map(moduleKey => {
131+
return moduleKey.split('#')[0];
132+
})
133+
const program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost)
134+
135+
this.codeGeneratorFactory(program)
136+
.reduce((files, generatedFile) => files.concat(generatedFile), [])
137+
.do(files => debug(`generated ${files.length} files`))
138+
.map(allGeneratedCode => {
139+
return lazyModules.reduce((lazyRoutes, lazyModule) => {
140+
const lazyPath = lazyModule + '.ngfactory';
141+
lazyRoutes[lazyPath] = path.join(path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app'), lazyModule + '.ngfactory.ts');
142+
return lazyRoutes;
143+
}, {});
144+
})
145+
.do(lazyRouteConfig => this.lazyRoutes = lazyRouteConfig)
146+
.forEach(v => console.log('codegen complete'))
147+
.then(
148+
_ => cb(),
149+
err => cb(err)
150+
);
151+
}
152+
153+
private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] {
154+
const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
155+
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
156+
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);
157+
158+
const moduleChildren = loadChildren.reduce((res, lc) => {
159+
const [childMoudle, childNgModule] = lc.split('#');
160+
161+
//TODO calculate a different containingFile for relative paths
162+
163+
const children = this._processNgModule(childMoudle, childNgModule, containingFile);
164+
return res.concat(children);
165+
}, loadChildren);
166+
167+
return moduleChildren;
168+
}
169+
170+
private _convertToModule(s: string): string {
171+
// TODO. Currently we assume that the string is the same as the import
172+
return s;
173+
}
174+
175+
private _resolve(compiler, resolver, requestObject, cb) {
176+
cb()
177+
}
178+
179+
180+
private _run(compiler, cb) {
181+
cb()
182+
}
183+
184+
private _watch(watcher, cb) {
185+
this._make(watcher.compiler, cb);
186+
}
187+
188+
private _readConfig(tsConfigPath): any {
189+
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
190+
if (error) {
191+
throw error;
192+
}
193+
return ts.parseJsonConfigFileContent(config, new ParseConfigHost(), "");
194+
}
195+
196+
private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
197+
const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
198+
if (ngModules.length === 0) {
199+
throw new Error(`${staticSymbol.name} is not an NgModule`);
200+
}
201+
return ngModules[0];
202+
}
203+
204+
private extractLoadChildren(ngModuleDecorator: any): any[] {
205+
const routes = ngModuleDecorator.imports.reduce((mem, m) => {
206+
return mem.concat(this.collectRoutes(m.providers));
207+
}, this.collectRoutes(ngModuleDecorator.providers));
208+
return this.collectLoadChildren(routes);
209+
}
210+
211+
private collectRoutes(providers: any[]): any[] {
212+
if (!providers) return [];
213+
const ROUTES = this.reflectorHost.findDeclaration("@angular/router/src/router_config_loader", "ROUTES", undefined);
214+
return providers.reduce((m, p) => {
215+
if (p.provide === ROUTES) {
216+
return m.concat(p.useValue);
217+
218+
} else if (Array.isArray(p)) {
219+
return m.concat(this.collectRoutes(p));
220+
221+
} else {
222+
return m;
223+
}
224+
}, []);
225+
}
226+
227+
private collectLoadChildren(routes: any[]): any[] {
228+
if (!routes) return [];
229+
return routes.reduce((m, r) => {
230+
if (r.loadChildren) {
231+
return m.concat([r.loadChildren]);
232+
233+
} else if (Array.isArray(r)) {
234+
return m.concat(this.collectLoadChildren(r));
235+
236+
} else if (r.children) {
237+
return m.concat(this.collectLoadChildren(r.children));
238+
239+
} else {
240+
return m;
241+
}
242+
}, []);
243+
}
244+
}
245+
246+
class ParseConfigHost implements ts.ParseConfigHost {
247+
useCaseSensitiveFileNames: boolean = true;
248+
249+
readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[] {
250+
return ts.sys.readDirectory(rootDir, extensions, excludes, includes);
251+
}
252+
/**
253+
* Gets a value indicating whether the specified path exists and is a file.
254+
* @param path The path to test.
255+
*/
256+
fileExists(path: string): boolean {
257+
return ts.sys.fileExists(path);
258+
}
259+
}
260+
261+
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//stub for ng2 compiler's ResourceLoader, responsible for fetching HTML and CSS files into the AoT compiler
2+
//TODO: integrate this with webpack loaders for less/sass
3+
4+
import { ResourceLoader } from '@angular/compiler'
5+
import * as fs from 'fs'
6+
7+
export class WebpackResourceLoader implements ResourceLoader {
8+
constructor(private compiler){}
9+
//called by AOT compiler to retrieve files from disk
10+
get(filePath){
11+
return Promise.resolve(fs.readFileSync(filePath, 'utf-8'))
12+
.then(resource => this.transform(resource));
13+
}
14+
transform(resource:string):string {
15+
return resource;
16+
}
17+
}

0 commit comments

Comments
 (0)