diff --git a/doc/api/esm.md b/doc/api/esm.md
index 3215cc100df651..fb1989b46eb0d0 100644
--- a/doc/api/esm.md
+++ b/doc/api/esm.md
@@ -1188,6 +1188,39 @@ export async function transformSource(source,
}
```
+#### getGlobalPreloadCode
hook
+
+> Note: The loaders API is being redesigned. This hook may disappear or its
+> signature may change. Do not rely on the API described below.
+
+Sometimes it can be necessary to run some code inside of the same global scope
+that the application will run in. This hook allows to return a string that will
+be ran as sloppy-mode script on startup.
+
+Similar to how CommonJS wrappers work, the code runs in an implicit function
+scope. The only argument is a `require`-like function that can be used to load
+builtins like "fs": `getBuiltin(request: string)`.
+
+If the code needs more advanced `require` features, it will have to construct
+its own `require` using `module.createRequire()`.
+
+```js
+/**
+ * @returns {string} Code to run before application startup
+ */
+export function getGlobalPreloadCode() {
+ return `\
+globalThis.someInjectedProperty = 42;
+console.log('I just set some globals!');
+
+const { createRequire } = getBuiltin('module');
+
+const require = createRequire(process.cwd + '/');
+// [...]
+`;
+}
+```
+
#### dynamicInstantiate
hook
> Note: The loaders API is being redesigned. This hook may disappear or its
diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js
index 5a27f4be7c9d1a..285f656fa99b11 100644
--- a/lib/internal/modules/esm/loader.js
+++ b/lib/internal/modules/esm/loader.js
@@ -7,6 +7,7 @@ const {
} = primordials;
const {
+ ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_PROPERTY,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
@@ -47,6 +48,14 @@ class Loader {
// Map of already-loaded CJS modules to use
this.cjsCache = new SafeMap();
+ // This hook is called before the first root module is imported. It's a
+ // function that returns a piece of code that runs as a sloppy-mode script.
+ // The script may evaluate to a function that can be called with a
+ // `getBuiltin` helper that can be used to retrieve builtins.
+ // If the hook returns `null` instead of a source string, it opts out of
+ // running any preload code.
+ // The preload code runs as soon as the hook module has finished evaluating.
+ this._getGlobalPreloadCode = null;
// The resolver has the signature
// (specifier : string, parentURL : string, defaultResolve)
// -> Promise<{ url : string }>
@@ -168,7 +177,16 @@ class Loader {
return module.getNamespace();
}
- hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }) {
+ hook(hooks) {
+ const {
+ resolve,
+ dynamicInstantiate,
+ getFormat,
+ getSource,
+ transformSource,
+ getGlobalPreloadCode,
+ } = hooks;
+
// Use .bind() to avoid giving access to the Loader instance when called.
if (resolve !== undefined)
this._resolve = FunctionPrototypeBind(resolve, null);
@@ -185,6 +203,37 @@ class Loader {
if (transformSource !== undefined) {
this._transformSource = FunctionPrototypeBind(transformSource, null);
}
+ if (getGlobalPreloadCode !== undefined) {
+ this._getGlobalPreloadCode =
+ FunctionPrototypeBind(getGlobalPreloadCode, null);
+ }
+ }
+
+ runGlobalPreloadCode() {
+ if (!this._getGlobalPreloadCode) {
+ return;
+ }
+ const preloadCode = this._getGlobalPreloadCode();
+ if (preloadCode === null) {
+ return;
+ }
+
+ if (typeof preloadCode !== 'string') {
+ throw new ERR_INVALID_RETURN_VALUE(
+ 'string', 'loader getGlobalPreloadCode', preloadCode);
+ }
+ const { compileFunction } = require('vm');
+ const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
+ filename: '',
+ });
+ const { NativeModule } = require('internal/bootstrap/loaders');
+
+ preloadInit.call(globalThis, (builtinName) => {
+ if (NativeModule.canBeRequiredByUsers(builtinName)) {
+ return require(builtinName);
+ }
+ throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
+ });
}
async getModuleJob(specifier, parentURL) {
diff --git a/lib/internal/process/esm_loader.js b/lib/internal/process/esm_loader.js
index 404be77338bfe1..cca1e3e07956a1 100644
--- a/lib/internal/process/esm_loader.js
+++ b/lib/internal/process/esm_loader.js
@@ -69,6 +69,7 @@ async function initializeLoader() {
await ESMLoader.import(userLoader, pathToFileURL(cwd).href);
ESMLoader = new Loader();
ESMLoader.hook(hooks);
+ ESMLoader.runGlobalPreloadCode();
return exports.ESMLoader = ESMLoader;
})();
}
diff --git a/test/es-module/test-esm-loader-side-effect.mjs b/test/es-module/test-esm-loader-side-effect.mjs
new file mode 100644
index 00000000000000..f76b10700ddc8f
--- /dev/null
+++ b/test/es-module/test-esm-loader-side-effect.mjs
@@ -0,0 +1,32 @@
+// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-side-effect.mjs --require ./test/fixtures/es-module-loaders/loader-side-effect-require-preload.js
+import { allowGlobals, mustCall } from '../common/index.mjs';
+import assert from 'assert';
+import { fileURLToPath } from 'url';
+import { Worker, isMainThread, parentPort } from 'worker_threads';
+
+/* global implicitGlobalProperty */
+assert.strictEqual(globalThis.implicitGlobalProperty, 42);
+allowGlobals(implicitGlobalProperty);
+
+/* global implicitGlobalConst */
+assert.strictEqual(implicitGlobalConst, 42 * 42);
+allowGlobals(implicitGlobalConst);
+
+/* global explicitGlobalProperty */
+assert.strictEqual(globalThis.explicitGlobalProperty, 42 * 42 * 42);
+allowGlobals(explicitGlobalProperty);
+
+/* global preloadOrder */
+assert.deepStrictEqual(globalThis.preloadOrder, ['--require', 'loader']);
+allowGlobals(preloadOrder);
+
+if (isMainThread) {
+ const worker = new Worker(fileURLToPath(import.meta.url));
+ const promise = new Promise((resolve, reject) => {
+ worker.on('message', resolve);
+ worker.on('error', reject);
+ });
+ promise.then(mustCall());
+} else {
+ parentPort.postMessage('worker done');
+}
diff --git a/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js b/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js
new file mode 100644
index 00000000000000..98820b9379748e
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js
@@ -0,0 +1,6 @@
+/**
+ * This file is combined with `loader-side-effect.mjs` via `--require`. Its
+ * purpose is to test execution order of the two kinds of preload code.
+ */
+
+(globalThis.preloadOrder || (globalThis.preloadOrder = [])).push('--require');
diff --git a/test/fixtures/es-module-loaders/loader-side-effect.mjs b/test/fixtures/es-module-loaders/loader-side-effect.mjs
new file mode 100644
index 00000000000000..5c80724fbb95f6
--- /dev/null
+++ b/test/fixtures/es-module-loaders/loader-side-effect.mjs
@@ -0,0 +1,32 @@
+// Arrow function so it closes over the this-value of the preload scope.
+const globalPreload = () => {
+ /* global getBuiltin */
+ const assert = getBuiltin('assert');
+ const vm = getBuiltin('vm');
+
+ assert.strictEqual(typeof require, 'undefined');
+ assert.strictEqual(typeof module, 'undefined');
+ assert.strictEqual(typeof exports, 'undefined');
+ assert.strictEqual(typeof __filename, 'undefined');
+ assert.strictEqual(typeof __dirname, 'undefined');
+
+ assert.strictEqual(this, globalThis);
+ (globalThis.preloadOrder || (globalThis.preloadOrder = [])).push('loader');
+
+ vm.runInThisContext(`\
+var implicitGlobalProperty = 42;
+const implicitGlobalConst = 42 * 42;
+`);
+
+ assert.strictEqual(globalThis.implicitGlobalProperty, 42);
+ (implicitGlobalProperty).foo = 'bar'; // assert: not strict mode
+
+ globalThis.explicitGlobalProperty = 42 * 42 * 42;
+}
+
+export function getGlobalPreloadCode() {
+ return `\
+
+(${globalPreload.toString()})();
+`;
+}