Skip to content

Commit 0d91b4d

Browse files
committed
module: add preImport loader hook
1 parent 5e05062 commit 0d91b4d

File tree

7 files changed

+116
-57
lines changed

7 files changed

+116
-57
lines changed

doc/api/esm.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,51 @@ A hook that returns without calling `next<hookName>()` _and_ without returning
726726
`shortCircuit: true` also triggers an exception. These errors are to help
727727
prevent unintentional breaks in the chain.
728728
729+
#### `preImport(specifier, context)`
730+
731+
<!-- YAML
732+
changes:
733+
- version: REPLACEME
734+
pr-url: REPLACEME
735+
description: Add support for preImport hook
736+
-->
737+
738+
> The loaders API is being redesigned. This hook may disappear or its
739+
> signature may change. Do not rely on the API described below.
740+
741+
* `specifier` {string}
742+
* `context` {Object}
743+
* `conditions` {string\[]} Resolution conditions of the current environment,
744+
as defined for the `package.json` imports and exports fields
745+
* `dynamic` {boolean} Whether this import is a dynamic `import()`
746+
* `importAssertions` {Object}
747+
* `parentURL` {string|undefined} The module importing this one, or undefined
748+
if this is the Node.js entry point
749+
750+
The `preImport` hook allows for tracking and asynchronous setup work for every
751+
top-level import operation.
752+
753+
The `preImport` hook is called for each top-level import operation by the
754+
module loader, both for the host-called imports (ie for the main entry) and for
755+
dynamic `import()` imports. These are distinguished by the `dynamic` context.
756+
757+
All `preImport` hooks for all loaders are run asynchronously in parallel,
758+
and block any further load operations (ie resolve and load) for the module graph
759+
being imported until they all complete successfully.
760+
761+
Multiple import calls to the same import specifier will re-call the hook
762+
multiple times. The first error thrown by the `preImport` hooks will be directly
763+
returned to the specific import operation as the load failure.
764+
765+
```js
766+
export async function preImport (specifier, context) {
767+
if (context.topLevel)
768+
console.log(`Top-level load of ${specifier}`);
769+
else
770+
console.log(`Dynamic import of ${specifier} in ${context.parentURL}`);
771+
}
772+
```
773+
729774
#### `resolve(specifier, context, nextResolve)`
730775
731776
<!-- YAML
@@ -747,7 +792,8 @@ changes:
747792
748793
* `specifier` {string}
749794
* `context` {Object}
750-
* `conditions` {string\[]} Export conditions of the relevant `package.json`
795+
* `conditions` {string\[]} Resolution conditions of the current environment,
796+
as defined for the `package.json` imports and exports fields
751797
* `importAssertions` {Object}
752798
* `parentURL` {string|undefined} The module importing this one, or undefined
753799
if this is the Node.js entry point
@@ -837,7 +883,8 @@ changes:
837883
838884
* `url` {string} The URL returned by the `resolve` chain
839885
* `context` {Object}
840-
* `conditions` {string\[]} Export conditions of the relevant `package.json`
886+
* `conditions` {string\[]} Resolution conditions of the current environment,
887+
as defined for the `package.json` imports and exports fields
841888
* `format` {string|null|undefined} The format optionally supplied by the
842889
`resolve` hook chain
843890
* `importAssertions` {Object}

