feat(ngc): support pathmapping using a separate reflector (#10985)
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.
This commit is contained in:
@ -18,6 +18,7 @@ import * as ts from 'typescript';
|
|||||||
|
|
||||||
import {CompileMetadataResolver, DirectiveNormalizer, DomElementSchemaRegistry, HtmlParser, Lexer, NgModuleCompiler, Parser, StyleCompiler, TemplateParser, TypeScriptEmitter, ViewCompiler} from './compiler_private';
|
import {CompileMetadataResolver, DirectiveNormalizer, DomElementSchemaRegistry, HtmlParser, Lexer, NgModuleCompiler, Parser, StyleCompiler, TemplateParser, TypeScriptEmitter, ViewCompiler} from './compiler_private';
|
||||||
import {Console} from './core_private';
|
import {Console} from './core_private';
|
||||||
|
import {PathMappedReflectorHost} from './path_mapped_reflector_host';
|
||||||
import {ReflectorHost, ReflectorHostContext} from './reflector_host';
|
import {ReflectorHost, ReflectorHostContext} from './reflector_host';
|
||||||
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
|
import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities';
|
||||||
import {StaticReflector, StaticSymbol} from './static_reflector';
|
import {StaticReflector, StaticSymbol} from './static_reflector';
|
||||||
@ -93,8 +94,9 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
codegen(): Promise<any> {
|
codegen(): Promise<any> {
|
||||||
const filePaths =
|
const filePaths = this.program.getSourceFiles()
|
||||||
this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f));
|
.map(sf => this.reflectorHost.getCanonicalFileName(sf.fileName))
|
||||||
|
.filter(f => !GENERATED_FILES.test(f));
|
||||||
const fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
|
const fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath));
|
||||||
const ngModules = fileMetas.reduce((ngModules, fileMeta) => {
|
const ngModules = fileMetas.reduce((ngModules, fileMeta) => {
|
||||||
ngModules.push(...fileMeta.ngModules);
|
ngModules.push(...fileMeta.ngModules);
|
||||||
@ -141,7 +143,10 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
|
const urlResolver: compiler.UrlResolver = compiler.createOfflineCompileUrlResolver();
|
||||||
const reflectorHost = new ReflectorHost(program, compilerHost, options, reflectorHostContext);
|
const usePathMapping = !!options.rootDirs && options.rootDirs.length > 0;
|
||||||
|
const reflectorHost = usePathMapping ?
|
||||||
|
new PathMappedReflectorHost(program, compilerHost, options, reflectorHostContext) :
|
||||||
|
new ReflectorHost(program, compilerHost, options, reflectorHostContext);
|
||||||
const staticReflector = new StaticReflector(reflectorHost);
|
const staticReflector = new StaticReflector(reflectorHost);
|
||||||
StaticAndDynamicReflectionCapabilities.install(staticReflector);
|
StaticAndDynamicReflectionCapabilities.install(staticReflector);
|
||||||
const htmlParser =
|
const htmlParser =
|
||||||
|
136
modules/@angular/compiler-cli/src/path_mapped_reflector_host.ts
Normal file
136
modules/@angular/compiler-cli/src/path_mapped_reflector_host.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,14 +27,14 @@ export interface ReflectorHostContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
||||||
private metadataCollector = new MetadataCollector();
|
protected metadataCollector = new MetadataCollector();
|
||||||
private context: ReflectorHostContext;
|
protected context: ReflectorHostContext;
|
||||||
private isGenDirChildOfRootDir: boolean;
|
private isGenDirChildOfRootDir: boolean;
|
||||||
private basePath: string;
|
protected basePath: string;
|
||||||
private genDir: string;
|
private genDir: string;
|
||||||
constructor(
|
constructor(
|
||||||
private program: ts.Program, private compilerHost: ts.CompilerHost,
|
protected program: ts.Program, protected compilerHost: ts.CompilerHost,
|
||||||
private options: AngularCompilerOptions, context?: ReflectorHostContext) {
|
protected options: AngularCompilerOptions, context?: ReflectorHostContext) {
|
||||||
// normalize the path so that it never ends with '/'.
|
// normalize the path so that it never ends with '/'.
|
||||||
this.basePath = path.normalize(path.join(this.options.basePath, '.'));
|
this.basePath = path.normalize(path.join(this.options.basePath, '.'));
|
||||||
this.genDir = path.normalize(path.join(this.options.genDir, '.'));
|
this.genDir = path.normalize(path.join(this.options.genDir, '.'));
|
||||||
@ -55,21 +55,25 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolve(m: string, containingFile: string) {
|
// We use absolute paths on disk as canonical.
|
||||||
|
getCanonicalFileName(fileName: string): string { return fileName; }
|
||||||
|
|
||||||
|
protected resolve(m: string, containingFile: string) {
|
||||||
const resolved =
|
const resolved =
|
||||||
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
|
ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule;
|
||||||
return resolved ? resolved.resolvedFileName : null;
|
return resolved ? resolved.resolvedFileName : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
private normalizeAssetUrl(url: string): string {
|
protected normalizeAssetUrl(url: string): string {
|
||||||
let assetUrl = AssetUrl.parse(url);
|
let assetUrl = AssetUrl.parse(url);
|
||||||
return assetUrl ? `${assetUrl.packageName}/${assetUrl.modulePath}` : null;
|
const path = assetUrl ? `${assetUrl.packageName}/${assetUrl.modulePath}` : null;
|
||||||
|
return this.getCanonicalFileName(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveAssetUrl(url: string, containingFile: string): string {
|
protected resolveAssetUrl(url: string, containingFile: string): string {
|
||||||
let assetUrl = this.normalizeAssetUrl(url);
|
let assetUrl = this.normalizeAssetUrl(url);
|
||||||
if (assetUrl) {
|
if (assetUrl) {
|
||||||
return this.resolve(assetUrl, containingFile);
|
return this.getCanonicalFileName(this.resolve(assetUrl, containingFile));
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
@ -198,7 +202,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
|||||||
symbol = tc.getAliasedSymbol(symbol);
|
symbol = tc.getAliasedSymbol(symbol);
|
||||||
}
|
}
|
||||||
const declaration = symbol.getDeclarations()[0];
|
const declaration = symbol.getDeclarations()[0];
|
||||||
const declarationFile = declaration.getSourceFile().fileName;
|
const declarationFile = this.getCanonicalFileName(declaration.getSourceFile().fileName);
|
||||||
|
|
||||||
return this.getStaticSymbol(declarationFile, symbol.getName());
|
return this.getStaticSymbol(declarationFile, symbol.getName());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -267,9 +271,9 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator {
|
|||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol {
|
protected resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol {
|
||||||
const resolveModule = (moduleName: string): string => {
|
const resolveModule = (moduleName: string): string => {
|
||||||
const resolvedModulePath = this.resolve(moduleName, filePath);
|
const resolvedModulePath = this.getCanonicalFileName(this.resolve(moduleName, filePath));
|
||||||
if (!resolvedModulePath) {
|
if (!resolvedModulePath) {
|
||||||
throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`);
|
throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user