diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index 13c0268d3e..efd32bc07f 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -12,7 +12,7 @@ import {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options export {ConsoleLogger} from './src/logging/console_logger'; export {Logger, LogLevel} from './src/logging/logger'; -export {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/ngcc_options'; +export {AsyncNgccOptions, clearTsConfigCache, NgccOptions, SyncNgccOptions} from './src/ngcc_options'; export {PathMappings} from './src/path_mappings'; export function process(options: AsyncNgccOptions): Promise; diff --git a/packages/compiler-cli/ngcc/src/ngcc_options.ts b/packages/compiler-cli/ngcc/src/ngcc_options.ts index 1ada2058d2..9bdcb47947 100644 --- a/packages/compiler-cli/ngcc/src/ngcc_options.ts +++ b/packages/compiler-cli/ngcc/src/ngcc_options.ts @@ -156,7 +156,7 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp const absBasePath = absoluteFrom(options.basePath); const projectPath = fileSystem.dirname(absBasePath); const tsConfig = - options.tsConfigPath !== null ? readConfiguration(options.tsConfigPath || projectPath) : null; + options.tsConfigPath !== null ? getTsConfig(options.tsConfigPath || projectPath) : null; let { basePath, @@ -200,3 +200,28 @@ export function getSharedSetup(options: NgccOptions): SharedSetup&RequiredNgccOp new InPlaceFileWriter(fileSystem, logger, errorOnFailedEntryPoint), }; } + +let tsConfigCache: ParsedConfiguration|null = null; +let tsConfigPathCache: string|null = null; + +/** + * Get the parsed configuration object for the given `tsConfigPath`. + * + * This function will cache the previous parsed configuration object to avoid unnecessary processing + * of the tsconfig.json in the case that it is requested repeatedly. + * + * This makes the assumption, which is true as of writing, that the contents of tsconfig.json and + * its dependencies will not change during the life of the process running ngcc. + */ +function getTsConfig(tsConfigPath: string): ParsedConfiguration|null { + if (tsConfigPath !== tsConfigPathCache) { + tsConfigPathCache = tsConfigPath; + tsConfigCache = readConfiguration(tsConfigPath); + } + return tsConfigCache; +} + +export function clearTsConfigCache() { + tsConfigPathCache = null; + tsConfigCache = null; +} diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 7503eeb6e6..8a022cacd3 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -14,6 +14,7 @@ import {Folder, MockFileSystem, runInEachFileSystem, TestFile} from '../../../sr import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers'; import {getLockFilePath} from '../../src/locking/lock_file'; import {mainNgcc} from '../../src/main'; +import {clearTsConfigCache} from '../../src/ngcc_options'; import {hasBeenProcessed, markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; import {EntryPointManifestFile} from '../../src/packages/entry_point_manifest'; @@ -41,6 +42,10 @@ runInEachFileSystem(() => { spyOn(os, 'cpus').and.returnValue([{model: 'Mock CPU'} as any]); }); + afterEach(() => { + clearTsConfigCache(); + }); + it('should run ngcc without errors for esm2015', () => { expect(() => mainNgcc({basePath: '/node_modules', propertiesToConsider: ['esm2015']})) .not.toThrow(); diff --git a/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts b/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts new file mode 100644 index 0000000000..b3f562a2c0 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/ngcc_options_spec.ts @@ -0,0 +1,78 @@ +/** + * @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 {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system'; +import {runInEachFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system/testing'; + +import {clearTsConfigCache, getSharedSetup, NgccOptions} from '../src/ngcc_options'; + +import {MockLogger} from './helpers/mock_logger'; + + +runInEachFileSystem(() => { + let fs: FileSystem; + let _abs: typeof absoluteFrom; + let projectPath: AbsoluteFsPath; + + beforeEach(() => { + fs = getFileSystem(); + _abs = absoluteFrom; + projectPath = _abs('/project'); + }); + + describe('getSharedSetup()', () => { + let pathToProjectTsConfig: AbsoluteFsPath; + let pathToCustomTsConfig: AbsoluteFsPath; + + beforeEach(() => { + clearTsConfigCache(); + pathToProjectTsConfig = fs.resolve(projectPath, 'tsconfig.json'); + fs.ensureDir(fs.dirname(pathToProjectTsConfig)); + fs.writeFile(pathToProjectTsConfig, '{"files": ["src/index.ts"]}'); + pathToCustomTsConfig = _abs('/path/to/tsconfig.json'); + fs.ensureDir(fs.dirname(pathToCustomTsConfig)); + fs.writeFile(pathToCustomTsConfig, '{"files": ["custom/index.ts"]}'); + }); + + it('should load the tsconfig.json at the project root if tsConfigPath is `undefined`', () => { + const setup = getSharedSetup({...createOptions()}); + expect(setup.tsConfigPath).toBeUndefined(); + expect(setup.tsConfig?.rootNames).toEqual([fs.resolve(projectPath, 'src/index.ts')]); + }); + + it('should load a specific tsconfig.json if tsConfigPath is a string', () => { + const setup = getSharedSetup({...createOptions(), tsConfigPath: pathToCustomTsConfig}); + expect(setup.tsConfigPath).toEqual(pathToCustomTsConfig); + expect(setup.tsConfig?.rootNames).toEqual([_abs('/path/to/custom/index.ts')]); + }); + + it('should not load a tsconfig.json if tsConfigPath is `null`', () => { + const setup = getSharedSetup({...createOptions(), tsConfigPath: null}); + expect(setup.tsConfigPath).toBe(null); + expect(setup.tsConfig).toBe(null); + }); + }); + + /** + * This function creates an object that contains the minimal required properties for NgccOptions. + */ + function createOptions(): NgccOptions { + return { + async: false, + basePath: fs.resolve(projectPath, 'node_modules'), + propertiesToConsider: ['es2015'], + compileAllFormats: false, + createNewEntryPointFormats: false, + logger: new MockLogger(), + fileSystem: getFileSystem(), + errorOnFailedEntryPoint: true, + enableI18nLegacyMessageIdFormat: true, + invalidateEntryPointManifest: false, + }; + } +});