Skip to content

Commit

Permalink
module: add preImport loader hook
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford committed Jun 18, 2022
1 parent f209aee commit b341f63
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 58 deletions.
51 changes: 49 additions & 2 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,51 @@ A hook that returns without calling `next<hookName>()` _and_ without returning
`shortCircuit: true` also triggers an exception. These errors are to help
prevent unintentional breaks in the chain.
#### `preImport(specifier, context)`
<!-- YAML
changes:
- version: REPLACEME
pr-url: REPLACEME
description: Add support for preImport hook
-->
> The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.
* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]} Resolution conditions of the current environment,
as defined for the `package.json` imports and exports fields
* `dynamic` {boolean} Whether this import is a dynamic `import()`
* `importAssertions` {Object}
* `parentURL` {string|undefined} The module importing this one, or undefined
if this is the Node.js entry point
The `preImport` hook allows for tracking and asynchronous setup work for every
top-level import operation.
The `preImport` hook is called for each top-level import operation by the
module loader, both for the host-called imports (ie for the main entry) and for
dynamic `import()` imports. These are distinguished by the `dynamic` context.
All `preImport` hooks for all loaders are run asynchronously in parallel,
and block any further load operations (ie resolve and load) for the module graph
being imported until they all complete successfully.
Multiple import calls to the same import specifier will re-call the hook
multiple times. The first error thrown by the `preImport` hooks will be directly
returned to the specific import operation as the load failure.
```js
export async function preImport (specifier, context) {
if (context.topLevel)
console.log(`Top-level load of ${specifier}`);
else
console.log(`Dynamic import of ${specifier} in ${context.parentURL}`);
}
```
#### `resolve(specifier, context, nextResolve)`
<!-- YAML
Expand All @@ -758,7 +803,8 @@ changes:
* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]} Export conditions of the relevant `package.json`
* `conditions` {string\[]} Resolution conditions of the current environment,
as defined for the `package.json` imports and exports fields
* `importAssertions` {Object}
* `parentURL` {string|undefined} The module importing this one, or undefined
if this is the Node.js entry point
Expand Down Expand Up @@ -851,7 +897,8 @@ changes:
* `url` {string} The URL returned by the `resolve` chain
* `context` {Object}
* `conditions` {string\[]} Export conditions of the relevant `package.json`
* `conditions` {string\[]} Resolution conditions of the current environment,
as defined for the `package.json` imports and exports fields
* `format` {string|null|undefined} The format optionally supplied by the
`resolve` hook chain
* `importAssertions` {Object}
Expand Down
10 changes: 6 additions & 4 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1036,8 +1036,9 @@ function wrapSafe(filename, content, cjsModuleInstance) {
displayErrors: true,
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
return loader.import(specifier,
normalizeReferrerURL(filename),
importAssertions, true);
},
});
}
Expand All @@ -1052,8 +1053,9 @@ function wrapSafe(filename, content, cjsModuleInstance) {
filename,
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
return loader.import(specifier,
normalizeReferrerURL(filename),
importAssertions, true);
},
});
} catch (err) {
Expand Down
95 changes: 52 additions & 43 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,19 @@
require('internal/modules/cjs/loader');

const {
Array,
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypePush,
FunctionPrototypeBind,
FunctionPrototypeCall,
ObjectAssign,
ObjectCreate,
ObjectDefineProperty,
ObjectFreeze,
ObjectSetPrototypeOf,
PromiseAll,
PromiseResolve,
PromisePrototypeThen,
ReflectApply,
RegExpPrototypeExec,
SafeArrayIterator,
SafeWeakMap,
StringPrototypeSlice,
StringPrototypeToUpperCase,
Expand Down Expand Up @@ -215,6 +212,8 @@ class ESMLoader {
},
];

#preImporters = [];

/**
* Phase 1 of 2 in ESM loading.
* @private
Expand Down Expand Up @@ -276,6 +275,7 @@ class ESMLoader {
*/
static pluckHooks({
globalPreload,
preImport,
resolve,
load,
// obsolete hooks:
Expand Down Expand Up @@ -324,6 +324,9 @@ class ESMLoader {
acceptedHooks.globalPreloader =
FunctionPrototypeBind(globalPreload, null);
}
if (preImport) {
acceptedHooks.preImporter = FunctionPrototypeBind(preImport, null);
}
if (resolve) {
acceptedHooks.resolver = FunctionPrototypeBind(resolve, null);
}
Expand Down Expand Up @@ -351,6 +354,7 @@ class ESMLoader {
} = customLoaders[i];
const {
globalPreloader,
preImporter,
resolver,
loader,
} = ESMLoader.pluckHooks(exports);
Expand All @@ -364,6 +368,12 @@ class ESMLoader {
},
);
}
if (preImporter) {
ArrayPrototypePush(
this.#preImporters,
preImporter
);
}
if (resolver) {
ArrayPrototypePush(
this.#resolvers,
Expand Down Expand Up @@ -398,7 +408,7 @@ class ESMLoader {
const module = new ModuleWrap(url, undefined, source, 0, 0);
callbackMap.set(module, {
importModuleDynamically: (specifier, { url }, importAssertions) => {
return this.import(specifier, url, importAssertions);
return this.import(specifier, url, importAssertions, true);
}
});

Expand Down Expand Up @@ -517,48 +527,47 @@ class ESMLoader {
* This method must NOT be renamed: it functions as a dynamic import on a
* loader module.
*
* @param {string | string[]} specifiers Path(s) to the module.
* @param {string} parentURL Path of the parent importing the module.
* @param {string} specifiers Imported specifier
* @param {string} parentURL URL of the parent importing the module.
* @param {Record<string, string>} importAssertions Validations for the
* module import.
* @returns {Promise<ExportedHooks | KeyedExports[]>}
* A collection of module export(s) or a list of collections of module
* export(s).
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
* @returns {Promise<ModuleNamespace>}
*/
async import(specifiers, parentURL, importAssertions) {
// For loaders, `import` is passed multiple things to process, it returns a
// list pairing the url and exports collected. This is especially useful for
// error messaging, to identity from where an export came. But, in most
// cases, only a single url is being "imported" (ex `import()`), so there is
// only 1 possible url from which the exports were collected and it is
// already known to the caller. Nesting that in a list would only ever
// create redundant work for the caller, so it is later popped off the
// internal list.
const wasArr = ArrayIsArray(specifiers);
if (!wasArr) { specifiers = [specifiers]; }

const count = specifiers.length;
const jobs = new Array(count);

for (let i = 0; i < count; i++) {
jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
.then((job) => job.run())
.then(({ module }) => module.getNamespace());
}

const namespaces = await PromiseAll(new SafeArrayIterator(jobs));

if (!wasArr) { return namespaces[0]; } // We can skip the pairing below

for (let i = 0; i < count; i++) {
const namespace = ObjectCreate(null);
namespace.url = specifiers[i];
namespace.exports = namespaces[i];

namespaces[i] = namespace;
}
async import(specifier, parentURL, importAssertions = ObjectCreate(null), dynamic = false) {
await this.preImport(specifier, parentURL, importAssertionsForResolve, dynamic);
const job = await this.getModuleJob(specifier, parentURL, importAssertions);
this.getModuleJob(specifier, parentURL, importAssertions);
const { module } = await job.run();
return module.getNamespace();
}

return namespaces;
/**
* Run the prepare hooks for a new import operation.
*
* Internally, this behaves like a backwards iterator, wherein the stack of
* hooks starts at the top and each call to `nextResolve()` moves down 1 step
* until it reaches the bottom or short-circuits.
*
* @param {string} specifier The import specifier.
* @param {string} parentURL The URL of the module's parent.
* @param {ImportAssertions} [importAssertions] Assertions from the import
* statement or expression.
* @param {boolean} dynamic Whether the import is a dynamic `import()`.
*/
async preImport(
specifier,
parentURL,
importAssertions,
dynamic
) {
const context = ObjectFreeze({
conditions: DEFAULT_CONDITIONS,
dynamic,
importAssertions,
parentURL
});
await Promise.all(this.#preImporters.map(preImport => preImport(specifier, context)));
}

/**
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ function errPath(url) {
}

async function importModuleDynamically(specifier, { url }, assertions) {
return asyncESM.esmLoader.import(specifier, url, assertions);
return asyncESM.esmLoader.import(specifier, url, assertions, true);
}

// Strategy for loading a standard JavaScript module.
Expand Down
13 changes: 8 additions & 5 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,14 @@ async function initializeLoader() {
const internalEsmLoader = new ESMLoader();

// Importation must be handled by internal loader to avoid poluting userland
const keyedExportsList = await internalEsmLoader.import(
customLoaders,
pathToFileURL(cwd).href,
ObjectCreate(null),
);
const parentURL = pathToFileURL(cwd).href;
const importAssertions = ObjectCreate(null);

const keyedExportsList = await Promise.all(customLoaders.map(url => {
const exports = await internalEsmLoader.import(url, parentURL,
importAssertions);
return { exports, url };
}));

// Hooks must then be added to external/public loader
// (so they're triggered in userland)
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function evalScript(name, body, breakFirstLine, print) {
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, baseUrl, importAssertions);
return loader.import(specifier, baseUrl, importAssertions, true);
}
}));
if (print) {
Expand Down
4 changes: 2 additions & 2 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ function REPLServer(prompt,
displayErrors: true,
importModuleDynamically: (specifier, _, importAssertions) => {
return asyncESM.esmLoader.import(specifier, parentURL,
importAssertions);
importAssertions, true);
}
});
} catch (fallbackError) {
Expand Down Expand Up @@ -509,7 +509,7 @@ function REPLServer(prompt,
displayErrors: true,
importModuleDynamically: (specifier, _, importAssertions) => {
return asyncESM.esmLoader.import(specifier, parentURL,
importAssertions);
importAssertions, true);
}
});
} catch (e) {
Expand Down

0 comments on commit b341f63

Please sign in to comment.