Skip to content

Commit

Permalink
fix(@angular/build): address prerendering in-memory ESM resolution in…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
alan-agius4 committed May 27, 2024
1 parent 767652b commit ea6e0d6
Show file tree
Hide file tree
Showing 9 changed files with 542 additions and 261 deletions.
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

0 comments on commit ea6e0d6

Please sign in to comment.