Skip to content

Commit 8d0b707

Browse files
authored
refactor(@angular-devkit/build-angular): update ESM in memory file loader to work with Node.js 20 (#25988)
This commit refactors the ESM Node.js in memory loader to work with Node.js 20+
1 parent 3ad028b commit 8d0b707

File tree

6 files changed

+76
-23
lines changed

6 files changed

+76
-23
lines changed

packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-file-loader.ts renamed to packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88

99
import { join, relative } from 'node:path';
1010
import { pathToFileURL } from 'node:url';
11-
import { workerData } from 'node:worker_threads';
1211
import { fileURLToPath } from 'url';
13-
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
12+
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer';
13+
import { callInitializeIfNeeded } from './node-18-utils';
1414

1515
/**
1616
* Node.js ESM loader to redirect imports to in memory files.
@@ -22,20 +22,26 @@ export interface ESMInMemoryFileLoaderWorkerData {
2222
workspaceRoot: string;
2323
}
2424

25-
const { outputFiles, workspaceRoot } = workerData as ESMInMemoryFileLoaderWorkerData;
26-
2725
const TRANSFORMED_FILES: Record<string, string> = {};
2826
const CHUNKS_REGEXP = /file:\/\/\/(main\.server|chunk-\w+)\.mjs/;
29-
const WORKSPACE_ROOT_FILE = pathToFileURL(join(workspaceRoot, 'index.mjs')).href;
27+
let workspaceRootFile: string;
28+
let outputFiles: Record<string, string>;
3029

31-
const JAVASCRIPT_TRANSFORMER = new JavaScriptTransformer(
30+
const javascriptTransformer = new JavaScriptTransformer(
3231
// Always enable JIT linking to support applications built with and without AOT.
3332
// In a development environment the additional scope information does not
3433
// have a negative effect unlike production where final output size is relevant.
3534
{ sourcemap: true, jit: true },
3635
1,
3736
);
3837

38+
callInitializeIfNeeded(initialize);
39+
40+
export function initialize(data: ESMInMemoryFileLoaderWorkerData) {
41+
workspaceRootFile = pathToFileURL(join(data.workspaceRoot, 'index.mjs')).href;
42+
outputFiles = data.outputFiles;
43+
}
44+
3945
export function resolve(
4046
specifier: string,
4147
context: { parentURL: undefined | string },
@@ -58,7 +64,7 @@ export function resolve(
5864
// Node.js default resolve if this is the last user-specified loader.
5965
return nextResolve(
6066
specifier,
61-
isBundleEntryPointOrChunk(context) ? { ...context, parentURL: WORKSPACE_ROOT_FILE } : context,
67+
isBundleEntryPointOrChunk(context) ? { ...context, parentURL: workspaceRootFile } : context,
6268
);
6369
}
6470

@@ -70,7 +76,7 @@ export async function load(url: string, context: { format?: string | null }, nex
7076

7177
if (source === undefined) {
7278
source = TRANSFORMED_FILES[filePath] = Buffer.from(
73-
await JAVASCRIPT_TRANSFORMER.transformFile(filePath),
79+
await javascriptTransformer.transformFile(filePath),
7480
).toString('utf-8');
7581
}
7682

@@ -94,7 +100,7 @@ function isFileProtocol(url: string): boolean {
94100
}
95101

96102
function handleProcessExit(): void {
97-
void JAVASCRIPT_TRANSFORMER.close();
103+
void javascriptTransformer.close();
98104
}
99105

100106
function isBundleEntryPointOrChunk(context: { parentURL: undefined | string }): boolean {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
import { join } from 'node:path';
10+
import { pathToFileURL } from 'node:url';
11+
import { workerData } from 'node:worker_threads';
12+
13+
let IS_NODE_18: boolean | undefined;
14+
function isNode18(): boolean {
15+
return (IS_NODE_18 ??= process.versions.node.startsWith('18.'));
16+
}
17+
18+
/** Call the initialize hook when running on Node.js 18 */
19+
export function callInitializeIfNeeded(
20+
initialize: (typeof import('./loader-hooks'))['initialize'],
21+
): void {
22+
if (isNode18()) {
23+
initialize(workerData);
24+
}
25+
}
26+
27+
export function getESMLoaderArgs(): string[] {
28+
if (isNode18()) {
29+
return [
30+
'--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`.
31+
'--loader',
32+
pathToFileURL(join(__dirname, 'loader-hooks.js')).href, // Loader cannot be an absolute path on Windows.
33+
];
34+
}
35+
36+
return [
37+
'--import',
38+
pathToFileURL(join(__dirname, 'register-hooks.js')).href, // Loader cannot be an absolute path on Windows.
39+
];
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
// TODO: remove the below once @types/node are version 20.x.x
10+
// @ts-expect-error "node:module"' has no exported member 'register'.ts(2305)
11+
import { register } from 'node:module';
12+
import { pathToFileURL } from 'node:url';
13+
import { workerData } from 'node:worker_threads';
14+
15+
register('./loader-hooks.js', pathToFileURL(__filename), { data: workerData });

packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts

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

99
import { readFile } from 'node:fs/promises';
10-
import { extname, join, posix } from 'node:path';
11-
import { pathToFileURL } from 'node:url';
10+
import { extname, posix } from 'node:path';
1211
import Piscina from 'piscina';
1312
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
13+
import { getESMLoaderArgs } from './esm-in-memory-loader/node-18-utils';
1414
import type { RenderResult, ServerContext } from './render-page';
1515
import type { RenderWorkerData } from './render-worker';
1616
import type {
@@ -85,11 +85,7 @@ export async function prerenderPages(
8585
inlineCriticalCss,
8686
document,
8787
} as RenderWorkerData,
88-
execArgv: [
89-
'--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`.
90-
'--loader',
91-
pathToFileURL(join(__dirname, 'esm-in-memory-file-loader.js')).href, // Loader cannot be an absolute path on Windows.
92-
],
88+
execArgv: getESMLoaderArgs(),
9389
});
9490

9591
try {
@@ -173,11 +169,7 @@ async function getAllRoutes(
173169
document,
174170
verbose,
175171
} as RoutesExtractorWorkerData,
176-
execArgv: [
177-
'--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`.
178-
'--loader',
179-
pathToFileURL(join(__dirname, 'esm-in-memory-file-loader.js')).href, // Loader cannot be an absolute path on Windows.
180-
],
172+
execArgv: getESMLoaderArgs(),
181173
});
182174

183175
const { routes: extractedRoutes, warnings }: RoutersExtractorWorkerResult = await renderWorker

packages/angular_devkit/build_angular/src/utils/server-rendering/render-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { workerData } from 'node:worker_threads';
10-
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-file-loader';
10+
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
1111
import { RenderResult, ServerContext, renderPage } from './render-page';
1212

1313
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {

packages/angular_devkit/build_angular/src/utils/server-rendering/routes-extractor-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import { workerData } from 'node:worker_threads';
1010
import { loadEsmModule } from '../load-esm';
11-
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-file-loader';
11+
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
1212
import { MainServerBundleExports } from './main-bundle-exports';
1313

1414
export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData {

0 commit comments

Comments
 (0)