Skip to content

Commit

Permalink
refactor(ngcc): add support for asynchronous execution (angular#32427)
Browse files Browse the repository at this point in the history
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 angular#32427
  • Loading branch information
gkalpak authored and matsko committed Sep 9, 2019
1 parent 5c213e5 commit 3127ba3
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 25 deletions.
10 changes: 6 additions & 4 deletions packages/compiler-cli/ngcc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
30 changes: 17 additions & 13 deletions packages/compiler-cli/ngcc/main-ngcc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
})();
}
2 changes: 1 addition & 1 deletion packages/compiler-cli/ngcc/src/execution/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
46 changes: 39 additions & 7 deletions packages/compiler-cli/ngcc/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,48 +32,73 @@ 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`.
*
* 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).
*
Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 = {
Expand Down

0 comments on commit 3127ba3

Please sign in to comment.