|
| 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 | + |
0 commit comments