diff --git a/packages/compiler-cli/src/compiler_host.ts b/packages/compiler-cli/src/compiler_host.ts index 422ca7b87a..1367de73a6 100644 --- a/packages/compiler-cli/src/compiler_host.ts +++ b/packages/compiler-cli/src/compiler_host.ts @@ -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 loadResource(filePath: string): Promise|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); } diff --git a/packages/compiler-cli/src/perform-compile.ts b/packages/compiler-cli/src/perform-compile.ts index 258a094ce8..821720a7ce 100644 --- a/packages/compiler-cli/src/perform-compile.ts +++ b/packages/compiler-cli/src/perform-compile.ts @@ -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}); diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 1d3b5b6d34..3567de65fa 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -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` it is assumed the user of the corresponding `Program` will call * `loadNgStructureAsync()`. Returing `Promise` 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; } diff --git a/packages/compiler-cli/src/transformers/compiler_host.ts b/packages/compiler-cli/src/transformers/compiler_host.ts new file mode 100644 index 0000000000..375b3af80a --- /dev/null +++ b/packages/compiler-cli/src/transformers/compiler_host.ts @@ -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(); + 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(); + 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, '/'); +} \ No newline at end of file diff --git a/packages/compiler-cli/src/transformers/entry_points.ts b/packages/compiler-cli/src/transformers/entry_points.ts index b6d991a4c9..9a574fa586 100644 --- a/packages/compiler-cli/src/transformers/entry_points.ts +++ b/packages/compiler-cli/src/transformers/entry_points.ts @@ -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; -} diff --git a/packages/compiler-cli/src/transformers/module_filename_resolver.ts b/packages/compiler-cli/src/transformers/module_filename_resolver.ts deleted file mode 100644 index d0a55da56a..0000000000 --- a/packages/compiler-cli/src/transformers/module_filename_resolver.ts +++ /dev/null @@ -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(); - - 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(); - 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; -} \ No newline at end of file diff --git a/packages/compiler-cli/test/transformers/compiler_host_spec.ts b/packages/compiler-cli/test/transformers/compiler_host_spec.ts new file mode 100644 index 0000000000..e0babe55c2 --- /dev/null +++ b/packages/compiler-cli/test/transformers/compiler_host_spec.ts @@ -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'); + }); + }); +}); \ No newline at end of file diff --git a/packages/compiler/src/aot/static_reflector.ts b/packages/compiler/src/aot/static_reflector.ts index fb76750878..06523cc5de 100644 --- a/packages/compiler/src/aot/static_reflector.ts +++ b/packages/compiler/src/aot/static_reflector.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 { diff --git a/packages/compiler/src/aot/static_symbol_resolver.ts b/packages/compiler/src/aot/static_symbol_resolver.ts index 80d3ec5be4..6a66d3ab78 100644 --- a/packages/compiler/src/aot/static_symbol_resolver.ts +++ b/packages/compiler/src/aot/static_symbol_resolver.ts @@ -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. *