lib/internal/modules/cjs/loader.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,7 +1025,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10251025
const loader = asyncESM.esmLoader;
10261026
return loader.import(specifier,
10271027
loader.getBaseURL(normalizeReferrerURL(filename)),
1028-
importAssertions);
1028+
importAssertions, true);
10291029
},
10301030
});
10311031
}
@@ -1042,7 +1042,7 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10421042
const loader = asyncESM.esmLoader;
10431043
return loader.import(specifier,
10441044
loader.getBaseURL(normalizeReferrerURL(filename)),
1045-
importAssertions);
1045+
importAssertions, true);
10461046
},
10471047
});
10481048
} catch (err) {

lib/internal/modules/esm/loader.js

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,15 @@
44
require('internal/modules/cjs/loader');
55

66
const {
7-
Array,
8-
ArrayIsArray,
97
ArrayPrototypeJoin,
108
ArrayPrototypePush,
119
FunctionPrototypeBind,
1210
FunctionPrototypeCall,
1311
ObjectAssign,
1412
ObjectCreate,
13+
ObjectFreeze,
1514
ObjectSetPrototypeOf,
16-
PromiseAll,
1715
RegExpPrototypeExec,
18-
SafeArrayIterator,
1916
SafeWeakMap,
2017
StringPrototypeStartsWith,
2118
globalThis,
@@ -59,7 +56,7 @@ const {
5956
fetchModule,
6057
inFetchCache,
6158
} = require('internal/modules/esm/fetch_module');
62-
59+
const { strictEqual } = require('internal/assert');
6360

6461
/**
6562
* @typedef {object} ExportedHooks
@@ -123,6 +120,8 @@ class ESMLoader {
123120
},
124121
];
125122

123+
#preImporters = [];
124+
126125
/**
127126
* Phase 1 of 2 in ESM loading.
128127
* @private
@@ -184,6 +183,7 @@ class ESMLoader {
184183
*/
185184
static pluckHooks({
186185
globalPreload,
186+
preImport,
187187
resolve,
188188
load,
189189
// obsolete hooks:
@@ -232,6 +232,9 @@ class ESMLoader {
232232
acceptedHooks.globalPreloader =
233233
FunctionPrototypeBind(globalPreload, null);
234234
}
235+
if (preImport) {
236+
acceptedHooks.preImporter = FunctionPrototypeBind(preImport, null);
237+
}
235238
if (resolve) {
236239
acceptedHooks.resolver = FunctionPrototypeBind(resolve, null);
237240
}
@@ -259,6 +262,7 @@ class ESMLoader {
259262
} = customLoaders[i];
260263
const {
261264
globalPreloader,
265+
preImporter,
262266
resolver,
263267
loader,
264268
} = ESMLoader.pluckHooks(exports);
@@ -272,6 +276,12 @@ class ESMLoader {
272276
},
273277
);
274278
}
279+
if (preImporter) {
280+
ArrayPrototypePush(
281+
this.#preImporters,
282+
preImporter
283+
);
284+
}
275285
if (resolver) {
276286
ArrayPrototypePush(
277287
this.#resolvers,
@@ -308,7 +318,7 @@ class ESMLoader {
308318
importModuleDynamically: (specifier, { url }, importAssertions) => {
309319
return this.import(specifier,
310320
this.getBaseURL(url),
311-
importAssertions);
321+
importAssertions, true);
312322
}
313323
});
314324

@@ -472,48 +482,47 @@ class ESMLoader {
472482
* This method must NOT be renamed: it functions as a dynamic import on a
473483
* loader module.
474484
*
475-
* @param {string | string[]} specifiers Path(s) to the module.
476-
* @param {string} parentURL Path of the parent importing the module.
485+
* @param {string} specifiers Imported specifier
486+
* @param {string} parentURL URL of the parent importing the module.
477487
* @param {Record<string, string>} importAssertions Validations for the
478488
* module import.
479-
* @returns {Promise<ExportedHooks | KeyedExports[]>}
480-
* A collection of module export(s) or a list of collections of module
481-
* export(s).
489+
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
490+
* @returns {Promise<ModuleNamespace>}
482491
*/
483-
async import(specifiers, parentURL, importAssertions) {
484-
// For loaders, `import` is passed multiple things to process, it returns a
485-
// list pairing the url and exports collected. This is especially useful for
486-
// error messaging, to identity from where an export came. But, in most
487-
// cases, only a single url is being "imported" (ex `import()`), so there is
488-
// only 1 possible url from which the exports were collected and it is
489-
// already known to the caller. Nesting that in a list would only ever
490-
// create redundant work for the caller, so it is later popped off the
491-
// internal list.
492-
const wasArr = ArrayIsArray(specifiers);
493-
if (!wasArr) { specifiers = [specifiers]; }
494-
495-
const count = specifiers.length;
496-
const jobs = new Array(count);
497-
498-
for (let i = 0; i < count; i++) {
499-
jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
500-
.then((job) => job.run())
501-
.then(({ module }) => module.getNamespace());
502-
}
503-
504-
const namespaces = await PromiseAll(new SafeArrayIterator(jobs));
505-
506-
if (!wasArr) { return namespaces[0]; } // We can skip the pairing below
507-
508-
for (let i = 0; i < count; i++) {
509-
const namespace = ObjectCreate(null);
510-
namespace.url = specifiers[i];
511-
namespace.exports = namespaces[i];
512-
513-
namespaces[i] = namespace;
514-
}
492+
async import(specifier, parentURL, importAssertions = ObjectCreate(null), dynamic = false) {
493+
await this.preImport(specifier, parentURL, importAssertionsForResolve, dynamic);
494+
const job = await this.getModuleJob(specifier, parentURL, importAssertions);
495+
this.getModuleJob(specifier, parentURL, importAssertions);
496+
const { module } = await job.run();
497+
return module.getNamespace();
498+
}
515499

516-
return namespaces;
500+
/**
501+
* Run the prepare hooks for a new import operation.
502+
*
503+
* Internally, this behaves like a backwards iterator, wherein the stack of
504+
* hooks starts at the top and each call to `nextResolve()` moves down 1 step
505+
* until it reaches the bottom or short-circuits.
506+
*
507+
* @param {string} specifier The import specifier.
508+
* @param {string} parentURL The URL of the module's parent.
509+
* @param {ImportAssertions} [importAssertions] Assertions from the import
510+
* statement or expression.
511+
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
512+
*/
513+
async preImport(
514+
specifier,
515+
parentURL,
516+
importAssertions,
517+
dynamic
518+
) {
519+
const context = ObjectFreeze({
520+
conditions: DEFAULT_CONDITIONS,
521+
dynamic,
522+
importAssertions,
523+
parentURL
524+
});
525+
await Promise.all(this.#preImporters.map(preImport => preImport(specifier, context)));
517526
}
518527

519528
/**

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function errPath(url) {
105105
async function importModuleDynamically(specifier, { url }, assertions) {
106106
return asyncESM.esmLoader.import(specifier,
107107
asyncESM.esmLoader.getBaseURL(url),
108-
assertions);
108+
assertions, true);
109109
}
110110

111111
// Strategy for loading a standard JavaScript module.

lib/internal/process/esm_loader.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,14 @@ async function initializeLoader() {
6666
const internalEsmLoader = new ESMLoader();
6767

6868
// Importation must be handled by internal loader to avoid poluting userland
69-
const keyedExportsList = await internalEsmLoader.import(
70-
customLoaders,
71-
pathToFileURL(cwd).href,
72-
ObjectCreate(null),
73-
);
69+
const parentURL = pathToFileURL(cwd).href;
70+
const importAssertions = ObjectCreate(null);
71+
72+
const keyedExportsList = await Promise.all(customLoaders.map(url => {
73+
const exports = await internalEsmLoader.import(url, parentURL,
74+
importAssertions);
75+
return { exports, url };
76+
}));
7477

7578
// Hooks must then be added to external/public loader
7679
// (so they're triggered in userland)

lib/internal/process/execution.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ function evalScript(name, body, breakFirstLine, print) {
7979
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
8080
importModuleDynamically(specifier, _, importAssertions) {
8181
const loader = asyncESM.esmLoader;
82-
return loader.import(specifier, baseUrl, importAssertions);
82+
return loader.import(specifier, baseUrl, importAssertions, true);
8383
}
8484
}));
8585
if (print) {

lib/repl.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ function REPLServer(prompt,
462462
displayErrors: true,
463463
importModuleDynamically: (specifier, _, importAssertions) => {
464464
return asyncESM.esmLoader.import(specifier, parentURL,
465-
importAssertions);
465+
importAssertions, true);
466466
}
467467
});
468468
} catch (fallbackError) {
@@ -506,7 +506,7 @@ function REPLServer(prompt,
506506
displayErrors: true,
507507
importModuleDynamically: (specifier, _, importAssertions) => {
508508
return asyncESM.esmLoader.import(specifier, parentURL,
509-
importAssertions);
509+
importAssertions, true);
510510
}
511511
});
512512
} catch (e) {

0 commit comments

Comments
 (0)