Keen Yee Liau bbb2798d41 fix(language-service): Use tsLSHost.fileExists() to resolve modules (#32642)
The ModuleResolutionHost implementation inside ReflectorHost currently
relies on reading the snapshot to determine if a file exists, and use
the snapshot to retrieve the file content.
It is more straightforward and efficient to use the already existing
method fileExists() instead.

At runtime, the TypeScript LanguageServiceHost is really a Project, so
both fileExists() and readFile() methods are defined.

As a micro-optimization, skip fs lookup for tsx files.

PR Close #32642
2019-09-12 17:18:06 -07:00

120 lines
4.9 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 {StaticSymbolResolverHost} from '@angular/compiler';
import {MetadataCollector, MetadataReaderHost, createMetadataReaderCache, readMetadata} from '@angular/compiler-cli/src/language_services';
import * as path from 'path';
import * as ts from 'typescript';
class ReflectorModuleModuleResolutionHost implements ts.ModuleResolutionHost, MetadataReaderHost {
private readonly metadataCollector = new MetadataCollector({
// Note: verboseInvalidExpressions is important so that
// the collector will collect errors instead of throwing
verboseInvalidExpression: true,
});
readonly directoryExists?: (directoryName: string) => boolean;
constructor(
private readonly tsLSHost: ts.LanguageServiceHost,
private readonly getProgram: () => ts.Program) {
if (tsLSHost.directoryExists) {
this.directoryExists = directoryName => tsLSHost.directoryExists !(directoryName);
}
}
fileExists(fileName: string): boolean {
// TypeScript resolution logic walks through the following sequence in order:
// package.json (read "types" field) -> .ts -> .tsx -> .d.ts
// For more info, see
// https://www.typescriptlang.org/docs/handbook/module-resolution.html
// For Angular specifically, we can skip .tsx lookup
if (fileName.endsWith('.tsx')) {
return false;
}
if (this.tsLSHost.fileExists) {
return this.tsLSHost.fileExists(fileName);
}
return !!this.tsLSHost.getScriptSnapshot(fileName);
}
readFile(fileName: string): string {
// readFile() is used by TypeScript to read package.json during module
// resolution, and it's used by Angular to read metadata.json during
// metadata resolution.
if (this.tsLSHost.readFile) {
return this.tsLSHost.readFile(fileName) !;
}
// As a fallback, read the JSON files from the editor snapshot.
const snapshot = this.tsLSHost.getScriptSnapshot(fileName);
if (!snapshot) {
// MetadataReaderHost readFile() declaration should be
// `readFile(fileName: string): string | undefined`
return undefined !;
}
return snapshot.getText(0, snapshot.getLength());
}
getSourceFileMetadata(fileName: string) {
const sf = this.getProgram().getSourceFile(fileName);
return sf ? this.metadataCollector.getMetadata(sf) : undefined;
}
cacheMetadata(fileName: string) {
// Don't cache the metadata for .ts files as they might change in the editor!
return fileName.endsWith('.d.ts');
}
}
export class ReflectorHost implements StaticSymbolResolverHost {
private readonly hostAdapter: ReflectorModuleModuleResolutionHost;
private readonly metadataReaderCache = createMetadataReaderCache();
private readonly moduleResolutionCache: ts.ModuleResolutionCache;
private readonly fakeContainingPath: string;
constructor(getProgram: () => ts.Program, private readonly tsLSHost: ts.LanguageServiceHost) {
// tsLSHost.getCurrentDirectory() returns the directory where tsconfig.json
// is located. This is not the same as process.cwd() because the language
// service host sets the "project root path" as its current directory.
const currentDir = tsLSHost.getCurrentDirectory();
this.fakeContainingPath = currentDir ? path.join(currentDir, 'fakeContainingFile.ts') : '';
this.hostAdapter = new ReflectorModuleModuleResolutionHost(tsLSHost, getProgram);
this.moduleResolutionCache = ts.createModuleResolutionCache(
currentDir,
s => s, // getCanonicalFileName
tsLSHost.getCompilationSettings());
}
getMetadataFor(modulePath: string): {[key: string]: any}[]|undefined {
return readMetadata(modulePath, this.hostAdapter, this.metadataReaderCache);
}
moduleNameToFileName(moduleName: string, containingFile?: string): string|null {
if (!containingFile) {
if (moduleName.startsWith('.')) {
throw new Error('Resolution of relative paths requires a containing file.');
}
if (!this.fakeContainingPath) {
// If current directory is empty then the file must belong to an inferred
// project (no tsconfig.json), in which case it's not possible to resolve
// the module without the caller explicitly providing a containing file.
throw new Error(`Could not resolve '${moduleName}' without a containing file.`);
}
containingFile = this.fakeContainingPath;
}
const compilerOptions = this.tsLSHost.getCompilationSettings();
const resolved = ts.resolveModuleName(
moduleName, containingFile, compilerOptions, this.hostAdapter,
this.moduleResolutionCache)
.resolvedModule;
return resolved ? resolved.resolvedFileName : null;
}
getOutputName(filePath: string) { return filePath; }
}