fix(compiler): various squashed fixes for the new ngc

introduce the option `allowEmptyCodegenFiles` to generate all generated files,
even if they are empty.
- also provides the original source files from which the file was generated
  in the write file callback
- needed e.g. for G3 when copying over pinto mod names from the original component
  to all generated files

use `importAs` from flat modules when writing summaries
- i.e. prevents incorrect entries like @angular/common/common in the .ngsummary.json files.

change interaction between ng and ts to prevent race conditions
- before Angular would rely on TS to first read the file for which we generate files,
  and then the generated files. However, this can break easily when we reuse an old program.

don’t generate files for sources that are outside of `rootDir`
(see #19337)
This commit is contained in:
Tobias Bosch
2017-09-21 18:05:07 -07:00
committed by Victor Berchet
parent 13613d4acb
commit a8a9660112
16 changed files with 746 additions and 459 deletions

View File

@ -14,7 +14,7 @@ import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata} from '../metadata/index';
import {CompilerHost, CompilerOptions} from './api';
import {CompilerHost, CompilerOptions, LibrarySummary} from './api';
import {GENERATED_FILES} from './util';
const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/;
@ -37,6 +37,11 @@ interface GenSourceFile {
emitCtx: EmitterVisitorContext;
}
export interface CodeGenerator {
generateFile(genFileName: string, baseFileName?: string): GeneratedFile;
findGeneratedFileNames(fileName: string): string[];
}
/**
* Implements the following hosts based on an api.CompilerHost:
* - ts.CompilerHost to be consumed by a ts.Program
@ -48,11 +53,12 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
TypeCheckHost {
private rootDirs: string[];
private moduleResolutionCache: ts.ModuleResolutionCache;
private originalSourceFiles = new Map<string, ts.SourceFile>();
private originalSourceFiles = new Map<string, ts.SourceFile|undefined>();
private originalFileExistsCache = new Map<string, boolean>();
private generatedSourceFiles = new Map<string, GenSourceFile>();
private generatedCodeFor = new Set<string>();
private generatedCodeFor = new Map<string, string[]>();
private emitter = new TypeScriptEmitter();
private librarySummaries = new Map<string, LibrarySummary>();
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
trace: (s: string) => void;
@ -61,10 +67,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
constructor(
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
private metadataProvider: MetadataProvider,
private codeGenerator: (fileName: string) => GeneratedFile[],
private summariesFromPreviousCompilations: Map<string, string>) {
private metadataProvider: MetadataProvider, private codeGenerator: CodeGenerator,
librarySummaries: LibrarySummary[]) {
super(options, context);
librarySummaries.forEach(summary => this.librarySummaries.set(summary.fileName, summary));
this.moduleResolutionCache = ts.createModuleResolutionCache(
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
const basePath = this.options.basePath !;
@ -168,6 +174,11 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
'fileNameToModuleName from containingFile', containingFile, 'to importedFile',
importedFile);
}
const importAs = this.getImportAs(importedFile);
if (importAs) {
return importAs;
}
// drop extension
importedFile = importedFile.replace(EXT, '');
const importedFilePackagName = getPackageName(importedFile);
@ -229,15 +240,18 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
private getOriginalSourceFile(
filePath: string, languageVersion?: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile|undefined {
let sf = this.originalSourceFiles.get(filePath);
if (sf) {
return sf;
onError?: ((message: string) => void)|undefined): ts.SourceFile|null {
// Note: we need the explicit check via `has` as we also cache results
// that were null / undefined.
if (this.originalSourceFiles.has(filePath)) {
return this.originalSourceFiles.get(filePath) !;
}
if (!languageVersion) {
languageVersion = this.options.target || ts.ScriptTarget.Latest;
}
sf = this.context.getSourceFile(filePath, languageVersion, onError);
// Note: This can also return undefined,
// as the TS typings are not correct!
const sf = this.context.getSourceFile(filePath, languageVersion, onError) || null;
this.originalSourceFiles.set(filePath, sf);
return sf;
}
@ -250,9 +264,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
return this.metadataProvider.getMetadata(sf);
}
updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile|null {
updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile {
if (!genFile.stmts) {
return null;
throw new Error(
`Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`);
}
const oldGenFile = this.generatedSourceFiles.get(genFile.genFileUrl);
if (!oldGenFile) {
@ -271,10 +286,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
return this.addGeneratedFile(genFile, newRefs);
}
private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): ts.SourceFile
|null {
private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): ts.SourceFile {
if (!genFile.stmts) {
return null;
throw new Error(
`Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`);
}
const {sourceText, context} = this.emitter.emitStatementsAndContext(
genFile.srcFileUrl, genFile.genFileUrl, genFile.stmts, /* preamble */ '',
@ -288,49 +303,95 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
return sf;
}
private ensureCodeGeneratedFor(fileName: string): void {
if (this.generatedCodeFor.has(fileName)) {
return;
shouldGenerateFile(fileName: string): {generate: boolean, baseFileName?: string} {
// TODO(tbosch): allow generating files that are not in the rootDir
// See https://github.com/angular/angular/issues/19337
if (this.options.rootDir && !fileName.startsWith(this.options.rootDir)) {
return {generate: false};
}
this.generatedCodeFor.add(fileName);
const genMatch = GENERATED_FILES.exec(fileName);
if (!genMatch) {
return {generate: false};
}
const [, base, genSuffix, suffix] = genMatch;
if (suffix !== 'ts') {
return {generate: false};
}
let baseFileName: string|undefined;
if (genSuffix.indexOf('ngstyle') >= 0) {
// Note: ngstyle files have names like `afile.css.ngstyle.ts`
if (!this.originalFileExists(base)) {
return {generate: false};
}
} else {
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
baseFileName = [`${base}.ts`, `${base}.d.ts`].find(
baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName));
if (!baseFileName) {
return {generate: false};
}
}
return {generate: true, baseFileName};
}
const baseNameFromGeneratedFile = this._getBaseNameForGeneratedSourceFile(fileName);
if (baseNameFromGeneratedFile) {
return this.ensureCodeGeneratedFor(baseNameFromGeneratedFile);
}
const sf = this.getOriginalSourceFile(fileName, this.options.target || ts.ScriptTarget.Latest);
if (!sf) {
return;
}
const genFileNames: string[] = [];
if (this.isSourceFile(fileName)) {
// Note: we can't exit early here,
// as we might need to clear out old changes to `SourceFile.referencedFiles`
// that were created by a previous run, given an original CompilerHost
// that caches source files.
const genFiles = this.codeGenerator(fileName);
genFiles.forEach(genFile => {
const sf = this.addGeneratedFile(genFile, genFileExternalReferences(genFile));
if (sf) {
genFileNames.push(sf.fileName);
}
});
}
addReferencesToSourceFile(sf, genFileNames);
shouldGenerateFilesFor(fileName: string) {
// TODO(tbosch): allow generating files that are not in the rootDir
// See https://github.com/angular/angular/issues/19337
return !GENERATED_FILES.test(fileName) && this.isSourceFile(fileName) &&
(!this.options.rootDir || pathStartsWithPrefix(this.options.rootDir, fileName));
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined): ts.SourceFile {
this.ensureCodeGeneratedFor(fileName);
const genFile = this.generatedSourceFiles.get(fileName);
if (genFile) {
return genFile.sourceFile;
// Note: Don't exit early in this method to make sure
// we always have up to date references on the file!
let genFileNames: string[] = [];
let sf = this.getGeneratedFile(fileName);
if (!sf) {
const summary = this.librarySummaries.get(fileName);
if (summary) {
if (!summary.sourceFile) {
summary.sourceFile = ts.createSourceFile(
fileName, summary.text, this.options.target || ts.ScriptTarget.Latest);
}
sf = summary.sourceFile;
genFileNames = [];
}
}
if (!sf) {
sf = this.getOriginalSourceFile(fileName);
const cachedGenFiles = this.generatedCodeFor.get(fileName);
if (cachedGenFiles) {
genFileNames = cachedGenFiles;
} else {
if (!this.options.noResolve && this.shouldGenerateFilesFor(fileName)) {
genFileNames = this.codeGenerator.findGeneratedFileNames(fileName);
}
this.generatedCodeFor.set(fileName, genFileNames);
}
}
if (sf) {
addReferencesToSourceFile(sf, genFileNames);
}
// TODO(tbosch): TypeScript's typings for getSourceFile are incorrect,
// as it can very well return undefined.
return this.getOriginalSourceFile(fileName, languageVersion, onError) !;
return sf !;
}
private getGeneratedFile(fileName: string): ts.SourceFile|null {
const genSrcFile = this.generatedSourceFiles.get(fileName);
if (genSrcFile) {
return genSrcFile.sourceFile;
}
const {generate, baseFileName} = this.shouldGenerateFile(fileName);
if (generate) {
const genFile = this.codeGenerator.generateFile(fileName, baseFileName);
return this.addGeneratedFile(genFile, genFileExternalReferences(genFile));
}
return null;
}
private originalFileExists(fileName: string): boolean {
@ -344,48 +405,19 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
fileExists(fileName: string): boolean {
fileName = stripNgResourceSuffix(fileName);
if (fileName.endsWith('.ngfactory.d.ts')) {
// Note: the factories of a previous program
// are not reachable via the regular fileExists
// as they might be in the outDir. So we derive their
// fileExist information based on the .ngsummary.json file.
if (this.summariesFromPreviousCompilations.has(summaryFileName(fileName))) {
return true;
}
}
// Note: Don't rely on this.generatedSourceFiles here,
// as it might not have been filled yet.
if (this._getBaseNameForGeneratedSourceFile(fileName)) {
if (this.librarySummaries.has(fileName) || this.generatedSourceFiles.has(fileName)) {
return true;
}
return this.summariesFromPreviousCompilations.has(fileName) ||
this.originalFileExists(fileName);
}
private _getBaseNameForGeneratedSourceFile(genFileName: string): string|undefined {
const genMatch = GENERATED_FILES.exec(genFileName);
if (!genMatch) {
return undefined;
}
const [, base, genSuffix, suffix] = genMatch;
if (suffix !== 'ts') {
return undefined;
}
if (genSuffix.indexOf('ngstyle') >= 0) {
// Note: ngstyle files have names like `afile.css.ngstyle.ts`
return base;
} else {
// Note: on-the-fly generated files always have a `.ts` suffix,
// but the file from which we generated it can be a `.ts`/ `.d.ts`
// (see options.generateCodeForLibraries).
return [`${base}.ts`, `${base}.d.ts`].find(
baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName));
if (this.shouldGenerateFile(fileName).generate) {
return true;
}
return this.originalFileExists(fileName);
}
loadSummary(filePath: string): string|null {
if (this.summariesFromPreviousCompilations.has(filePath)) {
return this.summariesFromPreviousCompilations.get(filePath) !;
const summary = this.librarySummaries.get(filePath);
if (summary) {
return summary.text;
}
return super.loadSummary(filePath);
}
@ -393,13 +425,19 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
isSourceFile(filePath: string): boolean {
// If we have a summary from a previous compilation,
// treat the file never as a source file.
if (this.summariesFromPreviousCompilations.has(summaryFileName(filePath))) {
if (this.librarySummaries.has(filePath)) {
return false;
}
return super.isSourceFile(filePath);
}
readFile = (fileName: string) => this.context.readFile(fileName);
readFile(fileName: string) {
const summary = this.librarySummaries.get(fileName);
if (summary) {
return summary.text;
}
return this.context.readFile(fileName);
}
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.context.getDefaultLibFileName(options)
getCurrentDirectory = () => this.context.getCurrentDirectory();
@ -451,12 +489,19 @@ function getPackageName(filePath: string): string|null {
export function relativeToRootDirs(filePath: string, rootDirs: string[]): string {
if (!filePath) return filePath;
for (const dir of rootDirs || []) {
const rel = path.relative(dir, filePath);
if (rel.indexOf('.') != 0) return rel;
const rel = pathStartsWithPrefix(dir, filePath);
if (rel) {
return rel;
}
}
return filePath;
}
function pathStartsWithPrefix(prefix: string, fullPath: string): string|null {
const rel = path.relative(prefix, fullPath);
return rel.startsWith('..') ? null : rel;
}
function stripNodeModulesPrefix(filePath: string): string {
return filePath.replace(/.*node_modules\//, '');
}
@ -473,12 +518,3 @@ function stripNgResourceSuffix(fileName: string): string {
function addNgResourceSuffix(fileName: string): string {
return `${fileName}.$ngresource$`;
}
function summaryFileName(fileName: string): string {
const genFileMatch = GENERATED_FILES.exec(fileName);
if (genFileMatch) {
const base = genFileMatch[1];
return base + '.ngsummary.json';
}
return fileName.replace(EXT, '') + '.ngsummary.json';
}