"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); // @ignoreDep typescript const fs = require("fs"); const path = require("path"); const ts = require("typescript"); const SourceMap = require("source-map"); const ContextElementDependency = require('webpack/lib/dependencies/ContextElementDependency'); const NodeWatchFileSystem = require('webpack/lib/node/NodeWatchFileSystem'); const ngtools_api_1 = require("./ngtools_api"); const resource_loader_1 = require("./resource_loader"); const compiler_host_1 = require("./compiler_host"); const entry_resolver_1 = require("./entry_resolver"); const paths_plugin_1 = require("./paths-plugin"); const lazy_routes_1 = require("./lazy_routes"); const virtual_file_system_decorator_1 = require("./virtual_file_system_decorator"); const benchmark_1 = require("./benchmark"); const inlineSourceMapRe = /\/\/# sourceMappingURL=data:application\/json;base64,([\s\S]+)$/; class AotPlugin { constructor(options) { this._lazyRoutes = Object.create(null); this._compiler = null; this._compilation = null; this._typeCheck = true; this._skipCodeGeneration = false; this._replaceExport = false; this._diagnoseFiles = {}; this._firstRun = true; ngtools_api_1.CompilerCliIsSupported(); this._options = Object.assign({}, options); this._setupOptions(this._options); } get options() { return this._options; } get basePath() { return this._basePath; } get compilation() { return this._compilation; } get compilerHost() { return this._compilerHost; } get compilerOptions() { return this._compilerOptions; } get done() { return this._donePromise; } get entryModule() { const splitted = this._entryModule.split('#'); const path = splitted[0]; const className = splitted[1] || 'default'; return { path, className }; } get genDir() { return this._genDir; } get program() { return this._program; } get moduleResolutionCache() { return this._moduleResolutionCache; } get skipCodeGeneration() { return this._skipCodeGeneration; } get replaceExport() { return this._replaceExport; } get typeCheck() { return this._typeCheck; } get i18nFile() { return this._i18nFile; } get i18nFormat() { return this._i18nFormat; } get locale() { return this._locale; } get missingTranslation() { return this._missingTranslation; } get firstRun() { return this._firstRun; } get lazyRoutes() { return this._lazyRoutes; } get discoveredLazyRoutes() { return this._discoveredLazyRoutes; } _setupOptions(options) { benchmark_1.time('AotPlugin._setupOptions'); // Fill in the missing options. if (!options.hasOwnProperty('tsConfigPath')) { throw new Error('Must specify "tsConfigPath" in the configuration of @ngtools/webpack.'); } // TS represents paths internally with '/' and expects the tsconfig path to be in this format this._tsConfigPath = options.tsConfigPath.replace(/\\/g, '/'); // Check the base path. const maybeBasePath = path.resolve(process.cwd(), this._tsConfigPath); let basePath = maybeBasePath; if (fs.statSync(maybeBasePath).isFile()) { basePath = path.dirname(basePath); } if (options.hasOwnProperty('basePath')) { basePath = path.resolve(process.cwd(), options.basePath); } const configResult = ts.readConfigFile(this._tsConfigPath, ts.sys.readFile); if (configResult.error) { const diagnostic = configResult.error; const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); if (diagnostic.file) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); throw new Error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`); } else { throw new Error(message); } } const tsConfigJson = configResult.config; if (options.hasOwnProperty('compilerOptions')) { tsConfigJson.compilerOptions = Object.assign({}, tsConfigJson.compilerOptions, options.compilerOptions); } // Default exclude to **/*.spec.ts files. if (!options.hasOwnProperty('exclude')) { options['exclude'] = ['**/*.spec.ts']; } // Add custom excludes to default TypeScript excludes. if (options.hasOwnProperty('exclude')) { // If the tsconfig doesn't contain any excludes, we must add the default ones before adding // any extra ones (otherwise we'd include all of these which can cause unexpected errors). // This is the same logic as present in TypeScript. if (!tsConfigJson.exclude) { tsConfigJson['exclude'] = ['node_modules', 'bower_components', 'jspm_packages']; if (tsConfigJson.compilerOptions && tsConfigJson.compilerOptions.outDir) { tsConfigJson.exclude.push(tsConfigJson.compilerOptions.outDir); } } // Join our custom excludes with the existing ones. tsConfigJson.exclude = tsConfigJson.exclude.concat(options.exclude); } const tsConfig = ts.parseJsonConfigFileContent(tsConfigJson, ts.sys, basePath, undefined, this._tsConfigPath); let fileNames = tsConfig.fileNames; this._rootFilePath = fileNames; // Check the genDir. We generate a default gendir that's under basepath; it will generate // a `node_modules` directory and because of that we don't want TypeScript resolution to // resolve to that directory but the real `node_modules`. let genDir = path.join(basePath, '$$_gendir'); this._compilerOptions = tsConfig.options; // Default plugin sourceMap to compiler options setting. if (!options.hasOwnProperty('sourceMap')) { options.sourceMap = this._compilerOptions.sourceMap || false; } // Force the right sourcemap options. if (options.sourceMap) { this._compilerOptions.sourceMap = true; this._compilerOptions.inlineSources = true; this._compilerOptions.inlineSourceMap = false; this._compilerOptions.sourceRoot = basePath; } else { this._compilerOptions.sourceMap = false; this._compilerOptions.sourceRoot = undefined; this._compilerOptions.inlineSources = undefined; this._compilerOptions.inlineSourceMap = undefined; this._compilerOptions.mapRoot = undefined; } // Default noEmitOnError to true if (this._compilerOptions.noEmitOnError !== false) { this._compilerOptions.noEmitOnError = true; } // Compose Angular Compiler Options. this._angularCompilerOptions = Object.assign({ genDir }, this._compilerOptions, tsConfig.raw['angularCompilerOptions'], { basePath }); if (this._angularCompilerOptions.hasOwnProperty('genDir')) { genDir = path.resolve(basePath, this._angularCompilerOptions.genDir); this._angularCompilerOptions.genDir = genDir; } this._basePath = basePath; this._genDir = genDir; if (options.typeChecking !== undefined) { this._typeCheck = options.typeChecking; } if (options.skipCodeGeneration !== undefined) { this._skipCodeGeneration = options.skipCodeGeneration; } this._compilerHost = new compiler_host_1.WebpackCompilerHost(this._compilerOptions, this._basePath); // Override some files in the FileSystem. if (options.hostOverrideFileSystem) { for (const filePath of Object.keys(options.hostOverrideFileSystem)) { this._compilerHost.writeFile(filePath, options.hostOverrideFileSystem[filePath], false); } } // Override some files in the FileSystem with paths from the actual file system. if (options.hostReplacementPaths) { for (const filePath of Object.keys(options.hostReplacementPaths)) { const replacementFilePath = options.hostReplacementPaths[filePath]; const content = this._compilerHost.readFile(replacementFilePath); this._compilerHost.writeFile(filePath, content, false); } } this._program = ts.createProgram(this._rootFilePath, this._compilerOptions, this._compilerHost); // We use absolute paths everywhere. if (ts.createModuleResolutionCache) { this._moduleResolutionCache = ts.createModuleResolutionCache(this._basePath, (fileName) => this._compilerHost.resolve(fileName)); } // We enable caching of the filesystem in compilerHost _after_ the program has been created, // because we don't want SourceFile instances to be cached past this point. this._compilerHost.enableCaching(); this._resourceLoader = new resource_loader_1.WebpackResourceLoader(); if (options.entryModule) { this._entryModule = options.entryModule; } else if (tsConfig.raw['angularCompilerOptions'] && tsConfig.raw['angularCompilerOptions'].entryModule) { this._entryModule = path.resolve(this._basePath, tsConfig.raw['angularCompilerOptions'].entryModule); } // still no _entryModule? => try to resolve from mainPath if (!this._entryModule && options.mainPath) { const mainPath = path.resolve(basePath, options.mainPath); this._entryModule = entry_resolver_1.resolveEntryModuleFromMain(mainPath, this._compilerHost, this._program); } if (options.hasOwnProperty('i18nFile')) { this._i18nFile = options.i18nFile; } if (options.hasOwnProperty('i18nFormat')) { this._i18nFormat = options.i18nFormat; } if (options.hasOwnProperty('locale')) { this._locale = options.locale; } if (options.hasOwnProperty('replaceExport')) { this._replaceExport = options.replaceExport || this._replaceExport; } if (options.hasOwnProperty('missingTranslation')) { const [MAJOR, MINOR, PATCH] = ngtools_api_1.VERSION.full.split('.').map((x) => parseInt(x, 10)); if (MAJOR < 4 || (MINOR == 2 && PATCH < 2)) { console.warn((`The --missing-translation parameter will be ignored because it is only ` + `compatible with Angular version 4.2.0 or higher. If you want to use it, please ` + `upgrade your Angular version.\n`)); } this._missingTranslation = options.missingTranslation; } benchmark_1.timeEnd('AotPlugin._setupOptions'); } _findLazyRoutesInAst() { benchmark_1.time('AotPlugin._findLazyRoutesInAst'); const result = Object.create(null); const changedFilePaths = this._compilerHost.getChangedFilePaths(); for (const filePath of changedFilePaths) { const fileLazyRoutes = lazy_routes_1.findLazyRoutes(filePath, this._compilerHost, this._program); for (const routeKey of Object.keys(fileLazyRoutes)) { const route = fileLazyRoutes[routeKey]; if (routeKey in this._lazyRoutes) { if (route === null) { this._lazyRoutes[routeKey] = null; } else if (this._lazyRoutes[routeKey] !== route) { this._compilation.warnings.push(new Error(`Duplicated path in loadChildren detected during a rebuild. ` + `We will take the latest version detected and override it to save rebuild time. ` + `You should perform a full build to validate that your routes don't overlap.`)); } } else { result[routeKey] = route; } } } benchmark_1.timeEnd('AotPlugin._findLazyRoutesInAst'); return result; } _getLazyRoutesFromNgtools() { try { benchmark_1.time('AotPlugin._getLazyRoutesFromNgtools'); const result = ngtools_api_1.__NGTOOLS_PRIVATE_API_2.listLazyRoutes({ program: this._program, host: this._compilerHost, angularCompilerOptions: this._angularCompilerOptions, entryModule: this._entryModule }); benchmark_1.timeEnd('AotPlugin._getLazyRoutesFromNgtools'); return result; } catch (err) { // We silence the error that the @angular/router could not be found. In that case, there is // basically no route supported by the app itself. if (err.message.startsWith('Could not resolve module @angular/router')) { return {}; } else { throw err; } } } // registration hook for webpack plugin apply(compiler) { this._compiler = compiler; // Decorate inputFileSystem to serve contents of CompilerHost. // Use decorated inputFileSystem in watchFileSystem. compiler.plugin('environment', () => { compiler.inputFileSystem = new virtual_file_system_decorator_1.VirtualFileSystemDecorator(compiler.inputFileSystem, this._compilerHost); compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem); }); compiler.plugin('invalid', () => { // Turn this off as soon as a file becomes invalid and we're about to start a rebuild. this._firstRun = false; this._diagnoseFiles = {}; }); // Add lazy modules to the context module for @angular/core/src/linker compiler.plugin('context-module-factory', (cmf) => { const angularCorePackagePath = require.resolve('@angular/core/package.json'); const angularCorePackageJson = require(angularCorePackagePath); const angularCoreModulePath = path.resolve(path.dirname(angularCorePackagePath), angularCorePackageJson['module']); // Pick the last part after the last node_modules instance. We do this to let people have // a linked @angular/core or cli which would not be under the same path as the project // being built. const angularCoreModuleDir = path.dirname(angularCoreModulePath).split(/node_modules/).pop(); // Also support the es2015 in Angular versions that have it. let angularCoreEs2015Dir; if (angularCorePackageJson['es2015']) { const angularCoreEs2015Path = path.resolve(path.dirname(angularCorePackagePath), angularCorePackageJson['es2015']); angularCoreEs2015Dir = path.dirname(angularCoreEs2015Path).split(/node_modules/).pop(); } cmf.plugin('after-resolve', (result, callback) => { if (!result) { return callback(); } // Alter only request from Angular; // @angular/core/src/linker matches for 2.*.*, // The other logic is for flat modules and requires reading the package.json of angular // (see above). if (!result.resource.endsWith(path.join('@angular/core/src/linker')) && !(angularCoreModuleDir && result.resource.endsWith(angularCoreModuleDir)) && !(angularCoreEs2015Dir && result.resource.endsWith(angularCoreEs2015Dir))) { return callback(null, result); } this.done.then(() => { result.resource = this.genDir; result.dependencies.forEach((d) => d.critical = false); result.resolveDependencies = (_fs, _resource, _recursive, _regExp, cb) => { const dependencies = Object.keys(this._lazyRoutes) .map((key) => { const value = this._lazyRoutes[key]; if (value !== null) { return new ContextElementDependency(value, key); } else { return null; } }) .filter(x => !!x); cb(null, dependencies); }; return callback(null, result); }, () => callback(null)) .catch(err => callback(err)); }); }); compiler.plugin('make', (compilation, cb) => this._make(compilation, cb)); compiler.plugin('after-emit', (compilation, cb) => { compilation._ngToolsWebpackPluginInstance = null; cb(); }); compiler.plugin('done', () => { this._donePromise = null; this._compilation = null; }); compiler.plugin('after-resolvers', (compiler) => { // Virtual file system. // Wait for the plugin to be done when requesting `.ts` files directly (entry points), or // when the issuer is a `.ts` file. compiler.resolvers.normal.plugin('before-resolve', (request, cb) => { if (this.done && (request.request.endsWith('.ts') || (request.context.issuer && request.context.issuer.endsWith('.ts')))) { this.done.then(() => cb(), () => cb()); } else { cb(); } }); }); compiler.plugin('normal-module-factory', (nmf) => { compiler.resolvers.normal.apply(new paths_plugin_1.PathsPlugin({ nmf, tsConfigPath: this._tsConfigPath, compilerOptions: this._compilerOptions, compilerHost: this._compilerHost })); }); } _translateSourceMap(sourceText, fileName, { line, character }) { const match = sourceText.match(inlineSourceMapRe); if (!match) { return { line, character, fileName }; } // On any error, return line and character. try { const sourceMapJson = JSON.parse(Buffer.from(match[1], 'base64').toString()); const consumer = new SourceMap.SourceMapConsumer(sourceMapJson); const original = consumer.originalPositionFor({ line, column: character }); return { line: typeof original.line == 'number' ? original.line : line, character: typeof original.column == 'number' ? original.column : character, fileName: original.source || fileName }; } catch (e) { return { line, character, fileName }; } } diagnose(fileName) { if (this._diagnoseFiles[fileName]) { return; } this._diagnoseFiles[fileName] = true; const sourceFile = this._program.getSourceFile(fileName); if (!sourceFile) { return; } const diagnostics = [ ...(this._program.getCompilerOptions().declaration ? this._program.getDeclarationDiagnostics(sourceFile) : []), ...this._program.getSyntacticDiagnostics(sourceFile), ...this._program.getSemanticDiagnostics(sourceFile) ]; if (diagnostics.length > 0) { diagnostics.forEach(diagnostic => { const messageText = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); let message; if (diagnostic.file) { const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); const sourceText = diagnostic.file.getFullText(); let { line, character, fileName } = this._translateSourceMap(sourceText, diagnostic.file.fileName, position); message = `${fileName} (${line + 1},${character + 1}): ${messageText}`; } else { message = messageText; } switch (diagnostic.category) { case ts.DiagnosticCategory.Error: this._compilation.errors.push(message); break; default: this._compilation.warnings.push(message); } }); } } _make(compilation, cb) { benchmark_1.time('AotPlugin._make'); this._compilation = compilation; if (this._compilation._ngToolsWebpackPluginInstance) { return cb(new Error('An @ngtools/webpack plugin already exist for this compilation.')); } this._compilation._ngToolsWebpackPluginInstance = this; this._resourceLoader.update(compilation); this._donePromise = Promise.resolve() .then(() => { if (this._skipCodeGeneration) { return; } benchmark_1.time('AotPlugin._make.codeGen'); // Create the Code Generator. return ngtools_api_1.__NGTOOLS_PRIVATE_API_2.codeGen({ basePath: this._basePath, compilerOptions: this._compilerOptions, program: this._program, host: this._compilerHost, angularCompilerOptions: this._angularCompilerOptions, i18nFile: this.i18nFile, i18nFormat: this.i18nFormat, locale: this.locale, missingTranslation: this.missingTranslation, readResource: (path) => this._resourceLoader.get(path) }) .then(() => benchmark_1.timeEnd('AotPlugin._make.codeGen')); }) .then(() => { // Get the ngfactory that were created by the previous step, and add them to the root // file path (if those files exists). const newRootFilePath = this._compilerHost.getChangedFilePaths() .filter(x => x.match(/\.ngfactory\.ts$/)); // Remove files that don't exist anymore, and add new files. this._rootFilePath = this._rootFilePath .filter(x => this._compilerHost.fileExists(x)) .concat(newRootFilePath); // Create a new Program, based on the old one. This will trigger a resolution of all // transitive modules, which include files that might just have been generated. // This needs to happen after the code generator has been created for generated files // to be properly resolved. benchmark_1.time('AotPlugin._make.createProgram'); this._program = ts.createProgram(this._rootFilePath, this._compilerOptions, this._compilerHost, this._program); benchmark_1.timeEnd('AotPlugin._make.createProgram'); }) .then(() => { // Re-diagnose changed files. benchmark_1.time('AotPlugin._make.diagnose'); const changedFilePaths = this._compilerHost.getChangedFilePaths(); changedFilePaths.forEach(filePath => this.diagnose(filePath)); benchmark_1.timeEnd('AotPlugin._make.diagnose'); }) .then(() => { if (this._typeCheck) { benchmark_1.time('AotPlugin._make._typeCheck'); const diagnostics = this._program.getGlobalDiagnostics(); if (diagnostics.length > 0) { const message = diagnostics .map(diagnostic => { const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); if (diagnostic.file) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message})`; } else { return message; } }) .join('\n'); throw new Error(message); } benchmark_1.timeEnd('AotPlugin._make._typeCheck'); } }) .then(() => { // We need to run the `listLazyRoutes` the first time because it also navigates libraries // and other things that we might miss using the findLazyRoutesInAst. benchmark_1.time('AotPlugin._make._discoveredLazyRoutes'); this._discoveredLazyRoutes = this.firstRun ? this._getLazyRoutesFromNgtools() : this._findLazyRoutesInAst(); // Process the lazy routes discovered. Object.keys(this.discoveredLazyRoutes) .forEach(k => { const lazyRoute = this.discoveredLazyRoutes[k]; k = k.split('#')[0]; if (lazyRoute === null) { return; } if (this.skipCodeGeneration) { this._lazyRoutes[k] = lazyRoute; } else { const factoryPath = lazyRoute.replace(/(\.d)?\.ts$/, '.ngfactory.ts'); const lr = path.relative(this.basePath, factoryPath); this._lazyRoutes[k + '.ngfactory'] = path.join(this.genDir, lr); } }); benchmark_1.timeEnd('AotPlugin._make._discoveredLazyRoutes'); }) .then(() => { if (this._compilation.errors == 0) { this._compilerHost.resetChangedFileTracker(); } benchmark_1.timeEnd('AotPlugin._make'); cb(); }, (err) => { compilation.errors.push(err.stack); benchmark_1.timeEnd('AotPlugin._make'); cb(); }); } } exports.AotPlugin = AotPlugin; //# sourceMappingURL=/users/hansl/sources/hansl/angular-cli/src/plugin.js.map