diff --git a/packages/compiler-cli/ngcc/src/execution/create_compile_function.ts b/packages/compiler-cli/ngcc/src/execution/create_compile_function.ts index c3671e6946a56..6f8d754ed2670 100644 --- a/packages/compiler-cli/ngcc/src/execution/create_compile_function.ts +++ b/packages/compiler-cli/ngcc/src/execution/create_compile_function.ts @@ -14,6 +14,7 @@ import {Logger} from '../../../src/ngtsc/logging'; import {ParsedConfiguration} from '../../../src/perform_compile'; import {getEntryPointFormat} from '../packages/entry_point'; import {makeEntryPointBundle} from '../packages/entry_point_bundle'; +import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache'; import {PathMappings} from '../path_mappings'; import {FileWriter} from '../writing/file_writer'; @@ -30,6 +31,8 @@ export function getCreateCompileFn( return (beforeWritingFiles, onTaskCompleted) => { const {Transformer} = require('../packages/transformer'); const transformer = new Transformer(fileSystem, logger, tsConfig); + const sharedFileCache = new SharedFileCache(fileSystem); + const moduleResolutionCache = createModuleResolutionCache(fileSystem); return (task: Task) => { const {entryPoint, formatProperty, formatPropertiesToMarkAsProcessed, processDts} = task; @@ -54,8 +57,8 @@ export function getCreateCompileFn( logger.info(`Compiling ${entryPoint.name} : ${formatProperty} as ${format}`); const bundle = makeEntryPointBundle( - fileSystem, entryPoint, formatPath, isCore, format, processDts, pathMappings, true, - enableI18nLegacyMessageIdFormat); + fileSystem, entryPoint, sharedFileCache, moduleResolutionCache, formatPath, isCore, + format, processDts, pathMappings, true, enableI18nLegacyMessageIdFormat); const result = transformer.transform(bundle); if (result.success) { 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 68eea9495df18..4acd7f0031de9 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point_bundle.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ import * as ts from 'typescript'; -import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system'; +import {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; import {PathMappings} from '../path_mappings'; import {BundleProgram, makeBundleProgram} from './bundle_program'; import {EntryPoint, EntryPointFormat} from './entry_point'; -import {NgccSourcesCompilerHost} from './ngcc_compiler_host'; +import {NgccDtsCompilerHost, NgccSourcesCompilerHost} from './ngcc_compiler_host'; +import {EntryPointFileCache, SharedFileCache} from './source_file_cache'; /** * A bundle of files and paths (and TS programs) that correspond to a particular @@ -31,6 +32,8 @@ export interface EntryPointBundle { * Get an object that describes a formatted bundle for an entry-point. * @param fs The current file-system being used. * @param entryPoint The entry-point that contains the bundle. + * @param sharedFileCache The cache to use for source files that are shared across all entry-points. + * @param moduleResolutionCache The module resolution cache to use. * @param formatPath The path to the source files for this bundle. * @param isCore This entry point is the Angular core package. * @param format The underlying format of the bundle. @@ -42,7 +45,8 @@ export interface EntryPointBundle { * component templates. */ export function makeEntryPointBundle( - fs: FileSystem, entryPoint: EntryPoint, formatPath: string, isCore: boolean, + fs: FileSystem, entryPoint: EntryPoint, sharedFileCache: SharedFileCache, + moduleResolutionCache: ts.ModuleResolutionCache, formatPath: string, isCore: boolean, format: EntryPointFormat, transformDts: boolean, pathMappings?: PathMappings, mirrorDtsFromSrc: boolean = false, enableI18nLegacyMessageIdFormat: boolean = true): EntryPointBundle { @@ -50,8 +54,10 @@ export function makeEntryPointBundle( const rootDir = entryPoint.packagePath; const options: ts .CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings}; - const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath); - const dtsHost = new NgtscCompilerHost(fs, options); + const entryPointCache = new EntryPointFileCache(fs, sharedFileCache); + const dtsHost = new NgccDtsCompilerHost(fs, options, entryPointCache, moduleResolutionCache); + const srcHost = new NgccSourcesCompilerHost( + fs, options, entryPointCache, moduleResolutionCache, entryPoint.packagePath); // Create the bundle programs, as necessary. const absFormatPath = fs.resolve(entryPoint.path, formatPath); 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 4c5d62f767e3d..c679a12503e94 100644 --- a/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts +++ b/packages/compiler-cli/ngcc/src/packages/ngcc_compiler_host.ts @@ -10,6 +10,7 @@ import * as ts from 'typescript'; import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system'; import {isWithinPackage} from '../analysis/util'; import {isRelativePath} from '../utils'; +import {EntryPointFileCache} from './source_file_cache'; /** * Represents a compiler host that resolves a module import as a JavaScript source file if @@ -18,19 +19,24 @@ import {isRelativePath} from '../utils'; * would otherwise let TypeScript prefer the .d.ts file instead of the JavaScript source file. */ export class NgccSourcesCompilerHost extends NgtscCompilerHost { - private cache = ts.createModuleResolutionCache( - this.getCurrentDirectory(), file => this.getCanonicalFileName(file)); - - constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) { + constructor( + fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache, + private moduleResolutionCache: ts.ModuleResolutionCache, + protected packagePath: AbsoluteFsPath) { super(fs, options); } + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + return this.cache.getCachedSourceFile(fileName, languageVersion); + } + 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); + moduleName, containingFile, this.options, this, this.moduleResolutionCache, + 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 @@ -59,3 +65,31 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost { }); } } + +/** + * A compiler host implementation that is used for the typings program. It leverages the entry-point + * cache for source files and module resolution, as these results can be reused across the sources + * program. + */ +export class NgccDtsCompilerHost extends NgtscCompilerHost { + constructor( + fs: FileSystem, options: ts.CompilerOptions, private cache: EntryPointFileCache, + private moduleResolutionCache: ts.ModuleResolutionCache) { + super(fs, options); + } + + getSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + return this.cache.getCachedSourceFile(fileName, languageVersion); + } + + 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.moduleResolutionCache, + redirectedReference); + return resolvedModule; + }); + } +} diff --git a/packages/compiler-cli/ngcc/src/packages/source_file_cache.ts b/packages/compiler-cli/ngcc/src/packages/source_file_cache.ts new file mode 100644 index 0000000000000..d9b0c33637c25 --- /dev/null +++ b/packages/compiler-cli/ngcc/src/packages/source_file_cache.ts @@ -0,0 +1,197 @@ +/** + * @license + * Copyright Google LLC 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 {AbsoluteFsPath, FileSystem} from '../../../src/ngtsc/file_system'; + +/** + * A cache that holds on to source files that can be shared for processing all entry-points in a + * single invocation of ngcc. In particular, the following files are shared across all entry-points + * through this cache: + * + * 1. Default library files such as `lib.dom.d.ts` and `lib.es5.d.ts`. These files don't change + * and some are very large, so parsing is expensive. Therefore, the parsed `ts.SourceFile`s for + * the default library files are cached. + * 2. The typings of @angular scoped packages. The typing files for @angular packages are typically + * used in the entry-points that ngcc processes, so benefit from a single source file cache. + * Especially `@angular/core/core.d.ts` is large and expensive to parse repeatedly. In contrast + * to default library files, we have to account for these files to be invalidated during a single + * invocation of ngcc, as ngcc will overwrite the .d.ts files during its processing. + * + * The lifecycle of this cache corresponds with a single invocation of ngcc. Separate invocations, + * e.g. the CLI's synchronous module resolution fallback will therefore all have their own cache. + * This allows for the source file cache to be garbage collected once ngcc processing has completed. + */ +export class SharedFileCache { + private sfCache = new Map(); + + constructor(private fs: FileSystem) {} + + /** + * Loads a `ts.SourceFile` if the provided `fileName` is deemed appropriate to be cached. To + * optimize for memory usage, only files that are generally used in all entry-points are cached. + * If `fileName` is not considered to benefit from caching or the requested file does not exist, + * then `undefined` is returned. + */ + getCachedSourceFile(fileName: string): ts.SourceFile|undefined { + const absPath = this.fs.resolve(fileName); + if (isDefaultLibrary(absPath, this.fs)) { + return this.getStableCachedFile(absPath); + } else if (isAngularDts(absPath, this.fs)) { + return this.getVolatileCachedFile(absPath); + } else { + return undefined; + } + } + + /** + * Attempts to load the source file from the cache, or parses the file into a `ts.SourceFile` if + * it's not yet cached. This method assumes that the file will not be modified for the duration + * that this cache is valid for. If that assumption does not hold, the `getVolatileCachedFile` + * method is to be used instead. + */ + private getStableCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined { + if (!this.sfCache.has(absPath)) { + const content = readFile(absPath, this.fs); + if (content === undefined) { + return undefined; + } + const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015); + this.sfCache.set(absPath, sf); + } + return this.sfCache.get(absPath)!; + } + + /** + * In contrast to `getStableCachedFile`, this method always verifies that the cached source file + * is the same as what's stored on disk. This is done for files that are expected to change during + * ngcc's processing, such as @angular scoped packages for which the .d.ts files are overwritten + * by ngcc. If the contents on disk have changed compared to a previously cached source file, the + * content from disk is re-parsed and the cache entry is replaced. + */ + private getVolatileCachedFile(absPath: AbsoluteFsPath): ts.SourceFile|undefined { + const content = readFile(absPath, this.fs); + if (content === undefined) { + return undefined; + } + if (!this.sfCache.has(absPath) || this.sfCache.get(absPath)!.text !== content) { + const sf = ts.createSourceFile(absPath, content, ts.ScriptTarget.ES2015); + this.sfCache.set(absPath, sf); + } + return this.sfCache.get(absPath)!; + } +} + +const DEFAULT_LIB_PATTERN = ['node_modules', 'typescript', 'lib', /^lib\..+\.d\.ts$/]; + +/** + * Determines whether the provided path corresponds with a default library file inside of the + * typescript package. + * + * @param absPath The path for which to determine if it corresponds with a default library file. + * @param fs The filesystem to use for inspecting the path. + */ +export function isDefaultLibrary(absPath: AbsoluteFsPath, fs: FileSystem): boolean { + return isFile(absPath, DEFAULT_LIB_PATTERN, fs); +} + +const ANGULAR_DTS_PATTERN = ['node_modules', '@angular', /./, /\.d\.ts$/]; + +/** + * Determines whether the provided path corresponds with a .d.ts file inside of an @angular + * scoped package. This logic only accounts for the .d.ts files in the root, which is sufficient + * to find the large, flattened entry-point files that benefit from caching. + * + * @param absPath The path for which to determine if it corresponds with an @angular .d.ts file. + * @param fs The filesystem to use for inspecting the path. + */ +export function isAngularDts(absPath: AbsoluteFsPath, fs: FileSystem): boolean { + return isFile(absPath, ANGULAR_DTS_PATTERN, fs); +} + +/** + * Helper function to determine whether a file corresponds with a given pattern of segments. + * + * @param path The path for which to determine if it corresponds with the provided segments. + * @param segments Array of segments; the `path` must have ending segments that match the + * patterns in this array. + * @param fs The filesystem to use for inspecting the path. + */ +function isFile( + path: AbsoluteFsPath, segments: ReadonlyArray, fs: FileSystem): boolean { + for (let i = segments.length - 1; i >= 0; i--) { + const pattern = segments[i]; + const segment = fs.basename(path); + if (typeof pattern === 'string') { + if (pattern !== segment) { + return false; + } + } else { + if (!pattern.test(segment)) { + return false; + } + } + path = fs.dirname(path); + } + return true; +} + +/** + * A cache for processing a single entry-point. This exists to share `ts.SourceFile`s between the + * source and typing programs that are created for a single program. + */ +export class EntryPointFileCache { + private readonly sfCache = new Map(); + + constructor(private fs: FileSystem, private sharedFileCache: SharedFileCache) {} + + /** + * Returns and caches a parsed `ts.SourceFile` for the provided `fileName`. If the `fileName` is + * cached in the shared file cache, that result is used. Otherwise, the source file is cached + * internally. This method returns `undefined` if the requested file does not exist. + * + * @param fileName The path of the file to retrieve a source file for. + * @param languageVersion The language version to use for parsing the file. + */ + getCachedSourceFile(fileName: string, languageVersion: ts.ScriptTarget): ts.SourceFile|undefined { + const staticSf = this.sharedFileCache.getCachedSourceFile(fileName); + if (staticSf !== undefined) { + return staticSf; + } + + const absPath = this.fs.resolve(fileName); + if (this.sfCache.has(absPath)) { + return this.sfCache.get(absPath); + } + + const content = readFile(absPath, this.fs); + if (content === undefined) { + return undefined; + } + const sf = ts.createSourceFile(fileName, content, languageVersion); + this.sfCache.set(absPath, sf); + return sf; + } +} + +function readFile(absPath: AbsoluteFsPath, fs: FileSystem): string|undefined { + if (!fs.exists(absPath) || !fs.stat(absPath).isFile()) { + return undefined; + } + return fs.readFile(absPath); +} + +/** + * Creates a `ts.ModuleResolutionCache` that uses the provided filesystem for path operations. + * + * @param fs The filesystem to use for path operations. + */ +export function createModuleResolutionCache(fs: FileSystem): ts.ModuleResolutionCache { + return ts.createModuleResolutionCache(fs.pwd(), fileName => { + return fs.isCaseSensitive() ? fileName : fileName.toLowerCase(); + }); +} diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index 75a23805543c5..382758f3c370b 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -14,6 +14,7 @@ import {NgccEntryPointConfig} from '../../src/packages/configuration'; import {EntryPoint, EntryPointFormat} from '../../src/packages/entry_point'; import {EntryPointBundle} from '../../src/packages/entry_point_bundle'; import {NgccSourcesCompilerHost} from '../../src/packages/ngcc_compiler_host'; +import {createModuleResolutionCache, EntryPointFileCache, SharedFileCache} from '../../src/packages/source_file_cache'; export type TestConfig = Pick; @@ -68,7 +69,10 @@ export function makeTestBundleProgram( const rootDir = fs.dirname(entryPointPath); const options: ts.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]}; - const host = new NgccSourcesCompilerHost(fs, options, rootDir); + const moduleResolutionCache = createModuleResolutionCache(fs); + const entryPointFileCache = new EntryPointFileCache(fs, new SharedFileCache(fs)); + const host = + new NgccSourcesCompilerHost(fs, options, entryPointFileCache, moduleResolutionCache, rootDir); return makeBundleProgram( fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles); } 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 index 8eb133ed0a23a..a5878794af169 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -10,6 +10,7 @@ import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {loadTestFiles} from '../../../test/helpers'; import {EntryPoint} from '../../src/packages/entry_point'; import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache'; runInEachFileSystem(() => { describe('entry point bundle', () => { @@ -180,7 +181,10 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; - const esm5bundle = makeEntryPointBundle(fs, entryPoint, './index.js', false, 'esm5', true); + const moduleResolutionCache = createModuleResolutionCache(fs); + const esm5bundle = makeEntryPointBundle( + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false, + 'esm5', true); expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) .toEqual(jasmine.arrayWithExactContents([ @@ -291,8 +295,11 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; + const moduleResolutionCache = createModuleResolutionCache(fs); const esm5bundle = makeEntryPointBundle( - fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true, + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false, + 'esm5', + /* transformDts */ true, /* pathMappings */ undefined, /* mirrorDtsFromSrc */ true); expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName))) @@ -328,8 +335,11 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; + const moduleResolutionCache = createModuleResolutionCache(fs); const esm5bundle = makeEntryPointBundle( - fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true, + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false, + 'esm5', + /* transformDts */ true, /* pathMappings */ undefined, /* mirrorDtsFromSrc */ true); expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) .toContain(absoluteFrom('/node_modules/test/internal.js')); @@ -351,8 +361,11 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; + const moduleResolutionCache = createModuleResolutionCache(fs); const esm5bundle = makeEntryPointBundle( - fs, entryPoint, './esm2015/index.js', false, 'esm2015', /* transformDts */ true, + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, + './esm2015/index.js', false, 'esm2015', + /* transformDts */ true, /* pathMappings */ undefined, /* mirrorDtsFromSrc */ true); expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) .toContain(absoluteFrom('/node_modules/internal/esm2015/src/internal.js')); @@ -374,8 +387,11 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; + const moduleResolutionCache = createModuleResolutionCache(fs); const esm5bundle = makeEntryPointBundle( - fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true, + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false, + 'esm5', + /* transformDts */ true, /* pathMappings */ undefined, /* mirrorDtsFromSrc */ false); expect(esm5bundle.src.program.getSourceFiles().map(sf => sf.fileName)) .toContain(absoluteFrom('/node_modules/test/internal.js')); @@ -398,8 +414,11 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, generateDeepReexports: false, }; + const moduleResolutionCache = createModuleResolutionCache(fs); const bundle = makeEntryPointBundle( - fs, entryPoint, './index.js', false, 'esm2015', /* transformDts */ true, + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, './index.js', false, + 'esm2015', + /* transformDts */ true, /* pathMappings */ undefined, /* mirrorDtsFromSrc */ true); expect(bundle.rootDirs).toEqual([absoluteFrom('/node_modules/primary')]); }); diff --git a/packages/compiler-cli/ngcc/test/packages/source_file_cache_spec.ts b/packages/compiler-cli/ngcc/test/packages/source_file_cache_spec.ts new file mode 100644 index 0000000000000..8c3ca880897ab --- /dev/null +++ b/packages/compiler-cli/ngcc/test/packages/source_file_cache_spec.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright Google LLC 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 {absoluteFrom, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system'; +import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; +import {loadTestFiles} from '../../../test/helpers'; +import {EntryPointFileCache, isAngularDts, isDefaultLibrary, SharedFileCache} from '../../src/packages/source_file_cache'; + +runInEachFileSystem(() => { + describe('caching', () => { + let _: typeof absoluteFrom; + let fs: FileSystem; + beforeEach(() => { + _ = absoluteFrom; + fs = getFileSystem(); + loadTestFiles([ + { + name: _('/node_modules/typescript/lib/lib.es5.d.ts'), + contents: `export declare interface Array {}`, + }, + { + name: _('/node_modules/typescript/lib/lib.dom.d.ts'), + contents: `export declare interface Window {}`, + }, + { + name: _('/node_modules/@angular/core/core.d.ts'), + contents: `export declare interface Component {}`, + }, + { + name: _('/node_modules/@angular/common/common.d.ts'), + contents: `export declare interface NgIf {}`, + }, + { + name: _('/index.ts'), + contents: `export const index = true;`, + }, + { + name: _('/main.ts'), + contents: `export const main = true;`, + }, + ]); + }); + + describe('SharedFileCache', () => { + it('should cache a parsed source file for default libraries', () => { + const cache = new SharedFileCache(fs); + + const libEs5 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!; + expect(libEs5).not.toBeUndefined(); + expect(libEs5.text).toContain('Array'); + + const libDom = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!; + expect(libDom).not.toBeUndefined(); + expect(libDom.text).toContain('Window'); + + const libEs5_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.es5.d.ts')!; + expect(libEs5_2).toBe(libEs5); + + const libDom_2 = cache.getCachedSourceFile('/node_modules/typescript/lib/lib.dom.d.ts')!; + expect(libDom_2).toBe(libDom); + }); + + it('should cache a parsed source file for @angular scoped packages', () => { + const cache = new SharedFileCache(fs); + + const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!; + expect(core).not.toBeUndefined(); + expect(core.text).toContain('Component'); + + const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!; + expect(common).not.toBeUndefined(); + expect(common.text).toContain('NgIf'); + + const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!; + expect(core_2).toBe(core); + + const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!; + expect(common_2).toBe(common); + }); + + it('should reparse @angular d.ts files when they change', () => { + const cache = new SharedFileCache(fs); + + const core = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!; + expect(core).not.toBeUndefined(); + expect(core.text).toContain('Component'); + + const common = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!; + expect(common).not.toBeUndefined(); + expect(common.text).toContain('NgIf'); + + fs.writeFile( + _('/node_modules/@angular/core/core.d.ts'), `export declare interface Directive {}`); + + const core_2 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!; + expect(core_2).not.toBe(core); + expect(core_2.text).toContain('Directive'); + + const core_3 = cache.getCachedSourceFile('/node_modules/@angular/core/core.d.ts')!; + expect(core_3).toBe(core_2); + + const common_2 = cache.getCachedSourceFile('/node_modules/@angular/common/common.d.ts')!; + expect(common_2).toBe(common); + }); + + it('should not cache files that are not default library files inside of the typescript package', + () => { + const cache = new SharedFileCache(fs); + + expect(cache.getCachedSourceFile('/node_modules/typescript/lib/typescript.d.ts')) + .toBeUndefined(); + expect(cache.getCachedSourceFile('/typescript/lib.es5.d.ts')).toBeUndefined(); + }); + }); + + describe('isDefaultLibrary()', () => { + it('should accept lib files inside of the typescript package', () => { + expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.d.ts'), fs)).toBe(true); + expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.dom.d.ts'), fs)).toBe(true); + expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es2015.core.d.ts'), fs)) + .toBe(true); + expect(isDefaultLibrary(_('/root/node_modules/typescript/lib/lib.es5.d.ts'), fs)) + .toBe(true); + }); + it('should reject non lib files inside of the typescript package', () => { + expect(isDefaultLibrary(_('/node_modules/typescript/lib/typescript.d.ts'), fs)).toBe(false); + expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.es5.ts'), fs)).toBe(false); + expect(isDefaultLibrary(_('/node_modules/typescript/lib/lib.d.ts'), fs)).toBe(false); + expect(isDefaultLibrary(_('/node_modules/typescript/lib.es5.d.ts'), fs)).toBe(false); + }); + it('should reject lib files outside of the typescript package', () => { + expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false); + expect(isDefaultLibrary(_('/node_modules/ttypescript/lib/lib.es5.d.ts'), fs)).toBe(false); + expect(isDefaultLibrary(_('/typescript/lib/lib.es5.d.ts'), fs)).toBe(false); + }); + }); + + describe('isAngularDts()', () => { + it('should accept .d.ts files inside of the @angular scope', () => { + expect(isAngularDts(_('/node_modules/@angular/core/core.d.ts'), fs)).toBe(true); + expect(isAngularDts(_('/node_modules/@angular/common/common.d.ts'), fs)).toBe(true); + }); + it('should reject non-.d.ts files inside @angular scoped packages', () => { + expect(isAngularDts(_('/node_modules/@angular/common/src/common.ts'), fs)).toBe(false); + }); + it('should reject .d.ts files nested deeply inside @angular scoped packages', () => { + expect(isAngularDts(_('/node_modules/@angular/common/src/common.d.ts'), fs)).toBe(false); + }); + it('should reject .d.ts files directly inside the @angular scope', () => { + expect(isAngularDts(_('/node_modules/@angular/common.d.ts'), fs)).toBe(false); + }); + it('should reject files that are not inside node_modules', () => { + expect(isAngularDts(_('/@angular/core/core.d.ts'), fs)).toBe(false); + }); + }); + + describe('EntryPointFileCache', () => { + let sharedFileCache: SharedFileCache; + beforeEach(() => { + sharedFileCache = new SharedFileCache(fs); + }); + + it('should prefer source files cached in SharedFileCache', () => { + const cache1 = new EntryPointFileCache(fs, sharedFileCache); + const libEs5_1 = cache1.getCachedSourceFile( + '/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!; + expect(libEs5_1).not.toBeUndefined(); + expect(libEs5_1.text).toContain('Array'); + expect(libEs5_1.languageVersion).toBe(ts.ScriptTarget.ES2015); + + const cache2 = new EntryPointFileCache(fs, sharedFileCache); + const libEs5_2 = cache2.getCachedSourceFile( + '/node_modules/typescript/lib/lib.es5.d.ts', ts.ScriptTarget.ESNext)!; + expect(libEs5_1).toBe(libEs5_2); + }); + + it('should cache source files that are not default library files', () => { + const cache = new EntryPointFileCache(fs, sharedFileCache); + const index = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!; + expect(index).not.toBeUndefined(); + expect(index.text).toContain('index'); + expect(index.languageVersion).toBe(ts.ScriptTarget.ESNext); + + const main = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!; + expect(main).not.toBeUndefined(); + expect(main.text).toContain('main'); + expect(main.languageVersion).toBe(ts.ScriptTarget.ESNext); + + const index_2 = cache.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!; + expect(index_2).toBe(index); + + const main_2 = cache.getCachedSourceFile('/main.ts', ts.ScriptTarget.ESNext)!; + expect(main_2).toBe(main); + }); + + it('should not share non-library files across multiple cache instances', () => { + const cache1 = new EntryPointFileCache(fs, sharedFileCache); + const cache2 = new EntryPointFileCache(fs, sharedFileCache); + + const index1 = cache1.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!; + const index2 = cache2.getCachedSourceFile('/index.ts', ts.ScriptTarget.ESNext)!; + expect(index1).not.toBe(index2); + }); + + it('should return undefined if the file does not exist', () => { + const cache = new EntryPointFileCache(fs, sharedFileCache); + expect(cache.getCachedSourceFile('/nonexistent.ts', ts.ScriptTarget.ESNext)) + .toBeUndefined(); + }); + + it('should return undefined if the path is a directory', () => { + const cache = new EntryPointFileCache(fs, sharedFileCache); + expect(cache.getCachedSourceFile('/node_modules', ts.ScriptTarget.ESNext)).toBeUndefined(); + }); + }); + }); +}); diff --git a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts index b8fb4aba3382b..255212a20c9d0 100644 --- a/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts +++ b/packages/compiler-cli/ngcc/test/writing/new_entry_point_file_writer_spec.ts @@ -12,6 +12,7 @@ import {loadTestFiles} from '../../../test/helpers'; import {NgccConfiguration} from '../../src/packages/configuration'; import {EntryPoint, EntryPointFormat, EntryPointJsonProperty, getEntryPointInfo, isEntryPoint} from '../../src/packages/entry_point'; import {EntryPointBundle, makeEntryPointBundle} from '../../src/packages/entry_point_bundle'; +import {createModuleResolutionCache, SharedFileCache} from '../../src/packages/source_file_cache'; import {FileWriter} from '../../src/writing/file_writer'; import {NewEntryPointFileWriter} from '../../src/writing/new_entry_point_file_writer'; import {DirectPackageJsonUpdater} from '../../src/writing/package_json_updater'; @@ -634,7 +635,9 @@ runInEachFileSystem(() => { function makeTestBundle( fs: FileSystem, entryPoint: EntryPoint, formatProperty: EntryPointJsonProperty, format: EntryPointFormat): EntryPointBundle { + const moduleResolutionCache = createModuleResolutionCache(fs); return makeEntryPointBundle( - fs, entryPoint, entryPoint.packageJson[formatProperty]!, false, format, true); + fs, entryPoint, new SharedFileCache(fs), moduleResolutionCache, + entryPoint.packageJson[formatProperty]!, false, format, true); } });