mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 19:13:34 +08:00
1189 lines
45 KiB
TypeScript
1189 lines
45 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import { Path, dirname, getSystemPath, normalize, resolve, virtualFs } from '@angular-devkit/core';
|
|
import { ChildProcess, ForkOptions, fork } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
import { Compiler, compilation } from 'webpack';
|
|
import { time, timeEnd } from './benchmark';
|
|
import { WebpackCompilerHost, workaroundResolve } from './compiler_host';
|
|
import { resolveEntryModuleFromMain } from './entry_resolver';
|
|
import { gatherDiagnostics, hasErrors } from './gather_diagnostics';
|
|
import { LazyRouteMap, findLazyRoutes } from './lazy_routes';
|
|
import {
|
|
CompilerCliIsSupported,
|
|
CompilerHost,
|
|
CompilerOptions,
|
|
DEFAULT_ERROR_CODE,
|
|
Diagnostic,
|
|
EmitFlags,
|
|
Program,
|
|
SOURCE,
|
|
UNKNOWN_ERROR_CODE,
|
|
VERSION,
|
|
__NGTOOLS_PRIVATE_API_2,
|
|
createCompilerHost,
|
|
createProgram,
|
|
formatDiagnostics,
|
|
readConfiguration,
|
|
} from './ngtools_api';
|
|
import { resolveWithPaths } from './paths-plugin';
|
|
import { WebpackResourceLoader } from './resource_loader';
|
|
import {
|
|
exportLazyModuleMap,
|
|
exportNgFactory,
|
|
findResources,
|
|
registerLocaleData,
|
|
removeDecorators,
|
|
replaceBootstrap,
|
|
replaceResources,
|
|
replaceServerBootstrap,
|
|
} from './transformers';
|
|
import { collectDeepNodes } from './transformers/ast_helpers';
|
|
import { AUTO_START_ARG, InitMessage, UpdateMessage } from './type_checker';
|
|
import {
|
|
VirtualFileSystemDecorator,
|
|
VirtualWatchFileSystemDecorator,
|
|
} from './virtual_file_system_decorator';
|
|
import {
|
|
Callback,
|
|
InputFileSystem,
|
|
NodeWatchFileSystemInterface,
|
|
NormalModuleFactoryRequest,
|
|
} from './webpack';
|
|
import { WebpackInputHost } from './webpack-input-host';
|
|
|
|
const treeKill = require('tree-kill');
|
|
|
|
export interface ContextElementDependency {}
|
|
|
|
export interface ContextElementDependencyConstructor {
|
|
new(modulePath: string, name: string): ContextElementDependency;
|
|
}
|
|
|
|
/**
|
|
* Option Constants
|
|
*/
|
|
export interface AngularCompilerPluginOptions {
|
|
sourceMap?: boolean;
|
|
tsConfigPath: string;
|
|
basePath?: string;
|
|
entryModule?: string;
|
|
mainPath?: string;
|
|
skipCodeGeneration?: boolean;
|
|
hostReplacementPaths?: { [path: string]: string } | ((path: string) => string);
|
|
forkTypeChecker?: boolean;
|
|
// TODO: remove singleFileIncludes for 2.0, this is just to support old projects that did not
|
|
// include 'polyfills.ts' in `tsconfig.spec.json'.
|
|
singleFileIncludes?: string[];
|
|
i18nInFile?: string;
|
|
i18nInFormat?: string;
|
|
i18nOutFile?: string;
|
|
i18nOutFormat?: string;
|
|
locale?: string;
|
|
missingTranslation?: string;
|
|
platform?: PLATFORM;
|
|
nameLazyFiles?: boolean;
|
|
|
|
// added to the list of lazy routes
|
|
additionalLazyModules?: { [module: string]: string };
|
|
|
|
// The ContextElementDependency of correct Webpack compilation.
|
|
// This is needed when there are multiple Webpack installs.
|
|
contextElementDependencyConstructor?: ContextElementDependencyConstructor;
|
|
|
|
// Use tsconfig to include path globs.
|
|
compilerOptions?: ts.CompilerOptions;
|
|
|
|
host?: virtualFs.Host<fs.Stats>;
|
|
platformTransformers?: ts.TransformerFactory<ts.SourceFile>[];
|
|
}
|
|
|
|
export enum PLATFORM {
|
|
Browser,
|
|
Server,
|
|
}
|
|
|
|
export class AngularCompilerPlugin {
|
|
private _options: AngularCompilerPluginOptions;
|
|
|
|
// TS compilation.
|
|
private _compilerOptions: CompilerOptions;
|
|
private _rootNames: string[];
|
|
private _singleFileIncludes: string[] = [];
|
|
private _program: (ts.Program | Program) | null;
|
|
private _compilerHost: WebpackCompilerHost & CompilerHost;
|
|
private _moduleResolutionCache: ts.ModuleResolutionCache;
|
|
private _resourceLoader: WebpackResourceLoader;
|
|
// Contains `moduleImportPath#exportName` => `fullModulePath`.
|
|
private _lazyRoutes: LazyRouteMap = Object.create(null);
|
|
private _tsConfigPath: string;
|
|
private _entryModule: string | null;
|
|
private _mainPath: string | undefined;
|
|
private _basePath: string;
|
|
private _transformers: ts.TransformerFactory<ts.SourceFile>[] = [];
|
|
private _platformTransformers: ts.TransformerFactory<ts.SourceFile>[] | null = null;
|
|
private _platform: PLATFORM;
|
|
private _JitMode = false;
|
|
private _emitSkipped = true;
|
|
private _changedFileExtensions = new Set(['ts', 'html', 'css']);
|
|
|
|
// Webpack plugin.
|
|
private _firstRun = true;
|
|
private _donePromise: Promise<void> | null;
|
|
private _normalizedLocale: string | null;
|
|
private _warnings: (string | Error)[] = [];
|
|
private _errors: (string | Error)[] = [];
|
|
private _contextElementDependencyConstructor: ContextElementDependencyConstructor;
|
|
|
|
// TypeChecker process.
|
|
private _forkTypeChecker = true;
|
|
private _typeCheckerProcess: ChildProcess | null;
|
|
private _forkedTypeCheckerInitialized = false;
|
|
|
|
private get _ngCompilerSupportsNewApi() {
|
|
if (this._JitMode) {
|
|
return false;
|
|
} else {
|
|
return !!(this._program as Program).listLazyRoutes;
|
|
}
|
|
}
|
|
|
|
constructor(options: AngularCompilerPluginOptions) {
|
|
CompilerCliIsSupported();
|
|
this._options = Object.assign({}, options);
|
|
this._setupOptions(this._options);
|
|
}
|
|
|
|
get options() { return this._options; }
|
|
get done() { return this._donePromise; }
|
|
get entryModule() {
|
|
if (!this._entryModule) {
|
|
return null;
|
|
}
|
|
const splitted = this._entryModule.split(/(#[a-zA-Z_]([\w]+))$/);
|
|
const path = splitted[0];
|
|
const className = !!splitted[1] ? splitted[1].substring(1) : 'default';
|
|
|
|
return { path, className };
|
|
}
|
|
|
|
get typeChecker(): ts.TypeChecker | null {
|
|
const tsProgram = this._getTsProgram();
|
|
|
|
return tsProgram ? tsProgram.getTypeChecker() : null;
|
|
}
|
|
|
|
static isSupported() {
|
|
return VERSION && parseInt(VERSION.major) >= 5;
|
|
}
|
|
|
|
private _setupOptions(options: AngularCompilerPluginOptions) {
|
|
time('AngularCompilerPlugin._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.basePath !== undefined) {
|
|
basePath = path.resolve(process.cwd(), options.basePath);
|
|
}
|
|
|
|
if (options.singleFileIncludes !== undefined) {
|
|
this._singleFileIncludes.push(...options.singleFileIncludes);
|
|
}
|
|
|
|
// Parse the tsconfig contents.
|
|
const config = readConfiguration(this._tsConfigPath);
|
|
if (config.errors && config.errors.length) {
|
|
throw new Error(formatDiagnostics(config.errors));
|
|
}
|
|
|
|
this._rootNames = config.rootNames.concat(...this._singleFileIncludes);
|
|
this._compilerOptions = { ...config.options, ...options.compilerOptions };
|
|
this._basePath = config.options.basePath || basePath || '';
|
|
|
|
// Overwrite outDir so we can find generated files next to their .ts origin in compilerHost.
|
|
this._compilerOptions.outDir = '';
|
|
this._compilerOptions.suppressOutputPathCheck = true;
|
|
|
|
// 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.mapRoot = undefined;
|
|
// We will set the source to the full path of the file in the loader, so we don't
|
|
// need sourceRoot here.
|
|
this._compilerOptions.sourceRoot = undefined;
|
|
} else {
|
|
this._compilerOptions.sourceMap = false;
|
|
this._compilerOptions.sourceRoot = undefined;
|
|
this._compilerOptions.inlineSources = undefined;
|
|
this._compilerOptions.inlineSourceMap = undefined;
|
|
this._compilerOptions.mapRoot = undefined;
|
|
this._compilerOptions.sourceRoot = undefined;
|
|
}
|
|
|
|
// We want to allow emitting with errors so that imports can be added
|
|
// to the webpack dependency tree and rebuilds triggered by file edits.
|
|
this._compilerOptions.noEmitOnError = false;
|
|
|
|
// Set JIT (no code generation) or AOT mode.
|
|
if (options.skipCodeGeneration !== undefined) {
|
|
this._JitMode = options.skipCodeGeneration;
|
|
}
|
|
|
|
// Process i18n options.
|
|
if (options.i18nInFile !== undefined) {
|
|
this._compilerOptions.i18nInFile = options.i18nInFile;
|
|
}
|
|
if (options.i18nInFormat !== undefined) {
|
|
this._compilerOptions.i18nInFormat = options.i18nInFormat;
|
|
}
|
|
if (options.i18nOutFile !== undefined) {
|
|
this._compilerOptions.i18nOutFile = options.i18nOutFile;
|
|
}
|
|
if (options.i18nOutFormat !== undefined) {
|
|
this._compilerOptions.i18nOutFormat = options.i18nOutFormat;
|
|
}
|
|
if (options.locale !== undefined) {
|
|
this._compilerOptions.i18nInLocale = options.locale;
|
|
this._compilerOptions.i18nOutLocale = options.locale;
|
|
this._normalizedLocale = this._validateLocale(options.locale);
|
|
}
|
|
if (options.missingTranslation !== undefined) {
|
|
this._compilerOptions.i18nInMissingTranslations =
|
|
options.missingTranslation as 'error' | 'warning' | 'ignore';
|
|
}
|
|
|
|
// Process forked type checker options.
|
|
if (options.forkTypeChecker !== undefined) {
|
|
this._forkTypeChecker = options.forkTypeChecker;
|
|
}
|
|
|
|
// Add custom platform transformers.
|
|
if (options.platformTransformers !== undefined) {
|
|
this._platformTransformers = options.platformTransformers;
|
|
}
|
|
|
|
// Default ContextElementDependency to the one we can import from here.
|
|
// Failing to use the right ContextElementDependency will throw the error below:
|
|
// "No module factory available for dependency type: ContextElementDependency"
|
|
// Hoisting together with peer dependencies can make it so the imported
|
|
// ContextElementDependency does not come from the same Webpack instance that is used
|
|
// in the compilation. In that case, we can pass the right one as an option to the plugin.
|
|
this._contextElementDependencyConstructor = options.contextElementDependencyConstructor
|
|
|| require('webpack/lib/dependencies/ContextElementDependency');
|
|
|
|
// Use entryModule if available in options, otherwise resolve it from mainPath after program
|
|
// creation.
|
|
if (this._options.entryModule) {
|
|
this._entryModule = this._options.entryModule;
|
|
} else if (this._compilerOptions.entryModule) {
|
|
this._entryModule = path.resolve(this._basePath,
|
|
this._compilerOptions.entryModule as string); // temporary cast for type issue
|
|
}
|
|
|
|
// Set platform.
|
|
this._platform = options.platform || PLATFORM.Browser;
|
|
|
|
// Make transformers.
|
|
this._makeTransformers();
|
|
|
|
timeEnd('AngularCompilerPlugin._setupOptions');
|
|
}
|
|
|
|
private _getTsProgram() {
|
|
return this._JitMode ? this._program as ts.Program : (this._program as Program).getTsProgram();
|
|
}
|
|
|
|
private _getChangedTsFiles() {
|
|
return this._compilerHost.getChangedFilePaths()
|
|
.filter(k => (k.endsWith('.ts') || k.endsWith('.tsx')) && !k.endsWith('.d.ts'))
|
|
.filter(k => this._compilerHost.fileExists(k));
|
|
}
|
|
|
|
updateChangedFileExtensions(extension: string) {
|
|
if (extension) {
|
|
this._changedFileExtensions.add(extension);
|
|
}
|
|
}
|
|
|
|
private _getChangedCompilationFiles() {
|
|
return this._compilerHost.getChangedFilePaths()
|
|
.filter(k => {
|
|
for (const ext of this._changedFileExtensions) {
|
|
if (k.endsWith(ext)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
private _createOrUpdateProgram() {
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
// Get the root files from the ts config.
|
|
// When a new root name (like a lazy route) is added, it won't be available from
|
|
// following imports on the existing files, so we need to get the new list of root files.
|
|
const config = readConfiguration(this._tsConfigPath);
|
|
this._rootNames = config.rootNames.concat(...this._singleFileIncludes);
|
|
|
|
// Update the forked type checker with all changed compilation files.
|
|
// This includes templates, that also need to be reloaded on the type checker.
|
|
if (this._forkTypeChecker && this._typeCheckerProcess && !this._firstRun) {
|
|
this._updateForkedTypeChecker(this._rootNames, this._getChangedCompilationFiles());
|
|
}
|
|
|
|
// Use an identity function as all our paths are absolute already.
|
|
this._moduleResolutionCache = ts.createModuleResolutionCache(this._basePath, x => x);
|
|
|
|
if (this._JitMode) {
|
|
// Create the TypeScript program.
|
|
time('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram');
|
|
this._program = ts.createProgram(
|
|
this._rootNames,
|
|
this._compilerOptions,
|
|
this._compilerHost,
|
|
this._program as ts.Program,
|
|
);
|
|
timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ts.createProgram');
|
|
|
|
return Promise.resolve();
|
|
} else {
|
|
time('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram');
|
|
// Create the Angular program.
|
|
this._program = createProgram({
|
|
rootNames: this._rootNames,
|
|
options: this._compilerOptions,
|
|
host: this._compilerHost,
|
|
oldProgram: this._program as Program,
|
|
});
|
|
timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.createProgram');
|
|
|
|
time('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync');
|
|
|
|
return this._program.loadNgStructureAsync()
|
|
.then(() => {
|
|
timeEnd('AngularCompilerPlugin._createOrUpdateProgram.ng.loadNgStructureAsync');
|
|
});
|
|
}
|
|
})
|
|
.then(() => {
|
|
// If there's still no entryModule try to resolve from mainPath.
|
|
if (!this._entryModule && this._mainPath) {
|
|
time('AngularCompilerPlugin._make.resolveEntryModuleFromMain');
|
|
this._entryModule = resolveEntryModuleFromMain(
|
|
this._mainPath, this._compilerHost, this._getTsProgram());
|
|
timeEnd('AngularCompilerPlugin._make.resolveEntryModuleFromMain');
|
|
}
|
|
});
|
|
}
|
|
|
|
private _getLazyRoutesFromNgtools() {
|
|
try {
|
|
time('AngularCompilerPlugin._getLazyRoutesFromNgtools');
|
|
const result = __NGTOOLS_PRIVATE_API_2.listLazyRoutes({
|
|
program: this._getTsProgram(),
|
|
host: this._compilerHost,
|
|
angularCompilerOptions: Object.assign({}, this._compilerOptions, {
|
|
// genDir seems to still be needed in @angular\compiler-cli\src\compiler_host.js:226.
|
|
genDir: '',
|
|
}),
|
|
// TODO: fix compiler-cli typings; entryModule should not be string, but also optional.
|
|
// tslint:disable-next-line:no-non-null-assertion
|
|
entryModule: this._entryModule !,
|
|
});
|
|
timeEnd('AngularCompilerPlugin._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;
|
|
}
|
|
}
|
|
}
|
|
|
|
private _findLazyRoutesInAst(changedFilePaths: string[]): LazyRouteMap {
|
|
time('AngularCompilerPlugin._findLazyRoutesInAst');
|
|
const result: LazyRouteMap = Object.create(null);
|
|
for (const filePath of changedFilePaths) {
|
|
const fileLazyRoutes = findLazyRoutes(filePath, this._compilerHost, undefined,
|
|
this._compilerOptions);
|
|
for (const routeKey of Object.keys(fileLazyRoutes)) {
|
|
const route = fileLazyRoutes[routeKey];
|
|
result[routeKey] = route;
|
|
}
|
|
}
|
|
timeEnd('AngularCompilerPlugin._findLazyRoutesInAst');
|
|
|
|
return result;
|
|
}
|
|
|
|
private _listLazyRoutesFromProgram(): LazyRouteMap {
|
|
const ngProgram = this._program as Program;
|
|
if (!ngProgram.listLazyRoutes) {
|
|
throw new Error('_listLazyRoutesFromProgram was called with an old program.');
|
|
}
|
|
|
|
const lazyRoutes = ngProgram.listLazyRoutes();
|
|
|
|
return lazyRoutes.reduce(
|
|
(acc, curr) => {
|
|
const ref = curr.route;
|
|
if (ref in acc && acc[ref] !== curr.referencedModule.filePath) {
|
|
throw new Error(
|
|
+ `Duplicated path in loadChildren detected: "${ref}" is used in 2 loadChildren, `
|
|
+ `but they point to different modules "(${acc[ref]} and `
|
|
+ `"${curr.referencedModule.filePath}"). Webpack cannot distinguish on context and `
|
|
+ 'would fail to load the proper one.',
|
|
);
|
|
}
|
|
acc[ref] = curr.referencedModule.filePath;
|
|
|
|
return acc;
|
|
},
|
|
{} as LazyRouteMap,
|
|
);
|
|
}
|
|
|
|
// Process the lazy routes discovered, adding then to _lazyRoutes.
|
|
// TODO: find a way to remove lazy routes that don't exist anymore.
|
|
// This will require a registry of known references to a lazy route, removing it when no
|
|
// module references it anymore.
|
|
private _processLazyRoutes(discoveredLazyRoutes: LazyRouteMap) {
|
|
Object.keys(discoveredLazyRoutes)
|
|
.forEach(lazyRouteKey => {
|
|
const [lazyRouteModule, moduleName] = lazyRouteKey.split('#');
|
|
|
|
if (!lazyRouteModule) {
|
|
return;
|
|
}
|
|
|
|
const lazyRouteTSFile = discoveredLazyRoutes[lazyRouteKey].replace(/\\/g, '/');
|
|
let modulePath: string, moduleKey: string;
|
|
|
|
if (this._JitMode) {
|
|
modulePath = lazyRouteTSFile;
|
|
moduleKey = `${lazyRouteModule}${moduleName ? '#' + moduleName : ''}`;
|
|
} else {
|
|
modulePath = lazyRouteTSFile.replace(/(\.d)?\.tsx?$/, '');
|
|
modulePath += '.ngfactory.js';
|
|
const factoryModuleName = moduleName ? `#${moduleName}NgFactory` : '';
|
|
moduleKey = `${lazyRouteModule}.ngfactory${factoryModuleName}`;
|
|
}
|
|
|
|
modulePath = workaroundResolve(modulePath);
|
|
|
|
if (moduleKey in this._lazyRoutes) {
|
|
if (this._lazyRoutes[moduleKey] !== modulePath) {
|
|
// Found a duplicate, this is an error.
|
|
this._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 {
|
|
// Found a new route, add it to the map.
|
|
this._lazyRoutes[moduleKey] = modulePath;
|
|
}
|
|
});
|
|
}
|
|
|
|
private _createForkedTypeChecker() {
|
|
// Bootstrap type checker is using local CLI.
|
|
const g: any = typeof global !== 'undefined' ? global : {}; // tslint:disable-line:no-any
|
|
const typeCheckerFile: string = g['_DevKitIsLocal']
|
|
? './type_checker_bootstrap.js'
|
|
: './type_checker_worker.js';
|
|
|
|
const debugArgRegex = /--inspect(?:-brk|-port)?|--debug(?:-brk|-port)/;
|
|
|
|
const execArgv = process.execArgv.filter((arg) => {
|
|
// Remove debug args.
|
|
// Workaround for https://github.com/nodejs/node/issues/9435
|
|
return !debugArgRegex.test(arg);
|
|
});
|
|
// Signal the process to start listening for messages
|
|
// Solves https://github.com/angular/angular-cli/issues/9071
|
|
const forkArgs = [AUTO_START_ARG];
|
|
const forkOptions: ForkOptions = { execArgv };
|
|
|
|
this._typeCheckerProcess = fork(
|
|
path.resolve(__dirname, typeCheckerFile),
|
|
forkArgs,
|
|
forkOptions);
|
|
|
|
// Handle child process exit.
|
|
this._typeCheckerProcess.once('exit', (_, signal) => {
|
|
this._typeCheckerProcess = null;
|
|
|
|
// If process exited not because of SIGTERM (see _killForkedTypeChecker), than something
|
|
// went wrong and it should fallback to type checking on the main thread.
|
|
if (signal !== 'SIGTERM') {
|
|
this._forkTypeChecker = false;
|
|
const msg = 'AngularCompilerPlugin: Forked Type Checker exited unexpectedly. ' +
|
|
'Falling back to type checking on main thread.';
|
|
this._warnings.push(msg);
|
|
}
|
|
});
|
|
}
|
|
|
|
private _killForkedTypeChecker() {
|
|
if (this._typeCheckerProcess && this._typeCheckerProcess.pid) {
|
|
treeKill(this._typeCheckerProcess.pid, 'SIGTERM');
|
|
this._typeCheckerProcess = null;
|
|
}
|
|
}
|
|
|
|
private _updateForkedTypeChecker(rootNames: string[], changedCompilationFiles: string[]) {
|
|
if (this._typeCheckerProcess) {
|
|
if (!this._forkedTypeCheckerInitialized) {
|
|
this._typeCheckerProcess.send(new InitMessage(this._compilerOptions, this._basePath,
|
|
this._JitMode, this._rootNames));
|
|
this._forkedTypeCheckerInitialized = true;
|
|
}
|
|
this._typeCheckerProcess.send(new UpdateMessage(rootNames, changedCompilationFiles));
|
|
}
|
|
}
|
|
|
|
// Registration hook for webpack plugin.
|
|
apply(compiler: Compiler) {
|
|
// Decorate inputFileSystem to serve contents of CompilerHost.
|
|
// Use decorated inputFileSystem in watchFileSystem.
|
|
compiler.hooks.environment.tap('angular-compiler', () => {
|
|
// The webpack types currently do not include these
|
|
const compilerWithFileSystems = compiler as Compiler & {
|
|
inputFileSystem: InputFileSystem,
|
|
watchFileSystem: NodeWatchFileSystemInterface,
|
|
};
|
|
|
|
let host: virtualFs.Host<fs.Stats> = this._options.host || new WebpackInputHost(
|
|
compilerWithFileSystems.inputFileSystem,
|
|
);
|
|
|
|
let replacements: Map<Path, Path> | ((path: Path) => Path) | undefined;
|
|
if (this._options.hostReplacementPaths) {
|
|
if (typeof this._options.hostReplacementPaths == 'function') {
|
|
const replacementResolver = this._options.hostReplacementPaths;
|
|
replacements = path => normalize(replacementResolver(getSystemPath(path)));
|
|
host = new class extends virtualFs.ResolverHost<fs.Stats> {
|
|
_resolve(path: Path) {
|
|
return normalize(replacementResolver(getSystemPath(path)));
|
|
}
|
|
}(host);
|
|
} else {
|
|
replacements = new Map();
|
|
const aliasHost = new virtualFs.AliasHost(host);
|
|
for (const from in this._options.hostReplacementPaths) {
|
|
const normalizedFrom = resolve(normalize(this._basePath), normalize(from));
|
|
const normalizedWith = resolve(
|
|
normalize(this._basePath),
|
|
normalize(this._options.hostReplacementPaths[from]),
|
|
);
|
|
aliasHost.aliases.set(normalizedFrom, normalizedWith);
|
|
replacements.set(normalizedFrom, normalizedWith);
|
|
}
|
|
host = aliasHost;
|
|
}
|
|
}
|
|
|
|
// Create the webpack compiler host.
|
|
const webpackCompilerHost = new WebpackCompilerHost(
|
|
this._compilerOptions,
|
|
this._basePath,
|
|
host,
|
|
);
|
|
webpackCompilerHost.enableCaching();
|
|
|
|
// Create and set a new WebpackResourceLoader.
|
|
this._resourceLoader = new WebpackResourceLoader();
|
|
webpackCompilerHost.setResourceLoader(this._resourceLoader);
|
|
|
|
// Use the WebpackCompilerHost with a resource loader to create an AngularCompilerHost.
|
|
this._compilerHost = createCompilerHost({
|
|
options: this._compilerOptions,
|
|
tsHost: webpackCompilerHost,
|
|
}) as CompilerHost & WebpackCompilerHost;
|
|
|
|
// Resolve mainPath if provided.
|
|
if (this._options.mainPath) {
|
|
this._mainPath = this._compilerHost.resolve(this._options.mainPath);
|
|
}
|
|
|
|
const inputDecorator = new VirtualFileSystemDecorator(
|
|
compilerWithFileSystems.inputFileSystem,
|
|
this._compilerHost,
|
|
);
|
|
compilerWithFileSystems.inputFileSystem = inputDecorator;
|
|
compilerWithFileSystems.watchFileSystem = new VirtualWatchFileSystemDecorator(
|
|
inputDecorator,
|
|
replacements,
|
|
);
|
|
});
|
|
|
|
// Add lazy modules to the context module for @angular/core
|
|
compiler.hooks.contextModuleFactory.tap('angular-compiler', cmf => {
|
|
const angularCorePackagePath = require.resolve('@angular/core/package.json');
|
|
|
|
// APFv6 does not have single FESM anymore. Instead of verifying if we're pointing to
|
|
// FESMs, we resolve the `@angular/core` path and verify that the path for the
|
|
// module starts with it.
|
|
|
|
// This may be slower but it will be compatible with both APF5, 6 and potential future
|
|
// versions (until the dynamic import appears outside of core I suppose).
|
|
// We resolve any symbolic links in order to get the real path that would be used in webpack.
|
|
const angularCoreDirname = fs.realpathSync(path.dirname(angularCorePackagePath));
|
|
|
|
cmf.hooks.afterResolve.tapPromise('angular-compiler', async result => {
|
|
// Alter only request from Angular.
|
|
if (!result || !this.done || !result.resource.startsWith(angularCoreDirname)) {
|
|
return result;
|
|
}
|
|
|
|
return this.done.then(
|
|
() => {
|
|
// This folder does not exist, but we need to give webpack a resource.
|
|
// TODO: check if we can't just leave it as is (angularCoreModuleDir).
|
|
result.resource = path.join(this._basePath, '$$_lazy_route_resource');
|
|
// tslint:disable-next-line:no-any
|
|
result.dependencies.forEach((d: any) => d.critical = false);
|
|
// tslint:disable-next-line:no-any
|
|
result.resolveDependencies = (_fs: any, options: any, callback: Callback) => {
|
|
const dependencies = Object.keys(this._lazyRoutes)
|
|
.map((key) => {
|
|
const modulePath = this._lazyRoutes[key];
|
|
const importPath = key.split('#')[0];
|
|
if (modulePath !== null) {
|
|
const name = importPath.replace(/(\.ngfactory)?\.(js|ts)$/, '');
|
|
|
|
return new this._contextElementDependencyConstructor(modulePath, name);
|
|
} else {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(x => !!x);
|
|
|
|
if (this._options.nameLazyFiles) {
|
|
options.chunkName = '[request]';
|
|
}
|
|
|
|
callback(null, dependencies);
|
|
};
|
|
|
|
return result;
|
|
},
|
|
() => undefined,
|
|
);
|
|
});
|
|
});
|
|
|
|
// Create and destroy forked type checker on watch mode.
|
|
compiler.hooks.watchRun.tap('angular-compiler', () => {
|
|
if (this._forkTypeChecker && !this._typeCheckerProcess) {
|
|
this._createForkedTypeChecker();
|
|
}
|
|
});
|
|
compiler.hooks.watchClose.tap('angular-compiler', () => this._killForkedTypeChecker());
|
|
|
|
// Remake the plugin on each compilation.
|
|
compiler.hooks.make.tapPromise('angular-compiler', compilation => this._make(compilation));
|
|
compiler.hooks.invalid.tap('angular-compiler', () => this._firstRun = false);
|
|
compiler.hooks.afterEmit.tap('angular-compiler', compilation => {
|
|
// tslint:disable-next-line:no-any
|
|
(compilation as any)._ngToolsWebpackPluginInstance = null;
|
|
});
|
|
compiler.hooks.done.tap('angular-compiler', () => {
|
|
this._donePromise = null;
|
|
});
|
|
|
|
compiler.hooks.afterResolvers.tap('angular-compiler', compiler => {
|
|
compiler.hooks.normalModuleFactory.tap('angular-compiler', nmf => {
|
|
// Virtual file system.
|
|
// TODO: consider if it's better to remove this plugin and instead make it wait on the
|
|
// VirtualFileSystemDecorator.
|
|
// Wait for the plugin to be done when requesting `.ts` files directly (entry points), or
|
|
// when the issuer is a `.ts` or `.ngfactory.js` file.
|
|
nmf.hooks.beforeResolve.tapPromise(
|
|
'angular-compiler',
|
|
async (request?: NormalModuleFactoryRequest) => {
|
|
if (this.done && request) {
|
|
const name = request.request;
|
|
const issuer = request.contextInfo.issuer;
|
|
if (name.endsWith('.ts') || name.endsWith('.tsx')
|
|
|| (issuer && /\.ts|ngfactory\.js$/.test(issuer))) {
|
|
try {
|
|
await this.done;
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
return request;
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
compiler.hooks.normalModuleFactory.tap('angular-compiler', nmf => {
|
|
nmf.hooks.beforeResolve.tapAsync(
|
|
'angular-compiler',
|
|
(request: NormalModuleFactoryRequest, callback: Callback<NormalModuleFactoryRequest>) => {
|
|
resolveWithPaths(
|
|
request,
|
|
callback,
|
|
this._compilerOptions,
|
|
this._compilerHost,
|
|
this._moduleResolutionCache,
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
private async _make(compilation: compilation.Compilation) {
|
|
time('AngularCompilerPlugin._make');
|
|
this._emitSkipped = true;
|
|
// tslint:disable-next-line:no-any
|
|
if ((compilation as any)._ngToolsWebpackPluginInstance) {
|
|
throw new Error('An @ngtools/webpack plugin already exist for this compilation.');
|
|
}
|
|
|
|
// Set a private variable for this plugin instance.
|
|
// tslint:disable-next-line:no-any
|
|
(compilation as any)._ngToolsWebpackPluginInstance = this;
|
|
|
|
// Update the resource loader with the new webpack compilation.
|
|
this._resourceLoader.update(compilation);
|
|
|
|
return this._donePromise = Promise.resolve()
|
|
.then(() => this._update())
|
|
.then(() => {
|
|
this.pushCompilationErrors(compilation);
|
|
timeEnd('AngularCompilerPlugin._make');
|
|
}, err => {
|
|
compilation.errors.push(err);
|
|
this.pushCompilationErrors(compilation);
|
|
timeEnd('AngularCompilerPlugin._make');
|
|
});
|
|
}
|
|
|
|
private pushCompilationErrors(compilation: compilation.Compilation) {
|
|
compilation.errors.push(...this._errors);
|
|
compilation.warnings.push(...this._warnings);
|
|
this._errors = [];
|
|
this._warnings = [];
|
|
}
|
|
|
|
private _makeTransformers() {
|
|
const isAppPath = (fileName: string) =>
|
|
!fileName.endsWith('.ngfactory.ts') && !fileName.endsWith('.ngstyle.ts');
|
|
const isMainPath = (fileName: string) => fileName === (
|
|
this._mainPath ? workaroundResolve(this._mainPath) : this._mainPath
|
|
);
|
|
const getEntryModule = () => this.entryModule
|
|
? { path: workaroundResolve(this.entryModule.path), className: this.entryModule.className }
|
|
: this.entryModule;
|
|
const getLazyRoutes = () => this._lazyRoutes;
|
|
const getTypeChecker = () => this._getTsProgram().getTypeChecker();
|
|
|
|
if (this._JitMode) {
|
|
// Replace resources in JIT.
|
|
this._transformers.push(replaceResources(isAppPath));
|
|
} else {
|
|
// Remove unneeded angular decorators.
|
|
this._transformers.push(removeDecorators(isAppPath, getTypeChecker));
|
|
}
|
|
|
|
if (this._platformTransformers !== null) {
|
|
this._transformers.push(...this._platformTransformers);
|
|
} else {
|
|
if (this._platform === PLATFORM.Browser) {
|
|
// If we have a locale, auto import the locale data file.
|
|
// This transform must go before replaceBootstrap because it looks for the entry module
|
|
// import, which will be replaced.
|
|
if (this._normalizedLocale) {
|
|
this._transformers.push(registerLocaleData(isAppPath, getEntryModule,
|
|
this._normalizedLocale));
|
|
}
|
|
|
|
if (!this._JitMode) {
|
|
// Replace bootstrap in browser AOT.
|
|
this._transformers.push(replaceBootstrap(isAppPath, getEntryModule, getTypeChecker));
|
|
}
|
|
} else if (this._platform === PLATFORM.Server) {
|
|
this._transformers.push(exportLazyModuleMap(isMainPath, getLazyRoutes));
|
|
if (!this._JitMode) {
|
|
this._transformers.push(
|
|
exportNgFactory(isMainPath, getEntryModule),
|
|
replaceServerBootstrap(isMainPath, getEntryModule, getTypeChecker));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private _update() {
|
|
time('AngularCompilerPlugin._update');
|
|
// We only want to update on TS and template changes, but all kinds of files are on this
|
|
// list, like package.json and .ngsummary.json files.
|
|
const changedFiles = this._getChangedCompilationFiles();
|
|
|
|
// If nothing we care about changed and it isn't the first run, don't do anything.
|
|
if (changedFiles.length === 0 && !this._firstRun) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return Promise.resolve()
|
|
// Make a new program and load the Angular structure.
|
|
.then(() => this._createOrUpdateProgram())
|
|
.then(() => {
|
|
if (this.entryModule) {
|
|
// Try to find lazy routes if we have an entry module.
|
|
// We need to run the `listLazyRoutes` the first time because it also navigates libraries
|
|
// and other things that we might miss using the (faster) findLazyRoutesInAst.
|
|
// Lazy routes modules will be read with compilerHost and added to the changed files.
|
|
const changedTsFiles = this._getChangedTsFiles();
|
|
if (this._ngCompilerSupportsNewApi) {
|
|
this._processLazyRoutes(this._listLazyRoutesFromProgram());
|
|
} else if (this._firstRun) {
|
|
this._processLazyRoutes(this._getLazyRoutesFromNgtools());
|
|
} else if (changedTsFiles.length > 0) {
|
|
this._processLazyRoutes(this._findLazyRoutesInAst(changedTsFiles));
|
|
}
|
|
if (this._options.additionalLazyModules) {
|
|
this._processLazyRoutes(this._options.additionalLazyModules);
|
|
}
|
|
}
|
|
})
|
|
.then(() => {
|
|
// Emit and report errors.
|
|
|
|
// We now have the final list of changed TS files.
|
|
// Go through each changed file and add transforms as needed.
|
|
const sourceFiles = this._getChangedTsFiles()
|
|
.map((fileName) => this._getTsProgram().getSourceFile(fileName))
|
|
// At this point we shouldn't need to filter out undefined files, because any ts file
|
|
// that changed should be emitted.
|
|
// But due to hostReplacementPaths there can be files (the environment files)
|
|
// that changed but aren't part of the compilation, specially on `ng test`.
|
|
// So we ignore missing source files files here.
|
|
// hostReplacementPaths needs to be fixed anyway to take care of the following issue.
|
|
// https://github.com/angular/angular-cli/issues/7305#issuecomment-332150230
|
|
.filter((x) => !!x) as ts.SourceFile[];
|
|
|
|
// Emit files.
|
|
time('AngularCompilerPlugin._update._emit');
|
|
const { emitResult, diagnostics } = this._emit(sourceFiles);
|
|
timeEnd('AngularCompilerPlugin._update._emit');
|
|
|
|
// Report diagnostics.
|
|
const errors = diagnostics
|
|
.filter((diag) => diag.category === ts.DiagnosticCategory.Error);
|
|
const warnings = diagnostics
|
|
.filter((diag) => diag.category === ts.DiagnosticCategory.Warning);
|
|
|
|
if (errors.length > 0) {
|
|
const message = formatDiagnostics(errors);
|
|
this._errors.push(new Error(message));
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
const message = formatDiagnostics(warnings);
|
|
this._warnings.push(message);
|
|
}
|
|
|
|
this._emitSkipped = !emitResult || emitResult.emitSkipped;
|
|
|
|
// Reset changed files on successful compilation.
|
|
if (!this._emitSkipped && this._errors.length === 0) {
|
|
this._compilerHost.resetChangedFileTracker();
|
|
}
|
|
timeEnd('AngularCompilerPlugin._update');
|
|
});
|
|
}
|
|
|
|
writeI18nOutFile() {
|
|
function _recursiveMkDir(p: string) {
|
|
if (!fs.existsSync(p)) {
|
|
_recursiveMkDir(path.dirname(p));
|
|
fs.mkdirSync(p);
|
|
}
|
|
}
|
|
|
|
// Write the extracted messages to disk.
|
|
if (this._compilerOptions.i18nOutFile) {
|
|
const i18nOutFilePath = path.resolve(this._basePath, this._compilerOptions.i18nOutFile);
|
|
const i18nOutFileContent = this._compilerHost.readFile(i18nOutFilePath);
|
|
if (i18nOutFileContent) {
|
|
_recursiveMkDir(path.dirname(i18nOutFilePath));
|
|
fs.writeFileSync(i18nOutFilePath, i18nOutFileContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
getCompiledFile(fileName: string) {
|
|
const outputFile = fileName.replace(/.tsx?$/, '.js');
|
|
let outputText: string;
|
|
let sourceMap: string | undefined;
|
|
let errorDependencies: string[] = [];
|
|
|
|
if (this._emitSkipped) {
|
|
const text = this._compilerHost.readFile(outputFile);
|
|
if (text) {
|
|
// If the compilation didn't emit files this time, try to return the cached files from the
|
|
// last compilation and let the compilation errors show what's wrong.
|
|
outputText = text;
|
|
sourceMap = this._compilerHost.readFile(outputFile + '.map');
|
|
} else {
|
|
// There's nothing we can serve. Return an empty string to prevent lenghty webpack errors,
|
|
// add the rebuild warning if it's not there yet.
|
|
// We also need to all changed files as dependencies of this file, so that all of them
|
|
// will be watched and trigger a rebuild next time.
|
|
outputText = '';
|
|
errorDependencies = this._getChangedCompilationFiles()
|
|
// These paths are used by the loader so we must denormalize them.
|
|
.map((p) => this._compilerHost.denormalizePath(p));
|
|
}
|
|
} else {
|
|
// Check if the TS input file and the JS output file exist.
|
|
if (((fileName.endsWith('.ts') || fileName.endsWith('.tsx'))
|
|
&& !this._compilerHost.fileExists(fileName, false))
|
|
|| !this._compilerHost.fileExists(outputFile, false)) {
|
|
let msg = `${fileName} is missing from the TypeScript compilation. `
|
|
+ `Please make sure it is in your tsconfig via the 'files' or 'include' property.`;
|
|
|
|
if (/(\\|\/)node_modules(\\|\/)/.test(fileName)) {
|
|
msg += '\nThe missing file seems to be part of a third party library. '
|
|
+ 'TS files in published libraries are often a sign of a badly packaged library. '
|
|
+ 'Please open an issue in the library repository to alert its author and ask them '
|
|
+ 'to package the library using the Angular Package Format (https://goo.gl/jB3GVv).';
|
|
}
|
|
|
|
throw new Error(msg);
|
|
}
|
|
|
|
outputText = this._compilerHost.readFile(outputFile) || '';
|
|
sourceMap = this._compilerHost.readFile(outputFile + '.map');
|
|
}
|
|
|
|
return { outputText, sourceMap, errorDependencies };
|
|
}
|
|
|
|
getDependencies(fileName: string): string[] {
|
|
const resolvedFileName = this._compilerHost.resolve(fileName);
|
|
const sourceFile = this._compilerHost.getSourceFile(resolvedFileName, ts.ScriptTarget.Latest);
|
|
if (!sourceFile) {
|
|
return [];
|
|
}
|
|
|
|
const options = this._compilerOptions;
|
|
const host = this._compilerHost;
|
|
const cache = this._moduleResolutionCache;
|
|
|
|
const esImports = collectDeepNodes<ts.ImportDeclaration>(sourceFile,
|
|
ts.SyntaxKind.ImportDeclaration)
|
|
.map(decl => {
|
|
const moduleName = (decl.moduleSpecifier as ts.StringLiteral).text;
|
|
const resolved = ts.resolveModuleName(moduleName, resolvedFileName, options, host, cache);
|
|
|
|
if (resolved.resolvedModule) {
|
|
return resolved.resolvedModule.resolvedFileName;
|
|
} else {
|
|
return null;
|
|
}
|
|
})
|
|
.filter(x => x);
|
|
|
|
const resourceImports = findResources(sourceFile)
|
|
.map((resourceReplacement) => resourceReplacement.resourcePaths)
|
|
.reduce((prev, curr) => prev.concat(curr), [])
|
|
.map((resourcePath) => resolve(dirname(resolvedFileName), normalize(resourcePath)));
|
|
|
|
// These paths are meant to be used by the loader so we must denormalize them.
|
|
const uniqueDependencies = new Set([
|
|
...esImports,
|
|
...resourceImports,
|
|
...this.getResourceDependencies(this._compilerHost.denormalizePath(resolvedFileName)),
|
|
].map((p) => p && this._compilerHost.denormalizePath(p)));
|
|
|
|
return [...uniqueDependencies]
|
|
.filter(x => !!x) as string[];
|
|
}
|
|
|
|
getResourceDependencies(fileName: string): string[] {
|
|
return this._resourceLoader.getResourceDependencies(fileName);
|
|
}
|
|
|
|
// This code mostly comes from `performCompilation` in `@angular/compiler-cli`.
|
|
// It skips the program creation because we need to use `loadNgStructureAsync()`,
|
|
// and uses CustomTransformers.
|
|
private _emit(sourceFiles: ts.SourceFile[]) {
|
|
time('AngularCompilerPlugin._emit');
|
|
const program = this._program;
|
|
const allDiagnostics: Array<ts.Diagnostic | Diagnostic> = [];
|
|
|
|
let emitResult: ts.EmitResult | undefined;
|
|
try {
|
|
if (this._JitMode) {
|
|
const tsProgram = program as ts.Program;
|
|
|
|
if (this._firstRun) {
|
|
// Check parameter diagnostics.
|
|
time('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics');
|
|
allDiagnostics.push(...tsProgram.getOptionsDiagnostics());
|
|
timeEnd('AngularCompilerPlugin._emit.ts.getOptionsDiagnostics');
|
|
}
|
|
|
|
if ((this._firstRun || !this._forkTypeChecker) && this._program) {
|
|
allDiagnostics.push(...gatherDiagnostics(this._program, this._JitMode,
|
|
'AngularCompilerPlugin._emit.ts'));
|
|
}
|
|
|
|
if (!hasErrors(allDiagnostics)) {
|
|
sourceFiles.forEach((sf) => {
|
|
const timeLabel = `AngularCompilerPlugin._emit.ts+${sf.fileName}+.emit`;
|
|
time(timeLabel);
|
|
emitResult = tsProgram.emit(sf, undefined, undefined, undefined,
|
|
{ before: this._transformers },
|
|
);
|
|
allDiagnostics.push(...emitResult.diagnostics);
|
|
timeEnd(timeLabel);
|
|
});
|
|
}
|
|
} else {
|
|
const angularProgram = program as Program;
|
|
|
|
// Check Angular structural diagnostics.
|
|
time('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics');
|
|
allDiagnostics.push(...angularProgram.getNgStructuralDiagnostics());
|
|
timeEnd('AngularCompilerPlugin._emit.ng.getNgStructuralDiagnostics');
|
|
|
|
if (this._firstRun) {
|
|
// Check TypeScript parameter diagnostics.
|
|
time('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics');
|
|
allDiagnostics.push(...angularProgram.getTsOptionDiagnostics());
|
|
timeEnd('AngularCompilerPlugin._emit.ng.getTsOptionDiagnostics');
|
|
|
|
// Check Angular parameter diagnostics.
|
|
time('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics');
|
|
allDiagnostics.push(...angularProgram.getNgOptionDiagnostics());
|
|
timeEnd('AngularCompilerPlugin._emit.ng.getNgOptionDiagnostics');
|
|
}
|
|
|
|
if ((this._firstRun || !this._forkTypeChecker) && this._program) {
|
|
allDiagnostics.push(...gatherDiagnostics(this._program, this._JitMode,
|
|
'AngularCompilerPlugin._emit.ng'));
|
|
}
|
|
|
|
if (!hasErrors(allDiagnostics)) {
|
|
time('AngularCompilerPlugin._emit.ng.emit');
|
|
const extractI18n = !!this._compilerOptions.i18nOutFile;
|
|
const emitFlags = extractI18n ? EmitFlags.I18nBundle : EmitFlags.Default;
|
|
emitResult = angularProgram.emit({
|
|
emitFlags, customTransformers: {
|
|
beforeTs: this._transformers,
|
|
},
|
|
});
|
|
allDiagnostics.push(...emitResult.diagnostics);
|
|
if (extractI18n) {
|
|
this.writeI18nOutFile();
|
|
}
|
|
timeEnd('AngularCompilerPlugin._emit.ng.emit');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
time('AngularCompilerPlugin._emit.catch');
|
|
// This function is available in the import below, but this way we avoid the dependency.
|
|
// import { isSyntaxError } from '@angular/compiler';
|
|
function isSyntaxError(error: Error): boolean {
|
|
return (error as any)['ngSyntaxError']; // tslint:disable-line:no-any
|
|
}
|
|
|
|
let errMsg: string;
|
|
let code: number;
|
|
if (isSyntaxError(e)) {
|
|
// don't report the stack for syntax errors as they are well known errors.
|
|
errMsg = e.message;
|
|
code = DEFAULT_ERROR_CODE;
|
|
} else {
|
|
errMsg = e.stack;
|
|
// It is not a syntax error we might have a program with unknown state, discard it.
|
|
this._program = null;
|
|
code = UNKNOWN_ERROR_CODE;
|
|
}
|
|
allDiagnostics.push(
|
|
{ category: ts.DiagnosticCategory.Error, messageText: errMsg, code, source: SOURCE });
|
|
timeEnd('AngularCompilerPlugin._emit.catch');
|
|
}
|
|
timeEnd('AngularCompilerPlugin._emit');
|
|
|
|
return { program, emitResult, diagnostics: allDiagnostics };
|
|
}
|
|
|
|
private _validateLocale(locale: string): string | null {
|
|
// Get the path of the common module.
|
|
const commonPath = path.dirname(require.resolve('@angular/common/package.json'));
|
|
// Check if the locale file exists
|
|
if (!fs.existsSync(path.resolve(commonPath, 'locales', `${locale}.js`))) {
|
|
// Check for an alternative locale (if the locale id was badly formatted).
|
|
const locales = fs.readdirSync(path.resolve(commonPath, 'locales'))
|
|
.filter(file => file.endsWith('.js'))
|
|
.map(file => file.replace('.js', ''));
|
|
|
|
let newLocale;
|
|
const normalizedLocale = locale.toLowerCase().replace(/_/g, '-');
|
|
for (const l of locales) {
|
|
if (l.toLowerCase() === normalizedLocale) {
|
|
newLocale = l;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (newLocale) {
|
|
locale = newLocale;
|
|
} else {
|
|
// Check for a parent locale
|
|
const parentLocale = normalizedLocale.split('-')[0];
|
|
if (locales.indexOf(parentLocale) !== -1) {
|
|
locale = parentLocale;
|
|
} else {
|
|
this._warnings.push(`AngularCompilerPlugin: Unable to load the locale data file ` +
|
|
`"@angular/common/locales/${locale}", ` +
|
|
`please check that "${locale}" is a valid locale id.
|
|
If needed, you can use "registerLocaleData" manually.`);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
return locale;
|
|
}
|
|
}
|