diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts index d61f12341b..166873090e 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -12,7 +12,7 @@ import {FileSystem} from '../file_system/file_system'; import {PathMappings} from '../utils'; import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPointFormat, EntryPointJsonProperty} from './entry_point'; -import {NgccCompilerHost} from './ngcc_compiler_host'; +import {NgccCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host'; /** * A bundle of files and paths (and TS programs) that correspond to a particular @@ -48,16 +48,17 @@ export function makeEntryPointBundle( noLib: true, rootDir: entryPointPath, ...pathMappings }; - const host = new NgccCompilerHost(fs, options); + const srcHost = new NgccSourcesCompilerHost(fs, options, entryPointPath); + const dtsHost = new NgccCompilerHost(fs, options); const rootDirs = [AbsoluteFsPath.from(entryPointPath)]; // Create the bundle programs, as necessary. const src = makeBundleProgram( fs, isCore, AbsoluteFsPath.resolve(entryPointPath, formatPath), 'r3_symbols.js', options, - host); + srcHost); const dts = transformDts ? makeBundleProgram( fs, isCore, AbsoluteFsPath.resolve(entryPointPath, typingsPath), - 'r3_symbols.d.ts', options, host) : + 'r3_symbols.d.ts', options, dtsHost) : null; const isFlatCore = isCore && src.r3SymbolsFile === null; diff --git a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts index 8f8d5cd597..19ddb626dd 100644 --- a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -10,11 +10,12 @@ import * as ts from 'typescript'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; import {FileSystem} from '../file_system/file_system'; +import {isRelativePath} from '../utils'; export class NgccCompilerHost implements ts.CompilerHost { private _caseSensitive = this.fs.exists(AbsoluteFsPath.fromUnchecked(__filename.toUpperCase())); - constructor(private fs: FileSystem, private options: ts.CompilerOptions) {} + constructor(protected fs: FileSystem, protected options: ts.CompilerOptions) {} getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { const text = this.readFile(fileName); @@ -64,3 +65,41 @@ export class NgccCompilerHost implements ts.CompilerHost { return this.fs.readFile(AbsoluteFsPath.fromUnchecked(fileName)); } } + +/** + * Represents a compiler host that resolves a module import as a JavaScript source file if + * available, instead of the .d.ts typings file that would have been resolved by TypeScript. This + * is necessary for packages that have their typings in the same directory as the sources, which + * would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file. + */ +export class NgccSourcesCompilerHost extends NgccCompilerHost { + private cache = ts.createModuleResolutionCache( + this.getCurrentDirectory(), file => this.getCanonicalFileName(file)); + + constructor(fs: FileSystem, options: ts.CompilerOptions, protected entryPointPath: string) { + super(fs, options); + } + + resolveModuleNames( + moduleNames: string[], containingFile: string, reusedNames?: string[], + redirectedReference?: ts.ResolvedProjectReference): Array { + return moduleNames.map(moduleName => { + const {resolvedModule} = ts.resolveModuleName( + moduleName, containingFile, this.options, this, this.cache, redirectedReference); + + // If the module request originated from a relative import in a JavaScript source file, + // TypeScript may have resolved the module to its .d.ts declaration file if the .js source + // file was in the same directory. This is undesirable, as we need to have the actual + // JavaScript being present in the program. This logic recognizes this scenario and rewrites + // the resolved .d.ts declaration file to its .js counterpart, if it exists. + if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts && + containingFile.endsWith('.js') && isRelativePath(moduleName)) { + const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js'); + if (this.fileExists(jsFile)) { + return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js}; + } + } + return resolvedModule; + }); + } +} diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts new file mode 100644 index 0000000000..b976f9487a --- /dev/null +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -0,0 +1,120 @@ +/** + * @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 {makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {MockFileSystem} from '../helpers/mock_file_system'; + +function createMockFileSystem() { + return new MockFileSystem({ + '/node_modules/test': { + 'package.json': + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', + 'index.d.ts': 'export * from "./public_api";', + 'index.js': 'export * from "./public_api";', + 'index.metadata.json': '...', + 'public_api.d.ts': ` + export * from "test/secondary"; + export * from "./nested"; + export declare class TestClass {}; + `, + 'public_api.js': ` + export * from "test/secondary"; + export * from "./nested"; + export const TestClass = function() {}; + `, + 'root.d.ts': ` + import * from 'other'; + export declare class RootClass {}; + `, + 'root.js': ` + import * from 'other'; + export const RootClass = function() {}; + `, + 'nested': { + 'index.d.ts': 'export * from "../root";', + 'index.js': 'export * from "../root";', + }, + 'es2015': { + 'index.js': 'export * from "./public_api";', + 'public_api.js': 'export class TestClass {};', + 'root.js': ` + import * from 'other'; + export class RootClass {}; + `, + 'nested': { + 'index.js': 'export * from "../root";', + }, + }, + 'secondary': { + 'package.json': + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', + 'index.d.ts': 'export * from "./public_api";', + 'index.js': 'export * from "./public_api";', + 'index.metadata.json': '...', + 'public_api.d.ts': 'export declare class SecondaryClass {};', + 'public_api.js': 'export class SecondaryClass {};', + 'es2015': { + 'index.js': 'export * from "./public_api";', + 'public_api.js': 'export class SecondaryClass {};', + }, + }, + }, + '/node_modules/other': { + 'package.json': + '{"module": "./index.js", "es2015": "./es2015/index.js", "typings": "./index.d.ts"}', + 'index.d.ts': 'export * from "./public_api";', + 'index.js': 'export * from "./public_api";', + 'index.metadata.json': '...', + 'public_api.d.ts': 'export declare class OtherClass {};', + 'public_api.js': 'export class OtherClass {};', + 'es2015': { + 'index.js': 'export * from "./public_api";', + 'public_api.js': 'export class OtherClass {};', + }, + }, + }); +} + +describe('entry point bundle', () => { + // https://github.com/angular/angular/issues/29939 + it('should resolve JavaScript sources instead of declaration files if they are adjacent', () => { + const fs = createMockFileSystem(); + const esm5bundle = makeEntryPointBundle( + fs, '/node_modules/test', './index.js', './index.d.ts', false, 'esm5', 'esm5', true) !; + + expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) + .toEqual(jasmine.arrayWithExactContents([ + // Modules from the entry-point itself should be source files + '/node_modules/test/index.js', + '/node_modules/test/public_api.js', + '/node_modules/test/nested/index.js', + '/node_modules/test/root.js', + + // Modules from a secondary entry-point should be declaration files + '/node_modules/test/secondary/public_api.d.ts', + '/node_modules/test/secondary/index.d.ts', + + // Modules resolved from "other" should be declaration files + '/node_modules/other/public_api.d.ts', + '/node_modules/other/index.d.ts', + ])); + + expect(esm5bundle.dts !.program.getSourceFiles().map(sf => sf.fileName)) + .toEqual(jasmine.arrayWithExactContents([ + // All modules in the dts program should be declaration files + '/node_modules/test/index.d.ts', + '/node_modules/test/public_api.d.ts', + '/node_modules/test/nested/index.d.ts', + '/node_modules/test/root.d.ts', + '/node_modules/test/secondary/public_api.d.ts', + '/node_modules/test/secondary/index.d.ts', + '/node_modules/other/public_api.d.ts', + '/node_modules/other/index.d.ts', + ])); + }); +});