120 lines
4.9 KiB
TypeScript
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; }
|
|
}
|