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
This commit is contained in:
George Kalpakas 2019-08-19 22:58:22 +03:00 committed by Matias Niemelä
parent 5c213e5474
commit 3127ba3c35
6 changed files with 120 additions and 25 deletions

View File

@ -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<typeof mainNgcc>) {
export function process(options: AsyncNgccOptions): Promise<void>;
export function process(options: SyncNgccOptions): void;
export function process(options: NgccOptions): void|Promise<void> {
// Recreate the file system on each call to reset the cache
setFileSystem(new CachedFileSystem(new NodeJSFileSystem()));
return mainNgcc(...args);
return mainNgcc(options);
}

View File

@ -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;
}
})();
}

View File

@ -33,7 +33,7 @@ export interface ExecutionOptions {
export interface Executor {
execute(
analyzeEntryPoints: AnalyzeEntryPointsFn, createCompileFn: CreateCompileFn,
options: ExecutionOptions): void;
options: ExecutionOptions): void|Promise<void>;
}
/** Represents metadata related to the processing of an entry-point. */

View File

@ -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<Executor['execute']>): Promise<void> {
return super.execute(...args);
}
}

View File

@ -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<SyncNgccOptions, 'async'>& {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<void>;
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<void> {
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(

View File

@ -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 = {