This commit adds support for compiling the same program repeatedly in a way that's similar to how incremental builds work in a tool such as the CLI. * support is added to the compiler entrypoint for reuse of the Program object between compilations. This is the basis of the compiler's incremental compilation model. * support is added to wrap the CompilerHost the compiler creates and cache ts.SourceFiles in between compilations. * support is added to track when files are emitted, for assertion purposes. * an 'exclude' section is added to the base tsconfig to prevent .d.ts outputs from the first compilation from becoming inputs to any subsequent compilations. PR Close #29380
		
			
				
	
	
		
			686 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			686 lines
		
	
	
		
			28 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 {AotCompilerHost, EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
 | |
| import * as path from 'path';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
 | |
| import {METADATA_VERSION, ModuleMetadata} from '../metadata/index';
 | |
| 
 | |
| import {CompilerHost, CompilerOptions, LibrarySummary} from './api';
 | |
| import {MetadataReaderHost, createMetadataReaderCache, readMetadata} from './metadata_reader';
 | |
| import {DTS, GENERATED_FILES, isInRootDir, relativeToRootDirs} from './util';
 | |
| 
 | |
| const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-|\.)+|(@(\w|-|\.)+\/(\w|-|\.)+))/;
 | |
| const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
 | |
| const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/;
 | |
| 
 | |
| let wrapHostForTest: ((host: ts.CompilerHost) => ts.CompilerHost)|null = null;
 | |
| 
 | |
| export function setWrapHostForTest(wrapFn: ((host: ts.CompilerHost) => ts.CompilerHost) | null):
 | |
|     void {
 | |
|   wrapHostForTest = wrapFn;
 | |
| }
 | |
| 
 | |
