Skip to content

Commit b640b32

Browse files
committed
fix(@angular/build): use module.registerHooks() for in-memory ESM loader
Node.js 26 runtime-deprecates `module.register()` (`DEP0205`) in favor of synchronous `module.registerHooks()`. Update the server-rendering in-memory ESM loader hooks to make `load` synchronous using `readFileSync` and register the hooks directly via `registerHooks({ resolve, load })`. Closes #34358
1 parent 2b5a9a6 commit b640b32

3 files changed

Lines changed: 56 additions & 26 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import assert from 'node:assert';
1010
import { randomUUID } from 'node:crypto';
11-
import { readFile } from 'node:fs/promises';
11+
import { readFileSync } from 'node:fs';
1212
import { join } from 'node:path';
1313
import { fileURLToPath, pathToFileURL } from 'node:url';
1414

@@ -110,7 +110,7 @@ export function resolve(
110110
});
111111
}
112112

113-
export async function load(url: string, context: { format?: string | null }, nextLoad: Function) {
113+
export function load(url: string, context: { format?: string | null }, nextLoad: Function) {
114114
const { format } = context;
115115

116116
// Load the file from memory if the URL is based in the virtual root
@@ -137,7 +137,7 @@ export async function load(url: string, context: { format?: string | null }, nex
137137
}
138138

139139
const filePath = fileURLToPath(url);
140-
const fileBytes = await readFile(filePath);
140+
const fileBytes = readFileSync(filePath);
141141
const source = Buffer.concat([NG_SERVER_MODE_INIT_BYTES, fileBytes]);
142142

143143
return {

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

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import assert from 'node:assert';
10+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
11+
import { join } from 'node:path';
12+
import { pathToFileURL } from 'node:url';
913
import { initialize, load, resolve } from './loader-hooks';
1014
import { createSharedServerFiles } from './utils';
1115

@@ -124,46 +128,46 @@ describe('esm-in-memory-loader loader-hooks', () => {
124128
});
125129

126130
describe('load', () => {
127-
it('should load in-memory file source from SharedArrayBuffer backed Uint8Array', async () => {
131+
it('should load in-memory file source from SharedArrayBuffer backed Uint8Array', () => {
128132
const memoryUrl = new URL('./main.server.mjs', 'memory://').href;
129133
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
130134
const nextLoad = jasmine.createSpy('nextLoad');
131135

132-
const result = await load(rootResolve.url, { format: 'module' }, nextLoad);
136+
const result = load(rootResolve.url, { format: 'module' }, nextLoad);
133137

134138
expect(nextLoad).not.toHaveBeenCalled();
135139
expect(result.format).toBe('module');
136140
expect(result.shortCircuit).toBeTrue();
137141
expect(result.source).toBe('export const main = true;');
138142
});
139143

140-
it('should load in-memory file with multi-byte UTF-8 characters', async () => {
144+
it('should load in-memory file with multi-byte UTF-8 characters', () => {
141145
const memoryUrl = new URL('./utf8.mjs', 'memory://').href;
142146
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
143147
const nextLoad = jasmine.createSpy('nextLoad');
144148

145-
const result = await load(rootResolve.url, { format: 'module' }, nextLoad);
149+
const result = load(rootResolve.url, { format: 'module' }, nextLoad);
146150

147151
expect(nextLoad).not.toHaveBeenCalled();
148152
expect(result.format).toBe('module');
149153
expect(result.shortCircuit).toBeTrue();
150154
expect(result.source).toBe('export const text = "🔥 UTF-8 🚀";');
151155
});
152156

153-
it('should load in-memory file with empty content', async () => {
157+
it('should load in-memory file with empty content', () => {
154158
const memoryUrl = new URL('./empty.mjs', 'memory://').href;
155159
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
156160
const nextLoad = jasmine.createSpy('nextLoad');
157161

158-
const result = await load(rootResolve.url, { format: 'module' }, nextLoad);
162+
const result = load(rootResolve.url, { format: 'module' }, nextLoad);
159163

160164
expect(nextLoad).not.toHaveBeenCalled();
161165
expect(result.format).toBe('module');
162166
expect(result.shortCircuit).toBeTrue();
163167
expect(result.source).toBe('');
164168
});
165169

166-
it('should load in-memory file source with non-zero byteOffset in Uint8Array', async () => {
170+
it('should load in-memory file source with non-zero byteOffset in Uint8Array', () => {
167171
const target = 'export const sliced = 42;';
168172
const fullBuffer = Buffer.from(`__PADDING__${target}__MORE__`);
169173
const offset = Buffer.byteLength('__PADDING__', 'utf-8');
@@ -181,15 +185,15 @@ describe('esm-in-memory-loader loader-hooks', () => {
181185
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
182186
const nextLoad = jasmine.createSpy('nextLoad');
183187

184-
const result = await load(rootResolve.url, { format: 'module' }, nextLoad);
188+
const result = load(rootResolve.url, { format: 'module' }, nextLoad);
185189

186190
expect(nextLoad).not.toHaveBeenCalled();
187191
expect(result.format).toBe('module');
188192
expect(result.shortCircuit).toBeTrue();
189193
expect(result.source).toBe(target);
190194
});
191195

192-
it('should load in-memory file source when outputFiles contain string values', async () => {
196+
it('should load in-memory file source when outputFiles contain string values', () => {
193197
initialize({
194198
workspaceRoot,
195199
outputFiles: {
@@ -201,29 +205,54 @@ describe('esm-in-memory-loader loader-hooks', () => {
201205
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
202206
const nextLoad = jasmine.createSpy('nextLoad');
203207

204-
const result = await load(rootResolve.url, { format: 'module' }, nextLoad);
208+
const result = load(rootResolve.url, { format: 'module' }, nextLoad);
205209

206210
expect(nextLoad).not.toHaveBeenCalled();
207211
expect(result.format).toBe('module');
208212
expect(result.shortCircuit).toBeTrue();
209213
expect(result.source).toBe('export const fromString = 1;');
210214
});
211215

212-
it('should reject when in-memory file does not exist in outputFiles', async () => {
216+
it('should throw when in-memory file does not exist in outputFiles', () => {
213217
const memoryUrl = new URL('./main.server.mjs', 'memory://').href;
214218
const rootResolve = resolve(memoryUrl, { parentURL: undefined }, () => {});
215219
const nonExistentVirtualUrl = rootResolve.url.replace('main.server.mjs', 'missing.mjs');
216220
const nextLoad = jasmine.createSpy('nextLoad');
217221

218-
await expectAsync(
219-
load(nonExistentVirtualUrl, { format: 'module' }, nextLoad),
220-
).toBeRejectedWithError(/Resolved in-memory ESM file should always exist/);
222+
expect(() => load(nonExistentVirtualUrl, { format: 'module' }, nextLoad)).toThrowError(
223+
/Resolved in-memory ESM file should always exist/,
224+
);
221225
expect(nextLoad).not.toHaveBeenCalled();
222226
});
223227

224-
it('should delegate to nextLoad for non-angular file URLs', async () => {
225-
const nextLoad = jasmine.createSpy('nextLoad').and.resolveTo({ format: 'module' });
226-
const result = await load(
228+
it('should prepend ngServerMode initialization for @angular/ file URLs', async () => {
229+
const baseTmpDir = process.env['TEST_TMPDIR'];
230+
assert(baseTmpDir, 'TEST_TMPDIR is not set');
231+
const tmpDir = await mkdtemp(join(baseTmpDir, 'loader-hooks-test-'));
232+
233+
try {
234+
const angularDir = join(tmpDir, '@angular/core');
235+
await mkdir(angularDir, { recursive: true });
236+
const filePath = join(angularDir, 'index.mjs');
237+
await writeFile(filePath, 'export const VERSION = "1.0.0";');
238+
239+
const nextLoad = jasmine.createSpy('nextLoad');
240+
const result = load(pathToFileURL(filePath).href, { format: 'module' }, nextLoad);
241+
242+
expect(nextLoad).not.toHaveBeenCalled();
243+
expect(result.format).toBe('module');
244+
expect(result.shortCircuit).toBeTrue();
245+
expect(new TextDecoder().decode(result.source)).toBe(
246+
'var ngServerMode=true;export const VERSION = "1.0.0";',
247+
);
248+
} finally {
249+
await rm(tmpDir, { recursive: true, force: true });
250+
}
251+
});
252+
253+
it('should delegate to nextLoad for non-angular file URLs', () => {
254+
const nextLoad = jasmine.createSpy('nextLoad').and.returnValue({ format: 'module' });
255+
const result = load(
227256
'file:///workspace/node_modules/rxjs/index.js',
228257
{ format: 'module' },
229258
nextLoad,
@@ -235,9 +264,9 @@ describe('esm-in-memory-loader loader-hooks', () => {
235264
expect(result.format).toBe('module');
236265
});
237266

238-
it('should delegate to nextLoad for non-memory non-file URLs', async () => {
239-
const nextLoad = jasmine.createSpy('nextLoad').and.resolveTo({ format: 'builtin' });
240-
const result = await load('node:fs', { format: 'builtin' }, nextLoad);
267+
it('should delegate to nextLoad for non-memory non-file URLs', () => {
268+
const nextLoad = jasmine.createSpy('nextLoad').and.returnValue({ format: 'builtin' });
269+
const result = load('node:fs', { format: 'builtin' }, nextLoad);
241270

242271
expect(nextLoad).toHaveBeenCalledWith('node:fs');
243272
expect(result.format).toBe('builtin');

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import { register } from 'node:module';
10-
import { pathToFileURL } from 'node:url';
9+
import { registerHooks } from 'node:module';
1110
import { workerData } from 'node:worker_threads';
11+
import { initialize, load, resolve } from './loader-hooks';
1212

13-
register('./loader-hooks.js', { parentURL: pathToFileURL(__filename), data: workerData });
13+
initialize(workerData);
14+
registerHooks({ resolve, load });

0 commit comments

Comments
 (0)