refactor(compiler): simplify the `CompilerHost` used for transformers
- remove unneeded methods (`getNgCanonicalFileName`, `assumeFileExists`) - simplify moduleName <-> fileName conversion logic as we don’t need to account for `genDir` anymore. - rename `createNgCompilerHost` -> `createCompilerHost`
This commit is contained in:
parent
27d5058e01
commit
6a1ab61cce
|
@ -6,7 +6,7 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AotCompilerHost, StaticSymbol} from '@angular/compiler';
|
||||
import {AotCompilerHost, StaticSymbol, syntaxError} from '@angular/compiler';
|
||||
import {AngularCompilerOptions, CollectorOptions, MetadataCollector, ModuleMetadata} from '@angular/tsc-wrapped';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
@ -131,6 +131,9 @@ export abstract class BaseAotCompilerHost<C extends BaseAotCompilerHostContext>
|
|||
|
||||
loadResource(filePath: string): Promise<string>|string {
|
||||
if (this.context.readResource) return this.context.readResource(filePath);
|
||||
if (!this.context.fileExists(filePath)) {
|
||||
throw syntaxError(`Error: Resource file not found: ${filePath}`);
|
||||
}
|
||||
return this.context.readFile(filePath);
|
||||
}
|
||||
|
||||
|
|
|
@ -131,7 +131,7 @@ export function performCompilation(
|
|||
let emitResult: api.EmitResult|undefined;
|
||||
try {
|
||||
if (!host) {
|
||||
host = ng.createNgCompilerHost({options});
|
||||
host = ng.createCompilerHost({options});
|
||||
}
|
||||
|
||||
program = ng.createProgram({rootNames, host, options, oldProgram});
|
||||
|
|
|
@ -117,34 +117,24 @@ export interface CompilerOptions extends ts.CompilerOptions {
|
|||
preserveWhitespaces?: boolean;
|
||||
}
|
||||
|
||||
export interface ModuleFilenameResolver {
|
||||
export interface CompilerHost extends ts.CompilerHost {
|
||||
/**
|
||||
* Converts a module name that is used in an `import` to a file path.
|
||||
* I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
|
||||
*/
|
||||
moduleNameToFileName(moduleName: string, containingFile?: string): string|null;
|
||||
|
||||
/**
|
||||
* Converts a file path to a module name that can be used as an `import.
|
||||
* Converts a file path to a module name that can be used as an `import ...`
|
||||
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
|
||||
*
|
||||
* See ImportResolver.
|
||||
*/
|
||||
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string|null;
|
||||
|
||||
getNgCanonicalFileName(fileName: string): string;
|
||||
|
||||
assumeFileExists(fileName: string): void;
|
||||
}
|
||||
|
||||
export interface CompilerHost extends ts.CompilerHost, ModuleFilenameResolver {
|
||||
/**
|
||||
* Load a referenced resource either statically or asynchronously. If the host returns a
|
||||
* `Promise<string>` it is assumed the user of the corresponding `Program` will call
|
||||
* `loadNgStructureAsync()`. Returing `Promise<string>` outside `loadNgStructureAsync()` will
|
||||
* cause a diagnostics diagnostic error or an exception to be thrown.
|
||||
*
|
||||
* If `loadResource()` is not provided, `readFile()` will be called to load the resource.
|
||||
*/
|
||||
readResource?(fileName: string): Promise<string>|string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
/**
|
||||
* @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 {syntaxError} from '@angular/compiler';
|
||||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CompilerHost, CompilerOptions} from './api';
|
||||
|
||||
const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/;
|
||||
const DTS = /\.d\.ts$/;
|
||||
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||
|
||||
export function createCompilerHost(
|
||||
{options, tsHost = ts.createCompilerHost(options, true)}:
|
||||
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
|
||||
const mixin = new CompilerHostMixin(tsHost, options);
|
||||
const host = Object.create(tsHost);
|
||||
|
||||
host.moduleNameToFileName = mixin.moduleNameToFileName.bind(mixin);
|
||||
host.fileNameToModuleName = mixin.fileNameToModuleName.bind(mixin);
|
||||
|
||||
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||
host.realpath = (fileName: string) => fileName;
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
class CompilerHostMixin {
|
||||
private moduleFileNames = new Map<string, string|null>();
|
||||
private rootDirs: string[];
|
||||
private basePath: string;
|
||||
private moduleResolutionHost: ModuleFilenameResolutionHost;
|
||||
|
||||
constructor(private context: ts.ModuleResolutionHost, private options: CompilerOptions) {
|
||||
// normalize the path so that it never ends with '/'.
|
||||
this.basePath = normalizePath(this.options.basePath !);
|
||||
this.rootDirs = (this.options.rootDirs || [
|
||||
this.options.basePath !
|
||||
]).map(p => path.resolve(this.basePath, normalizePath(p)));
|
||||
this.moduleResolutionHost = createModuleFilenameResolverHost(context);
|
||||
}
|
||||
|
||||
moduleNameToFileName(m: string, containingFile: string): string|null {
|
||||
const key = m + ':' + (containingFile || '');
|
||||
let result: string|null = this.moduleFileNames.get(key) || null;
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
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 = path.join(this.basePath, 'index.ts');
|
||||
}
|
||||
const resolved =
|
||||
ts.resolveModuleName(m, containingFile, this.options, this.moduleResolutionHost)
|
||||
.resolvedModule;
|
||||
if (resolved) {
|
||||
if (this.options.traceResolution) {
|
||||
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
|
||||
}
|
||||
result = resolved.resolvedFileName;
|
||||
}
|
||||
this.moduleFileNames.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 originalImportedFile = importedFile;
|
||||
if (this.options.traceResolution) {
|
||||
console.error(
|
||||
'fileNameToModuleName from containingFile', containingFile, 'to importedFile',
|
||||
importedFile);
|
||||
}
|
||||
// If a file does not yet exist (because we compile it later), we still need to
|
||||
// assume it exists it so that the `resolve` method works!
|
||||
if (!this.moduleResolutionHost.fileExists(importedFile)) {
|
||||
this.moduleResolutionHost.assumeFileExists(importedFile);
|
||||
}
|
||||
// drop extension
|
||||
importedFile = importedFile.replace(EXT, '');
|
||||
const importedFilePackagName = getPackageName(importedFile);
|
||||
const containingFilePackageName = getPackageName(containingFile);
|
||||
|
||||
let moduleName: string;
|
||||
if (importedFilePackagName === containingFilePackageName) {
|
||||
moduleName = dotRelative(
|
||||
path.dirname(stripRootDir(this.rootDirs, containingFile)),
|
||||
stripRootDir(this.rootDirs, importedFile));
|
||||
} else if (importedFilePackagName) {
|
||||
moduleName = stripNodeModulesPrefix(importedFile);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Trying to import a source file from a node_modules package: import ${originalImportedFile} from ${containingFile}`);
|
||||
}
|
||||
return moduleName;
|
||||
}
|
||||
}
|
||||
|
||||
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
|
||||
assumeFileExists(fileName: string): void;
|
||||
}
|
||||
|
||||
function createModuleFilenameResolverHost(host: ts.ModuleResolutionHost):
|
||||
ModuleFilenameResolutionHost {
|
||||
const assumedExists = new Set<string>();
|
||||
const resolveModuleNameHost = Object.create(host);
|
||||
// When calling ts.resolveModuleName, additional allow checks for .d.ts files to be done based on
|
||||
// checks for .ngsummary.json files, so that our codegen depends on fewer inputs and requires
|
||||
// to be called less often.
|
||||
// This is needed as we use ts.resolveModuleName in DefaultModuleFilenameResolver
|
||||
// and it should be able to resolve summary file names.
|
||||
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
|
||||
if (assumedExists.has(fileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (host.fileExists(fileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DTS.test(fileName)) {
|
||||
const base = fileName.substring(0, fileName.length - 5);
|
||||
return host.fileExists(base + '.ngsummary.json');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
resolveModuleNameHost.assumeFileExists = (fileName: string) => assumedExists.add(fileName);
|
||||
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||
resolveModuleNameHost.realpath = (fileName: string) => fileName;
|
||||
|
||||
return resolveModuleNameHost;
|
||||
}
|
||||
|
||||
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 stripRootDir(rootDirs: string[], fileName: string): string {
|
||||
if (!fileName) return fileName;
|
||||
// NB: the rootDirs should have been sorted longest-first
|
||||
for (const dir of rootDirs) {
|
||||
if (fileName.indexOf(dir) === 0) {
|
||||
fileName = fileName.substring(dir.length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
function stripNodeModulesPrefix(filePath: string): string {
|
||||
return filePath.replace(/.*node_modules\//, '');
|
||||
}
|
||||
|
||||
function normalizePath(p: string): string {
|
||||
return path.normalize(path.join(p, '.')).replace(/\\/g, '/');
|
||||
}
|
|
@ -9,25 +9,6 @@
|
|||
import * as ts from 'typescript';
|
||||
|
||||
import {CompilerHost, CompilerOptions, Program} from './api';
|
||||
import {createModuleFilenameResolver} from './module_filename_resolver';
|
||||
|
||||
export {createCompilerHost} from './compiler_host';
|
||||
export {createProgram} from './program';
|
||||
export {createModuleFilenameResolver};
|
||||
|
||||
export function createNgCompilerHost(
|
||||
{options, tsHost = ts.createCompilerHost(options, true)}:
|
||||
{options: CompilerOptions, tsHost?: ts.CompilerHost}): CompilerHost {
|
||||
const resolver = createModuleFilenameResolver(tsHost, options);
|
||||
|
||||
const host = Object.create(tsHost);
|
||||
|
||||
host.moduleNameToFileName = resolver.moduleNameToFileName.bind(resolver);
|
||||
host.fileNameToModuleName = resolver.fileNameToModuleName.bind(resolver);
|
||||
host.getNgCanonicalFileName = resolver.getNgCanonicalFileName.bind(resolver);
|
||||
host.assumeFileExists = resolver.assumeFileExists.bind(resolver);
|
||||
|
||||
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||
host.realpath = (fileName: string) => fileName;
|
||||
|
||||
return host;
|
||||
}
|
||||
|
|
|
@ -1,292 +0,0 @@
|
|||
/**
|
||||
* @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 * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CompilerOptions, ModuleFilenameResolver} from './api';
|
||||
|
||||
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
||||
const DTS = /\.d\.ts$/;
|
||||
const NODE_MODULES = '/node_modules/';
|
||||
const IS_GENERATED = /\.(ngfactory|ngstyle|ngsummary)$/;
|
||||
const SHALLOW_IMPORT = /^((\w|-)+|(@(\w|-)+(\/(\w|-)+)+))$/;
|
||||
|
||||
export function createModuleFilenameResolver(
|
||||
tsHost: ts.ModuleResolutionHost, options: CompilerOptions): ModuleFilenameResolver {
|
||||
const host = createModuleFilenameResolverHost(tsHost);
|
||||
|
||||
return options.rootDirs && options.rootDirs.length > 0 ?
|
||||
new MultipleRootDirModuleFilenameResolver(host, options) :
|
||||
new SingleRootDirModuleFilenameResolver(host, options);
|
||||
}
|
||||
|
||||
class SingleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
|
||||
private isGenDirChildOfRootDir: boolean;
|
||||
private basePath: string;
|
||||
private genDir: string;
|
||||
private moduleFileNames = new Map<string, string|null>();
|
||||
|
||||
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
|
||||
// normalize the path so that it never ends with '/'.
|
||||
this.basePath = path.normalize(path.join(options.basePath !, '.')).replace(/\\/g, '/');
|
||||
this.genDir = path.normalize(path.join(options.genDir !, '.')).replace(/\\/g, '/');
|
||||
|
||||
const genPath: string = path.relative(this.basePath, this.genDir);
|
||||
this.isGenDirChildOfRootDir = genPath === '' || !genPath.startsWith('..');
|
||||
}
|
||||
|
||||
moduleNameToFileName(m: string, containingFile: string): string|null {
|
||||
const key = m + ':' + (containingFile || '');
|
||||
let result: string|null = this.moduleFileNames.get(key) || null;
|
||||
if (!result) {
|
||||
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.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
|
||||
}
|
||||
m = m.replace(EXT, '');
|
||||
const resolved =
|
||||
ts.resolveModuleName(m, containingFile.replace(/\\/g, '/'), this.options, this.host)
|
||||
.resolvedModule;
|
||||
result = resolved ? this.getNgCanonicalFileName(resolved.resolvedFileName) : null;
|
||||
this.moduleFileNames.set(key, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* We want a moduleId that will appear in import statements in the generated code.
|
||||
* These need to be in a form that system.js can load, so absolute file paths don't work.
|
||||
*
|
||||
* The `containingFile` is always in the `genDir`, where as the `importedFile` can be in
|
||||
* `genDir`, `node_module` or `basePath`. The `importedFile` is either a generated file or
|
||||
* existing file.
|
||||
*
|
||||
* | genDir | node_module | rootDir
|
||||
* --------------+----------+-------------+----------
|
||||
* generated | relative | relative | n/a
|
||||
* existing file | n/a | absolute | relative(*)
|
||||
*
|
||||
* NOTE: (*) the relative path is computed depending on `isGenDirChildOfRootDir`.
|
||||
*/
|
||||
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
||||
// If a file does not yet exist (because we compile it later), we still need to
|
||||
// assume it exists it so that the `resolve` method works!
|
||||
if (!this.host.fileExists(importedFile)) {
|
||||
this.host.assumeFileExists(importedFile);
|
||||
}
|
||||
|
||||
containingFile = this.rewriteGenDirPath(containingFile);
|
||||
const containingDir = path.dirname(containingFile);
|
||||
// drop extension
|
||||
importedFile = importedFile.replace(EXT, '');
|
||||
|
||||
const nodeModulesIndex = importedFile.indexOf(NODE_MODULES);
|
||||
const importModule = nodeModulesIndex === -1 ?
|
||||
null :
|
||||
importedFile.substring(nodeModulesIndex + NODE_MODULES.length);
|
||||
const isGeneratedFile = IS_GENERATED.test(importedFile);
|
||||
|
||||
if (isGeneratedFile) {
|
||||
// rewrite to genDir path
|
||||
if (importModule) {
|
||||
// it is generated, therefore we do a relative path to the factory
|
||||
return this.dotRelative(containingDir, this.genDir + NODE_MODULES + importModule);
|
||||
} else {
|
||||
// assume that import is also in `genDir`
|
||||
importedFile = this.rewriteGenDirPath(importedFile);
|
||||
return this.dotRelative(containingDir, importedFile);
|
||||
}
|
||||
} else {
|
||||
// user code import
|
||||
if (importModule) {
|
||||
return importModule;
|
||||
} else {
|
||||
if (!this.isGenDirChildOfRootDir) {
|
||||
// assume that they are on top of each other.
|
||||
importedFile = importedFile.replace(this.basePath, this.genDir);
|
||||
}
|
||||
if (SHALLOW_IMPORT.test(importedFile)) {
|
||||
return importedFile;
|
||||
}
|
||||
return this.dotRelative(containingDir, importedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use absolute paths on disk as canonical.
|
||||
getNgCanonicalFileName(fileName: string): string { return fileName; }
|
||||
|
||||
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
|
||||
|
||||
private 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.
|
||||
*/
|
||||
private rewriteGenDirPath(filepath: string) {
|
||||
const nodeModulesIndex = filepath.indexOf(NODE_MODULES);
|
||||
if (nodeModulesIndex !== -1) {
|
||||
// If we are in node_module, transplant them into `genDir`.
|
||||
return path.join(this.genDir, filepath.substring(nodeModulesIndex));
|
||||
} else {
|
||||
// pretend that containing file is on top of the `genDir` to normalize the paths.
|
||||
// we apply the `genDir` => `rootDir` delta through `rootDirPrefix` later.
|
||||
return filepath.replace(this.basePath, this.genDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This version of the AotCompilerHost expects that the program will be compiled
|
||||
* and executed with a "path mapped" directory structure, where generated files
|
||||
* are in a parallel tree with the sources, and imported using a `./` relative
|
||||
* import. This requires using TS `rootDirs` option and also teaching the module
|
||||
* loader what to do.
|
||||
*/
|
||||
class MultipleRootDirModuleFilenameResolver implements ModuleFilenameResolver {
|
||||
private basePath: string;
|
||||
|
||||
constructor(private host: ModuleFilenameResolutionHost, private options: CompilerOptions) {
|
||||
// normalize the path so that it never ends with '/'.
|
||||
this.basePath = path.normalize(path.join(options.basePath !, '.')).replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
getNgCanonicalFileName(fileName: string): string {
|
||||
if (!fileName) return fileName;
|
||||
// NB: the rootDirs should have been sorted longest-first
|
||||
for (const dir of this.options.rootDirs || []) {
|
||||
if (fileName.indexOf(dir) === 0) {
|
||||
fileName = fileName.substring(dir.length);
|
||||
}
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
assumeFileExists(fileName: string) { this.host.assumeFileExists(fileName); }
|
||||
|
||||
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.getNgCanonicalFileName(path.join(this.basePath, 'index.ts'));
|
||||
}
|
||||
for (const root of this.options.rootDirs || ['']) {
|
||||
const rootedContainingFile = path.join(root, containingFile);
|
||||
const resolved =
|
||||
ts.resolveModuleName(m, rootedContainingFile, this.options, this.host).resolvedModule;
|
||||
if (resolved) {
|
||||
if (this.options.traceResolution) {
|
||||
console.error('resolve', m, containingFile, '=>', resolved.resolvedFileName);
|
||||
}
|
||||
return this.getNgCanonicalFileName(resolved.resolvedFileName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* We want a moduleId that will appear in import statements in the generated code.
|
||||
* These need to be in a form that system.js can load, so absolute file paths don't work.
|
||||
* Relativize the paths by checking candidate prefixes of the absolute path, to see if
|
||||
* they are resolvable by the moduleResolution strategy from the CompilerHost.
|
||||
*/
|
||||
fileNameToModuleName(importedFile: string, containingFile: string): string {
|
||||
if (this.options.traceResolution) {
|
||||
console.error(
|
||||
'getImportPath from containingFile', containingFile, 'to importedFile', importedFile);
|
||||
}
|
||||
|
||||
// If a file does not yet exist (because we compile it later), we still need to
|
||||
// assume it exists so that the `resolve` method works!
|
||||
if (!this.host.fileExists(importedFile)) {
|
||||
if (this.options.rootDirs && this.options.rootDirs.length > 0) {
|
||||
this.host.assumeFileExists(path.join(this.options.rootDirs[0], importedFile));
|
||||
} else {
|
||||
this.host.assumeFileExists(importedFile);
|
||||
}
|
||||
}
|
||||
|
||||
const resolvable = (candidate: string) => {
|
||||
const resolved = this.moduleNameToFileName(candidate, importedFile);
|
||||
return resolved && resolved.replace(EXT, '') === importedFile.replace(EXT, '');
|
||||
};
|
||||
|
||||
const importModuleName = importedFile.replace(EXT, '');
|
||||
const parts = importModuleName.split(path.sep).filter(p => !!p);
|
||||
let foundRelativeImport: string|undefined;
|
||||
|
||||
for (let index = parts.length - 1; index >= 0; index--) {
|
||||
let candidate = parts.slice(index, parts.length).join(path.sep);
|
||||
if (resolvable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
candidate = '.' + path.sep + candidate;
|
||||
if (resolvable(candidate)) {
|
||||
foundRelativeImport = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundRelativeImport) return foundRelativeImport;
|
||||
|
||||
// Try a relative import
|
||||
const candidate = path.relative(path.dirname(containingFile), importModuleName);
|
||||
if (resolvable(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to find any resolvable import for ${importedFile} relative to ${containingFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface ModuleFilenameResolutionHost extends ts.ModuleResolutionHost {
|
||||
assumeFileExists(fileName: string): void;
|
||||
}
|
||||
|
||||
function createModuleFilenameResolverHost(host: ts.ModuleResolutionHost):
|
||||
ModuleFilenameResolutionHost {
|
||||
const assumedExists = new Set<string>();
|
||||
const resolveModuleNameHost = Object.create(host);
|
||||
// When calling ts.resolveModuleName, additional allow checks for .d.ts files to be done based on
|
||||
// checks for .ngsummary.json files, so that our codegen depends on fewer inputs and requires
|
||||
// to be called less often.
|
||||
// This is needed as we use ts.resolveModuleName in reflector_host and it should be able to
|
||||
// resolve summary file names.
|
||||
resolveModuleNameHost.fileExists = (fileName: string): boolean => {
|
||||
if (assumedExists.has(fileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (host.fileExists(fileName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DTS.test(fileName)) {
|
||||
const base = fileName.substring(0, fileName.length - 5);
|
||||
return host.fileExists(base + '.ngsummary.json');
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
resolveModuleNameHost.assumeFileExists = (fileName: string) => assumedExists.add(fileName);
|
||||
// Make sure we do not `host.realpath()` from TS as we do not want to resolve symlinks.
|
||||
// https://github.com/Microsoft/TypeScript/issues/9552
|
||||
resolveModuleNameHost.realpath = (fileName: string) => fileName;
|
||||
|
||||
return resolveModuleNameHost;
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* @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 * as ts from 'typescript';
|
||||
|
||||
import {CompilerHost, CompilerOptions} from '../../src/transformers/api';
|
||||
import {createCompilerHost} from '../../src/transformers/compiler_host';
|
||||
import {Directory, Entry, MockAotContext, MockCompilerHost} from '../mocks';
|
||||
|
||||
const dummyModule = 'export let foo: any[];';
|
||||
|
||||
describe('NgCompilerHost', () => {
|
||||
function createHost(
|
||||
{files = {}, options = {basePath: '/tmp'}}: {files?: Directory,
|
||||
options?: CompilerOptions} = {}) {
|
||||
const context = new MockAotContext('/tmp/', files);
|
||||
const tsHost = new MockCompilerHost(context);
|
||||
return createCompilerHost({tsHost, options});
|
||||
}
|
||||
|
||||
describe('fileNameToModuleName', () => {
|
||||
let ngHost: CompilerHost;
|
||||
beforeEach(() => { ngHost = createHost(); });
|
||||
|
||||
it('should use a package import when accessing a package from a source file', () => {
|
||||
expect(ngHost.fileNameToModuleName('/tmp/node_modules/@angular/core.d.ts', '/tmp/main.ts'))
|
||||
.toBe('@angular/core');
|
||||
});
|
||||
|
||||
it('should use a package import when accessing a package from another package', () => {
|
||||
expect(ngHost.fileNameToModuleName(
|
||||
'/tmp/node_modules/mod1/index.d.ts', '/tmp/node_modules/mod2/index.d.ts'))
|
||||
.toBe('mod1/index');
|
||||
expect(ngHost.fileNameToModuleName(
|
||||
'/tmp/node_modules/@angular/core/index.d.ts',
|
||||
'/tmp/node_modules/@angular/common/index.d.ts'))
|
||||
.toBe('@angular/core/index');
|
||||
});
|
||||
|
||||
it('should use a relative import when accessing a file in the same package', () => {
|
||||
expect(ngHost.fileNameToModuleName(
|
||||
'/tmp/node_modules/mod/a/child.d.ts', '/tmp/node_modules/mod/index.d.ts'))
|
||||
.toBe('./a/child');
|
||||
expect(ngHost.fileNameToModuleName(
|
||||
'/tmp/node_modules/@angular/core/src/core.d.ts',
|
||||
'/tmp/node_modules/@angular/core/index.d.ts'))
|
||||
.toBe('./src/core');
|
||||
});
|
||||
|
||||
it('should use a relative import when accessing a source file from a source file', () => {
|
||||
expect(ngHost.fileNameToModuleName('/tmp/src/a/child.ts', '/tmp/src/index.ts'))
|
||||
.toBe('./a/child');
|
||||
});
|
||||
|
||||
it('should support multiple rootDirs when accessing a source file form a source file', () => {
|
||||
const ngHostWithMultipleRoots = createHost({
|
||||
options: {
|
||||
basePath: '/tmp/',
|
||||
rootDirs: [
|
||||
'src/a',
|
||||
'src/b',
|
||||
]
|
||||
}
|
||||
});
|
||||
expect(ngHostWithMultipleRoots.fileNameToModuleName('/tmp/src/b/b.ts', '/tmp/src/a/a.ts'))
|
||||
.toBe('./b');
|
||||
});
|
||||
|
||||
it('should error if accessing a source file from a package', () => {
|
||||
expect(
|
||||
() => ngHost.fileNameToModuleName(
|
||||
'/tmp/src/a/child.ts', '/tmp/node_modules/@angular/core.d.ts'))
|
||||
.toThrowError(
|
||||
'Trying to import a source file from a node_modules package: import /tmp/src/a/child.ts from /tmp/node_modules/@angular/core.d.ts');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('moduleNameToFileName', () => {
|
||||
it('should resolve a package import without a containing file', () => {
|
||||
const ngHost = createHost(
|
||||
{files: {'tmp': {'node_modules': {'@angular': {'core': {'index.d.ts': dummyModule}}}}}});
|
||||
expect(ngHost.moduleNameToFileName('@angular/core'))
|
||||
.toBe('/tmp/node_modules/@angular/core/index.d.ts');
|
||||
});
|
||||
|
||||
it('should resolve an import using the containing file', () => {
|
||||
const ngHost = createHost({files: {'tmp': {'src': {'a': {'child.d.ts': dummyModule}}}}});
|
||||
expect(ngHost.moduleNameToFileName('./a/child', '/tmp/src/index.ts'))
|
||||
.toBe('/tmp/src/a/child.d.ts');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -82,12 +82,11 @@ export class StaticReflector implements CompileReflector {
|
|||
}
|
||||
|
||||
resolveExternalReference(ref: o.ExternalReference): StaticSymbol {
|
||||
const importSymbol = this.getStaticSymbol(ref.moduleName !, ref.name !);
|
||||
const rootSymbol = this.findDeclaration(ref.moduleName !, ref.name !);
|
||||
if (importSymbol != rootSymbol) {
|
||||
this.symbolResolver.recordImportAs(rootSymbol, importSymbol);
|
||||
}
|
||||
return rootSymbol;
|
||||
const refSymbol = this.symbolResolver.getSymbolByModule(ref.moduleName !, ref.name !);
|
||||
const declarationSymbol = this.findSymbolDeclaration(refSymbol);
|
||||
this.symbolResolver.recordModuleNameForFileName(refSymbol.filePath, ref.moduleName !);
|
||||
this.symbolResolver.recordImportAs(declarationSymbol, refSymbol);
|
||||
return declarationSymbol;
|
||||
}
|
||||
|
||||
findDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol {
|
||||
|
|
|
@ -172,6 +172,10 @@ export class StaticSymbolResolver {
|
|||
this.importAs.set(sourceSymbol, targetSymbol);
|
||||
}
|
||||
|
||||
recordModuleNameForFileName(fileName: string, moduleName: string) {
|
||||
this.knownFileNameToModuleNames.set(fileName, moduleName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate all information derived from the given file.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue