
In tsc 3.0 the check that enables program structure reuse in tryReuseStructureFromOldProgram has changed and now uses identity comparison on arrays within CompilerOptions. Since we recreate the options on each incremental compilation, we now fail this check. After this change the default set of options is reused in between incremental compilations, but we still allow options to be overriden if needed. PR Close #25275
651 lines
26 KiB
TypeScript
651 lines
26 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)$/;
|
|
|
|
export function createCompilerHost(
|
|
{options, tsHost = ts.createCompilerHost(options, true)}:
|
|
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
|
|
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[];
|
|
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.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 (e) {
|
|
// 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}`;
|
|
}
|
|
const filePathWithNgResource =
|
|
this.moduleNameToFileName(addNgResourceSuffix(resourceName), 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) &&
|
|
this.context.amdModuleName) {
|
|
const moduleName = this.context.amdModuleName(sf);
|
|
if (moduleName) sf.moduleName = moduleName;
|
|
}
|
|
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 (e) {
|
|
// 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$`;
|
|
}
|