| export function createCompilerHost(
 | |
|     {options, tsHost = ts.createCompilerHost(options, true)}:
 | |
|         {options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
 | |
|   if (wrapHostForTest !== null) {
 | |
|     tsHost = wrapHostForTest(tsHost);
 | |
|   }
 | |
|   return tsHost;
 | |
| }
 | |
| 
 | |
| export interface MetadataProvider {
 | |
|   getMetadata(sourceFile: ts.SourceFile): ModuleMetadata|undefined;
 | |
| }
 | |
| 
 | |
| interface GenSourceFile {
 | |
|   externalReferences: Set<string>;
 | |
|   sourceFile: ts.SourceFile;
 | |
|   emitCtx: EmitterVisitorContext;
 | |
| }
 | |
| 
 | |
| export interface CodeGenerator {
 | |
|   generateFile(genFileName: string, baseFileName?: string): GeneratedFile;
 | |
|   findGeneratedFileNames(fileName: string): string[];
 | |
| }
 | |
| 
 | |
| function assert<T>(condition: T | null | undefined) {
 | |
|   if (!condition) {
 | |
|     // TODO(chuckjaz): do the right thing
 | |
|   }
 | |
|   return condition !;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Implements the following hosts based on an api.CompilerHost:
 | |
|  * - ts.CompilerHost to be consumed by a ts.Program
 | |
|  * - AotCompilerHost for @angular/compiler
 | |
|  * - TypeCheckHost for mapping ts errors to ng errors (via translateDiagnostics)
 | |
|  */
 | |
| export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHost, AotCompilerHost,
 | |
|     TypeCheckHost {
 | |
|   private metadataReaderCache = createMetadataReaderCache();
 | |
|   private fileNameToModuleNameCache = new Map<string, string>();
 | |
|   private flatModuleIndexCache = new Map<string, boolean>();
 | |
|   private flatModuleIndexNames = new Set<string>();
 | |
|   private flatModuleIndexRedirectNames = new Set<string>();
 | |
|   private rootDirs: string[];
 | |
|   private moduleResolutionCache: ts.ModuleResolutionCache;
 | |
|   private originalSourceFiles = new Map<string, ts.SourceFile|null>();
 | |
|   private originalFileExistsCache = new Map<string, boolean>();
 | |
|   private generatedSourceFiles = new Map<string, GenSourceFile>();
 | |
|   private generatedCodeFor = new Map<string, string[]>();
 | |
|   private emitter = new TypeScriptEmitter();
 | |
|   private metadataReaderHost: MetadataReaderHost;
 | |
| 
 | |
|   // TODO(issue/24571): remove '!'.
 | |
|   getCancellationToken !: () => ts.CancellationToken;
 | |
|   // TODO(issue/24571): remove '!'.
 | |
|   getDefaultLibLocation !: () => string;
 | |
|   // TODO(issue/24571): remove '!'.
 | |
|   trace !: (s: string) => void;
 | |
|   // TODO(issue/24571): remove '!'.
 | |
|   getDirectories !: (path: string) => string[];
 | |
|   resolveTypeReferenceDirectives?:
 | |
|       (names: string[], containingFile: string) => ts.ResolvedTypeReferenceDirective[];
 | |
|   directoryExists?: (directoryName: string) => boolean;
 | |
| 
 | |
|   constructor(
 | |
|       private rootFiles: ReadonlyArray<string>, private options: CompilerOptions,
 | |
|       private context: CompilerHost, private metadataProvider: MetadataProvider,
 | |
|       private codeGenerator: CodeGenerator,
 | |
|       private librarySummaries = new Map<string, LibrarySummary>()) {
 | |
|     this.moduleResolutionCache = ts.createModuleResolutionCache(
 | |
|         this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
 | |
|     const basePath = this.options.basePath !;
 | |
|     this.rootDirs =
 | |
|         (this.options.rootDirs || [this.options.basePath !]).map(p => path.resolve(basePath, p));
 | |
|     if (context.getDirectories) {
 | |
|       this.getDirectories = path => context.getDirectories !(path);
 | |
|     }
 | |
|     if (context.directoryExists) {
 | |
|       this.directoryExists = directoryName => context.directoryExists !(directoryName);
 | |
|     }
 | |
|     if (context.getCancellationToken) {
 | |
|       this.getCancellationToken = () => context.getCancellationToken !();
 | |
|     }
 | |
|     if (context.getDefaultLibLocation) {
 | |
|       this.getDefaultLibLocation = () => context.getDefaultLibLocation !();
 | |
|     }
 | |
|     if (context.resolveTypeReferenceDirectives) {
 | |
|       // Backward compatibility with TypeScript 2.9 and older since return
 | |
|       // type has changed from (ts.ResolvedTypeReferenceDirective | undefined)[]
 | |
|       // to ts.ResolvedTypeReferenceDirective[] in Typescript 3.0
 | |
|       type ts3ResolveTypeReferenceDirectives = (names: string[], containingFile: string) =>
 | |
|           ts.ResolvedTypeReferenceDirective[];
 | |
|       this.resolveTypeReferenceDirectives = (names: string[], containingFile: string) =>
 | |
|           (context.resolveTypeReferenceDirectives as ts3ResolveTypeReferenceDirectives) !(
 | |
|               names, containingFile);
 | |
|     }
 | |
|     if (context.trace) {
 | |
|       this.trace = s => context.trace !(s);
 | |
|     }
 | |
|     if (context.fileNameToModuleName) {
 | |
|       this.fileNameToModuleName = context.fileNameToModuleName.bind(context);
 | |
|     }
 | |
|     // Note: don't copy over context.moduleNameToFileName as we first
 | |
|     // normalize undefined containingFile to a filled containingFile.
 | |
|     if (context.resourceNameToFileName) {
 | |
|       this.resourceNameToFileName = context.resourceNameToFileName.bind(context);
 | |
|     }
 | |
|     if (context.toSummaryFileName) {
 | |
|       this.toSummaryFileName = context.toSummaryFileName.bind(context);
 | |
|     }
 | |
|     if (context.fromSummaryFileName) {
 | |
|       this.fromSummaryFileName = context.fromSummaryFileName.bind(context);
 | |
|     }
 | |
|     this.metadataReaderHost = {
 | |
|       cacheMetadata: () => true,
 | |
|       getSourceFileMetadata: (filePath) => {
 | |
|         const sf = this.getOriginalSourceFile(filePath);
 | |
|         return sf ? this.metadataProvider.getMetadata(sf) : undefined;
 | |
|       },
 | |
|       fileExists: (filePath) => this.originalFileExists(filePath),
 | |
|       readFile: (filePath) => assert(this.context.readFile(filePath)),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   private resolveModuleName(moduleName: string, containingFile: string): ts.ResolvedModule
 | |
|       |undefined {
 | |
|     const rm = ts.resolveModuleName(
 | |
|                      moduleName, containingFile.replace(/\\/g, '/'), this.options, this,
 | |
|                      this.moduleResolutionCache)
 | |
|                    .resolvedModule;
 | |
|     if (rm && this.isSourceFile(rm.resolvedFileName) && DTS.test(rm.resolvedFileName)) {
 | |
|       // Case: generateCodeForLibraries = true and moduleName is
 | |
|       // a .d.ts file in a node_modules folder.
 | |
|       // Need to set isExternalLibraryImport to false so that generated files for that file
 | |
|       // are emitted.
 | |
|       rm.isExternalLibraryImport = false;
 | |
|     }
 | |
|     return rm;
 | |
|   }
 | |
| 
 | |
|   // Note: We implement this method so that TypeScript and Angular share the same
 | |
|   // ts.ModuleResolutionCache
 | |
|   // and that we can tell ts.Program about our different opinion about
 | |
|   // ResolvedModule.isExternalLibraryImport
 | |
|   // (see our isSourceFile method).
 | |
|   resolveModuleNames(moduleNames: string[], containingFile: string): ts.ResolvedModule[] {
 | |
|     // TODO(tbosch): this seems to be a typing error in TypeScript,
 | |
|     // as it contains assertions that the result contains the same number of entries
 | |
|     // as the given module names.
 | |
|     return <ts.ResolvedModule[]>moduleNames.map(
 | |
|         moduleName => this.resolveModuleName(moduleName, containingFile));
 | |
|   }
 | |
| 
 | |
|   moduleNameToFileName(m: string, containingFile?: string): string|null {
 | |
|     if (!containingFile) {
 | |
|       if (m.indexOf('.') === 0) {
 | |
|         throw new Error('Resolution of relative paths requires a containing file.');
 | |
|       }
 | |
|       // Any containing file gives the same result for absolute imports
 | |
|       containingFile = this.rootFiles[0];
 | |
|     }
 | |
|     if (this.context.moduleNameToFileName) {
 | |
|       return this.context.moduleNameToFileName(m, containingFile);
 | |
|     }
 | |
|     const resolved = this.resolveModuleName(m, containingFile);
 | |
|     return resolved ? resolved.resolvedFileName : null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * We want a moduleId that will appear in import statements in the generated code
 | |
|    * which will be written to `containingFile`.
 | |
|    *
 | |
|    * Note that we also generate files for files in node_modules, as libraries
 | |
|    * only ship .metadata.json files but not the generated code.
 | |
|    *
 | |
|    * Logic:
 | |
|    * 1. if the importedFile and the containingFile are from the project sources
 | |
|    *    or from the same node_modules package, use a relative path
 | |
|    * 2. if the importedFile is in a node_modules package,
 | |
|    *    use a path that starts with the package name.
 | |
|    * 3. Error if the containingFile is in the node_modules package
 | |
|    *    and the importedFile is in the project soures,
 | |
|    *    as that is a violation of the principle that node_modules packages cannot
 | |
|    *    import project sources.
 | |
|    */
 | |
|   fileNameToModuleName(importedFile: string, containingFile: string): string {
 | |
|     const cacheKey = `${importedFile}:${containingFile}`;
 | |
|     let moduleName = this.fileNameToModuleNameCache.get(cacheKey);
 | |
|     if (moduleName != null) {
 | |
|       return moduleName;
 | |
|     }
 | |
| 
 | |
|     const originalImportedFile = importedFile;
 | |
|     if (this.options.traceResolution) {
 | |
|       console.error(
 | |
|           'fileNameToModuleName from containingFile', containingFile, 'to importedFile',
 | |
|           importedFile);
 | |
|     }
 | |
| 
 | |
|     // drop extension
 | |
|     importedFile = importedFile.replace(EXT, '');
 | |
|     const importedFilePackageName = getPackageName(importedFile);
 | |
|     const containingFilePackageName = getPackageName(containingFile);
 | |
| 
 | |
|     if (importedFilePackageName === containingFilePackageName ||
 | |
|         GENERATED_FILES.test(originalImportedFile)) {
 | |
|       const rootedContainingFile = relativeToRootDirs(containingFile, this.rootDirs);
 | |
|       const rootedImportedFile = relativeToRootDirs(importedFile, this.rootDirs);
 | |
| 
 | |
|       if (rootedContainingFile !== containingFile && rootedImportedFile !== importedFile) {
 | |
|         // if both files are contained in the `rootDirs`, then strip the rootDirs
 | |
|         containingFile = rootedContainingFile;
 | |
|         importedFile = rootedImportedFile;
 | |
|       }
 | |
|       moduleName = dotRelative(path.dirname(containingFile), importedFile);
 | |
|     } else if (importedFilePackageName) {
 | |
|       moduleName = stripNodeModulesPrefix(importedFile);
 | |
|       if (originalImportedFile.endsWith('.d.ts')) {
 | |
|         // the moduleName for these typings could be shortented to the npm package name
 | |
|         // if the npm package typings matches the importedFile
 | |
|         try {
 | |
|           const modulePath = importedFile.substring(0, importedFile.length - moduleName.length) +
 | |
|               importedFilePackageName;
 | |
|           const packageJson = require(modulePath + '/package.json');
 | |
|           const packageTypings = path.posix.join(modulePath, packageJson.typings);
 | |
|           if (packageTypings === originalImportedFile) {
 | |
|             moduleName = importedFilePackageName;
 | |
|           }
 | |
|         } catch {
 | |
|           // the above require() will throw if there is no package.json file
 | |
|           // and this is safe to ignore and correct to keep the longer
 | |
|           // moduleName in this case
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       throw new Error(
 | |
|           `Trying to import a source file from a node_modules package: import ${originalImportedFile} from ${containingFile}`);
 | |
|     }
 | |
| 
 | |
|     this.fileNameToModuleNameCache.set(cacheKey, moduleName);
 | |
|     return moduleName;
 | |
|   }
 | |
| 
 | |
|   resourceNameToFileName(resourceName: string, containingFile: string): string|null {
 | |
|     // Note: we convert package paths into relative paths to be compatible with the the
 | |
|     // previous implementation of UrlResolver.
 | |
|     const firstChar = resourceName[0];
 | |
|     if (firstChar === '/') {
 | |
|       resourceName = resourceName.slice(1);
 | |
|     } else if (firstChar !== '.') {
 | |
|       resourceName = `./${resourceName}`;
 | |
|     }
 | |
|     let filePathWithNgResource =
 | |
|         this.moduleNameToFileName(addNgResourceSuffix(resourceName), containingFile);
 | |
|     // If the user specified styleUrl pointing to *.scss, but the Sass compiler was run before
 | |
|     // Angular, then the resource may have been generated as *.css. Simply try the resolution again.
 | |
|     if (!filePathWithNgResource && CSS_PREPROCESSOR_EXT.test(resourceName)) {
 | |
|       const fallbackResourceName = resourceName.replace(CSS_PREPROCESSOR_EXT, '.css');
 | |
|       filePathWithNgResource =
 | |
|           this.moduleNameToFileName(addNgResourceSuffix(fallbackResourceName), containingFile);
 | |
|     }
 | |
|     const result = filePathWithNgResource ? stripNgResourceSuffix(filePathWithNgResource) : null;
 | |
|     // Used under Bazel to report more specific error with remediation advice
 | |
|     if (!result && (this.context as any).reportMissingResource) {
 | |
|       (this.context as any).reportMissingResource(resourceName);
 | |
|     }
 | |
|     return result;
 | |
|   }
 | |
| 
 | |
|   toSummaryFileName(fileName: string, referringSrcFileName: string): string {
 | |
|     return this.fileNameToModuleName(fileName, referringSrcFileName);
 | |
|   }
 | |
| 
 | |
|   fromSummaryFileName(fileName: string, referringLibFileName: string): string {
 | |
|     const resolved = this.moduleNameToFileName(fileName, referringLibFileName);
 | |
|     if (!resolved) {
 | |
|       throw new Error(`Could not resolve ${fileName} from ${referringLibFileName}`);
 | |
|     }
 | |
|     return resolved;
 | |
|   }
 | |
| 
 | |
|   parseSourceSpanOf(fileName: string, line: number, character: number): ParseSourceSpan|null {
 | |
|     const data = this.generatedSourceFiles.get(fileName);
 | |
|     if (data && data.emitCtx) {
 | |
|       return data.emitCtx.spanOf(line, character);
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   private getOriginalSourceFile(
 | |
|       filePath: string, languageVersion?: ts.ScriptTarget,
 | |
|       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;
 | |
|     }
 | |
|     // 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;
 | |
|   }
 | |
| 
 | |
|   updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile {
 | |
|     if (!genFile.stmts) {
 | |
|       throw new Error(
 | |
|           `Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`);
 | |
|     }
 | |
|     const oldGenFile = this.generatedSourceFiles.get(genFile.genFileUrl);
 | |
|     if (!oldGenFile) {
 | |
|       throw new Error(`Illegal State: previous GeneratedFile not found for ${genFile.genFileUrl}.`);
 | |
|     }
 | |
|     const newRefs = genFileExternalReferences(genFile);
 | |
|     const oldRefs = oldGenFile.externalReferences;
 | |
|     let refsAreEqual = oldRefs.size === newRefs.size;
 | |
|     if (refsAreEqual) {
 | |
|       newRefs.forEach(r => refsAreEqual = refsAreEqual && oldRefs.has(r));
 | |
|     }
 | |
|     if (!refsAreEqual) {
 | |
|       throw new Error(
 | |
|           `Illegal State: external references changed in ${genFile.genFileUrl}.\nOld: ${Array.from(oldRefs)}.\nNew: ${Array.from(newRefs)}`);
 | |
|     }
 | |
|     return this.addGeneratedFile(genFile, newRefs);
 | |
|   }
 | |
| 
 | |
|   private addGeneratedFile(genFile: GeneratedFile, externalReferences: Set<string>): ts.SourceFile {
 | |
|     if (!genFile.stmts) {
 | |
|       throw new Error(
 | |
|           `Invalid Argument: Expected a GenerateFile with statements. ${genFile.genFileUrl}`);
 | |
|     }
 | |
|     const {sourceText, context} = this.emitter.emitStatementsAndContext(
 | |
|         genFile.genFileUrl, genFile.stmts, /* preamble */ '',
 | |
|         /* emitSourceMaps */ false);
 | |
|     const sf = ts.createSourceFile(
 | |
|         genFile.genFileUrl, sourceText, this.options.target || ts.ScriptTarget.Latest);
 | |
|     if (this.options.module === ts.ModuleKind.AMD || this.options.module === ts.ModuleKind.UMD) {
 | |
|       if (this.context.amdModuleName) {
 | |
|         const moduleName = this.context.amdModuleName(sf);
 | |
|         if (moduleName) sf.moduleName = moduleName;
 | |
|       } else if (/node_modules/.test(genFile.genFileUrl)) {
 | |
|         // If we are generating an ngModule file under node_modules, we know the right module name
 | |
|         // We don't need the host to supply a function in this case.
 | |
|         sf.moduleName = stripNodeModulesPrefix(genFile.genFileUrl.replace(EXT, ''));
 | |
|       }
 | |
|     }
 | |
|     this.generatedSourceFiles.set(genFile.genFileUrl, {
 | |
|       sourceFile: sf,
 | |
|       emitCtx: context, externalReferences,
 | |
|     });
 | |
|     return sf;
 | |
|   }
 | |
| 
 | |
|   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 (!isInRootDir(fileName, this.options)) {
 | |
|       return {generate: false};
 | |
|     }
 | |
|     const genMatch = GENERATED_FILES.exec(fileName);
 | |
|     if (!genMatch) {
 | |
|       return {generate: false};
 | |
|     }
 | |
|     const [, base, genSuffix, suffix] = genMatch;
 | |
|     if (suffix !== 'ts' && suffix !== 'tsx') {
 | |
|       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`/ `.tsx`/ `.d.ts`
 | |
|       // (see options.generateCodeForLibraries).
 | |
|       baseFileName = [`${base}.ts`, `${base}.tsx`, `${base}.d.ts`].find(
 | |
|           baseFileName => this.isSourceFile(baseFileName) && this.originalFileExists(baseFileName));
 | |
|       if (!baseFileName) {
 | |
|         return {generate: false};
 | |
|       }
 | |
|     }
 | |
|     return {generate: true, baseFileName};
 | |
|   }
 | |
| 
 | |
|   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) &&
 | |
|         isInRootDir(fileName, this.options);
 | |
|   }
 | |
| 
 | |
|   getSourceFile(
 | |
|       fileName: string, languageVersion: ts.ScriptTarget,
 | |
|       onError?: ((message: string) => void)|undefined): ts.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).filter(
 | |
|               fileName => this.shouldGenerateFile(fileName).generate);
 | |
|         }
 | |
|         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 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 {
 | |
|     let fileExists = this.originalFileExistsCache.get(fileName);
 | |
|     if (fileExists == null) {
 | |
|       fileExists = this.context.fileExists(fileName);
 | |
|       this.originalFileExistsCache.set(fileName, fileExists);
 | |
|     }
 | |
|     return fileExists;
 | |
|   }
 | |
| 
 | |
|   fileExists(fileName: string): boolean {
 | |
|     fileName = stripNgResourceSuffix(fileName);
 | |
|     if (this.librarySummaries.has(fileName) || this.generatedSourceFiles.has(fileName)) {
 | |
|       return true;
 | |
|     }
 | |
|     if (this.shouldGenerateFile(fileName).generate) {
 | |
|       return true;
 | |
|     }
 | |
|     return this.originalFileExists(fileName);
 | |
|   }
 | |
| 
 | |
|   loadSummary(filePath: string): string|null {
 | |
|     const summary = this.librarySummaries.get(filePath);
 | |
|     if (summary) {
 | |
|       return summary.text;
 | |
|     }
 | |
|     if (this.originalFileExists(filePath)) {
 | |
|       return assert(this.context.readFile(filePath));
 | |
|     }
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   isSourceFile(filePath: string): boolean {
 | |
|     // Don't generate any files nor typecheck them
 | |
|     // if skipTemplateCodegen is set and fullTemplateTypeCheck is not yet set,
 | |
|     // for backwards compatibility.
 | |
|     if (this.options.skipTemplateCodegen && !this.options.fullTemplateTypeCheck) {
 | |
|       return false;
 | |
|     }
 | |
|     // If we have a summary from a previous compilation,
 | |
|     // treat the file never as a source file.
 | |
|     if (this.librarySummaries.has(filePath)) {
 | |
|       return false;
 | |
|     }
 | |
|     if (GENERATED_FILES.test(filePath)) {
 | |
|       return false;
 | |
|     }
 | |
|     if (this.options.generateCodeForLibraries === false && DTS.test(filePath)) {
 | |
|       return false;
 | |
|     }
 | |
|     if (DTS.test(filePath)) {
 | |
|       // Check for a bundle index.
 | |
|       if (this.hasBundleIndex(filePath)) {
 | |
|         const normalFilePath = path.normalize(filePath);
 | |
|         return this.flatModuleIndexNames.has(normalFilePath) ||
 | |
|             this.flatModuleIndexRedirectNames.has(normalFilePath);
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   readFile(fileName: string) {
 | |
|     const summary = this.librarySummaries.get(fileName);
 | |
|     if (summary) {
 | |
|       return summary.text;
 | |
|     }
 | |
|     return this.context.readFile(fileName);
 | |
|   }
 | |
| 
 | |
|   getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
 | |
|     return readMetadata(filePath, this.metadataReaderHost, this.metadataReaderCache);
 | |
|   }
 | |
| 
 | |
|   loadResource(filePath: string): Promise<string>|string {
 | |
|     if (this.context.readResource) return this.context.readResource(filePath);
 | |
|     if (!this.originalFileExists(filePath)) {
 | |
|       throw syntaxError(`Error: Resource file not found: ${filePath}`);
 | |
|     }
 | |
|     return assert(this.context.readFile(filePath));
 | |
|   }
 | |
| 
 | |
|   getOutputName(filePath: string): string {
 | |
|     return path.relative(this.getCurrentDirectory(), filePath);
 | |
|   }
 | |
| 
 | |
|   private hasBundleIndex(filePath: string): boolean {
 | |
|     const checkBundleIndex = (directory: string): boolean => {
 | |
|       let result = this.flatModuleIndexCache.get(directory);
 | |
|       if (result == null) {
 | |
|         if (path.basename(directory) == 'node_module') {
 | |
|           // Don't look outside the node_modules this package is installed in.
 | |
|           result = false;
 | |
|         } else {
 | |
|           // A bundle index exists if the typings .d.ts file has a metadata.json that has an
 | |
|           // importAs.
 | |
|           try {
 | |
|             const packageFile = path.join(directory, 'package.json');
 | |
|             if (this.originalFileExists(packageFile)) {
 | |
|               // Once we see a package.json file, assume false until it we find the bundle index.
 | |
|               result = false;
 | |
|               const packageContent: any = JSON.parse(assert(this.context.readFile(packageFile)));
 | |
|               if (packageContent.typings) {
 | |
|                 const typings = path.normalize(path.join(directory, packageContent.typings));
 | |
|                 if (DTS.test(typings)) {
 | |
|                   const metadataFile = typings.replace(DTS, '.metadata.json');
 | |
|                   if (this.originalFileExists(metadataFile)) {
 | |
|                     const metadata = JSON.parse(assert(this.context.readFile(metadataFile)));
 | |
|                     if (metadata.flatModuleIndexRedirect) {
 | |
|                       this.flatModuleIndexRedirectNames.add(typings);
 | |
|                       // Note: don't set result = true,
 | |
|                       // as this would mark this folder
 | |
|                       // as having a bundleIndex too early without
 | |
|                       // filling the bundleIndexNames.
 | |
|                     } else if (metadata.importAs) {
 | |
|                       this.flatModuleIndexNames.add(typings);
 | |
|                       result = true;
 | |
|                     }
 | |
|                   }
 | |
|                 }
 | |
|               }
 | |
|             } else {
 | |
|               const parent = path.dirname(directory);
 | |
|               if (parent != directory) {
 | |
|                 // Try the parent directory.
 | |
|                 result = checkBundleIndex(parent);
 | |
|               } else {
 | |
|                 result = false;
 | |
|               }
 | |
|             }
 | |
|           } catch {
 | |
|             // If we encounter any errors assume we this isn't a bundle index.
 | |
|             result = false;
 | |
|           }
 | |
|         }
 | |
|         this.flatModuleIndexCache.set(directory, result);
 | |
|       }
 | |
|       return result;
 | |
|     };
 | |
| 
 | |
|     return checkBundleIndex(path.dirname(filePath));
 | |
|   }
 | |
| 
 | |
|   getDefaultLibFileName = (options: ts.CompilerOptions) =>
 | |
|       this.context.getDefaultLibFileName(options)
 | |
|   getCurrentDirectory = () => this.context.getCurrentDirectory();
 | |
|   getCanonicalFileName = (fileName: string) => this.context.getCanonicalFileName(fileName);
 | |
|   useCaseSensitiveFileNames = () => this.context.useCaseSensitiveFileNames();
 | |
|   getNewLine = () => this.context.getNewLine();
 | |
|   // Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
 | |
|   // https://github.com/Microsoft/TypeScript/issues/9552
 | |
|   realpath = (p: string) => p;
 | |
|   writeFile = this.context.writeFile.bind(this.context);
 | |
| }
 | |
| 
 | |
| function genFileExternalReferences(genFile: GeneratedFile): Set<string> {
 | |
|   return new Set(collectExternalReferences(genFile.stmts !).map(er => er.moduleName !));
 | |
| }
 | |
| 
 | |
| function addReferencesToSourceFile(sf: ts.SourceFile, genFileNames: string[]) {
 | |
|   // Note: as we modify ts.SourceFiles we need to keep the original
 | |
|   // value for `referencedFiles` around in cache the original host is caching ts.SourceFiles.
 | |
|   // Note: cloning the ts.SourceFile is expensive as the nodes in have parent pointers,
 | |
|   // i.e. we would also need to clone and adjust all nodes.
 | |
|   let originalReferencedFiles: ReadonlyArray<ts.FileReference> =
 | |
|       (sf as any).originalReferencedFiles;
 | |
|   if (!originalReferencedFiles) {
 | |
|     originalReferencedFiles = sf.referencedFiles;
 | |
|     (sf as any).originalReferencedFiles = originalReferencedFiles;
 | |
|   }
 | |
|   const newReferencedFiles = [...originalReferencedFiles];
 | |
|   genFileNames.forEach(gf => newReferencedFiles.push({fileName: gf, pos: 0, end: 0}));
 | |
|   sf.referencedFiles = newReferencedFiles;
 | |
| }
 | |
| 
 | |
| export function getOriginalReferences(sourceFile: ts.SourceFile): ts.FileReference[]|undefined {
 | |
|   return sourceFile && (sourceFile as any).originalReferencedFiles;
 | |
| }
 | |
| 
 | |
| function dotRelative(from: string, to: string): string {
 | |
|   const rPath: string = path.relative(from, to).replace(/\\/g, '/');
 | |
|   return rPath.startsWith('.') ? rPath : './' + rPath;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Moves the path into `genDir` folder while preserving the `node_modules` directory.
 | |
|  */
 | |
| function getPackageName(filePath: string): string|null {
 | |
|   const match = NODE_MODULES_PACKAGE_NAME.exec(filePath);
 | |
|   return match ? match[1] : null;
 | |
| }
 | |
| 
 | |
| function stripNodeModulesPrefix(filePath: string): string {
 | |
|   return filePath.replace(/.*node_modules\//, '');
 | |
| }
 | |
| 
 | |
| function getNodeModulesPrefix(filePath: string): string|null {
 | |
|   const match = /.*node_modules\//.exec(filePath);
 | |
|   return match ? match[1] : null;
 | |
| }
 | |
| 
 | |
| function stripNgResourceSuffix(fileName: string): string {
 | |
|   return fileName.replace(/\.\$ngresource\$.*/, '');
 | |
| }
 | |
| 
 | |
| function addNgResourceSuffix(fileName: string): string {
 | |
|   return `${fileName}.$ngresource$`;
 | |
| }
 |