Skip to content

Commit 63aa5d7

Browse files
aduh95targos
authored andcommitted
esm: add back globalPreload tests and fix failing ones
PR-URL: #48779 Backport-PR-URL: #50669 Fixes: #48778 Fixes: #48516 Refs: #46402 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent 65dfe85 commit 63aa5d7

File tree

7 files changed

+475
-31
lines changed

7 files changed

+475
-31
lines changed

lib/internal/main/eval_string.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ markBootstrapComplete();
2222

2323
const source = getOptionValue('--eval');
2424
const print = getOptionValue('--print');
25-
const loadESM = getOptionValue('--import').length > 0;
25+
const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
2626
if (getOptionValue('--input-type') === 'module')
2727
evalModule(source, print);
2828
else

lib/internal/modules/esm/hooks.js

+52-19
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
Promise,
1111
SafeSet,
1212
StringPrototypeSlice,
13+
StringPrototypeStartsWith,
1314
StringPrototypeToUpperCase,
1415
globalThis,
1516
} = primordials;
@@ -30,6 +31,7 @@ const {
3031
ERR_INVALID_RETURN_PROPERTY_VALUE,
3132
ERR_INVALID_RETURN_VALUE,
3233
ERR_LOADER_CHAIN_INCOMPLETE,
34+
ERR_UNKNOWN_BUILTIN_MODULE,
3335
ERR_WORKER_UNSERIALIZABLE_ERROR,
3436
} = require('internal/errors').codes;
3537
const { URL } = require('internal/url');
@@ -520,14 +522,14 @@ class HooksProxy {
520522
this.#worker.on('exit', process.exit);
521523
}
522524

523-
#waitForWorker() {
525+
waitForWorker() {
524526
if (!this.#isReady) {
525527
const { kIsOnline } = require('internal/worker');
526528
if (!this.#worker[kIsOnline]) {
527529
debug('wait for signal from worker');
528530
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
529531
const response = this.#worker.receiveMessageSync();
530-
if (response.message.status === 'exit') { return; }
532+
if (response == null || response.message.status === 'exit') { return; }
531533
const { preloadScripts } = this.#unwrapMessage(response);
532534
this.#executePreloadScripts(preloadScripts);
533535
}
@@ -537,7 +539,7 @@ class HooksProxy {
537539
}
538540

539541
async makeAsyncRequest(method, ...args) {
540-
this.#waitForWorker();
542+
this.waitForWorker();
541543

542544
MessageChannel ??= require('internal/worker/io').MessageChannel;
543545
const asyncCommChannel = new MessageChannel();
@@ -577,7 +579,7 @@ class HooksProxy {
577579
}
578580

579581
makeSyncRequest(method, ...args) {
580-
this.#waitForWorker();
582+
this.waitForWorker();
581583

582584
// Pass work to the worker.
583585
debug('post sync message to worker', { method, args });
@@ -619,35 +621,66 @@ class HooksProxy {
619621
}
620622
}
621623

624+
#importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
625+
626+
importMetaInitialize(meta, context, loader) {
627+
this.#importMetaInitializer(meta, context, loader);
628+
}
629+
622630
#executePreloadScripts(preloadScripts) {
623631
for (let i = 0; i < preloadScripts.length; i++) {
624632
const { code, port } = preloadScripts[i];
625633
const { compileFunction } = require('vm');
626634
const preloadInit = compileFunction(
627635
code,
628-
['getBuiltin', 'port'],
636+
['getBuiltin', 'port', 'setImportMetaCallback'],
629637
{
630638
filename: '<preload>',
631639
},
632640
);
641+
let finished = false;
642+
let replacedImportMetaInitializer = false;
643+
let next = this.#importMetaInitializer;
633644
const { BuiltinModule } = require('internal/bootstrap/realm');
634645
// Calls the compiled preload source text gotten from the hook
635646
// Since the parameters are named we use positional parameters
636647
// see compileFunction above to cross reference the names
637-
FunctionPrototypeCall(
638-
preloadInit,
639-
globalThis,
640-
// Param getBuiltin
641-
(builtinName) => {
642-
if (BuiltinModule.canBeRequiredByUsers(builtinName) &&
643-
BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
644-
return require(builtinName);
645-
}
646-
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
647-
},
648-
// Param port
649-
port,
650-
);
648+
try {
649+
FunctionPrototypeCall(
650+
preloadInit,
651+
globalThis,
652+
// Param getBuiltin
653+
(builtinName) => {
654+
if (StringPrototypeStartsWith(builtinName, 'node:')) {
655+
builtinName = StringPrototypeSlice(builtinName, 5);
656+
} else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) {
657+
throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
658+
}
659+
if (BuiltinModule.canBeRequiredByUsers(builtinName)) {
660+
return require(builtinName);
661+
}
662+
throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName);
663+
},
664+
// Param port
665+
port,
666+
// setImportMetaCallback
667+
(fn) => {
668+
if (finished || typeof fn !== 'function') {
669+
throw new ERR_INVALID_ARG_TYPE('fn', fn);
670+
}
671+
replacedImportMetaInitializer = true;
672+
const parent = next;
673+
next = (meta, context) => {
674+
return fn(meta, context, parent);
675+
};
676+
},
677+
);
678+
} finally {
679+
finished = true;
680+
if (replacedImportMetaInitializer) {
681+
this.#importMetaInitializer = next;
682+
}
683+
}
651684
}
652685
}
653686
}

