Skip to content

Commit fe3da00

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

21 files changed

+619
-0
lines changed

packages/webpack/package.json

Lines changed: 21 additions & 0 deletions
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

Lines changed: 52 additions & 0 deletions
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

Lines changed: 17 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
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({
82+
ngcOptions: tsConfig.ngOptions,
83+
i18nOptions,
84+
compilerHost: this.compilerHost,
85+
resourceLoader: undefined //TODO: handle styles/templatess here.
86+
});
87+
}
88+
89+
//registration hook for webpack plugin
90+
apply(compiler) {
91+
compiler.plugin('context-module-factory', (cmf) => this._resolveImports(cmf));
92+
compiler.plugin('make', (compiler, cb) => this._make(compiler, cb));
93+
94+
}
95+
96+
private _resolveImports(contextModuleFactory){
97+
const plugin = this;
98+
contextModuleFactory.plugin('before-resolve',(request, callback) => plugin._beforeResolveImports(request, callback));
99+
contextModuleFactory.plugin('after-resolve', (request, callback) => plugin._afterResolveImports(request, callback));
100+
return contextModuleFactory;
101+
}
102+
103+
private _beforeResolveImports(result, callback){
104+
if(!result) return callback();
105+
if(this.contextRegex.test(result.request)){
106+
result.request = path.resolve(process.cwd(), 'app/ngfactory');
107+
result.recursive = true;
108+
result.dependencies.forEach(d => d.critical = false);
109+
110+
}
111+
return callback(null, result);
112+
}
113+
114+
private _afterResolveImports(result, callback){
115+
if(!result) return callback();
116+
if(this.contextRegex.test(result.resource)) {
117+
result.resource = path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app');
118+
result.recursive = true;
119+
result.dependencies.forEach(d => d.critical = false);
120+
result.resolveDependencies = createResolveDependenciesFromContextMap((fs, cb) => cb(null, this.lazyRoutes));
121+
}
122+
return callback(null, result);
123+
}
124+
125+
private _make(compilation, cb) {
126+
127+
const rootModulePath = this.rootModule + '.ts';
128+
const rootModuleName = this.rootModuleName;
129+
130+
//process the lazy routes
131+
const lazyModules = this._processNgModule("./" + rootModulePath, rootModuleName, "./" + rootModulePath).map(moduleKey => {
132+
return moduleKey.split('#')[0];
133+
})
134+
const program = ts.createProgram(this.files, this.compilerOptions, this.compilerHost)
135+
136+
this.codeGeneratorFactory(program)
137+
.reduce((files, generatedFile) => files.concat(generatedFile), [])
138+
.do(files => debug(`generated ${files.length} files`))
139+
.map(allGeneratedCode => {
140+
return lazyModules.reduce((lazyRoutes, lazyModule) => {
141+
const lazyPath = lazyModule + '.ngfactory';
142+
lazyRoutes[lazyPath] = path.join(path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app'), lazyModule + '.ngfactory.ts');
143+
return lazyRoutes;
144+
}, {});
145+
})
146+
.do(lazyRouteConfig => this.lazyRoutes = lazyRouteConfig)
147+
.forEach(v => console.log('codegen complete'))
148+
.then(
149+
_ => cb(),
150+
err => cb(err)
151+
);
152+
}
153+
154+
private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] {
155+
const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
156+
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
157+
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);
158+
159+
const moduleChildren = loadChildren.reduce((res, lc) => {
160+
const [childMoudle, childNgModule] = lc.split('#');
161+
162+
//TODO calculate a different containingFile for relative paths
163+
164+
const children = this._processNgModule(childMoudle, childNgModule, containingFile);
165+
return res.concat(children);
166+
}, loadChildren);
167+
168+
return moduleChildren;
169+
}
170+
171+
private _convertToModule(s: string): string {
172+
// TODO. Currently we assume that the string is the same as the import
173+
return s;
174+
}
175+
176+
private _resolve(compiler, resolver, requestObject, cb) {
177+
cb()
178+
}
179+
180+
181+
private _run(compiler, cb) {
182+
cb()
183+
}
184+
185+
private _watch(watcher, cb) {
186+
this._make(watcher.compiler, cb);
187+
}
188+
189+
private _readConfig(tsConfigPath): any {
190+
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
191+
if (error) {
192+
throw error;
193+
}
194+
return ts.parseJsonConfigFileContent(config, new ParseConfigHost(), "");
195+
}
196+
197+
private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
198+
const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
199+
if (ngModules.length === 0) {
200+
throw new Error(`${staticSymbol.name} is not an NgModule`);
201+
}
202+
return ngModules[0];
203+
}
204+
205+
private extractLoadChildren(ngModuleDecorator: any): any[] {
206+
const routes = ngModuleDecorator.imports.reduce((mem, m) => {
207+
return mem.concat(this.collectRoutes(m.providers));
208+
}, this.collectRoutes(ngModuleDecorator.providers));
209+
return this.collectLoadChildren(routes);
210+
}
211+
212+
private collectRoutes(providers: any[]): any[] {
213+
if (!providers) return [];
214+
const ROUTES = this.reflectorHost.findDeclaration("@angular/router/src/router_config_loader", "ROUTES", undefined);
215+
return providers.reduce((m, p) => {
216+
if (p.provide === ROUTES) {
217+
return m.concat(p.useValue);
218+
219+
} else if (Array.isArray(p)) {
220+
return m.concat(this.collectRoutes(p));
221+
222+
} else {
223+
return m;
224+
}
225+
}, []);
226+
}
227+
228+
private collectLoadChildren(routes: any[]): any[] {
229+
if (!routes) return [];
230+
return routes.reduce((m, r) => {
231+
if (r.loadChildren) {
232+
return m.concat([r.loadChildren]);
233+
234+
} else if (Array.isArray(r)) {
235+
return m.concat(this.collectLoadChildren(r));
236+
237+
} else if (r.children) {
238+
return m.concat(this.collectLoadChildren(r.children));
239+
240+
} else {
241+
return m;
242+
}
243+
}, []);
244+
}
245+
}
246+
247+
class ParseConfigHost implements ts.ParseConfigHost {
248+
useCaseSensitiveFileNames: boolean = true;
249+
250+
readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[] {
251+
return ts.sys.readDirectory(rootDir, extensions, excludes, includes);
252+
}
253+
/**
254+
* Gets a value indicating whether the specified path exists and is a file.
255+
* @param path The path to test.
256+
*/
257+
fileExists(path: string): boolean {
258+
return ts.sys.fileExists(path);
259+
}
260+
}
261+
262+
Lines changed: 17 additions & 0 deletions
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)