Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(@angular/build): address prerendering in-memory ESM resolution in Node.js 22.2.0 and later #27721

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ nodejs_register_toolchains(
name = "node22",
# The below can be removed once @rules_nodejs/nodejs is updated to latest which contains https://github.com/bazelbuild/rules_nodejs/pull/3701
node_repositories = {
"22.0.0-darwin_arm64": ("node-v22.0.0-darwin-arm64.tar.gz", "node-v22.0.0-darwin-arm64", "ea96d349cfaa67aa87ceeaa3e5b52c9167f7ac302fd8d1ff162d0785e9dc0785"),
"22.0.0-darwin_amd64": ("node-v22.0.0-darwin-x64.tar.gz", "node-v22.0.0-darwin-x64", "422a3887ff5418f0a4552d89cf99346ab8ab51bb5d384660baa88b8444d2c111"),
"22.0.0-linux_arm64": ("node-v22.0.0-linux-arm64.tar.xz", "node-v22.0.0-linux-arm64", "83711d29cbe46375bdffab5419f3d831892e24294169272f6c39edc364556241"),
"22.0.0-linux_ppc64le": ("node-v22.0.0-linux-ppc64le.tar.xz", "node-v22.0.0-linux-ppc64le", "2b3fb8707a79243bfb3131312b86716ddc3855bce21bb168095b6b916798e5e9"),
"22.0.0-linux_s390x": ("node-v22.0.0-linux-s390x.tar.xz", "node-v22.0.0-linux-s390x", "89a8efeeb9f94ce9ea251b8109e079c14919f4c0dc2cbc9f545ec47ef0886737"),
"22.0.0-linux_amd64": ("node-v22.0.0-linux-x64.tar.xz", "node-v22.0.0-linux-x64", "9122e50f2642afd5f6078cafd1f52ede60fc464284384f05c18a04d13d07ae5a"),
"22.0.0-windows_amd64": ("node-v22.0.0-win-x64.zip", "node-v22.0.0-win-x64", "32d639b47d4c0a651ff8f8d7d41a454168a3d4045be37985f9a810cf8cef6174"),
"22.2.0-darwin_arm64": ("node-v22.2.0-darwin-arm64.tar.gz", "node-v22.2.0-darwin-arm64", "66dd98bd28d19603f2e5ab0aa0e07b64f8cad28bbc446bb44fb61cc3da62e685"),
"22.2.0-darwin_amd64": ("node-v22.2.0-darwin-x64.tar.gz", "node-v22.2.0-darwin-x64", "b3cd4ab4bb4ac7f9bd5c7603baf6bbdcf466c86bb6ca49abf5e221ab8fad7ceb"),
"22.2.0-linux_arm64": ("node-v22.2.0-linux-arm64.tar.xz", "node-v22.2.0-linux-arm64", "e3d580cb7738dd9a0f8672f684de86b621d8755a6cf349df8c01b8dd875b59ab"),
"22.2.0-linux_ppc64le": ("node-v22.2.0-linux-ppc64le.tar.xz", "node-v22.2.0-linux-ppc64le", "235dc30116f378d1ec326b49ad0ea08c3d84cc057238749e7ada6bb4307b1186"),
"22.2.0-linux_s390x": ("node-v22.2.0-linux-s390x.tar.xz", "node-v22.2.0-linux-s390x", "cb3cce70aeb29072aad450fd0b09130d34a36e38ad689f3bc4a6d72caade281f"),
"22.2.0-linux_amd64": ("node-v22.2.0-linux-x64.tar.xz", "node-v22.2.0-linux-x64", "3544eee9cb1414d6e9003efd56bc807ffb0f4445d2fc383e1df04c3e5e72c91b"),
"22.2.0-windows_amd64": ("node-v22.2.0-win-x64.zip", "node-v22.2.0-win-x64", "f83e956bd90c7f5066a7e96e9372839fcc263795525fa0c03cfdf4b43be9457f"),
},
node_version = "22.0.0",
node_version = "22.2.0",
)

