Skip to content

Commit e9822be

Browse files
committed
esm: allow resolve to return import assertions
1 parent e8db11c commit e9822be

File tree

4 files changed

+70
-43
lines changed

4 files changed

+70
-43
lines changed

doc/api/esm.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -754,8 +754,9 @@ changes:
754754
* `specifier` {string}
755755
* `context` {Object}
756756
* `conditions` {string\[]} Export conditions of the relevant `package.json`
757-
* `importAssertions` {Object}
758-
* `parentURL` {string|undefined} The module importing this one, or undefined
757+
* `importAssertions` {Object} The object after the `assert` in an `import`
758+
statement, or the value of the `assert` property in the second argument of
759+
an `import()` expression; or an empty object
759760
if this is the Node.js entry point
760761
* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
761762
Node.js default `resolve` hook after the last user-supplied `resolve` hook
@@ -765,23 +766,26 @@ changes:
765766
* `format` {string|null|undefined} A hint to the load hook (it might be
766767
ignored)
767768
`'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
769+
* `importAssertions` {Object|undefined} The import assertions to use when
770+
caching the module (optional; if excluded the input will be used)
768771
* `shortCircuit` {undefined|boolean} A signal that this hook intends to
769772
terminate the chain of `resolve` hooks. **Default:** `false`
770773
* `url` {string} The absolute URL to which this input resolves
771774
772-
The `resolve` hook chain is responsible for resolving file URL for a given
773-
module specifier and parent URL, and optionally its format (such as `'module'`)
774-
as a hint to the `load` hook. If a format is specified, the `load` hook is
775-
ultimately responsible for providing the final `format` value (and it is free to
776-
ignore the hint provided by `resolve`); if `resolve` provides a `format`, a
777-
custom `load` hook is required even if only to pass the value to the Node.js
778-
default `load` hook.
779-
780-
The module specifier is the string in an `import` statement or
781-
`import()` expression.
782-
783-
The parent URL is the URL of the module that imported this one, or `undefined`
784-
if this is the main entry point for the application.
775+
The `resolve` hook chain is responsible for telling Node.js where to find and
776+
how to cache a given `import` statement or expression. It can optionally return
777+
its format (such as `'module'`) as a hint to the `load` hook. If a format is
778+
specified, the `load` hook is ultimately responsible for providing the final
779+
`format` value (and it is free to ignore the hint provided by `resolve`); if
780+
`resolve` provides a `format`, a custom `load` hook is required even if only to
781+
pass the value to the Node.js default `load` hook.
782+
783+
Import type assertions are part of the cache key for saving loaded modules into
784+
the Node.js internal module cache. The `resolve` hook is responsible for
785+
returning an `importAssertions` object if the module should be cached with
786+
different assertions than were present in the source code (for example, if no
787+
assertions were present but the module should be cached with assertions
788+
`{ type: 'json' }`).
785789
786790
The `conditions` property in `context` is an array of conditions for
787791
[package exports conditions][Conditional Exports] that apply to this resolution
@@ -857,7 +861,8 @@ changes:
857861
* `format` {string}
858862
* `shortCircuit` {undefined|boolean} A signal that this hook intends to
859863
terminate the chain of `resolve` hooks. **Default:** `false`
860-
* `source` {string|ArrayBuffer|TypedArray} The source for Node.js to evaluate
864+
* `source` {string|ArrayBuffer|TypedArray} The source for Node.js to
865+
evaluate
861866
862867
The `load` hook provides a way to define a custom method of determining how
863868
a URL should be interpreted, retrieved, and parsed. It is also in charge of

lib/internal/modules/esm/hooks.js

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Hooks {
116116
} = pluckHooks(exports);
117117

118118
if (globalPreload) {
119+
this.hasCustomLoadHooks = true;
119120
ArrayPrototypePush(
120121
this.#hooks.globalPreload,
121122
{
@@ -125,6 +126,7 @@ class Hooks {
125126
);
126127
}
127128
if (resolve) {
129+
this.hasCustomLoadHooks = true;
128130
ArrayPrototypePush(
129131
this.#hooks.resolve,
130132
{
@@ -318,21 +320,10 @@ class Hooks {
318320

319321
const {
320322
format,
323+
importAssertions: resolvedImportAssertions,
321324
url,
322325
} = resolution;
323326

324-
if (
325-
format != null &&
326-
typeof format !== 'string' // [2]
327-
) {
328-
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
329-
'a string',
330-
hookErrIdentifier,
331-
'format',
332-
format,
333-
);
334-
}
335-
336327
if (typeof url !== 'string') {
337328
// non-strings can be coerced to a URL string
338329
// validateString() throws a less-specific error
@@ -359,9 +350,34 @@ class Hooks {
359350
}
360351
}
361352

353+
if (
354+
resolvedImportAssertions != null &&
355+
typeof resolvedImportAssertions !== 'object'
356+
) {
357+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
358+
'an object',
359+
hookErrIdentifier,
360+
'importAssertions',
361+
resolvedImportAssertions,
362+
);
363+
}
364+
365+
if (
366+
format != null &&
367+
typeof format !== 'string' // [2]
368+
) {
369+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
370+
'a string',
371+
hookErrIdentifier,
372+
'format',
373+
format,
374+
);
375+
}
376+
362377
return {
363378
__proto__: null,
364379
format,
380+
importAssertions: resolvedImportAssertions,
365381
url,
366382
};
367383
}

lib/internal/modules/esm/loader.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ class ESMLoader {
155155
* module import.
156156
* @returns {Promise<ModuleJob>} The (possibly pending) module job
157157
*/
158-
async getModuleJob(specifier, parentURL, importAssertions) {
158+
async getModuleJob(specifier, parentURL, importAssertions = { __proto__: null }) {
159159
let importAssertionsForResolve;
160160

161161
// We can skip cloning if there are no user-provided loaders because
@@ -167,18 +167,19 @@ class ESMLoader {
167167
};
168168
}
169169

170-
const { format, url } =
171-
await this.resolve(specifier, parentURL, importAssertionsForResolve);
170+
const resolveResult = await this.resolve(specifier, parentURL, importAssertionsForResolve);
171+
const { url, format } = resolveResult;
172+
const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions;
172173

173-
let job = this.moduleMap.get(url, importAssertions.type);
174+
let job = this.moduleMap.get(url, resolvedImportAssertions.type);
174175

175176
// CommonJS will set functions for lazy job evaluation.
176177
if (typeof job === 'function') {
177178
this.moduleMap.set(url, undefined, job = job());
178179
}
179180

180181
if (job === undefined) {
181-
job = this.#createModuleJob(url, importAssertions, parentURL, format);
182+
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format);
182183
}
183184

184185
return job;
@@ -223,6 +224,7 @@ class ESMLoader {
223224
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
224225
process.send({ 'watch:import': [url] });
225226
}
227+
226228
const ModuleJob = require('internal/modules/esm/module_job');
227229
const job = new ModuleJob(
228230
this,
Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
const DATA_URL_PATTERN = /^data:application\/json(?:[^,]*?)(;base64)?,([\s\S]*)$/;
2-
const JSON_URL_PATTERN = /\.json(\?[^#]*)?(#.*)?$/;
2+
const JSON_URL_PATTERN = /^[^?]+\.json(\?[^#]*)?(#.*)?$/;
3+
4+
export async function resolve(specifier, context, next) {
5+
const noAssertionSpecified = context.importAssertions.type == null;
36

4-
export function resolve(url, context, next) {
57
// Mutation from resolve hook should be discarded.
68
context.importAssertions.type = 'whatever';
7-
return next(url);
8-
}
99

10-
export function load(url, context, next) {
11-
if (context.importAssertions.type == null &&
12-
(DATA_URL_PATTERN.test(url) || JSON_URL_PATTERN.test(url))) {
13-
const { importAssertions } = context;
14-
importAssertions.type = 'json';
10+
// This fixture assumes that no other resolve hooks in the chain will error on invalid import assertions
11+
// (as defaultResolve doesn't).
12+
const result = await next(specifier, context);
13+
14+
if (noAssertionSpecified &&
15+
(DATA_URL_PATTERN.test(result.url) || JSON_URL_PATTERN.test(result.url))) {
16+
result.importAssertions ??= context.importAssertions;
17+
result.importAssertions.type = 'json';
1518
}
16-
return next(url);
19+
20+
return result;
1721
}

0 commit comments

Comments
 (0)