Until we have comprehensive E2E tests, it's too risky to change the reflector_host Misko wrote before final. But google3 uses path mapping and needs all imports to be and all paths to be canonicalized to the longest rootDir. This change introduces a subclass of ReflectorHost with overrides for methods that differ. After final (or when we have good tests), we'll refactor them back into one class.
137 lines
5.1 KiB
TypeScript
137 lines
5.1 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 {AngularCompilerOptions, ModuleMetadata} from '@angular/tsc-wrapped';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as ts from 'typescript';
|
|
|
|
import {ReflectorHost, ReflectorHostContext} from './reflector_host';
|
|
import {StaticSymbol} from './static_reflector';
|
|
|
|
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
|
|
const DTS = /\.d\.ts$/;
|
|
|
|
/**
|
|
* This version of the reflector host 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.
|
|
*/
|
|
export class PathMappedReflectorHost extends ReflectorHost {
|
|
constructor(
|
|
program: ts.Program, compilerHost: ts.CompilerHost, options: AngularCompilerOptions,
|
|
context?: ReflectorHostContext) {
|
|
super(program, compilerHost, options, context);
|
|
}
|
|
|
|
getCanonicalFileName(fileName: string): string {
|
|
if (!fileName) return fileName;
|
|
// NB: the rootDirs should have been sorted longest-first
|
|
for (let dir of this.options.rootDirs || []) {
|
|
if (fileName.indexOf(dir) === 0) {
|
|
fileName = fileName.substring(dir.length);
|
|
}
|
|
}
|
|
return fileName;
|
|
}
|
|
|
|
protected resolve(m: string, containingFile: string) {
|
|
for (const root of this.options.rootDirs || ['']) {
|
|
const rootedContainingFile = path.join(root, containingFile);
|
|
const resolved =
|
|
ts.resolveModuleName(m, rootedContainingFile, this.options, this.context).resolvedModule;
|
|
if (resolved) {
|
|
if (this.options.traceResolution) {
|
|
console.log('resolve', m, containingFile, '=>', resolved.resolvedFileName);
|
|
}
|
|
return resolved.resolvedFileName;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
getImportPath(containingFile: string, importedFile: string): string {
|
|
importedFile = this.resolveAssetUrl(importedFile, containingFile);
|
|
containingFile = this.resolveAssetUrl(containingFile, '');
|
|
|
|
if (this.options.traceResolution) {
|
|
console.log(
|
|
'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.context.fileExists(importedFile)) {
|
|
this.context.assumeFileExists(importedFile);
|
|
}
|
|
|
|
const resolvable = (candidate: string) => {
|
|
const resolved = this.getCanonicalFileName(this.resolve(candidate, importedFile));
|
|
return resolved && resolved.replace(EXT, '') === importedFile.replace(EXT, '');
|
|
};
|
|
|
|
let importModuleName = importedFile.replace(EXT, '');
|
|
const parts = importModuleName.split(path.sep).filter(p => !!p);
|
|
let foundRelativeImport: string;
|
|
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}`);
|
|
}
|
|
|
|
getMetadataFor(filePath: string): ModuleMetadata {
|
|
for (const root of this.options.rootDirs || []) {
|
|
const rootedPath = path.join(root, filePath);
|
|
if (!this.compilerHost.fileExists(rootedPath)) {
|
|
// If the file doesn't exists then we cannot return metadata for the file.
|
|
// This will occur if the user refernced a declared module for which no file
|
|
// exists for the module (i.e. jQuery or angularjs).
|
|
continue;
|
|
}
|
|
if (DTS.test(rootedPath)) {
|
|
const metadataPath = rootedPath.replace(DTS, '.metadata.json');
|
|
if (this.context.fileExists(metadataPath)) {
|
|
const metadata = this.readMetadata(metadataPath);
|
|
return (Array.isArray(metadata) && metadata.length == 0) ? undefined : metadata;
|
|
}
|
|
} else {
|
|
const sf = this.program.getSourceFile(rootedPath);
|
|
if (!sf) {
|
|
throw new Error(`Source file ${rootedPath} not present in program.`);
|
|
}
|
|
sf.fileName = this.getCanonicalFileName(sf.fileName);
|
|
return this.metadataCollector.getMetadata(sf);
|
|
}
|
|
}
|
|
}
|
|
}
|