Skip to content

Commit 7574c2b

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

21 files changed

+617
-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: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
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+
lazyRoutes[lazyModule] = path.join(path.resolve(process.cwd(), this.angularCompilerOptions.genDir + '/app'), lazyModule + '.ngfactory.ts');
141+
return lazyRoutes;
142+
}, {});
143+
})
144+
.do(lazyRouteConfig => this.lazyRoutes = lazyRouteConfig)
145+
.forEach(v => console.log('codegen complete'))
146+
.then(
147+
_ => cb(),
148+
err => cb(err)
149+
);
150+
}
151+
152+
private _processNgModule(mod: string, ngModuleName: string, containingFile: string): string[] {
153+
const staticSymbol = this.reflectorHost.findDeclaration(mod, ngModuleName, containingFile);
154+
const entryNgModuleMetadata = this.getNgModuleMetadata(staticSymbol);
155+
const loadChildren = this.extractLoadChildren(entryNgModuleMetadata);
156+
157+
const moduleChildren = loadChildren.reduce((res, lc) => {
158+
const [childMoudle, childNgModule] = lc.split('#');
159+
160+
//TODO calculate a different containingFile for relative paths
161+
162+
const children = this._processNgModule(childMoudle, childNgModule, containingFile);
163+
return res.concat(children);
164+
}, loadChildren);
165+
166+
return moduleChildren;
167+
}
168+
169+
private _convertToModule(s: string): string {
170+
// TODO. Currently we assume that the string is the same as the import
171+
return s;
172+
}
173+
174+
private _resolve(compiler, resolver, requestObject, cb) {
175+
cb()
176+
}
177+
178+
179+
private _run(compiler, cb) {
180+
cb()
181+
}
182+
183+
private _watch(watcher, cb) {
184+
this._make(watcher.compiler, cb);
185+
}
186+
187+
private _readConfig(tsConfigPath): any {
188+
let {config, error} = ts.readConfigFile(tsConfigPath, ts.sys.readFile);
189+
if (error) {
190+
throw error;
191+
}
192+
return ts.parseJsonConfigFileContent(config, new ParseConfigHost(), "");
193+
}
194+
195+
private getNgModuleMetadata(staticSymbol: ngCompiler.StaticSymbol) {
196+
const ngModules = this.reflector.annotations(staticSymbol).filter(s => s instanceof NgModule);
197+
if (ngModules.length === 0) {
198+
throw new Error(`${staticSymbol.name} is not an NgModule`);
199+
}
200+
return ngModules[0];
201+
}
202+
203+
private extractLoadChildren(ngModuleDecorator: any): any[] {
204+
const routes = ngModuleDecorator.imports.reduce((mem, m) => {
205+
return mem.concat(this.collectRoutes(m.providers));
206+
}, this.collectRoutes(ngModuleDecorator.providers));
207+
return this.collectLoadChildren(routes);
208+
}
209+
210+
private collectRoutes(providers: any[]): any[] {
211+
if (!providers) return [];
212+
const ROUTES = this.reflectorHost.findDeclaration("@angular/router/src/router_config_loader", "ROUTES", undefined);
213+
return providers.reduce((m, p) => {
214+
if (p.provide === ROUTES) {
215+
return m.concat(p.useValue);
216+
217+
} else if (Array.isArray(p)) {
218+
return m.concat(this.collectRoutes(p));
219+
220+
} else {
221+
return m;
222+
}
223+
}, []);
224+
}
225+
226+
private collectLoadChildren(routes: any[]): any[] {
227+
if (!routes) return [];
228+
return routes.reduce((m, r) => {
229+
if (r.loadChildren) {
230+
return m.concat([r.loadChildren]);
231+
232+
} else if (Array.isArray(r)) {
233+
return m.concat(this.collectLoadChildren(r));
234+
235+
} else if (r.children) {
236+
return m.concat(this.collectLoadChildren(r.children));
237+
238+
} else {
239+
return m;
240+
}
241+
}, []);
242+
}
243+
}
244+
245+
class ParseConfigHost implements ts.ParseConfigHost {
246+
useCaseSensitiveFileNames: boolean = true;
247+
248+
readDirectory(rootDir: string, extensions: string[], excludes: string[], includes: string[]): string[] {
249+
return ts.sys.readDirectory(rootDir, extensions, excludes, includes);
250+
}
251+
/**
252+
* Gets a value indicating whether the specified path exists and is a file.
253+
* @param path The path to test.
254+
*/
255+
fileExists(path: string): boolean {
256+
return ts.sys.fileExists(path);
257+
}
258+
}
259+
260+
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)