Skip to content

Commit b1d6fad

Browse files
devversiondylhunn
authored andcommitted
refactor(compiler-cli): do not use __filename or __dirname global for ESM compatibility (#43431)
Switches the compiler-cli usage of `__filename` to `import.meta.url` when ESM bundles are generated. Unfortunately we cannot start using only `import.meta` yet as we still build and run all code in Angular in CommonJS module output for devmode tests. This commit also fixes various instances where a jasmine spy was applied on a namespace export that will break with ES module (and the interop for CommonJS output). We fix these spies by using a default import. PR Close #43431
1 parent b46b3cf commit b1d6fad

File tree

15 files changed

+113
-16
lines changed

15 files changed

+113
-16
lines changed

packages/compiler-cli/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ ts_config(
6868
deps = ["//packages:tsconfig-build.json"],
6969
)
7070

71+
ts_library(
72+
name = "import_meta_url_types",
73+
srcs = ["import_meta_url.d.ts"],
74+
)
75+
7176
ts_library(
7277
name = "compiler-cli",
7378
srcs = glob(
@@ -76,11 +81,13 @@ ts_library(
7681
"src/**/*.ts",
7782
],
7883
exclude = [
84+
"import_meta_url.d.ts",
7985
"src/integrationtest/**/*.ts",
8086
],
8187
),
8288
tsconfig = ":tsconfig",
8389
deps = [
90+
":import_meta_url_types",
8491
"//packages/compiler",
8592
"//packages/compiler-cli/src/ngtsc/core",
8693
"//packages/compiler-cli/src/ngtsc/core:api",
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
10+
// Note: The `__ESM_IMPORT_META_URL__` identifier will be defined as part of bundling.
11+
// We cannot use `import.meta.url` directly as this code currently still runs in CommonJS
12+
// for devmode. This is a solution allowing for both ESModule and CommonJS to run this file.
13+
// TODO(devversion): replace all of this with `import.meta.url` once devmode is using ESM.
14+
declare const __ESM_IMPORT_META_URL__: string;

packages/compiler-cli/ngcc/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ts_library(
1313
"//packages:types",
1414
"//packages/compiler",
1515
"//packages/compiler-cli",
16+
"//packages/compiler-cli:import_meta_url_types",
1617
"//packages/compiler-cli/src/ngtsc/annotations",
1718
"//packages/compiler-cli/src/ngtsc/cycles",
1819
"//packages/compiler-cli/src/ngtsc/diagnostics",

packages/compiler-cli/ngcc/index.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {join} from 'path';
9+
import {dirname, join} from 'path';
10+
import {fileURLToPath} from 'url';
11+
1012
import {NodeJSFileSystem, setFileSystem} from '../src/ngtsc/file_system';
1113

1214
import {mainNgcc} from './src/main';
@@ -23,10 +25,17 @@ export function process(options: AsyncNgccOptions|SyncNgccOptions): void|Promise
2325
return mainNgcc(options);
2426
}
2527

28+
29+
// CommonJS/ESM interop for determining the current file name and containing
30+
// directory. These path is needed for providing an absolute path to the ngcc
31+
// command line entry-point script (for the CLI).
32+
export const containingDirPath =
33+
typeof __dirname !== 'undefined' ? __dirname : dirname(fileURLToPath(__ESM_IMPORT_META_URL__));
34+
2635
/**
27-
* Absolute file path that points to the `ngcc` entry-point.
36+
* Absolute file path that points to the `ngcc` command line entry-point.
2837
*
2938
* This can be used by the Angular CLI to spawn a process running ngcc using
3039
* command line options.
3140
*/
32-
export const ngccMainFilePath = join(__dirname, './main-ngcc.js');
41+
export const ngccMainFilePath = join(containingDirPath, './main-ngcc.js');

packages/compiler-cli/ngcc/src/execution/cluster/master.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/// <reference types="node" />
1010

1111
import cluster from 'cluster';
12+
import module from 'module';
1213

1314
import {AbsoluteFsPath, PathManipulation} from '../../../../src/ngtsc/file_system';
1415
import {Logger} from '../../../../src/ngtsc/logging';
@@ -21,7 +22,6 @@ import {stringifyTask} from '../tasks/utils';
2122
import {MessageFromWorker, TaskCompletedMessage, TransformedFilesMessage, UpdatePackageJsonMessage} from './api';
2223
import {Deferred, sendMessageToWorker} from './utils';
2324

24-
2525
/**
2626
* The cluster master is responsible for analyzing all entry-points, planning the work that needs to
2727
* be done, distributing it to worker-processes and collecting/post-processing the results.
@@ -44,7 +44,7 @@ export class ClusterMaster {
4444
}
4545

4646
// Set the worker entry-point
47-
cluster.setupMaster({exec: this.fileSystem.resolve(__dirname, 'ngcc_cluster_worker.js')});
47+
cluster.setupMaster({exec: getClusterWorkerScriptPath(fileSystem)});
4848

4949
this.taskQueue = analyzeEntryPoints();
5050
this.onTaskCompleted = createTaskCompletedCallback(this.taskQueue);
@@ -330,3 +330,16 @@ export class ClusterMaster {
330330
};
331331
}
332332
}
333+
334+
/** Gets the absolute file path to the cluster worker script. */
335+
export function getClusterWorkerScriptPath(fileSystem: PathManipulation): AbsoluteFsPath {
336+
// This is an interop allowing for the worker script to be determined in both
337+
// a CommonJS module, or an ES module which does not come with `require` by default.
338+
const requireFn =
339+
typeof require !== 'undefined' ? require : module.createRequire(__ESM_IMPORT_META_URL__);
340+
// We resolve the worker script using module resolution as in the package output,
341+
// the worker might be bundled but exposed through a subpath export mapping.
342+
const workerScriptPath =
343+
requireFn.resolve('@angular/compiler-cli/ngcc/src/execution/cluster/ngcc_cluster_worker');
344+
return fileSystem.resolve(workerScriptPath);
345+
}

packages/compiler-cli/ngcc/src/locking/lock_file_with_child_process/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {ChildProcess, fork} from 'child_process';
9+
import module from 'module';
910

1011
import {AbsoluteFsPath, FileSystem} from '../../../../src/ngtsc/file_system';
1112
import {Logger, LogLevel} from '../../../../src/ngtsc/logging';
@@ -78,7 +79,7 @@ export class LockFileWithChildProcess implements LockFile {
7879
this.logger.level !== undefined ? this.logger.level.toString() : LogLevel.info.toString();
7980
const isWindows = process.platform === 'win32';
8081
const unlocker = fork(
81-
__dirname + '/ngcc_lock_unlocker.js', [path, logLevel],
82+
getLockFileUnlockerScriptPath(this.fs), [path, logLevel],
8283
{detached: true, stdio: isWindows ? 'pipe' : 'inherit'});
8384
if (isWindows) {
8485
unlocker.stdout?.on('data', process.stdout.write.bind(process.stdout));
@@ -87,3 +88,16 @@ export class LockFileWithChildProcess implements LockFile {
8788
return unlocker;
8889
}
8990
}
91+
92+
/** Gets the absolute file path to the lock file unlocker script. */
93+
export function getLockFileUnlockerScriptPath(fileSystem: FileSystem): AbsoluteFsPath {
94+
// This is an interop allowing for the unlocking script to be determined in both
95+
// a CommonJS module, or an ES module which does not come with `require` by default.
96+
const requireFn =
97+
typeof require !== 'undefined' ? require : module.createRequire(__ESM_IMPORT_META_URL__);
98+
// We resolve the worker script using module resolution as in the package output,
99+
// the worker might be bundled but exposed through a subpath export mapping.
100+
const unlockerScriptPath = requireFn.resolve(
101+
'@angular/compiler-cli/ngcc/src/locking/lock_file_with_child_process/ngcc_lock_unlocker');
102+
return fileSystem.resolve(unlockerScriptPath);
103+
}

packages/compiler-cli/ngcc/src/packages/configuration.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {createHash} from 'crypto';
9+
import module from 'module';
910
import semver from 'semver';
1011
import * as vm from 'vm';
1112

@@ -267,6 +268,11 @@ export const DEFAULT_NGCC_CONFIG: NgccProjectConfig = {
267268

268269
const NGCC_CONFIG_FILENAME = 'ngcc.config.js';
269270

271+
// CommonJS/ESM interop for determining the current file name and containing
272+
// directory. The path is needed for loading the user configuration.
273+
const isCommonJS = typeof require !== 'undefined';
274+
const currentFileUrl = isCommonJS ? null : __ESM_IMPORT_META_URL__;
275+
270276
/**
271277
* The processed package level configuration as a result of processing a raw package level config.
272278
*/
@@ -435,12 +441,13 @@ export class NgccConfiguration {
435441
}
436442

437443
private evalSrcFile(srcPath: AbsoluteFsPath): any {
444+
const requireFn = isCommonJS ? require : module.createRequire(currentFileUrl!);
438445
const src = this.fs.readFile(srcPath);
439446
const theExports = {};
440447
const sandbox = {
441448
module: {exports: theExports},
442449
exports: theExports,
443-
require,
450+
require: requireFn,
444451
__dirname: this.fs.dirname(srcPath),
445452
__filename: srcPath
446453
};

packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
*/
88

99
/// <reference types="node" />
10-
import {readFileSync} from 'fs';
11-
import * as os from 'os';
10+
import realFs from 'fs';
11+
import os from 'os';
1212

1313
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../../src/ngtsc/file_system';
1414
import {Folder, MockFileSystem, runInEachFileSystem, TestFile} from '../../../src/ngtsc/file_system/testing';
@@ -69,7 +69,7 @@ runInEachFileSystem(() => {
6969
fs.ensureDir(fs.join(pkgPath, 'fesm5'));
7070
fs.writeFile(
7171
fs.join(pkgPath, 'fesm5/core.js'),
72-
readFileSync(require.resolve('../fesm5_angular_core.js'), 'utf8'));
72+
realFs.readFileSync(require.resolve('../fesm5_angular_core.js'), 'utf8'));
7373

7474
pkgJson.esm5 = './fesm5/core.js';
7575
pkgJson.fesm5 = './fesm5/core.js';

packages/compiler-cli/ngcc/test/ngcc_options_spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import * as os from 'os';
8+
// Note: We do not use a namespace import here because this will result in the
9+
// named exports being modified if we apply jasmine spies on `realFs`. Using
10+
// the default export gives us an object where we can patch properties on.
11+
import os from 'os';
912

1013
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem} from '../../src/ngtsc/file_system';
1114
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';

packages/compiler-cli/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
"./private/migrations": {
4141
"types": "./private/migrations.d.ts",
4242
"default": "./bundles/private/migrations.js"
43+
},
44+
"./ngcc/src/execution/cluster/ngcc_cluster_worker": {
45+
"default": "./bundles/ngcc/src/execution/cluster/ngcc_cluster_worker.js"
46+
},
47+
"./ngcc/src/locking/lock_file_with_child_process/ngcc_lock_unlocker": {
48+
"default": "./bundles/ngcc/src/locking/lock_file_with_child_process/ngcc_lock_unlocker.js"
4349
}
4450
},
4551
"dependencies": {

0 commit comments

Comments
 (0)