Skip to content

Commit b341f63

Browse files
committed
module: add preImport loader hook
1 parent f209aee commit b341f63

File tree

7 files changed

+119
-58
lines changed

7 files changed

+119
-58
lines changed

doc/api/esm.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,51 @@ A hook that returns without calling `next<hookName>()` _and_ without returning
734734
`shortCircuit: true` also triggers an exception. These errors are to help
735735
prevent unintentional breaks in the chain.
736736
737+
#### `preImport(specifier, context)`
738+
739+
<!-- YAML
740+
changes:
741+
- version: REPLACEME
742+
pr-url: REPLACEME
743+
description: Add support for preImport hook
744+
-->
745+
746+
> The loaders API is being redesigned. This hook may disappear or its
747+
> signature may change. Do not rely on the API described below.
748+
749+
* `specifier` {string}
750+
* `context` {Object}
751+
* `conditions` {string\[]} Resolution conditions of the current environment,
752+
as defined for the `package.json` imports and exports fields
753+
* `dynamic` {boolean} Whether this import is a dynamic `import()`
754+
* `importAssertions` {Object}
755+
* `parentURL` {string|undefined} The module importing this one, or undefined
756+
if this is the Node.js entry point
757+
758+
The `preImport` hook allows for tracking and asynchronous setup work for every
759+
top-level import operation.
760+
761+
The `preImport` hook is called for each top-level import operation by the
762+
module loader, both for the host-called imports (ie for the main entry) and for
763+
dynamic `import()` imports. These are distinguished by the `dynamic` context.
764+
765+
All `preImport` hooks for all loaders are run asynchronously in parallel,
766+
and block any further load operations (ie resolve and load) for the module graph
767+
being imported until they all complete successfully.
768+
769+
Multiple import calls to the same import specifier will re-call the hook
770+
multiple times. The first error thrown by the `preImport` hooks will be directly
771+
returned to the specific import operation as the load failure.
772+
773+
```js
774+
export async function preImport (specifier, context) {
775+
if (context.topLevel)
776+
console.log(`Top-level load of ${specifier}`);
777+
else
778+
console.log(`Dynamic import of ${specifier} in ${context.parentURL}`);
779+
}
780+
```
781+
737782
#### `resolve(specifier, context, nextResolve)`
738783
739784
<!-- YAML
@@ -758,7 +803,8 @@ changes:
758803
759804
* `specifier` {string}
760805
* `context` {Object}
761-
* `conditions` {string\[]} Export conditions of the relevant `package.json`
806+
* `conditions` {string\[]} Resolution conditions of the current environment,
807+
as defined for the `package.json` imports and exports fields
762808
* `importAssertions` {Object}
763809
* `parentURL` {string|undefined} The module importing this one, or undefined
764810
if this is the Node.js entry point
@@ -851,7 +897,8 @@ changes:
851897
852898
* `url` {string} The URL returned by the `resolve` chain
853899
* `context` {Object}
854-
* `conditions` {string\[]} Export conditions of the relevant `package.json`
900+
* `conditions` {string\[]} Resolution conditions of the current environment,
901+
as defined for the `package.json` imports and exports fields
855902
* `format` {string|null|undefined} The format optionally supplied by the
856903
`resolve` hook chain
857904
* `importAssertions` {Object}

lib/internal/modules/cjs/loader.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,8 +1036,9 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10361036
displayErrors: true,
10371037
importModuleDynamically: async (specifier, _, importAssertions) => {
10381038
const loader = asyncESM.esmLoader;
1039-
return loader.import(specifier, normalizeReferrerURL(filename),
1040-
importAssertions);
1039+
return loader.import(specifier,
1040+
normalizeReferrerURL(filename),
1041+
importAssertions, true);
10411042
},
10421043
});
10431044
}
@@ -1052,8 +1053,9 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10521053
filename,
10531054
importModuleDynamically(specifier, _, importAssertions) {
10541055
const loader = asyncESM.esmLoader;
1055-
return loader.import(specifier, normalizeReferrerURL(filename),
1056-
importAssertions);
1056+
return loader.import(specifier,
1057+
normalizeReferrerURL(filename),
1058+
importAssertions, true);
10571059
},
10581060
});
10591061
} catch (err) {

lib/internal/modules/esm/loader.js

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,19 @@
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,
1513
ObjectDefineProperty,
14+
ObjectFreeze,
1615
ObjectSetPrototypeOf,
17-
PromiseAll,
1816
PromiseResolve,
1917
PromisePrototypeThen,
2018
ReflectApply,
2119
RegExpPrototypeExec,
22-
SafeArrayIterator,
2320
SafeWeakMap,
2421
StringPrototypeSlice,
2522
StringPrototypeToUpperCase,
@@ -215,6 +212,8 @@ class ESMLoader {
215212
},
216213
];
217214

