Skip to content

Commit

Permalink
refactor(@angular-devkit/build-angular): update ESM in memory file lo…
Browse files Browse the repository at this point in the history
…ader to work with Node.js 20 (#25988)

This commit refactors the ESM Node.js in memory loader to work with Node.js 20+
  • Loading branch information
alan-agius4 committed Oct 9, 2023
1 parent 3ad028b commit 8d0b707
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,20 +22,26 @@ export interface ESMInMemoryFileLoaderWorkerData {
workspaceRoot: string;
}

const { outputFiles, workspaceRoot } = workerData as ESMInMemoryFileLoaderWorkerData;

const TRANSFORMED_FILES: Record<string, string> = {};
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<string, string>;

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.
{ sourcemap: true, jit: true },
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 },
Expand All @@ -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,
);
}

Expand All @@ -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');
}

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
];
}
Original file line number Diff line number Diff line change
@@ -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 });
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 8d0b707

Please sign in to comment.