Skip to content

Commit 7356c43

Browse files
hybristMylesBorins
authored andcommitted
module: add hook for global preload code
PR-URL: #32068 Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com>
1 parent 2e28783 commit 7356c43

6 files changed

Lines changed: 154 additions & 1 deletion

File tree

‎doc/api/esm.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,39 @@ export async function transformSource(source,
11881188
}
11891189
```
11901190

1191+
#### <code>getGlobalPreloadCode</code>hook
1192+
1193+
>Note:TheloadersAPIisbeingredesigned. Thishookmaydisappearorits
1194+
>signaturemaychange. DonotrelyontheAPIdescribedbelow.
1195+
1196+
Sometimesitcanbenecessarytorunsomecodeinsideofthesameglobalscope
1197+
thattheapplicationwillrunin. Thishookallowstoreturnastringthatwill
1198+
beranassloppy-modescriptonstartup.
1199+
1200+
SimilartohowCommonJSwrapperswork, thecoderunsinanimplicitfunction
1201+
scope. The only argument is a `require`-like function that can be used to load
1202+
builtins like "fs": `getBuiltin(request:string)`.
1203+
1204+
If the code needs more advanced `require` features, it will have to construct
1205+
its own `require` using `module.createRequire()`.
1206+
1207+
```js
1208+
/**
1209+
* @returns{string} Code to run before application startup
1210+
*/
1211+
export function getGlobalPreloadCode() {
1212+
return`\
1213+
globalThis.someInjectedProperty = 42;
1214+
console.log('I just set some globals!');
1215+
1216+
const { createRequire } = getBuiltin('module');
1217+
1218+
const require = createRequire(process.cwd + '/<preload>');
1219+
// [...]
1220+
`;
1221+
}
1222+
```
1223+
11911224
#### <code>dynamicInstantiate</code> hook
11921225
11931226
> Note: The loaders API is being redesigned. This hook may disappear or its

‎lib/internal/modules/esm/loader.js‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
}=primordials;
88

99
const{
10+
ERR_INVALID_ARG_VALUE,
1011
ERR_INVALID_RETURN_PROPERTY,
1112
ERR_INVALID_RETURN_PROPERTY_VALUE,
1213
ERR_INVALID_RETURN_VALUE,
@@ -47,6 +48,14 @@ class Loader {
4748
// Map of already-loaded CJS modules to use
4849
this.cjsCache=newSafeMap();
4950

51+
// This hook is called before the first root module is imported. It's a
52+
// function that returns a piece of code that runs as a sloppy-mode script.
53+
// The script may evaluate to a function that can be called with a
54+
// `getBuiltin` helper that can be used to retrieve builtins.
55+
// If the hook returns `null` instead of a source string, it opts out of
56+
// running any preload code.
57+
// The preload code runs as soon as the hook module has finished evaluating.
58+
this._getGlobalPreloadCode=null;
5059
// The resolver has the signature
5160
// (specifier : string, parentURL : string, defaultResolve)
5261
// -> Promise<{ url : string }>
@@ -168,7 +177,16 @@ class Loader {
168177
returnmodule.getNamespace();
169178
}
170179

171-
hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }){
180+
hook(hooks){
181+
const{
182+
resolve,
183+
dynamicInstantiate,
184+
getFormat,
185+
getSource,
186+
transformSource,
187+
getGlobalPreloadCode,
188+
}=hooks;
189+
172190
// Use .bind() to avoid giving access to the Loader instance when called.
173191
if(resolve!==undefined)
174192
this._resolve=FunctionPrototypeBind(resolve,null);
@@ -185,6 +203,37 @@ class Loader {
185203
if(transformSource!==undefined){
186204
this._transformSource=FunctionPrototypeBind(transformSource,null);
187205
}
206+
if(getGlobalPreloadCode!==undefined){
207+
this._getGlobalPreloadCode=
208+
FunctionPrototypeBind(getGlobalPreloadCode,null);
209+
}
210+
}
211+
212+
runGlobalPreloadCode(){
213+
if(!this._getGlobalPreloadCode){
214+
return;
215+
}
216+
constpreloadCode=this._getGlobalPreloadCode();
217+
if(preloadCode===null){
218+
return;
219+
}
220+
221+
if(typeofpreloadCode!=='string'){
222+
thrownewERR_INVALID_RETURN_VALUE(
223+
'string','loader getGlobalPreloadCode',preloadCode);
224+
}
225+
const{ compileFunction }=require('vm');
226+
constpreloadInit=compileFunction(preloadCode,['getBuiltin'],{
227+
filename: '<preload>',
228+
});
229+
const{ NativeModule }=require('internal/bootstrap/loaders');
230+
231+
preloadInit.call(globalThis,(builtinName)=>{
232+
if(NativeModule.canBeRequiredByUsers(builtinName)){
233+
returnrequire(builtinName);
234+
}
235+
thrownewERR_INVALID_ARG_VALUE('builtinName',builtinName);
236+
});
188237
}
189238

190239
asyncgetModuleJob(specifier,parentURL){

‎lib/internal/process/esm_loader.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async function initializeLoader() {
6969
awaitESMLoader.import(userLoader,pathToFileURL(cwd).href);
7070
ESMLoader=newLoader();
7171
ESMLoader.hook(hooks);
72+
ESMLoader.runGlobalPreloadCode();
7273
returnexports.ESMLoader=ESMLoader;
7374
})();
7475
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-side-effect.mjs --require ./test/fixtures/es-module-loaders/loader-side-effect-require-preload.js
2+
import{allowGlobals,mustCall}from'../common/index.mjs';
3+
importassertfrom'assert';
4+
import{fileURLToPath}from'url';
5+
import{Worker,isMainThread,parentPort}from'worker_threads';
6+
7+
/* global implicitGlobalProperty */
8+
assert.strictEqual(globalThis.implicitGlobalProperty,42);
9+
allowGlobals(implicitGlobalProperty);
10+
11+
/* global implicitGlobalConst */
12+
assert.strictEqual(implicitGlobalConst,42*42);
13+
allowGlobals(implicitGlobalConst);
14+
15+
/* global explicitGlobalProperty */
16+
assert.strictEqual(globalThis.explicitGlobalProperty,42*42*42);
17+
allowGlobals(explicitGlobalProperty);
18+
19+
/* global preloadOrder */
20+
assert.deepStrictEqual(globalThis.preloadOrder,['--require','loader']);
21+
allowGlobals(preloadOrder);
22+
23+
if(isMainThread){
24+
constworker=newWorker(fileURLToPath(import.meta.url));
25+
constpromise=newPromise((resolve,reject)=>{
26+
worker.on('message',resolve);
27+
worker.on('error',reject);
28+
});
29+
promise.then(mustCall());
30+
}else{
31+
parentPort.postMessage('worker done');
32+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* This file is combined with `loader-side-effect.mjs` via `--require`. Its
3+
* purpose is to test execution order of the two kinds of preload code.
4+
*/
5+
6+
(globalThis.preloadOrder||(globalThis.preloadOrder=[])).push('--require');
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Arrow function so it closes over the this-value of the preload scope.
2+
constglobalPreload=()=>{
3+
/* global getBuiltin */
4+
constassert=getBuiltin('assert');
5+
constvm=getBuiltin('vm');
6+
7+
assert.strictEqual(typeofrequire,'undefined');
8+
assert.strictEqual(typeofmodule,'undefined');
9+
assert.strictEqual(typeofexports,'undefined');
10+
assert.strictEqual(typeof__filename,'undefined');
11+
assert.strictEqual(typeof__dirname,'undefined');
12+
13+
assert.strictEqual(this,globalThis);
14+
(globalThis.preloadOrder||(globalThis.preloadOrder=[])).push('loader');
15+
16+
vm.runInThisContext(`\
17+
var implicitGlobalProperty = 42;
18+
const implicitGlobalConst = 42 * 42;
19+
`);
20+
21+
assert.strictEqual(globalThis.implicitGlobalProperty,42);
22+
(implicitGlobalProperty).foo='bar';// assert: not strict mode
23+
24+
globalThis.explicitGlobalProperty=42*42*42;
25+
}
26+
27+
exportfunctiongetGlobalPreloadCode(){
28+
return`\
29+
<!-- assert: inside of script goal -->
30+
(${globalPreload.toString()})();
31+
`;
32+
}

0 commit comments

Comments
 (0)