Skip to content

Commit a559747

Browse files
aduh95RafaelGSS
authored andcommitted
esm: add back globalPreload tests and fix failing ones
PR-URL: #48779 Fixes: #48778 Fixes: #48516 Refs: #46402 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent 84c0c68 commit a559747

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
@@ -24,7 +24,7 @@ markBootstrapComplete();
2424

2525
const source = getOptionValue('--eval');
2626
const print = getOptionValue('--print');
27-
const loadESM = getOptionValue('--import').length > 0;
27+
const loadESM = getOptionValue('--import').length > 0 || getOptionValue('--experimental-loader').length > 0;
2828
if (getOptionValue('--input-type') === 'module')
2929
evalModule(source, print);
3030
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 { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors');
@@ -521,14 +523,14 @@ class HooksProxy {
521523
this.#worker.on('exit', process.exit);
522524
}
523525

524-
#waitForWorker() {
526+
waitForWorker() {
525527
if (!this.#isReady) {
526528
const { kIsOnline } = require('internal/worker');
527529
if (!this.#worker[kIsOnline]) {
528530
debug('wait for signal from worker');
529531
AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0);
530532
const response = this.#worker.receiveMessageSync();
531-
if (response.message.status === 'exit') { return; }
533+
if (response == null || response.message.status === 'exit') { return; }
532534
const { preloadScripts } = this.#unwrapMessage(response);
533535
this.#executePreloadScripts(preloadScripts);
534536
}
@@ -538,7 +540,7 @@ class HooksProxy {
538540
}
539541

540542
async makeAsyncRequest(method, ...args) {
541-
this.#waitForWorker();
543+
this.waitForWorker();
542544

543545
MessageChannel ??= require('internal/worker/io').MessageChannel;
544546
const asyncCommChannel = new MessageChannel();
@@ -578,7 +580,7 @@ class HooksProxy {
578580
}
579581

580582
makeSyncRequest(method, ...args) {
581-
this.#waitForWorker();
583+
this.waitForWorker();
582584

583585
// Pass work to the worker.
584586
debug('post sync message to worker', { method, args });
@@ -620,35 +622,66 @@ class HooksProxy {
620622
}
621623
}
622624

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

lib/internal/modules/esm/loader.js

+13
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ class DefaultModuleLoader {
277277
meta = importMetaInitializer(meta, context, this);
278278
return meta;
279279
}
280+
281+
/**
282+
* No-op when no hooks have been supplied.
283+
*/
284+
forceLoadHooks() {}
280285
}
281286
ObjectSetPrototypeOf(DefaultModuleLoader.prototype, null);
282287

@@ -349,6 +354,14 @@ class CustomizedModuleLoader extends DefaultModuleLoader {
349354

350355
return result;
351356
}
357+
358+
importMetaInitialize(meta, context) {
359+
hooksProxy.importMetaInitialize(meta, context, this);
360+
}
361+
362+
forceLoadHooks() {
363+
hooksProxy.waitForWorker();
364+
}
352365
}
353366

354367

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

@@ -422,18 +423,119 @@ describe('Loader hooks', { concurrency: true }, () => {
422423
});
423424
});
424425

425-
it('should handle globalPreload returning undefined', async () => {
426-
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
427-
'--no-warnings',
428-
'--experimental-loader',
429-
'data:text/javascript,export function globalPreload(){}',
430-
fixtures.path('empty.js'),
431-
]);
426+
describe('globalPreload', () => {
427+
it('should handle globalPreload returning undefined', async () => {
428+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
429+
'--no-warnings',
430+
'--experimental-loader',
431+
'data:text/javascript,export function globalPreload(){}',
432+
fixtures.path('empty.js'),
433+
]);
432434

433-
assert.strictEqual(stderr, '');
434-
assert.strictEqual(stdout, '');
435-
assert.strictEqual(code, 0);
436-
assert.strictEqual(signal, null);
435+
assert.strictEqual(stderr, '');
436+
assert.strictEqual(stdout, '');
437+
assert.strictEqual(code, 0);
438+
assert.strictEqual(signal, null);
439+
});
440+
441+
it('should handle loading node:test', async () => {
442+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
443+
'--no-warnings',
444+
'--experimental-loader',
445+
'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}',
446+
fixtures.path('empty.js'),
447+
]);
448+
449+
assert.strictEqual(stderr, '');
450+
assert.match(stdout, /\n# pass 1\r?\n/);
451+
assert.strictEqual(code, 0);
452+
assert.strictEqual(signal, null);
453+
});
454+
455+
it('should handle loading node:os with node: prefix', async () => {
456+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
457+
'--no-warnings',
458+
'--experimental-loader',
459+
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}',
460+
fixtures.path('empty.js'),
461+
]);
462+
463+
assert.strictEqual(stderr, '');
464+
assert.strictEqual(stdout.trim(), os.arch());
465+
assert.strictEqual(code, 0);
466+
assert.strictEqual(signal, null);
467+
});
468+
469+
// `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter).
470+
it('should handle loading builtin module without node: prefix', async () => {
471+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
472+
'--no-warnings',
473+
'--experimental-loader',
474+
'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}',
475+
fixtures.path('empty.js'),
476+
]);
477+
478+
assert.strictEqual(stderr, '');
479+
assert.strictEqual(stdout.trim(), os.arch());
480+
assert.strictEqual(code, 0);
481+
assert.strictEqual(signal, null);
482+
});
483+
484+
it('should throw when loading node:test without node: prefix', async () => {
485+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
486+
'--no-warnings',
487+
'--experimental-loader',
488+
'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}',
489+
fixtures.path('empty.js'),
490+
]);
491+
492+
assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/);
493+
assert.strictEqual(stdout, '');
494+
assert.strictEqual(code, 1);
495+
assert.strictEqual(signal, null);
496+
});
497+
498+
it('should register globals set from globalPreload', async () => {
499+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
500+
'--no-warnings',
501+
'--experimental-loader',
502+
'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}',
503+
'--print', 'myGlobal',
504+
]);
505+
506+
assert.strictEqual(stderr, '');
507+
assert.strictEqual(stdout.trim(), '4');
508+
assert.strictEqual(code, 0);
509+
assert.strictEqual(signal, null);
510+
});
511+
512+
it('should log console.log calls returned from globalPreload', async () => {
513+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
514+
'--no-warnings',
515+
'--experimental-loader',
516+
'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}',
517+
fixtures.path('empty.js'),
518+
]);
519+
520+
assert.strictEqual(stderr, '');
521+
assert.strictEqual(stdout.trim(), 'Hello from globalPreload');
522+
assert.strictEqual(code, 0);
523+
assert.strictEqual(signal, null);
524+
});
525+
526+
it('should crash if globalPreload returns code that throws', async () => {
527+
const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [
528+
'--no-warnings',
529+
'--experimental-loader',
530+
'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}',
531+
fixtures.path('empty.js'),
532+
]);
533+
534+
assert.match(stderr, /error from globalPreload/);
535+
assert.strictEqual(stdout, '');
536+
assert.strictEqual(code, 1);
537+
assert.strictEqual(signal, null);
538+
});
437539
});
438540

439541
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)