Skip to content

Commit a559747

Browse files
aduh95RafaelGSS
authored andcommitted
esm: add back globalPreload tests and fix failing ones
PR-URL: #48779Fixes: #48778Fixes: #48516 Refs: #46402 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Jacob Smith <[email protected]>
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‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ markBootstrapComplete();
2424

2525
constsource=getOptionValue('--eval');
2626
constprint=getOptionValue('--print');
27-
constloadESM=getOptionValue('--import').length>0;
27+
constloadESM=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‎

Lines changed: 52 additions & 19 deletions
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
constresponse=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
asyncmakeAsyncRequest(method, ...args){
541-
this.#waitForWorker();
543+
this.waitForWorker();
542544

543545
MessageChannel??=require('internal/worker/io').MessageChannel;
544546
constasyncCommChannel=newMessageChannel();
@@ -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(leti=0;i<preloadScripts.length;i++){
625633
const{ code, port }=preloadScripts[i];
626634
const{ compileFunction }=require('vm');
627635
constpreloadInit=compileFunction(
628636
code,
629-
['getBuiltin','port'],
637+
['getBuiltin','port','setImportMetaCallback'],
630638
{
631639
filename: '<preload>',
632640
},
633641
);
642+
letfinished=false;
643+
letreplacedImportMetaInitializer=false;
644+
letnext=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-
returnrequire(builtinName);
646-
}
647-
thrownewERR_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+
}elseif(!BuiltinModule.canBeRequiredWithoutScheme(builtinName)){
658+
thrownewERR_UNKNOWN_BUILTIN_MODULE(builtinName);
659+
}
660+
if(BuiltinModule.canBeRequiredByUsers(builtinName)){
661+
returnrequire(builtinName);
662+
}
663+
thrownewERR_UNKNOWN_BUILTIN_MODULE(builtinName);
664+
},
665+
// Param port
666+
port,
667+
// setImportMetaCallback
668+
(fn)=>{
669+
if(finished||typeoffn!=='function'){
670+
thrownewERR_INVALID_ARG_TYPE('fn',fn);
671+
}
672+
replacedImportMetaInitializer=true;
673+
constparent=next;
674+
next=(meta,context)=>{
675+
returnfn(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‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ class DefaultModuleLoader{
277277
meta=importMetaInitializer(meta,context,this);
278278
returnmeta;
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
returnresult;
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‎

Lines changed: 2 additions & 0 deletions
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
awaitcallback(esmLoader);
4143
}catch(err){

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

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import{spawnPromisified}from'../common/index.mjs';
22
import*asfixturesfrom'../common/fixtures.mjs';
33
importassertfrom'node:assert';
4+
importosfrom'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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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#pass1\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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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 }=awaitspawnPromisified(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,/errorfromglobalPreload/);
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()=>{
Lines changed: 45 additions & 0 deletions
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+
importassertfrom'assert/strict';
4+
5+
// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs
6+
importmockfrom'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(awaitimport('events'),Object.defineProperty({
15+
__proto__: null,
16+
EventEmitter: 'This is mocked!'
17+
},Symbol.toStringTag,{
18+
enumerable: false,
19+
value: 'Module'
20+
}));
21+
22+
constmutator=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+
constmockedV2=awaitimport('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)