Skip to content

Commit 7356c43

Browse files
jkremsMylesBorins
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

File tree

6 files changed

+154
-1
lines changed

6 files changed

+154
-1
lines changed

doc/api/esm.md

+33
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: The loaders API is being redesigned. This hook may disappear or its
1194+
> signature may change. Do not rely on the API described below.
1195+
1196+
Sometimes it can be necessary to run some code inside of the same global scope
1197+
that the application will run in. This hook allows to return a string that will
1198+
be ran as sloppy-mode script on startup.
1199+
1200+
Similar to how CommonJS wrappers work, the code runs in an implicit function
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

+50-1
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 = new SafeMap();
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
return module.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+
const preloadCode = this._getGlobalPreloadCode();
217+
if (preloadCode === null) {
218+
return;
219+
}
220+
221+
if (typeof preloadCode !== 'string') {
222+
throw new ERR_INVALID_RETURN_VALUE(
223+
'string', 'loader getGlobalPreloadCode', preloadCode);
224+
}
225+
const { compileFunction } = require('vm');
226+
const preloadInit = 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+
return require(builtinName);
234+
}
235+
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
236+
});
188237
}
189238

190239
async getModuleJob(specifier, parentURL) {

lib/internal/process/esm_loader.js

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async function initializeLoader() {
6969
await ESMLoader.import(userLoader, pathToFileURL(cwd).href);
7070
ESMLoader = new Loader();
7171
ESMLoader.hook(hooks);
72+
ESMLoader.runGlobalPreloadCode();
7273
return exports.ESMLoader = ESMLoader;
7374
})();
7475
}
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+
import assert from '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+
const worker = new Worker(fileURLToPath(import.meta.url));
25+
const promise = new Promise((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+
}
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');
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+
const globalPreload = () => {
3+
/* global getBuiltin */
4+
const assert = getBuiltin('assert');
5+
const vm = getBuiltin('vm');
6+
7+
assert.strictEqual(typeof require, 'undefined');
8+
assert.strictEqual(typeof module, 'undefined');
9+
assert.strictEqual(typeof exports, '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+
export function getGlobalPreloadCode() {
28+
return `\
29+
<!-- assert: inside of script goal -->
30+
(${globalPreload.toString()})();
31+
`;
32+
}

0 commit comments

Comments
 (0)