2018-05-15 15:31:12 -07:00

1150 lines
46 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 {AotCompiler, AotCompilerHost, AotCompilerOptions, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, StaticSymbol, TypeScriptEmitter, Xliff, Xliff2, Xmb, core, createAotCompiler, getParseErrors, isFormattedError, isSyntaxError} from '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {compareVersions} from '../diagnostics/typescript_version';
import {MetadataCollector, ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {NgtscProgram} from '../ngtsc/program';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api';
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {InlineResourcesMetadataTransformer, getInlineResourcesTransformFactory} from './inline_resources';
import {LowerMetadataTransform, getExpressionLoweringTransformFactory} from './lower_expressions';
import {MetadataCache, MetadataTransformer} from './metadata_cache';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {PartialModuleMetadataTransformer} from './r3_metadata_transform';
import {StripDecoratorsMetadataTransformer, getDecoratorStripTransformerFactory} from './r3_strip_decorators';
import {getAngularClassTransformerFactory} from './r3_transform';
import {DTS, GENERATED_FILES, StructureIsReused, TS, createMessageDiagnostic, isInRootDir, ngToTsDiagnostic, tsStructureIsReused, userError} from './util';
// Closure compiler transforms the form `Service.ngInjectableDef = X` into
// `Service$ngInjectableDef = X`. To prevent this transformation, such assignments need to be
// annotated with @nocollapse. Unfortunately, a bug in Typescript where comments aren't propagated
// through the TS transformations precludes adding the comment via the AST. This workaround detects
// the static assignments to R3 properties such as ngInjectableDef using a regex, as output files
// are written, and applies the annotation through regex replacement.
//
// TODO(alxhub): clean up once fix for TS transformers lands in upstream
//
// Typescript reference issue: https://github.com/Microsoft/TypeScript/issues/22497
// Pattern matching all Render3 property names.
const R3_DEF_NAME_PATTERN = ['ngInjectableDef'].join('|');
// Pattern matching `Identifier.property` where property is a Render3 property.
const R3_DEF_ACCESS_PATTERN = `[^\\s\\.()[\\]]+\.(${R3_DEF_NAME_PATTERN})`;
// Pattern matching a source line that contains a Render3 static property assignment.
// It declares two matching groups - one for the preceding whitespace, the second for the rest
// of the assignment expression.
const R3_DEF_LINE_PATTERN = `^(\\s*)(${R3_DEF_ACCESS_PATTERN} = .*)$`;
// Regex compilation of R3_DEF_LINE_PATTERN. Matching group 1 yields the whitespace preceding the
// assignment, matching group 2 gives the rest of the assignment expressions.
const R3_MATCH_DEFS = new RegExp(R3_DEF_LINE_PATTERN, 'gmu');
// Replacement string that complements R3_MATCH_DEFS. It inserts `/** @nocollapse */` before the
// assignment but after any indentation. Note that this will mess up any sourcemaps on this line
// (though there shouldn't be any, since Render3 properties are synthetic).
const R3_NOCOLLAPSE_DEFS = '$1\/** @nocollapse *\/ $2';
/**
* Maximum number of files that are emitable via calling ts.Program.emit
* passing individual targetSourceFiles.
*/
const MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT = 20;
/**
* Fields to lower within metadata in render2 mode.
*/
const LOWER_FIELDS = ['useValue', 'useFactory', 'data', 'id', 'loadChildren'];
/**
* Fields to lower within metadata in render3 mode.
*/
const R3_LOWER_FIELDS = [...LOWER_FIELDS, 'providers', 'imports', 'exports'];
const R3_REIFIED_DECORATORS = [
'Component',
'Directive',
'Injectable',
'NgModule',
'Pipe',
];
const emptyModules: NgAnalyzedModules = {
ngModules: [],
ngModuleByPipeOrDirective: new Map(),
files: []
};
const defaultEmitCallback: TsEmitCallback =
({program, targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles,
customTransformers}) =>
program.emit(
targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
/**
* Minimum supported TypeScript version
* ∀ supported typescript version v, v >= MIN_TS_VERSION
*/
const MIN_TS_VERSION = '2.7.2';
/**
* Supremum of supported TypeScript versions
* ∀ supported typescript version v, v < MAX_TS_VERSION
* MAX_TS_VERSION is not considered as a supported TypeScript version
*/
const MAX_TS_VERSION = '2.9.0';
class AngularCompilerProgram implements Program {
private rootNames: string[];
private metadataCache: MetadataCache;
// Metadata cache used exclusively for the flat module index
private flatModuleMetadataCache: MetadataCache;
private loweringMetadataTransform: LowerMetadataTransform;
private oldProgramLibrarySummaries: Map<string, LibrarySummary>|undefined;
private oldProgramEmittedGeneratedFiles: Map<string, GeneratedFile>|undefined;
private oldProgramEmittedSourceFiles: Map<string, ts.SourceFile>|undefined;
// Note: This will be cleared out as soon as we create the _tsProgram
private oldTsProgram: ts.Program|undefined;
private emittedLibrarySummaries: LibrarySummary[]|undefined;
private emittedGeneratedFiles: GeneratedFile[]|undefined;
private emittedSourceFiles: ts.SourceFile[]|undefined;
// Lazily initialized fields
private _compiler: AotCompiler;
private _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter;
private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined;
private _analyzedInjectables: NgAnalyzedFileWithInjectables[]|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined;
private _programWithStubs: ts.Program|undefined;
private _optionsDiagnostics: Diagnostic[] = [];
private _reifiedDecorators: Set<StaticSymbol>;
constructor(
rootNames: ReadonlyArray<string>, private options: CompilerOptions,
private host: CompilerHost, oldProgram?: Program) {
this.rootNames = [...rootNames];
checkVersion(ts.version, MIN_TS_VERSION, MAX_TS_VERSION, options.disableTypeScriptVersionCheck);
this.oldTsProgram = oldProgram ? oldProgram.getTsProgram() : undefined;
if (oldProgram) {
this.oldProgramLibrarySummaries = oldProgram.getLibrarySummaries();
this.oldProgramEmittedGeneratedFiles = oldProgram.getEmittedGeneratedFiles();
this.oldProgramEmittedSourceFiles = oldProgram.getEmittedSourceFiles();
}
if (options.flatModuleOutFile) {
const {host: bundleHost, indexName, errors} =
createBundleIndexHost(options, this.rootNames, host, () => this.flatModuleMetadataCache);
if (errors) {
this._optionsDiagnostics.push(...errors.map(e => ({
category: e.category,
messageText: e.messageText as string,
source: SOURCE,
code: DEFAULT_ERROR_CODE
})));
} else {
this.rootNames.push(indexName !);
this.host = bundleHost;
}
}
this.loweringMetadataTransform =
new LowerMetadataTransform(options.enableIvy ? R3_LOWER_FIELDS : LOWER_FIELDS);
this.metadataCache = this.createMetadataCache([this.loweringMetadataTransform]);
}
private createMetadataCache(transformers: MetadataTransformer[]) {
return new MetadataCache(
new MetadataCollector({quotedNames: true}), !!this.options.strictMetadataEmit,
transformers);
}
getLibrarySummaries(): Map<string, LibrarySummary> {
const result = new Map<string, LibrarySummary>();
if (this.oldProgramLibrarySummaries) {
this.oldProgramLibrarySummaries.forEach((summary, fileName) => result.set(fileName, summary));
}
if (this.emittedLibrarySummaries) {
this.emittedLibrarySummaries.forEach(
(summary, fileName) => result.set(summary.fileName, summary));
}
return result;
}
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
const result = new Map<string, GeneratedFile>();
if (this.oldProgramEmittedGeneratedFiles) {
this.oldProgramEmittedGeneratedFiles.forEach(
(genFile, fileName) => result.set(fileName, genFile));
}
if (this.emittedGeneratedFiles) {
this.emittedGeneratedFiles.forEach((genFile) => result.set(genFile.genFileUrl, genFile));
}
return result;
}
getEmittedSourceFiles(): Map<string, ts.SourceFile> {
const result = new Map<string, ts.SourceFile>();
if (this.oldProgramEmittedSourceFiles) {
this.oldProgramEmittedSourceFiles.forEach((sf, fileName) => result.set(fileName, sf));
}
if (this.emittedSourceFiles) {
this.emittedSourceFiles.forEach((sf) => result.set(sf.fileName, sf));
}
return result;
}
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken) {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken): ReadonlyArray<Diagnostic> {
return [...this._optionsDiagnostics, ...getNgOptionDiagnostics(this.options)];
}
getTsSyntacticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ReadonlyArray<ts.Diagnostic> {
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken): ReadonlyArray<Diagnostic> {
return this.structuralDiagnostics;
}
getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken):
ReadonlyArray<ts.Diagnostic> {
const sourceFiles = sourceFile ? [sourceFile] : this.tsProgram.getSourceFiles();
let diags: ts.Diagnostic[] = [];
sourceFiles.forEach(sf => {
if (!GENERATED_FILES.test(sf.fileName)) {
diags.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken));
}
});
return diags;
}
getNgSemanticDiagnostics(fileName?: string, cancellationToken?: ts.CancellationToken):
ReadonlyArray<Diagnostic> {
let diags: ts.Diagnostic[] = [];
this.tsProgram.getSourceFiles().forEach(sf => {
if (GENERATED_FILES.test(sf.fileName) && !sf.isDeclarationFile) {
diags.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken));
}
});
const {ng} = translateDiagnostics(this.hostAdapter, diags);
return ng;
}
loadNgStructureAsync(): Promise<void> {
if (this._analyzedModules) {
throw new Error('Angular structure already loaded');
}
return Promise.resolve()
.then(() => {
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles, tsFiles)
.then(({analyzedModules, analyzedInjectables}) => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronously');
}
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
});
})
.catch(e => this._createProgramOnError(e));
}
listLazyRoutes(route?: string): LazyRoute[] {
// Note: Don't analyzedModules if a route is given
// to be fast enough.
return this.compiler.listLazyRoutes(route, route ? undefined : this.analyzedModules);
}
emit(parameters: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback,
mergeEmitResultsCallback?: TsMergeEmitResultsCallback,
} = {}): ts.EmitResult {
if (this.options.enableIvy === 'ngtsc') {
throw new Error('Cannot run legacy compiler in ngtsc mode');
}
return this.options.enableIvy === true ? this._emitRender3(parameters) :
this._emitRender2(parameters);
}
private _annotateR3Properties(contents: string): string {
return contents.replace(R3_MATCH_DEFS, R3_NOCOLLAPSE_DEFS);
}
private _emitRender3(
{
emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback, mergeEmitResultsCallback = mergeEmitResults,
}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback,
mergeEmitResultsCallback?: TsMergeEmitResultsCallback,
} = {}): ts.EmitResult {
const emitStart = Date.now();
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
// analyzedModules and analyzedInjectables are created together. If one exists, so does the
// other.
const modules =
this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables !);
const writeTsFile: ts.WriteFileCallback =
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
let genFile: GeneratedFile|undefined;
if (this.options.annotateForClosureCompiler && sourceFile &&
TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData);
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles);
};
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
const tsCustomTransformers = this.calculateTransforms(
/* genFiles */ undefined, /* partialModules */ modules,
/* stripDecorators */ this.reifiedDecorators, customTransformers);
const emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTransformers
});
return emitResult;
}
private _emitRender2(
{
emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback, mergeEmitResultsCallback = mergeEmitResults,
}: {
emitFlags?: EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: CustomTransformers,
emitCallback?: TsEmitCallback,
mergeEmitResultsCallback?: TsMergeEmitResultsCallback,
} = {}): ts.EmitResult {
const emitStart = Date.now();
if (emitFlags & EmitFlags.I18nBundle) {
const locale = this.options.i18nOutLocale || null;
const file = this.options.i18nOutFile || null;
const format = this.options.i18nOutFormat || null;
const bundle = this.compiler.emitMessageBundle(this.analyzedModules, locale);
i18nExtract(format, file, this.host, this.options, bundle);
}
if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) ===
0) {
return {emitSkipped: true, diagnostics: [], emittedFiles: []};
}
let {genFiles, genDiags} = this.generateFilesForEmit(emitFlags);
if (genDiags.length) {
return {
diagnostics: genDiags,
emitSkipped: true,
emittedFiles: [],
};
}
this.emittedGeneratedFiles = genFiles;
const outSrcMapping: Array<{sourceFile: ts.SourceFile, outFileName: string}> = [];
const genFileByFileName = new Map<string, GeneratedFile>();
genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile));
this.emittedLibrarySummaries = [];
const emittedSourceFiles = [] as ts.SourceFile[];
const writeTsFile: ts.WriteFileCallback =
(outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => {
const sourceFile = sourceFiles && sourceFiles.length == 1 ? sourceFiles[0] : null;
let genFile: GeneratedFile|undefined;
if (sourceFile) {
outSrcMapping.push({outFileName: outFileName, sourceFile});
genFile = genFileByFileName.get(sourceFile.fileName);
if (!sourceFile.isDeclarationFile && !GENERATED_FILES.test(sourceFile.fileName)) {
// Note: sourceFile is the transformed sourcefile, not the original one!
const originalFile = this.tsProgram.getSourceFile(sourceFile.fileName);
if (originalFile) {
emittedSourceFiles.push(originalFile);
}
}
if (this.options.annotateForClosureCompiler && TS.test(sourceFile.fileName)) {
outData = this._annotateR3Properties(outData);
}
}
this.writeFile(outFileName, outData, writeByteOrderMark, onError, genFile, sourceFiles);
};
const modules = this._analyzedInjectables &&
this.compiler.emitAllPartialModules2(this._analyzedInjectables);
const tsCustomTransformers = this.calculateTransforms(
genFileByFileName, modules, /* stripDecorators */ undefined, customTransformers);
const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS;
// Restore the original references before we emit so TypeScript doesn't emit
// a reference to the .d.ts file.
const augmentedReferences = new Map<ts.SourceFile, ReadonlyArray<ts.FileReference>>();
for (const sourceFile of this.tsProgram.getSourceFiles()) {
const originalReferences = getOriginalReferences(sourceFile);
if (originalReferences) {
augmentedReferences.set(sourceFile, sourceFile.referencedFiles);
sourceFile.referencedFiles = originalReferences;
}
}
const genTsFiles: GeneratedFile[] = [];
const genJsonFiles: GeneratedFile[] = [];
genFiles.forEach(gf => {
if (gf.stmts) {
genTsFiles.push(gf);
}
if (gf.source) {
genJsonFiles.push(gf);
}
});
let emitResult: ts.EmitResult;
let emittedUserTsCount: number;
try {
const sourceFilesToEmit = this.getSourceFilesForEmit();
if (sourceFilesToEmit &&
(sourceFilesToEmit.length + genTsFiles.length) < MAX_FILE_COUNT_FOR_SINGLE_FILE_EMIT) {
const fileNamesToEmit =
[...sourceFilesToEmit.map(sf => sf.fileName), ...genTsFiles.map(gf => gf.genFileUrl)];
emitResult = mergeEmitResultsCallback(
fileNamesToEmit.map((fileName) => emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTransformers,
targetSourceFile: this.tsProgram.getSourceFile(fileName),
})));
emittedUserTsCount = sourceFilesToEmit.length;
} else {
emitResult = emitCallback({
program: this.tsProgram,
host: this.host,
options: this.options,
writeFile: writeTsFile, emitOnlyDtsFiles,
customTransformers: tsCustomTransformers
});
emittedUserTsCount = this.tsProgram.getSourceFiles().length - genTsFiles.length;
}
} finally {
// Restore the references back to the augmented value to ensure that the
// checks that TypeScript makes for project structure reuse will succeed.
for (const [sourceFile, references] of Array.from(augmentedReferences)) {
// TODO(chuckj): Remove any cast after updating build to 2.6
(sourceFile as any).referencedFiles = references;
}
}
this.emittedSourceFiles = emittedSourceFiles;
// Match behavior of tsc: only produce emit diagnostics if it would block
// emit. If noEmitOnError is false, the emit will happen in spite of any
// errors, so we should not report them.
if (this.options.noEmitOnError === true) {
// translate the diagnostics in the emitResult as well.
const translatedEmitDiags = translateDiagnostics(this.hostAdapter, emitResult.diagnostics);
emitResult.diagnostics = translatedEmitDiags.ts.concat(
this.structuralDiagnostics.concat(translatedEmitDiags.ng).map(ngToTsDiagnostic));
}
if (!outSrcMapping.length) {
// if no files were emitted by TypeScript, also don't emit .json files
emitResult.diagnostics =
emitResult.diagnostics.concat([createMessageDiagnostic(`Emitted no files.`)]);
return emitResult;
}
let sampleSrcFileName: string|undefined;
let sampleOutFileName: string|undefined;
if (outSrcMapping.length) {
sampleSrcFileName = outSrcMapping[0].sourceFile.fileName;
sampleOutFileName = outSrcMapping[0].outFileName;
}
const srcToOutPath =
createSrcToOutPathMapper(this.options.outDir, sampleSrcFileName, sampleOutFileName);
if (emitFlags & EmitFlags.Codegen) {
genJsonFiles.forEach(gf => {
const outFileName = srcToOutPath(gf.genFileUrl);
this.writeFile(outFileName, gf.source !, false, undefined, gf);
});
}
let metadataJsonCount = 0;
if (emitFlags & EmitFlags.Metadata) {
this.tsProgram.getSourceFiles().forEach(sf => {
if (!sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName)) {
metadataJsonCount++;
const metadata = this.metadataCache.getMetadata(sf);
if (metadata) {
const metadataText = JSON.stringify([metadata]);
const outFileName = srcToOutPath(sf.fileName.replace(/\.tsx?$/, '.metadata.json'));
this.writeFile(outFileName, metadataText, false, undefined, undefined, [sf]);
}
}
});
}
const emitEnd = Date.now();
if (this.options.diagnostics) {
emitResult.diagnostics = emitResult.diagnostics.concat([createMessageDiagnostic([
`Emitted in ${emitEnd - emitStart}ms`,
`- ${emittedUserTsCount} user ts files`,
`- ${genTsFiles.length} generated ts files`,
`- ${genJsonFiles.length + metadataJsonCount} generated json files`,
].join('\n'))]);
}
return emitResult;
}
// Private members
private get compiler(): AotCompiler {
if (!this._compiler) {
this._createCompiler();
}
return this._compiler !;
}
private get hostAdapter(): TsCompilerAotCompilerTypeCheckHostAdapter {
if (!this._hostAdapter) {
this._createCompiler();
}
return this._hostAdapter !;
}
private get analyzedModules(): NgAnalyzedModules {
if (!this._analyzedModules) {
this.initSync();
}
return this._analyzedModules !;
}
private get structuralDiagnostics(): ReadonlyArray<Diagnostic> {
let diagnostics = this._structuralDiagnostics;
if (!diagnostics) {
this.initSync();
diagnostics = (this._structuralDiagnostics = this._structuralDiagnostics || []);
}
return diagnostics;
}
private get tsProgram(): ts.Program {
if (!this._tsProgram) {
this.initSync();
}
return this._tsProgram !;
}
private get reifiedDecorators(): Set<StaticSymbol> {
if (!this._reifiedDecorators) {
const reflector = this.compiler.reflector;
this._reifiedDecorators = new Set(
R3_REIFIED_DECORATORS.map(name => reflector.findDeclaration('@angular/core', name)));
}
return this._reifiedDecorators;
}
private calculateTransforms(
genFiles: Map<string, GeneratedFile>|undefined, partialModules: PartialModule[]|undefined,
stripDecorators: Set<StaticSymbol>|undefined,
customTransformers?: CustomTransformers): ts.CustomTransformers {
const beforeTs: Array<ts.TransformerFactory<ts.SourceFile>> = [];
const metadataTransforms: MetadataTransformer[] = [];
const flatModuleMetadataTransforms: MetadataTransformer[] = [];
if (this.options.enableResourceInlining) {
beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter));
const transformer = new InlineResourcesMetadataTransformer(this.hostAdapter);
metadataTransforms.push(transformer);
flatModuleMetadataTransforms.push(transformer);
}
if (!this.options.disableExpressionLowering) {
beforeTs.push(
getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram));
metadataTransforms.push(this.loweringMetadataTransform);
}
if (genFiles) {
beforeTs.push(getAngularEmitterTransformFactory(genFiles, this.getTsProgram()));
}
if (partialModules) {
beforeTs.push(getAngularClassTransformerFactory(partialModules));
// If we have partial modules, the cached metadata might be incorrect as it doesn't reflect
// the partial module transforms.
const transformer = new PartialModuleMetadataTransformer(partialModules);
metadataTransforms.push(transformer);
flatModuleMetadataTransforms.push(transformer);
}
if (stripDecorators) {
beforeTs.push(getDecoratorStripTransformerFactory(
stripDecorators, this.compiler.reflector, this.getTsProgram().getTypeChecker()));
const transformer =
new StripDecoratorsMetadataTransformer(stripDecorators, this.compiler.reflector);
metadataTransforms.push(transformer);
flatModuleMetadataTransforms.push(transformer);
}
if (customTransformers && customTransformers.beforeTs) {
beforeTs.push(...customTransformers.beforeTs);
}
if (metadataTransforms.length > 0) {
this.metadataCache = this.createMetadataCache(metadataTransforms);
}
if (flatModuleMetadataTransforms.length > 0) {
this.flatModuleMetadataCache = this.createMetadataCache(flatModuleMetadataTransforms);
}
const afterTs = customTransformers ? customTransformers.afterTs : undefined;
return {before: beforeTs, after: afterTs};
}
private initSync() {
if (this._analyzedModules) {
return;
}
try {
const {tmpProgram, sourceFiles, tsFiles, rootNames} = this._createProgramWithBasicStubs();
const {analyzedModules, analyzedInjectables} =
this.compiler.loadFilesSync(sourceFiles, tsFiles);
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, analyzedInjectables, rootNames);
} catch (e) {
this._createProgramOnError(e);
}
}
private _createCompiler() {
const codegen: CodeGenerator = {
generateFile: (genFileName, baseFileName) =>
this._compiler.emitBasicStub(genFileName, baseFileName),
findGeneratedFileNames: (fileName) => this._compiler.findGeneratedFileNames(fileName),
};
this._hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen,
this.oldProgramLibrarySummaries);
const aotOptions = getAotCompilerOptions(this.options);
const errorCollector = (this.options.collectAllErrors || this.options.fullTemplateTypeCheck) ?
(err: any) => this._addStructuralDiagnostics(err) :
undefined;
this._compiler = createAotCompiler(this._hostAdapter, aotOptions, errorCollector).compiler;
}
private _createProgramWithBasicStubs(): {
tmpProgram: ts.Program,
rootNames: string[],
sourceFiles: string[],
tsFiles: string[],
} {
if (this._analyzedModules) {
throw new Error(`Internal Error: already initialized!`);
}
// Note: This is important to not produce a memory leak!
const oldTsProgram = this.oldTsProgram;
this.oldTsProgram = undefined;
const codegen: CodeGenerator = {
generateFile: (genFileName, baseFileName) =>
this.compiler.emitBasicStub(genFileName, baseFileName),
findGeneratedFileNames: (fileName) => this.compiler.findGeneratedFileNames(fileName),
};
let rootNames = [...this.rootNames];
if (this.options.generateCodeForLibraries !== false) {
// if we should generateCodeForLibraries, never include
// generated files in the program as otherwise we will
// overwrite them and typescript will report the error
// TS5055: Cannot write file ... because it would overwrite input file.
rootNames = rootNames.filter(fn => !GENERATED_FILES.test(fn));
}
if (this.options.noResolve) {
this.rootNames.forEach(rootName => {
if (this.hostAdapter.shouldGenerateFilesFor(rootName)) {
rootNames.push(...this.compiler.findGeneratedFileNames(rootName));
}
});
}
const tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram);
const sourceFiles: string[] = [];
const tsFiles: string[] = [];
tmpProgram.getSourceFiles().forEach(sf => {
if (this.hostAdapter.isSourceFile(sf.fileName)) {
sourceFiles.push(sf.fileName);
}
if (TS.test(sf.fileName) && !DTS.test(sf.fileName)) {
tsFiles.push(sf.fileName);
}
});
return {tmpProgram, sourceFiles, tsFiles, rootNames};
}
private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules,
analyzedInjectables: NgAnalyzedFileWithInjectables[], rootNames: string[]) {
this._analyzedModules = analyzedModules;
this._analyzedInjectables = analyzedInjectables;
tmpProgram.getSourceFiles().forEach(sf => {
if (sf.fileName.endsWith('.ngfactory.ts')) {
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);
if (generate) {
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a baseFileName
// for .ngfactory.ts files.
const genFile = this.compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
if (genFile) {
this.hostAdapter.updateGeneratedFile(genFile);
}
}
}
});
this._tsProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, tmpProgram);
// Note: the new ts program should be completely reusable by TypeScript as:
// - we cache all the files in the hostAdapter
// - new new stubs use the exactly same imports/exports as the old once (we assert that in
// hostAdapter.updateGeneratedFile).
if (tsStructureIsReused(tmpProgram) !== StructureIsReused.Completely) {
throw new Error(`Internal Error: The structure of the program changed during codegen.`);
}
}
private _createProgramOnError(e: any) {
// Still fill the analyzedModules and the tsProgram
// so that we don't cause other errors for users who e.g. want to emit the ngProgram.
this._analyzedModules = emptyModules;
this.oldTsProgram = undefined;
this._hostAdapter.isSourceFile = () => false;
this._tsProgram = ts.createProgram(this.rootNames, this.options, this.hostAdapter);
if (isSyntaxError(e)) {
this._addStructuralDiagnostics(e);
return;
}
throw e;
}
private _addStructuralDiagnostics(error: Error) {
const diagnostics = this._structuralDiagnostics || (this._structuralDiagnostics = []);
if (isSyntaxError(error)) {
diagnostics.push(...syntaxErrorToDiagnostics(error));
} else {
diagnostics.push({
messageText: error.toString(),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
}
}
// Note: this returns a ts.Diagnostic so that we
// can return errors in a ts.EmitResult
private generateFilesForEmit(emitFlags: EmitFlags):
{genFiles: GeneratedFile[], genDiags: ts.Diagnostic[]} {
try {
if (!(emitFlags & EmitFlags.Codegen)) {
return {genFiles: [], genDiags: []};
}
// TODO(tbosch): allow generating files that are not in the rootDir
// See https://github.com/angular/angular/issues/19337
let genFiles = this.compiler.emitAllImpls(this.analyzedModules)
.filter(genFile => isInRootDir(genFile.genFileUrl, this.options));
if (this.oldProgramEmittedGeneratedFiles) {
const oldProgramEmittedGeneratedFiles = this.oldProgramEmittedGeneratedFiles;
genFiles = genFiles.filter(genFile => {
const oldGenFile = oldProgramEmittedGeneratedFiles.get(genFile.genFileUrl);
return !oldGenFile || !genFile.isEquivalent(oldGenFile);
});
}
return {genFiles, genDiags: []};
} catch (e) {
// TODO(tbosch): check whether we can actually have syntax errors here,
// as we already parsed the metadata and templates before to create the type check block.
if (isSyntaxError(e)) {
const genDiags: ts.Diagnostic[] = [{
file: undefined,
start: undefined,
length: undefined,
messageText: e.message,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}];
return {genFiles: [], genDiags};
}
throw e;
}
}
/**
* Returns undefined if all files should be emitted.
*/
private getSourceFilesForEmit(): ts.SourceFile[]|undefined {
// TODO(tbosch): if one of the files contains a `const enum`
// always emit all files -> return undefined!
let sourceFilesToEmit = this.tsProgram.getSourceFiles().filter(
sf => { return !sf.isDeclarationFile && !GENERATED_FILES.test(sf.fileName); });
if (this.oldProgramEmittedSourceFiles) {
sourceFilesToEmit = sourceFilesToEmit.filter(sf => {
const oldFile = this.oldProgramEmittedSourceFiles !.get(sf.fileName);
return sf !== oldFile;
});
}
return sourceFilesToEmit;
}
private writeFile(
outFileName: string, outData: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, genFile?: GeneratedFile,
sourceFiles?: ReadonlyArray<ts.SourceFile>) {
// collect emittedLibrarySummaries
let baseFile: ts.SourceFile|undefined;
if (genFile) {
baseFile = this.tsProgram.getSourceFile(genFile.srcFileUrl);
if (baseFile) {
if (!this.emittedLibrarySummaries) {
this.emittedLibrarySummaries = [];
}
if (genFile.genFileUrl.endsWith('.ngsummary.json') && baseFile.fileName.endsWith('.d.ts')) {
this.emittedLibrarySummaries.push({
fileName: baseFile.fileName,
text: baseFile.text,
sourceFile: baseFile,
});
this.emittedLibrarySummaries.push({fileName: genFile.genFileUrl, text: outData});
if (!this.options.declaration) {
// If we don't emit declarations, still record an empty .ngfactory.d.ts file,
// as we might need it later on for resolving module names from summaries.
const ngFactoryDts =
genFile.genFileUrl.substring(0, genFile.genFileUrl.length - 15) + '.ngfactory.d.ts';
this.emittedLibrarySummaries.push({fileName: ngFactoryDts, text: ''});
}
} else if (outFileName.endsWith('.d.ts') && baseFile.fileName.endsWith('.d.ts')) {
const dtsSourceFilePath = genFile.genFileUrl.replace(/\.ts$/, '.d.ts');
// Note: Don't use sourceFiles here as the created .d.ts has a path in the outDir,
// but we need one that is next to the .ts file
this.emittedLibrarySummaries.push({fileName: dtsSourceFilePath, text: outData});
}
}
}
// Filter out generated files for which we didn't generate code.
// This can happen as the stub calculation is not completely exact.
// Note: sourceFile refers to the .ngfactory.ts / .ngsummary.ts file
// node_emitter_transform already set the file contents to be empty,
// so this code only needs to skip the file if !allowEmptyCodegenFiles.
const isGenerated = GENERATED_FILES.test(outFileName);
if (isGenerated && !this.options.allowEmptyCodegenFiles &&
(!genFile || !genFile.stmts || genFile.stmts.length === 0)) {
return;
}
if (baseFile) {
sourceFiles = sourceFiles ? [...sourceFiles, baseFile] : [baseFile];
}
// TODO: remove any when TS 2.4 support is removed.
this.host.writeFile(outFileName, outData, writeByteOrderMark, onError, sourceFiles as any);
}
}
/**
* Checks whether a given version ∈ [minVersion, maxVersion[
* An error will be thrown if the following statements are simultaneously true:
* - the given version ∉ [minVersion, maxVersion[,
* - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck
* is false)
*
* @param version The version on which the check will be performed
* @param minVersion The lower bound version. A valid version needs to be greater than minVersion
* @param maxVersion The upper bound version. A valid version needs to be strictly less than
* maxVersion
* @param disableVersionCheck Indicates whether version check should be bypassed
*
* @throws Will throw an error if the following statements are simultaneously true:
* - the given version ∉ [minVersion, maxVersion[,
* - the result of the version check is not meant to be bypassed (the parameter disableVersionCheck
* is false)
*/
export function checkVersion(
version: string, minVersion: string, maxVersion: string,
disableVersionCheck: boolean | undefined) {
if ((compareVersions(version, minVersion) < 0 || compareVersions(version, maxVersion) >= 0) &&
!disableVersionCheck) {
throw new Error(
`The Angular Compiler requires TypeScript >=${minVersion} and <${maxVersion} but ${version} was found instead.`);
}
}
export function createProgram({rootNames, options, host, oldProgram}: {
rootNames: ReadonlyArray<string>,
options: CompilerOptions,
host: CompilerHost, oldProgram?: Program
}): Program {
if (options.enableIvy === 'ngtsc') {
return new NgtscProgram(rootNames, options, host, oldProgram);
}
return new AngularCompilerProgram(rootNames, options, host, oldProgram);
}
// Compute the AotCompiler options
function getAotCompilerOptions(options: CompilerOptions): AotCompilerOptions {
let missingTranslation = core.MissingTranslationStrategy.Warning;
switch (options.i18nInMissingTranslations) {
case 'ignore':
missingTranslation = core.MissingTranslationStrategy.Ignore;
break;
case 'error':
missingTranslation = core.MissingTranslationStrategy.Error;
break;
}
let translations: string = '';
if (options.i18nInFile) {
if (!options.i18nInLocale) {
throw new Error(`The translation file (${options.i18nInFile}) locale must be provided.`);
}
translations = fs.readFileSync(options.i18nInFile, 'utf8');
} else {
// No translations are provided, ignore any errors
// We still go through i18n to remove i18n attributes
missingTranslation = core.MissingTranslationStrategy.Ignore;
}
return {
locale: options.i18nInLocale,
i18nFormat: options.i18nInFormat || options.i18nOutFormat, translations, missingTranslation,
enableSummariesForJit: options.enableSummariesForJit,
preserveWhitespaces: options.preserveWhitespaces,
fullTemplateTypeCheck: options.fullTemplateTypeCheck,
allowEmptyCodegenFiles: options.allowEmptyCodegenFiles,
enableIvy: options.enableIvy,
};
}
function getNgOptionDiagnostics(options: CompilerOptions): ReadonlyArray<Diagnostic> {
if (options.annotationsAs) {
switch (options.annotationsAs) {
case 'decorators':
case 'static fields':
break;
default:
return [{
messageText:
'Angular compiler options "annotationsAs" only supports "static fields" and "decorators"',
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}];
}
}
return [];
}
function normalizeSeparators(path: string): string {
return path.replace(/\\/g, '/');
}
/**
* Returns a function that can adjust a path from source path to out path,
* based on an existing mapping from source to out path.
*
* TODO(tbosch): talk to the TypeScript team to expose their logic for calculating the `rootDir`
* if none was specified.
*
* Note: This function works on normalized paths from typescript.
*
* @param outDir
* @param outSrcMappings
*/
export function createSrcToOutPathMapper(
outDir: string | undefined, sampleSrcFileName: string | undefined,
sampleOutFileName: string | undefined, host: {
dirname: typeof path.dirname,
resolve: typeof path.resolve,
relative: typeof path.relative
} = path): (srcFileName: string) => string {
let srcToOutPath: (srcFileName: string) => string;
if (outDir) {
let path: {} = {}; // Ensure we error if we use `path` instead of `host`.
if (sampleSrcFileName == null || sampleOutFileName == null) {
throw new Error(`Can't calculate the rootDir without a sample srcFileName / outFileName. `);
}
const srcFileDir = normalizeSeparators(host.dirname(sampleSrcFileName));
const outFileDir = normalizeSeparators(host.dirname(sampleOutFileName));
if (srcFileDir === outFileDir) {
return (srcFileName) => srcFileName;
}
// calculate the common suffix, stopping
// at `outDir`.
const srcDirParts = srcFileDir.split('/');
const outDirParts = normalizeSeparators(host.relative(outDir, outFileDir)).split('/');
let i = 0;
while (i < Math.min(srcDirParts.length, outDirParts.length) &&
srcDirParts[srcDirParts.length - 1 - i] === outDirParts[outDirParts.length - 1 - i])
i++;
const rootDir = srcDirParts.slice(0, srcDirParts.length - i).join('/');
srcToOutPath = (srcFileName) => host.resolve(outDir, host.relative(rootDir, srcFileName));
} else {
srcToOutPath = (srcFileName) => srcFileName;
}
return srcToOutPath;
}
export function i18nExtract(
formatName: string | null, outFile: string | null, host: ts.CompilerHost,
options: CompilerOptions, bundle: MessageBundle): string[] {
formatName = formatName || 'xlf';
// Checks the format and returns the extension
const ext = i18nGetExtension(formatName);
const content = i18nSerialize(bundle, formatName, options);
const dstFile = outFile || `messages.${ext}`;
const dstPath = path.resolve(options.outDir || options.basePath, dstFile);
host.writeFile(dstPath, content, false, undefined, []);
return [dstPath];
}
export function i18nSerialize(
bundle: MessageBundle, formatName: string, options: CompilerOptions): string {
const format = formatName.toLowerCase();
let serializer: Serializer;
switch (format) {
case 'xmb':
serializer = new Xmb();
break;
case 'xliff2':
case 'xlf2':
serializer = new Xliff2();
break;
case 'xlf':
case 'xliff':
default:
serializer = new Xliff();
}
return bundle.write(serializer, getPathNormalizer(options.basePath));
}
function getPathNormalizer(basePath?: string) {
// normalize source paths by removing the base path and always using "/" as a separator
return (sourcePath: string) => {
sourcePath = basePath ? path.relative(basePath, sourcePath) : sourcePath;
return sourcePath.split(path.sep).join('/');
};
}
export function i18nGetExtension(formatName: string): string {
const format = formatName.toLowerCase();
switch (format) {
case 'xmb':
return 'xmb';
case 'xlf':
case 'xlif':
case 'xliff':
case 'xlf2':
case 'xliff2':
return 'xlf';
}
throw new Error(`Unsupported format "${formatName}"`);
}
function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
const diagnostics: ts.Diagnostic[] = [];
let emitSkipped = false;
const emittedFiles: string[] = [];
for (const er of emitResults) {
diagnostics.push(...er.diagnostics);
emitSkipped = emitSkipped || er.emitSkipped;
emittedFiles.push(...(er.emittedFiles || []));
}
return {diagnostics, emitSkipped, emittedFiles};
}
function diagnosticSourceOfSpan(span: ParseSourceSpan): ts.SourceFile {
// For diagnostics, TypeScript only uses the fileName and text properties.
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
return ({ fileName: span.start.file.url, text: span.start.file.content } as any);
}
function diagnosticSourceOfFileName(fileName: string, program: ts.Program): ts.SourceFile {
const sourceFile = program.getSourceFile(fileName);
if (sourceFile) return sourceFile;
// If we are reporting diagnostics for a source file that is not in the project then we need
// to fake a source file so the diagnostic formatting routines can emit the file name.
// The redundant '()' are here is to avoid having clang-format breaking the line incorrectly.
return ({ fileName, text: '' } as any);
}
function diagnosticChainFromFormattedDiagnosticChain(chain: FormattedMessageChain):
DiagnosticMessageChain {
return {
messageText: chain.message,
next: chain.next && diagnosticChainFromFormattedDiagnosticChain(chain.next),
position: chain.position
};
}
function syntaxErrorToDiagnostics(error: Error): Diagnostic[] {
const parserErrors = getParseErrors(error);
if (parserErrors && parserErrors.length) {
return parserErrors.map<Diagnostic>(e => ({
messageText: e.contextualMessage(),
file: diagnosticSourceOfSpan(e.span),
start: e.span.start.offset,
length: e.span.end.offset - e.span.start.offset,
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
}));
} else if (isFormattedError(error)) {
return [{
messageText: error.message,
chain: error.chain && diagnosticChainFromFormattedDiagnosticChain(error.chain),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE,
position: error.position
}];
}
// Produce a Diagnostic anyway since we know for sure `error` is a SyntaxError
return [{
messageText: error.message,
category: ts.DiagnosticCategory.Error,
code: DEFAULT_ERROR_CODE,
source: SOURCE,
}];
}