diff --git a/modules/@angular/compiler-cli/src/codegen.ts b/modules/@angular/compiler-cli/src/codegen.ts index b439fed53a..8e597f1173 100644 --- a/modules/@angular/compiler-cli/src/codegen.ts +++ b/modules/@angular/compiler-cli/src/codegen.ts @@ -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 {Console} from './core_private'; +import {PathMappedReflectorHost} from './path_mapped_reflector_host'; import {ReflectorHost, ReflectorHostContext} from './reflector_host'; import {StaticAndDynamicReflectionCapabilities} from './static_reflection_capabilities'; import {StaticReflector, StaticSymbol} from './static_reflector'; @@ -93,8 +94,9 @@ export class CodeGenerator { } codegen(): Promise { - const filePaths = - this.program.getSourceFiles().map(sf => sf.fileName).filter(f => !GENERATED_FILES.test(f)); + const filePaths = this.program.getSourceFiles() + .map(sf => this.reflectorHost.getCanonicalFileName(sf.fileName)) + .filter(f => !GENERATED_FILES.test(f)); const fileMetas = filePaths.map((filePath) => this.readFileMetadata(filePath)); const ngModules = fileMetas.reduce((ngModules, fileMeta) => { ngModules.push(...fileMeta.ngModules); @@ -141,7 +143,10 @@ export class CodeGenerator { } 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); StaticAndDynamicReflectionCapabilities.install(staticReflector); const htmlParser = diff --git a/modules/@angular/compiler-cli/src/path_mapped_reflector_host.ts b/modules/@angular/compiler-cli/src/path_mapped_reflector_host.ts new file mode 100644 index 0000000000..d72eb5bf58 --- /dev/null +++ b/modules/@angular/compiler-cli/src/path_mapped_reflector_host.ts @@ -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); + } + } + } +} diff --git a/modules/@angular/compiler-cli/src/reflector_host.ts b/modules/@angular/compiler-cli/src/reflector_host.ts index ce56d96473..2011c2c80a 100644 --- a/modules/@angular/compiler-cli/src/reflector_host.ts +++ b/modules/@angular/compiler-cli/src/reflector_host.ts @@ -27,14 +27,14 @@ export interface ReflectorHostContext { } export class ReflectorHost implements StaticReflectorHost, ImportGenerator { - private metadataCollector = new MetadataCollector(); - private context: ReflectorHostContext; + protected metadataCollector = new MetadataCollector(); + protected context: ReflectorHostContext; private isGenDirChildOfRootDir: boolean; - private basePath: string; + protected basePath: string; private genDir: string; constructor( - private program: ts.Program, private compilerHost: ts.CompilerHost, - private options: AngularCompilerOptions, context?: ReflectorHostContext) { + protected program: ts.Program, protected compilerHost: ts.CompilerHost, + protected options: AngularCompilerOptions, context?: ReflectorHostContext) { // normalize the path so that it never ends with '/'. this.basePath = path.normalize(path.join(this.options.basePath, '.')); 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 = ts.resolveModuleName(m, containingFile, this.options, this.context).resolvedModule; return resolved ? resolved.resolvedFileName : null; }; - private normalizeAssetUrl(url: string): string { + protected normalizeAssetUrl(url: string): string { 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); if (assetUrl) { - return this.resolve(assetUrl, containingFile); + return this.getCanonicalFileName(this.resolve(assetUrl, containingFile)); } return url; } @@ -198,7 +202,7 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { symbol = tc.getAliasedSymbol(symbol); } const declaration = symbol.getDeclarations()[0]; - const declarationFile = declaration.getSourceFile().fileName; + const declarationFile = this.getCanonicalFileName(declaration.getSourceFile().fileName); return this.getStaticSymbol(declarationFile, symbol.getName()); } catch (e) { @@ -267,9 +271,9 @@ export class ReflectorHost implements StaticReflectorHost, ImportGenerator { return metadata; } - private resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol { + protected resolveExportedSymbol(filePath: string, symbolName: string): StaticSymbol { const resolveModule = (moduleName: string): string => { - const resolvedModulePath = this.resolve(moduleName, filePath); + const resolvedModulePath = this.getCanonicalFileName(this.resolve(moduleName, filePath)); if (!resolvedModulePath) { throw new Error(`Could not resolve module '${moduleName}' relative to file ${filePath}`); }