215+
#preImporters = [];
216+
218217
/**
219218
* Phase 1 of 2 in ESM loading.
220219
* @private
@@ -276,6 +275,7 @@ class ESMLoader {
276275
*/
277276
static pluckHooks({
278277
globalPreload,
278+
preImport,
279279
resolve,
280280
load,
281281
// obsolete hooks:
@@ -324,6 +324,9 @@ class ESMLoader {
324324
acceptedHooks.globalPreloader =
325325
FunctionPrototypeBind(globalPreload, null);
326326
}
327+
if (preImport) {
328+
acceptedHooks.preImporter = FunctionPrototypeBind(preImport, null);
329+
}
327330
if (resolve) {
328331
acceptedHooks.resolver = FunctionPrototypeBind(resolve, null);
329332
}
@@ -351,6 +354,7 @@ class ESMLoader {
351354
} = customLoaders[i];
352355
const {
353356
globalPreloader,
357+
preImporter,
354358
resolver,
355359
loader,
356360
} = ESMLoader.pluckHooks(exports);
@@ -364,6 +368,12 @@ class ESMLoader {
364368
},
365369
);
366370
}
371+
if (preImporter) {
372+
ArrayPrototypePush(
373+
this.#preImporters,
374+
preImporter
375+
);
376+
}
367377
if (resolver) {
368378
ArrayPrototypePush(
369379
this.#resolvers,
@@ -398,7 +408,7 @@ class ESMLoader {
398408
const module = new ModuleWrap(url, undefined, source, 0, 0);
399409
callbackMap.set(module, {
400410
importModuleDynamically: (specifier, { url }, importAssertions) => {
401-
return this.import(specifier, url, importAssertions);
411+
return this.import(specifier, url, importAssertions, true);
402412
}
403413
});
404414

@@ -517,48 +527,47 @@ class ESMLoader {
517527
* This method must NOT be renamed: it functions as a dynamic import on a
518528
* loader module.
519529
*
520-
* @param {string | string[]} specifiers Path(s) to the module.
521-
* @param {string} parentURL Path of the parent importing the module.
530+
* @param {string} specifiers Imported specifier
531+
* @param {string} parentURL URL of the parent importing the module.
522532
* @param {Record<string, string>} importAssertions Validations for the
523533
* module import.
524-
* @returns {Promise<ExportedHooks | KeyedExports[]>}
525-
* A collection of module export(s) or a list of collections of module
526-
* export(s).
534+
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
535+
* @returns {Promise<ModuleNamespace>}
527536
*/
528-
async import(specifiers, parentURL, importAssertions) {
529-
// For loaders, `import` is passed multiple things to process, it returns a
530-
// list pairing the url and exports collected. This is especially useful for
531-
// error messaging, to identity from where an export came. But, in most
532-
// cases, only a single url is being "imported" (ex `import()`), so there is
533-
// only 1 possible url from which the exports were collected and it is
534-
// already known to the caller. Nesting that in a list would only ever
535-
// create redundant work for the caller, so it is later popped off the
536-
// internal list.
537-
const wasArr = ArrayIsArray(specifiers);
538-
if (!wasArr) { specifiers = [specifiers]; }
539-
540-
const count = specifiers.length;
541-
const jobs = new Array(count);
542-
543-
for (let i = 0; i < count; i++) {
544-
jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
545-
.then((job) => job.run())
546-
.then(({ module }) => module.getNamespace());
547-
}
548-
549-
const namespaces = await PromiseAll(new SafeArrayIterator(jobs));
550-
551-
if (!wasArr) { return namespaces[0]; } // We can skip the pairing below
552-
553-
for (let i = 0; i < count; i++) {
554-
const namespace = ObjectCreate(null);
555-
namespace.url = specifiers[i];
556-
namespace.exports = namespaces[i];
557-
558-
namespaces[i] = namespace;
559-
}
537+
async import(specifier, parentURL, importAssertions = ObjectCreate(null), dynamic = false) {
538+
await this.preImport(specifier, parentURL, importAssertionsForResolve, dynamic);
539+
const job = await this.getModuleJob(specifier, parentURL, importAssertions);
540+
this.getModuleJob(specifier, parentURL, importAssertions);
541+
const { module } = await job.run();
542+
return module.getNamespace();
543+
}
560544

561-
return namespaces;
545+
/**
546+
* Run the prepare hooks for a new import operation.
547+
*
548+
* Internally, this behaves like a backwards iterator, wherein the stack of
549+
* hooks starts at the top and each call to `nextResolve()` moves down 1 step
550+
* until it reaches the bottom or short-circuits.
551+
*
552+
* @param {string} specifier The import specifier.
553+
* @param {string} parentURL The URL of the module's parent.
554+
* @param {ImportAssertions} [importAssertions] Assertions from the import
555+
* statement or expression.
556+
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
557+
*/
558+
async preImport(
559+
specifier,
560+
parentURL,
561+
importAssertions,
562+
dynamic
563+
) {
564+
const context = ObjectFreeze({
565+
conditions: DEFAULT_CONDITIONS,
566+
dynamic,
567+
importAssertions,
568+
parentURL
569+
});
570+
await Promise.all(this.#preImporters.map(preImport => preImport(specifier, context)));
562571
}
563572

564573
/**

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function errPath(url) {
103103
}
104104

105105
async function importModuleDynamically(specifier, { url }, assertions) {
106-
return asyncESM.esmLoader.import(specifier, url, assertions);
106+
return asyncESM.esmLoader.import(specifier, url, assertions, true);
107107
}
108108

109109
// 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
@@ -465,7 +465,7 @@ function REPLServer(prompt,
465465
displayErrors: true,
466466
importModuleDynamically: (specifier, _, importAssertions) => {
467467
return asyncESM.esmLoader.import(specifier, parentURL,
468-
importAssertions);
468+
importAssertions, true);
469469
}
470470
});
471471
} catch (fallbackError) {
@@ -509,7 +509,7 @@ function REPLServer(prompt,
509509
displayErrors: true,
510510
importModuleDynamically: (specifier, _, importAssertions) => {
511511
return asyncESM.esmLoader.import(specifier, parentURL,
512-
importAssertions);
512+
importAssertions, true);
513513
}
514514
});
515515
} catch (e) {

0 commit comments

Comments
 (0)