load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install")
Expand Down
4 changes: 4 additions & 0 deletions goldens/circular-deps/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"packages/angular/build/src/tools/esbuild/bundler-execution-result.ts",
"packages/angular/build/src/tools/esbuild/utils.ts"
],
[
"packages/angular/build/src/utils/server-rendering/prerender-child-process.ts",
"packages/angular/build/src/utils/server-rendering/prerender.ts"
],
[
"packages/angular/cli/src/analytics/analytics-collector.ts",
"packages/angular/cli/src/command-builder/command-module.ts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function executePostBundleSteps(

allErrors.push(...errors);
allWarnings.push(...warnings);
prerenderedRoutes.push(...Array.from(generatedRoutes));
prerenderedRoutes.push(...generatedRoutes);

for (const [path, content] of Object.entries(output)) {
additionalHtmlOutputFiles.set(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import assert from 'node:assert';
import { randomUUID } from 'node:crypto';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { MessagePort } from 'node:worker_threads';
import { fileURLToPath } from 'url';
import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer';

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

export interface ESMInMemoryFileLoaderWorkerData {
outputFiles: Record<string, string>;
jsOutputFilesForWorker: Record<string, string>;
workspaceRoot: string;
}

let memoryVirtualRootUrl: string;
let outputFiles: Record<string, string>;
interface ESMInMemoryFileLoaderResolutionData {
memoryVirtualRootUrl: string;
outputFiles: Record<string, string>;
}

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

export function initialize(data: ESMInMemoryFileLoaderWorkerData) {
// This path does not actually exist but is used to overlay the in memory files with the
// actual filesystem for resolution purposes.
// A custom URL schema (such as `memory://`) cannot be used for the resolve output because
// the in-memory files may use `import.meta.url` in ways that assume a file URL.
// `createRequire` is one example of this usage.
memoryVirtualRootUrl = pathToFileURL(
join(data.workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
).href;
outputFiles = data.outputFiles;
let loaderData: Promise<ESMInMemoryFileLoaderResolutionData>;

export function initialize(data: { port: MessagePort } | ESMInMemoryFileLoaderWorkerData) {
loaderData = new Promise<ESMInMemoryFileLoaderResolutionData>((resolve) => {
if (!('port' in data)) {
/** TODO: Remove when Node.js versions < 22.2 are no longer supported. */
resolve({
outputFiles: data.jsOutputFilesForWorker,
memoryVirtualRootUrl: pathToFileURL(
join(data.workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
).href,
});

return;
}

const { port } = data;
port.once(
'message',
({ jsOutputFilesForWorker, workspaceRoot }: ESMInMemoryFileLoaderWorkerData) => {
resolve({
outputFiles: jsOutputFilesForWorker,
memoryVirtualRootUrl: pathToFileURL(
join(workspaceRoot, `.angular/prerender-root/${randomUUID()}/`),
).href,
});

port.close();
},
);
});
}

export function resolve(
export async function resolve(
specifier: string,
context: { parentURL: undefined | string },
nextResolve: Function,
) {
const { outputFiles, memoryVirtualRootUrl } = await loaderData;

// In-memory files loaded from external code will contain a memory scheme
if (specifier.startsWith(MEMORY_URL_SCHEME)) {
let memoryUrl;
Expand Down Expand Up @@ -89,7 +115,7 @@ export function resolve(

if (
specifierUrl?.pathname &&
Object.hasOwn(outputFiles, specifierUrl.href.slice(memoryVirtualRootUrl.length))
outputFiles[specifierUrl.href.slice(memoryVirtualRootUrl.length)] !== undefined
) {
return {
format: 'module',
Expand All @@ -114,6 +140,7 @@ export function resolve(
}

export async function load(url: string, context: { format?: string | null }, nextLoad: Function) {
const { outputFiles, memoryVirtualRootUrl } = await loaderData;
const { format } = context;

// Load the file from memory if the URL is based in the virtual root
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@

import { register } from 'node:module';
import { pathToFileURL } from 'node:url';
import { workerData } from 'node:worker_threads';
import { MessageChannel, workerData } from 'node:worker_threads';
import { isLegacyESMLoaderImplementation } from './utils-lts-node';

register('./loader-hooks.js', { parentURL: pathToFileURL(__filename), data: workerData });
if (isLegacyESMLoaderImplementation && workerData) {
/** TODO: Remove when Node.js versions < 22.2 are no longer supported. */
register('./loader-hooks.js', {
parentURL: pathToFileURL(__filename),
data: workerData,
});
} else {
const { port1, port2 } = new MessageChannel();

process.once('message', (msg) => {
port1.postMessage(msg);
port1.close();
});

register('./loader-hooks.js', {
parentURL: pathToFileURL(__filename),
data: { port: port2 },
transferList: [port2],
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @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 { lt } from 'semver';

/** TODO: Remove when Node.js versions < 22.2 are no longer supported. */
export const isLegacyESMLoaderImplementation = lt(process.version, '22.2.0');
Loading