Skip to content

Commit 05781cf

Browse files
committed
fix(@angular/build): address prerendering in-memory ESM resolution in Node.js 22.2.0 and later
Node.js 22.2.0 introduced a breaking change affecting custom ESM resolution. For more context, see: [Node.js issue #53097](nodejs/node#53097) Closes: #53097
1 parent 767652b commit 05781cf

File tree

7 files changed

+538
-260
lines changed

7 files changed

+538
-260
lines changed

packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import assert from 'node:assert';
1010
import { randomUUID } from 'node:crypto';
1111
import { join } from 'node:path';
1212
import { pathToFileURL } from 'node:url';
13+
import { MessagePort } from 'node:worker_threads';
1314
import { fileURLToPath } from 'url';
1415
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer';
1516

@@ -21,12 +22,14 @@ import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transfo
2122
const MEMORY_URL_SCHEME = 'memory://';
2223

2324
export interface ESMInMemoryFileLoaderWorkerData {
24-
outputFiles: Record<string, string>;
25+
jsOutputFilesForWorker: Record<string, string>;
2526
workspaceRoot: string;
2627
}
2728

28-
let memoryVirtualRootUrl: string;
29-
let outputFiles: Record<string, string>;
29+
interface ESMInMemoryFileLoaderResolutionData {
30+
memoryVirtualRootUrl: string;
31+
outputFiles: Record<string, string>;
32+
}
3033

3134
const javascriptTransformer = new JavaScriptTransformer(
3235
// Always enable JIT linking to support applications built with and without AOT.
@@ -36,23 +39,46 @@ const javascriptTransformer = new JavaScriptTransformer(
3639
1,
3740
);
3841

39-
export function initialize(data: ESMInMemoryFileLoaderWorkerData) {
40-
// This path does not actually exist but is used to overlay the in memory files with the
41-
// actual filesystem for resolution purposes.
42-
// A custom URL schema (such as `memory://`) cannot be used for the resolve output because
43-
// the in-memory files may use `import.meta.url` in ways that assume a file URL.
44-
// `createRequire` is one example of this usage.
45-
memoryVirtualRootUrl = pathToFileURL(
46-
join(data.workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
47-
).href;
48-
outputFiles = data.outputFiles;
42+
let resolveData: Promise<ESMInMemoryFileLoaderResolutionData>;
43+
44+
export function initialize(data: { port: MessagePort } | ESMInMemoryFileLoaderWorkerData) {
45+
resolveData = new Promise<ESMInMemoryFileLoaderResolutionData>((resolve) => {
46+
if (!('port' in data)) {
47+
/** TODO: Remove when Node.js Removes < 22.2 are no longer supported. */
48+
resolve({
49+
outputFiles: data.jsOutputFilesForWorker,
50+
memoryVirtualRootUrl: pathToFileURL(
51+
join(data.workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
52+
).href,
53+
});
54+
55+
return;
56+
}
57+
58+
const { port } = data;
59+
port.once(
60+
'message',
61+
({ jsOutputFilesForWorker, workspaceRoot }: ESMInMemoryFileLoaderWorkerData) => {
62+
resolve({
63+
outputFiles: jsOutputFilesForWorker,
64+
memoryVirtualRootUrl: pathToFileURL(
65+
join(workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
66+
).href,
67+
});
68+
69+
port.close();
70+
},
71+
);
72+
});
4973
}
5074

51-
export function resolve(
75+
export async function resolve(
5276
specifier: string,
5377
context: { parentURL: undefined | string },
5478
nextResolve: Function,
5579
) {
80+
const { outputFiles, memoryVirtualRootUrl } = await resolveData;
81+
5682
// In-memory files loaded from external code will contain a memory scheme
5783
if (specifier.startsWith(MEMORY_URL_SCHEME)) {
5884
let memoryUrl;
@@ -89,7 +115,7 @@ export function resolve(
89115

90116
if (
91117
specifierUrl?.pathname &&
92-
Object.hasOwn(outputFiles, specifierUrl.href.slice(memoryVirtualRootUrl.length))
118+
outputFiles[specifierUrl.href.slice(memoryVirtualRootUrl.length)] !== undefined
93119
) {
94120
return {
95121
format: 'module',
@@ -114,6 +140,7 @@ export function resolve(
114140
}
115141

116142
export async function load(url: string, context: { format?: string | null }, nextLoad: Function) {
143+
const { outputFiles, memoryVirtualRootUrl } = await resolveData;
117144
const { format } = context;
118145

119146
// Load the file from memory if the URL is based in the virtual root

packages/angular/build/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@
88

99
import { register } from 'node:module';
1010
import { pathToFileURL } from 'node:url';
11-
import { workerData } from 'node:worker_threads';
11+
import { MessageChannel, workerData } from 'node:worker_threads';
12+
import { isLegacyESMLoaderImplementation } from './utils-lts-node';
1213

13-
register('./loader-hooks.js', { parentURL: pathToFileURL(__filename), data: workerData });
14+
if (isLegacyESMLoaderImplementation && workerData) {
15+
/** TODO: Remove when Node.js Removes < 22.2 are no longer supported. */
16+
register('./loader-hooks.js', {
17+
parentURL: pathToFileURL(__filename),
18+
data: workerData,
19+
});
20+
} else {
21+
const { port1, port2 } = new MessageChannel();
22+
23+
process.once('message', (msg) => {
24+
port1.postMessage(msg);
25+
port1.close();
26+
});
27+
28+
register('./loader-hooks.js', {
29+
parentURL: pathToFileURL(__filename),
30+
data: { port: port2 },
31+
transferList: [port2],
32+
});
33+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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 { lt } from 'semver';
10+
11+
/** TODO: Remove when Node.js Removes < 22.2 are no longer supported. */
12+
export const isLegacyESMLoaderImplementation = lt(process.version, '22.2.0');

0 commit comments

Comments
 (0)