lib/internal/modules/esm/loader.js

+13
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ class DefaultModuleLoader {
287287
meta = importMetaInitializer(meta, context, this);
288288
return meta;
289289
}
290+
291+
/**
292+
* No-op when no hooks have been supplied.
293+
*/
294+
forceLoadHooks() {}
290295
}
291296
ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null);
292297

@@ -359,6 +364,14 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
359364

360365
return result;
361366
}
367+
368+
importMetaInitialize(meta, context) {
369+
hooksProxy.importMetaInitialize(meta, context, this);
370+
}
371+
372+
forceLoadHooks() {
373+
hooksProxy.waitForWorker();
374+
}
362375
}
363376

364377

lib/internal/process/esm_loader.js

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ module.exports = {
3636
parentURL,
3737
kEmptyObject,
3838
));
39+
} else {
40+
esmLoader.forceLoadHooks();
3941
}
4042
await callback(esmLoader);
4143
} catch (err) {

test/es-module/test-esm-loader-hooks.mjs

+113-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { spawnPromisified } from '../common/index.mjs';
22
import * as fixtures from '../common/fixtures.mjs';
33
import assert from 'node:assert';
4+
import os from 'node:os';
45
import { execPath } from 'node:process';
56
import { describe, it } from 'node:test';
67

@@ -370,18 +371,119 @@ describe('Loader hooks', { concurrency: true }, () => {
370371
});
371372
});
372373

