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:
parent
5c213e5474
commit
3127ba3c35
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
(async() => {
|
||||
try {
|
||||
mainNgcc({
|
||||
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;
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -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. */
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
function getExecutor(async: boolean, logger: Logger, pkgJsonUpdater: PackageJsonUpdater): Executor {
|
||||
if (async) {
|
||||
return new AsyncSingleProcessExecutor(logger, pkgJsonUpdater);
|
||||
} else {
|
||||
return new SingleProcessExecutor(logger, pkgJsonUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
function getEntryPoints(
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue