refactor(compiler-cli): remove dynamic require in ngcc to allow for bundling (#43431)

ngcc currently dynamially loads the `Transformer` code. It does this
to avoid unnecessary parsing and loading of transformer-related code
if there is nothing to process (so-called noop case). Unfortunately
this dynamic require is not recognized by ESBuild. The import needs
to be discovered as otheriwse the transformer code would not be included
in the bundled package output of the CLI.

The ngcc code needs to use an async runtime import as it would work
in ES modules. This introduces async code into to the compililation
pipeline, breaking the `ngccMain` synchronous invocation feature.

To avoid this, we just move the dynamic require/async import to
the file top-level so that we do not break synchronous processing
which the CLI relies on. This has the downside of slowing-down
the noop case a little but I believe that should be mitigated
through bundling of ngcc anyway. In the future with full-ESM
we won't be able to get around this anyway (unless we remove the
sync variant of ngcc processing).

PR Close #43431
This commit is contained in:
Paul Gschwendtner
2021-09-19 18:07:20 +02:00
committed by Dylan Hunn
parent f7cd8a0e45
commit 11a81898be
8 changed files with 52 additions and 42 deletions
+2
View File
@@ -34,6 +34,8 @@ esbuild(
"//packages/compiler-cli/private:bazel.ts",
"//packages/compiler-cli/private:localize.ts",
"//packages/compiler-cli/private:migrations.ts",
"//packages/compiler-cli/ngcc:src/locking/lock_file_with_child_process/ngcc_lock_unlocker.ts",
"//packages/compiler-cli/ngcc:src/execution/cluster/ngcc_cluster_worker.ts",
],
external = [
"@angular/compiler",
@@ -44,7 +44,7 @@ export class ClusterMaster {
}
// Set the worker entry-point
cluster.setupMaster({exec: this.fileSystem.resolve(__dirname, 'worker.js')});
cluster.setupMaster({exec: this.fileSystem.resolve(__dirname, 'ngcc_cluster_worker.js')});
this.taskQueue = analyzeEntryPoints();
this.onTaskCompleted = createTaskCompletedCallback(this.taskQueue);
@@ -0,0 +1,46 @@
/**
* @license
* Copyright Google LLC 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 {parseCommandLineOptions} from '../../command_line_options';
import {getSharedSetup} from '../../ngcc_options';
import {getCreateCompileFn} from '../create_compile_function';
import {ClusterWorkerPackageJsonUpdater} from './package_json_updater';
import {startWorker} from './worker';
// Cluster worker entry point
(async () => {
process.title = 'ngcc (worker)';
try {
const {
logger,
pathMappings,
enableI18nLegacyMessageIdFormat,
fileSystem,
tsConfig,
getFileWriter,
} = getSharedSetup(parseCommandLineOptions(process.argv.slice(2)));
// NOTE: To avoid file corruption, `ngcc` invocation only creates _one_ instance of
// `PackageJsonUpdater` that actually writes to disk (across all processes).
// In cluster workers we use a `PackageJsonUpdater` that delegates to the cluster master.
const pkgJsonUpdater = new ClusterWorkerPackageJsonUpdater();
const fileWriter = getFileWriter(pkgJsonUpdater);
// The function for creating the `compile()` function.
const createCompileFn = getCreateCompileFn(
fileSystem, logger, fileWriter, enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
await startWorker(logger, createCompileFn);
process.exitCode = 0;
} catch (e) {
console.error(e.stack || e.message);
process.exit(1);
}
})();
@@ -10,50 +10,12 @@
import * as cluster from 'cluster';
import {Logger} from '../../../../src/ngtsc/logging';
import {parseCommandLineOptions} from '../../command_line_options';
import {getSharedSetup} from '../../ngcc_options';
import {CreateCompileFn} from '../api';
import {getCreateCompileFn} from '../create_compile_function';
import {stringifyTask} from '../tasks/utils';
import {MessageToWorker} from './api';
import {ClusterWorkerPackageJsonUpdater} from './package_json_updater';
import {sendMessageToMaster} from './utils';
// Cluster worker entry point
if (require.main === module) {
(async () => {
process.title = 'ngcc (worker)';
try {
const {
logger,
pathMappings,
enableI18nLegacyMessageIdFormat,
fileSystem,
tsConfig,
getFileWriter,
} = getSharedSetup(parseCommandLineOptions(process.argv.slice(2)));
// NOTE: To avoid file corruption, `ngcc` invocation only creates _one_ instance of
// `PackageJsonUpdater` that actually writes to disk (across all processes).
// In cluster workers we use a `PackageJsonUpdater` that delegates to the cluster master.
const pkgJsonUpdater = new ClusterWorkerPackageJsonUpdater();
const fileWriter = getFileWriter(pkgJsonUpdater);
// The function for creating the `compile()` function.
const createCompileFn = getCreateCompileFn(
fileSystem, logger, fileWriter, enableI18nLegacyMessageIdFormat, tsConfig, pathMappings);
await startWorker(logger, createCompileFn);
process.exitCode = 0;
} catch (e) {
console.error(e.stack || e.message);
process.exit(1);
}
})();
}
export async function startWorker(logger: Logger, createCompileFn: CreateCompileFn): Promise<void> {
if (cluster.isMaster) {
throw new Error('Tried to run cluster worker on the master process.');
@@ -15,6 +15,7 @@ import {ParsedConfiguration} from '../../../src/perform_compile';
import {getEntryPointFormat} from '../packages/entry_point';
import {makeEntryPointBundle} from '../packages/entry_point_bundle';
import {createModuleResolutionCache, SharedFileCache} from '../packages/source_file_cache';
import {Transformer} from '../packages/transformer';
import {PathMappings} from '../path_mappings';
import {FileWriter} from '../writing/file_writer';
@@ -29,7 +30,6 @@ export function getCreateCompileFn(
enableI18nLegacyMessageIdFormat: boolean, tsConfig: ParsedConfiguration|null,
pathMappings: PathMappings|undefined): CreateCompileFn {
return (beforeWritingFiles, onTaskCompleted) => {
const {Transformer} = require('../packages/transformer');
const transformer = new Transformer(fileSystem, logger, tsConfig);
const sharedFileCache = new SharedFileCache(fileSystem);
const moduleResolutionCache = createModuleResolutionCache(fileSystem);
@@ -78,7 +78,7 @@ export class LockFileWithChildProcess implements LockFile {
this.logger.level !== undefined ? this.logger.level.toString() : LogLevel.info.toString();
const isWindows = process.platform === 'win32';
const unlocker = fork(
__dirname + '/unlocker.js', [path, logLevel],
__dirname + '/ngcc_lock_unlocker.js', [path, logLevel],
{detached: true, stdio: isWindows ? 'pipe' : 'inherit'});
if (isWindows) {
unlocker.stdout?.on('data', process.stdout.write.bind(process.stdout));
@@ -11,7 +11,7 @@
describe('unlocker', () => {
it('should attach a handler to the `disconnect` event', () => {
spyOn(process, 'on');
require('../../../src/locking/lock_file_with_child_process/unlocker');
require('../../../src/locking/lock_file_with_child_process/ngcc_lock_unlocker');
// TODO: @JiaLiPassion, need to wait for @types/jasmine to handle the override case
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42455
expect(process.on).toHaveBeenCalledWith('disconnect' as any, jasmine.any(Function));