373-
it('should handle globalPreload returning undefined', async () => {
374-
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
375-
'--no-warnings',
376-
'--experimental-loader',
377-
'data:text/javascript,export function globalPreload(){}',
378-
fixtures.path('empty.js'),
379-
]);
374+
describe('globalPreload', () => {
375+
it('should handle globalPreload returning undefined', async () => {
376+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
377+
'--no-warnings',
378+
'--experimental-loader',
379+
'data:text/javascript,export function globalPreload(){}',
380+
fixtures.path('empty.js'),
381+
]);
380382

381-
assert.strictEqual(stderr, '');
382-
assert.strictEqual(stdout, '');
383-
assert.strictEqual(code, 0);
384-
assert.strictEqual(signal, null);
383+
assert.strictEqual(stderr, '');
384+
assert.strictEqual(stdout, '');
385+
assert.strictEqual(code, 0);
386+
assert.strictEqual(signal, null);
387+
});
388+
389+
it('should handle loading node:test', async () => {
390+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
391+
'--no-warnings',
392+
'--experimental-loader',
393+
'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}',
394+
fixtures.path('empty.js'),
395+
]);
396+
397+
assert.strictEqual(stderr, '');
398+
assert.match(stdout, /\n# pass 1\r?\n/);
399+
assert.strictEqual(code, 0);
400+
assert.strictEqual(signal, null);
401+
});
402+
403+
it('should handle loading node:os with node: prefix', async () => {
404+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
405+
'--no-warnings',
406+
'--experimental-loader',
407+
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}',
408+
fixtures.path('empty.js'),
409+
]);
410+
411+
assert.strictEqual(stderr, '');
412+
assert.strictEqual(stdout.trim(), os.arch());
413+
assert.strictEqual(code, 0);
414+
assert.strictEqual(signal, null);
415+
});
416+
417+
// `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter).
418+
it('should handle loading builtin module without node: prefix', async () => {
419+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
420+
'--no-warnings',
421+
'--experimental-loader',
422+
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}',
423+
fixtures.path('empty.js'),
424+
]);
425+
426+
assert.strictEqual(stderr, '');
427+
assert.strictEqual(stdout.trim(), os.arch());
428+
assert.strictEqual(code, 0);
429+
assert.strictEqual(signal, null);
430+
});
431+
432+
it('should throw when loading node:test without node: prefix', async () => {
433+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
434+
'--no-warnings',
435+
'--experimental-loader',
436+
'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}',
437+
fixtures.path('empty.js'),
438+
]);
439+
440+
assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/);
441+
assert.strictEqual(stdout, '');
442+
assert.strictEqual(code, 1);
443+
assert.strictEqual(signal, null);
444+
});
445+
446+
it('should register globals set from globalPreload', async () => {
447+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
448+
'--no-warnings',
449+
'--experimental-loader',
450+
'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}',
451+
'--print', 'myGlobal',
452+
]);
453+
454+
assert.strictEqual(stderr, '');
455+
assert.strictEqual(stdout.trim(), '4');
456+
assert.strictEqual(code, 0);
457+
assert.strictEqual(signal, null);
458+
});
459+
460+
it('should log console.log calls returned from globalPreload', async () => {
461+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
462+
'--no-warnings',
463+
'--experimental-loader',
464+
'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}',
465+
fixtures.path('empty.js'),
466+
]);
467+
468+
assert.strictEqual(stderr, '');
469+
assert.strictEqual(stdout.trim(), 'Hello from globalPreload');
470+
assert.strictEqual(code, 0);
471+
assert.strictEqual(signal, null);
472+
});
473+
474+
it('should crash if globalPreload returns code that throws', async () => {
475+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
476+
'--no-warnings',
477+
'--experimental-loader',
478+
'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}',
479+
fixtures.path('empty.js'),
480+
]);
481+
482+
assert.match(stderr, /error from globalPreload/);
483+
assert.strictEqual(stdout, '');
484+
assert.strictEqual(code, 1);
485+
assert.strictEqual(signal, null);
486+
});
385487
});
386488

387489
it('should be fine to call `process.removeAllListeners("beforeExit")` from the main thread', async () => {
+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs
2+
import '../common/index.mjs';
3+
import assert from 'assert/strict';
4+
5+
// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
6+
import mock from 'node:mock';
7+
8+
mock('node:events', {
9+
EventEmitter: 'This is mocked!'
10+
});
11+
12+
// This resolves to node:events
13+
// It is intercepted by mock-loader and doesn't return the normal value
14+
assert.deepStrictEqual(await import('events'), Object.defineProperty({
15+
__proto__: null,
16+
EventEmitter: 'This is mocked!'
17+
}, Symbol.toStringTag, {
18+
enumerable: false,
19+
value: 'Module'
20+
}));
21+
22+
const mutator = mock('node:events', {
23+
EventEmitter: 'This is mocked v2!'
24+
});
25+
26+
// It is intercepted by mock-loader and doesn't return the normal value.
27+
// This is resolved separately from the import above since the specifiers
28+
// are different.
29+
const mockedV2 = await import('node:events');
30+
assert.deepStrictEqual(mockedV2, Object.defineProperty({
31+
__proto__: null,
32+
EventEmitter: 'This is mocked v2!'
33+
}, Symbol.toStringTag, {
34+
enumerable: false,
35+
value: 'Module'
36+
}));
37+
38+
mutator.EventEmitter = 'This is mocked v3!';
39+
assert.deepStrictEqual(mockedV2, Object.defineProperty({
40+
__proto__: null,
41+
EventEmitter: 'This is mocked v3!'
42+
}, Symbol.toStringTag, {
43+
enumerable: false,
44+
value: 'Module'
45+
}));

0 commit comments

Comments
 (0)