From 69d157dda9c69cecc5ee35779505f175844a107c Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 9 Oct 2023 11:35:11 +0000 Subject: [PATCH 1/2] refactor(@angular-devkit/build-angular): update ESM in memory file loader to work with Node.js 20 This commit refactors the ESM Node.js in memory loader to work with Node.js 20+ --- .../loader-hooks.ts} | 24 ++++++----- .../esm-in-memory-loader/node-18-utils.ts | 40 +++++++++++++++++++ .../esm-in-memory-loader/register-hooks.ts | 15 +++++++ .../src/utils/server-rendering/prerender.ts | 16 ++------ .../utils/server-rendering/render-worker.ts | 2 +- .../routes-extractor-worker.ts | 2 +- 6 files changed, 76 insertions(+), 23 deletions(-) rename packages/angular_devkit/build_angular/src/utils/server-rendering/{esm-in-memory-file-loader.ts => esm-in-memory-loader/loader-hooks.ts} (82%) create mode 100644 packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/node-18-utils.ts create mode 100644 packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-file-loader.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts similarity index 82% rename from packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-file-loader.ts rename to packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts index 710b9e72e6fe..e3e419fb853f 100644 --- a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-file-loader.ts +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -8,9 +8,9 @@ import { join, relative } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { workerData } from 'node:worker_threads'; import { fileURLToPath } from 'url'; -import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer'; +import { JavaScriptTransformer } from '../../../tools/esbuild/javascript-transformer'; +import { callInitializeIfNeeded } from './node-18-utils'; /** * Node.js ESM loader to redirect imports to in memory files. @@ -22,13 +22,12 @@ export interface ESMInMemoryFileLoaderWorkerData { workspaceRoot: string; } -const { outputFiles, workspaceRoot } = workerData as ESMInMemoryFileLoaderWorkerData; - const TRANSFORMED_FILES: Record = {}; const CHUNKS_REGEXP = /file:\/\/\/(main\.server|chunk-\w+)\.mjs/; -const WORKSPACE_ROOT_FILE = pathToFileURL(join(workspaceRoot, 'index.mjs')).href; +let workspaceRootFile: string; +let outputFiles: Record; -const JAVASCRIPT_TRANSFORMER = new JavaScriptTransformer( +const JavascriptTransformer = new JavaScriptTransformer( // Always enable JIT linking to support applications built with and without AOT. // In a development environment the additional scope information does not // have a negative effect unlike production where final output size is relevant. @@ -36,6 +35,13 @@ const JAVASCRIPT_TRANSFORMER = new JavaScriptTransformer( 1, ); +callInitializeIfNeeded(initialize); + +export function initialize(data: ESMInMemoryFileLoaderWorkerData) { + workspaceRootFile = pathToFileURL(join(data.workspaceRoot, 'index.mjs')).href; + outputFiles = data.outputFiles; +} + export function resolve( specifier: string, context: { parentURL: undefined | string }, @@ -58,7 +64,7 @@ export function resolve( // Node.js default resolve if this is the last user-specified loader. return nextResolve( specifier, - isBundleEntryPointOrChunk(context) ? { ...context, parentURL: WORKSPACE_ROOT_FILE } : context, + isBundleEntryPointOrChunk(context) ? { ...context, parentURL: workspaceRootFile } : context, ); } @@ -70,7 +76,7 @@ export async function load(url: string, context: { format?: string | null }, nex if (source === undefined) { source = TRANSFORMED_FILES[filePath] = Buffer.from( - await JAVASCRIPT_TRANSFORMER.transformFile(filePath), + await JavascriptTransformer.transformFile(filePath), ).toString('utf-8'); } @@ -94,7 +100,7 @@ function isFileProtocol(url: string): boolean { } function handleProcessExit(): void { - void JAVASCRIPT_TRANSFORMER.close(); + void JavascriptTransformer.close(); } function isBundleEntryPointOrChunk(context: { parentURL: undefined | string }): boolean { diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/node-18-utils.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/node-18-utils.ts new file mode 100644 index 000000000000..344248434c2b --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/node-18-utils.ts @@ -0,0 +1,40 @@ +/** + * @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 { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { workerData } from 'node:worker_threads'; + +let IS_NODE_18: boolean | undefined; +function isNode18(): boolean { + return (IS_NODE_18 ??= process.versions.node.startsWith('18.')); +} + +/** Call the initialize hook when running on Node.js 18 */ +export function callInitializeIfNeeded( + initialize: (typeof import('./loader-hooks'))['initialize'], +): void { + if (isNode18()) { + initialize(workerData); + } +} + +export function getESMLoaderArgs(): string[] { + if (isNode18()) { + return [ + '--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`. + '--loader', + pathToFileURL(join(__dirname, 'loader-hooks.js')).href, // Loader cannot be an absolute path on Windows. + ]; + } + + return [ + '--import', + pathToFileURL(join(__dirname, 'register-hooks.js')).href, // Loader cannot be an absolute path on Windows. + ]; +} diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts new file mode 100644 index 000000000000..5180617629a2 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/register-hooks.ts @@ -0,0 +1,15 @@ +/** + * @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 + */ + +// TODO: remove the below once @types/node are version 20.x.x +// @ts-expect-error "node:module"' has no exported member 'register'.ts(2305) +import { register } from 'node:module'; +import { pathToFileURL } from 'node:url'; +import { workerData } from 'node:worker_threads'; + +register('./loader-hooks.js', pathToFileURL(__filename), { data: workerData }); diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts index d351475172e6..66c052c34a55 100644 --- a/packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/prerender.ts @@ -7,10 +7,10 @@ */ import { readFile } from 'node:fs/promises'; -import { extname, join, posix } from 'node:path'; -import { pathToFileURL } from 'node:url'; +import { extname, posix } from 'node:path'; import Piscina from 'piscina'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { getESMLoaderArgs } from './esm-in-memory-loader/node-18-utils'; import type { RenderResult, ServerContext } from './render-page'; import type { RenderWorkerData } from './render-worker'; import type { @@ -85,11 +85,7 @@ export async function prerenderPages( inlineCriticalCss, document, } as RenderWorkerData, - execArgv: [ - '--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`. - '--loader', - pathToFileURL(join(__dirname, 'esm-in-memory-file-loader.js')).href, // Loader cannot be an absolute path on Windows. - ], + execArgv: getESMLoaderArgs(), }); try { @@ -173,11 +169,7 @@ async function getAllRoutes( document, verbose, } as RoutesExtractorWorkerData, - execArgv: [ - '--no-warnings', // Suppress `ExperimentalWarning: Custom ESM Loaders is an experimental feature...`. - '--loader', - pathToFileURL(join(__dirname, 'esm-in-memory-file-loader.js')).href, // Loader cannot be an absolute path on Windows. - ], + execArgv: getESMLoaderArgs(), }); const { routes: extractedRoutes, warnings }: RoutersExtractorWorkerResult = await renderWorker diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/render-worker.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/render-worker.ts index e75e3c6fa968..6a1449bcca22 100644 --- a/packages/angular_devkit/build_angular/src/utils/server-rendering/render-worker.ts +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/render-worker.ts @@ -7,7 +7,7 @@ */ import { workerData } from 'node:worker_threads'; -import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-file-loader'; +import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks'; import { RenderResult, ServerContext, renderPage } from './render-page'; export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData { diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/routes-extractor-worker.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/routes-extractor-worker.ts index f995413e74c9..1b4c71bd33b1 100644 --- a/packages/angular_devkit/build_angular/src/utils/server-rendering/routes-extractor-worker.ts +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/routes-extractor-worker.ts @@ -8,7 +8,7 @@ import { workerData } from 'node:worker_threads'; import { loadEsmModule } from '../load-esm'; -import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-file-loader'; +import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks'; import { MainServerBundleExports } from './main-bundle-exports'; export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData { From 7492bb479bedf45c233120f458db75d4f783ed0b Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 9 Oct 2023 19:38:33 +0200 Subject: [PATCH 2/2] fixup! refactor(@angular-devkit/build-angular): update ESM in memory file loader to work with Node.js 20 --- .../server-rendering/esm-in-memory-loader/loader-hooks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts index e3e419fb853f..6ab02473781a 100644 --- a/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts +++ b/packages/angular_devkit/build_angular/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.ts @@ -27,7 +27,7 @@ const CHUNKS_REGEXP = /file:\/\/\/(main\.server|chunk-\w+)\.mjs/; let workspaceRootFile: string; let outputFiles: Record; -const JavascriptTransformer = new JavaScriptTransformer( +const javascriptTransformer = new JavaScriptTransformer( // Always enable JIT linking to support applications built with and without AOT. // In a development environment the additional scope information does not // have a negative effect unlike production where final output size is relevant. @@ -76,7 +76,7 @@ export async function load(url: string, context: { format?: string | null }, nex if (source === undefined) { source = TRANSFORMED_FILES[filePath] = Buffer.from( - await JavascriptTransformer.transformFile(filePath), + await javascriptTransformer.transformFile(filePath), ).toString('utf-8'); } @@ -100,7 +100,7 @@ function isFileProtocol(url: string): boolean { } function handleProcessExit(): void { - void JavascriptTransformer.close(); + void javascriptTransformer.close(); } function isBundleEntryPointOrChunk(context: { parentURL: undefined | string }): boolean {