From 3127ba3c35acd30d79517f5d03af9592d1136ad9 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Mon, 19 Aug 2019 22:58:22 +0300 Subject: [PATCH] refactor(ngcc): add support for asynchronous execution (#32427) Previously, `ngcc`'s programmatic API would run and complete synchronously. This was necessary for specific usecases (such as how the `@angular/cli` invokes `ngcc` as part of the TypeScript module resolution process), but not for others (e.g. running `ivy-ngcc` as a `postinstall` script). This commit adds a new option (`async`) that enables turning on asynchronous execution. I.e. it signals that the caller is OK with the function call to complete asynchronously, which allows `ngcc` to potentially run in a more efficient mode. Currently, there is no difference in the way tasks are executed in sync vs async mode, but this change sets the ground for adding new execution options (that require asynchronous operation), such as processing tasks in parallel on multiple processes. NOTE: When using the programmatic API, the default value for `async` is `false`, thus retaining backwards compatibility. When running `ngcc` from the command line (i.e. via the `ivy-ngcc` script), it runs in async mode (to be able to take advantage of future optimizations), but that is transparent to the caller. PR Close #32427 --- packages/compiler-cli/ngcc/index.ts | 10 ++-- packages/compiler-cli/ngcc/main-ngcc.ts | 30 +++++++----- .../compiler-cli/ngcc/src/execution/api.ts | 2 +- .../src/execution/single_process_executor.ts | 9 ++++ packages/compiler-cli/ngcc/src/main.ts | 46 +++++++++++++++--- .../ngcc/test/integration/ngcc_spec.ts | 48 +++++++++++++++++++ 6 files changed, 120 insertions(+), 25 deletions(-) diff --git a/packages/compiler-cli/ngcc/index.ts b/packages/compiler-cli/ngcc/index.ts index 619de61b3a..90d58bb7d1 100644 --- a/packages/compiler-cli/ngcc/index.ts +++ b/packages/compiler-cli/ngcc/index.ts @@ -7,14 +7,16 @@ */ import {CachedFileSystem, NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system'; -import {mainNgcc} from './src/main'; +import {AsyncNgccOptions, NgccOptions, SyncNgccOptions, mainNgcc} from './src/main'; export {ConsoleLogger, LogLevel} from './src/logging/console_logger'; export {Logger} from './src/logging/logger'; -export {NgccOptions} from './src/main'; +export {AsyncNgccOptions, NgccOptions, SyncNgccOptions} from './src/main'; export {PathMappings} from './src/utils'; -export function process(...args: Parameters) { +export function process(options: AsyncNgccOptions): Promise; +export function process(options: SyncNgccOptions): void; +export function process(options: NgccOptions): void|Promise { // Recreate the file system on each call to reset the cache setFileSystem(new CachedFileSystem(new NodeJSFileSystem())); - return mainNgcc(...args); + return mainNgcc(options); } diff --git a/packages/compiler-cli/ngcc/main-ngcc.ts b/packages/compiler-cli/ngcc/main-ngcc.ts index 665200a6a5..8943c4531c 100644 --- a/packages/compiler-cli/ngcc/main-ngcc.ts +++ b/packages/compiler-cli/ngcc/main-ngcc.ts @@ -64,17 +64,21 @@ if (require.main === module) { const targetEntryPointPath = options['t'] ? options['t'] : undefined; const compileAllFormats = !options['first-only']; const logLevel = options['l'] as keyof typeof LogLevel | undefined; - try { - mainNgcc({ - basePath: baseSourcePath, - propertiesToConsider, - targetEntryPointPath, - compileAllFormats, - logger: logLevel && new ConsoleLogger(LogLevel[logLevel]), - }); - process.exitCode = 0; - } catch (e) { - console.error(e.stack || e.message); - process.exitCode = 1; - } + + (async() => { + try { + await mainNgcc({ + basePath: baseSourcePath, + propertiesToConsider, + targetEntryPointPath, + compileAllFormats, + logger: logLevel && new ConsoleLogger(LogLevel[logLevel]), + async: true, + }); + process.exitCode = 0; + } catch (e) { + console.error(e.stack || e.message); + process.exitCode = 1; + } + })(); } diff --git a/packages/compiler-cli/ngcc/src/execution/api.ts b/packages/compiler-cli/ngcc/src/execution/api.ts index e1a9eb6731..c72156f770 100644 --- a/packages/compiler-cli/ngcc/src/execution/api.ts +++ b/packages/compiler-cli/ngcc/src/execution/api.ts @@ -33,7 +33,7 @@ export interface ExecutionOptions { export interface Executor { execute( analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn, - options: ExecutionOptions): void; + options: ExecutionOptions): void|Promise; } /** Represents metadata related to the processing of an entry-point. */ diff --git a/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts b/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts index 0d54b678cc..92d9031203 100644 --- a/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts +++ b/packages/compiler-cli/ngcc/src/execution/single_process_executor.ts @@ -44,3 +44,12 @@ export class SingleProcessExecutor implements Executor { checkForUnprocessedEntryPoints(processingMetadataPerEntryPoint, options.propertiesToConsider); } } + +/** + * An `Executor` that processes all tasks serially, but still completes asynchronously. + */ +export class AsyncSingleProcessExecutor extends SingleProcessExecutor { + async execute(...args: Parameters): Promise { + return super.execute(...args); + } +} diff --git a/packages/compiler-cli/ngcc/src/main.ts b/packages/compiler-cli/ngcc/src/main.ts index a8e00da5b7..9b29773e25 100644 --- a/packages/compiler-cli/ngcc/src/main.ts +++ b/packages/compiler-cli/ngcc/src/main.ts @@ -17,7 +17,7 @@ import {UmdDependencyHost} from './dependencies/umd_dependency_host'; import {DirectoryWalkerEntryPointFinder} from './entry_point_finder/directory_walker_entry_point_finder'; import {TargetedEntryPointFinder} from './entry_point_finder/targeted_entry_point_finder'; import {AnalyzeEntryPointsFn, CreateCompileFn, EntryPointProcessingMetadata, Executor, Task, TaskProcessingOutcome} from './execution/api'; -import {SingleProcessExecutor} from './execution/single_process_executor'; +import {AsyncSingleProcessExecutor, SingleProcessExecutor} from './execution/single_process_executor'; import {ConsoleLogger, LogLevel} from './logging/console_logger'; import {Logger} from './logging/logger'; import {hasBeenProcessed, markAsProcessed} from './packages/build_marker'; @@ -32,11 +32,12 @@ import {NewEntryPointFileWriter} from './writing/new_entry_point_file_writer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from './writing/package_json_updater'; /** - * The options to configure the ngcc compiler. + * The options to configure the ngcc compiler for synchronous execution. */ -export interface NgccOptions { +export interface SyncNgccOptions { /** The absolute path to the `node_modules` folder that contains the packages to process. */ basePath: string; + /** * The path to the primary package to be processed. If not absolute then it must be relative to * `basePath`. @@ -44,36 +45,60 @@ export interface NgccOptions { * All its dependencies will need to be processed too. */ targetEntryPointPath?: string; + /** * Which entry-point properties in the package.json to consider when processing an entry-point. * Each property should hold a path to the particular bundle format for the entry-point. * Defaults to all the properties in the package.json. */ propertiesToConsider?: string[]; + /** * Whether to process all formats specified by (`propertiesToConsider`) or to stop processing * this entry-point at the first matching format. Defaults to `true`. */ compileAllFormats?: boolean; + /** * Whether to create new entry-points bundles rather than overwriting the original files. */ createNewEntryPointFormats?: boolean; + /** * Provide a logger that will be called with log messages. */ logger?: Logger; + /** * Paths mapping configuration (`paths` and `baseUrl`), as found in `ts.CompilerOptions`. * These are used to resolve paths to locally built Angular libraries. */ pathMappings?: PathMappings; + /** * Provide a file-system service that will be used by ngcc for all file interactions. */ fileSystem?: FileSystem; + + /** + * Whether the compilation should run and return asynchronously. Allowing asynchronous execution + * may speed up the compilation by utilizing multiple CPU cores (if available). + * + * Default: `false` (i.e. run synchronously) + */ + async?: false; } +/** + * The options to configure the ngcc compiler for asynchronous execution. + */ +export type AsyncNgccOptions = Omit& {async: true}; + +/** + * The options to configure the ngcc compiler. + */ +export type NgccOptions = AsyncNgccOptions | SyncNgccOptions; + /** * This is the main entry-point into ngcc (aNGular Compatibility Compiler). * @@ -82,10 +107,13 @@ export interface NgccOptions { * * @param options The options telling ngcc what to compile and how. */ +export function mainNgcc(options: AsyncNgccOptions): Promise; +export function mainNgcc(options: SyncNgccOptions): void; export function mainNgcc( {basePath, targetEntryPointPath, propertiesToConsider = SUPPORTED_FORMAT_PROPERTIES, compileAllFormats = true, createNewEntryPointFormats = false, - logger = new ConsoleLogger(LogLevel.info), pathMappings}: NgccOptions): void { + logger = new ConsoleLogger(LogLevel.info), pathMappings, async = false}: NgccOptions): void| + Promise { const fileSystem = getFileSystem(); const pkgJsonUpdater = new DirectPackageJsonUpdater(fileSystem); @@ -191,7 +219,7 @@ export function mainNgcc( }; // The executor for actually planning and getting the work done. - const executor = getExecutor(logger, pkgJsonUpdater); + const executor = getExecutor(async, logger, pkgJsonUpdater); const execOpts = {compileAllFormats, propertiesToConsider}; return executor.execute(analyzeEntryPoints, createCompileFn, execOpts); @@ -226,8 +254,12 @@ function getFileWriter( new InPlaceFileWriter(fs); } -function getExecutor(logger: Logger, pkgJsonUpdater: PackageJsonUpdater): Executor { - return new SingleProcessExecutor(logger, pkgJsonUpdater); +function getExecutor(async: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater): Executor { + if (async) { + return new AsyncSingleProcessExecutor(logger, pkgJsonUpdater); + } else { + return new SingleProcessExecutor(logger, pkgJsonUpdater); + } } function getEntryPoints( diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 31351e81c6..68970b6b0e 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -11,6 +11,7 @@ import {loadStandardTestFiles, loadTestFiles} from '../../../test/helpers'; import {mainNgcc} from '../../src/main'; import {markAsProcessed} from '../../src/packages/build_marker'; import {EntryPointJsonProperty, EntryPointPackageJson, SUPPORTED_FORMAT_PROPERTIES} from '../../src/packages/entry_point'; +import {Transformer} from '../../src/packages/transformer'; import {DirectPackageJsonUpdater, PackageJsonUpdater} from '../../src/writing/package_json_updater'; import {MockLogger} from '../helpers/mock_logger'; @@ -56,6 +57,53 @@ runInEachFileSystem(() => { }); }); + it('should throw, if an error happens during processing', () => { + spyOn(Transformer.prototype, 'transform').and.throwError('Test error.'); + + expect(() => mainNgcc({ + basePath: '/dist', + targetEntryPointPath: 'local-package', + propertiesToConsider: ['main', 'es2015'], + logger: new MockLogger(), + })) + .toThrowError(`Test error.`); + + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined(); + expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toBeUndefined(); + }); + + describe('in async mode', () => { + it('should run ngcc without errors for fesm2015', async() => { + const promise = mainNgcc({ + basePath: '/node_modules', + propertiesToConsider: ['fesm2015'], + async: true, + }); + + expect(promise).toEqual(jasmine.any(Promise)); + await promise; + }); + + it('should reject, if an error happens during processing', async() => { + spyOn(Transformer.prototype, 'transform').and.throwError('Test error.'); + + const promise = mainNgcc({ + basePath: '/dist', + targetEntryPointPath: 'local-package', + propertiesToConsider: ['main', 'es2015'], + logger: new MockLogger(), + async: true, + }); + + await promise.then( + () => Promise.reject('Expected promise to be rejected.'), + err => expect(err).toEqual(new Error('Test error.'))); + + expect(loadPackage('@angular/core').__processed_by_ivy_ngcc__).toBeUndefined(); + expect(loadPackage('local-package', _('/dist')).__processed_by_ivy_ngcc__).toBeUndefined(); + }); + }); + describe('with targetEntryPointPath', () => { it('should only compile the given package entry-point (and its dependencies).', () => { const STANDARD_MARKERS = {