Skip to content

Commit 91ca2d4

Browse files
esm: allow resolve to return import assertions
PR-URL: #46153 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Michaël Zasso <targos@protonmail.com> Reviewed-By: Jacob Smith <jacob@frende.me>
1 parent a9bc3cf commit 91ca2d4

File tree

4 files changed

+71
-43
lines changed

4 files changed

+71
-43
lines changed

doc/api/esm.md

+16-14
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,8 @@ changes:
754754
* `specifier` {string}
755755
* `context` {Object}
756756
* `conditions` {string\[]} Export conditions of the relevant `package.json`
757-
* `importAssertions` {Object}
757+
* `importAssertions` {Object} An object whose key-value pairs represent the
758+
assertions for the module to import
758759
* `parentURL` {string|undefined} The module importing this one, or undefined
759760
if this is the Node.js entry point
760761
* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
@@ -765,23 +766,24 @@ 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 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.
785787
786788
The `conditions` property in `context` is an array of conditions for
787789
[package exports conditions][Conditional Exports] that apply to this resolution

lib/internal/modules/esm/hooks.js

+29-14
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class Hooks {
8484
};
8585

8686
// Enable an optimization in ESMLoader.getModuleJob
87-
hasCustomLoadHooks = false;
87+
hasCustomResolveOrLoadHooks = false;
8888

8989
// Cache URLs we've already validated to avoid repeated validation
9090
#validatedUrls = new SafeSet();
@@ -125,6 +125,7 @@ class Hooks {
125125
);
126126
}
127127
if (resolve) {
128+
this.hasCustomResolveOrLoadHooks = true;
128129
ArrayPrototypePush(
129130
this.#hooks.resolve,
130131
{
@@ -134,7 +135,7 @@ class Hooks {
134135
);
135136
}
136137
if (load) {
137-
this.hasCustomLoadHooks = true;
138+
this.hasCustomResolveOrLoadHooks = true;
138139
ArrayPrototypePush(
139140
this.#hooks.load,
140141
{
@@ -318,21 +319,10 @@ class Hooks {
318319

319320
const {
320321
format,
322+
importAssertions: resolvedImportAssertions,
321323
url,
322324
} = resolution;
323325

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-
336326
if (typeof url !== 'string') {
337327
// non-strings can be coerced to a URL string
338328
// validateString() throws a less-specific error
@@ -359,9 +349,34 @@ class Hooks {
359349
}
360350
}
361351

352+
if (
353+
resolvedImportAssertions != null &&
354+
typeof resolvedImportAssertions !== 'object'
355+
) {
356+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
357+
'an object',
358+
hookErrIdentifier,
359+
'importAssertions',
360+
resolvedImportAssertions,
361+
);
362+
}
363+
364+
if (
365+
format != null &&
366+
typeof format !== 'string' // [2]
367+
) {
368+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
369+
'a string',
370+
hookErrIdentifier,
371+
'format',
372+
format,
373+
);
374+
}
375+
362376
return {
363377
__proto__: null,
364378
format,
379+
importAssertions: resolvedImportAssertions,
365380
url,
366381
};
367382
}

lib/internal/modules/esm/loader.js

+9-5
Original file line numberDiff line numberDiff line change
@@ -160,25 +160,28 @@ class ESMLoader {
160160

161161
// We can skip cloning if there are no user-provided loaders because
162162
// the Node.js default resolve hook does not use import assertions.
163-
if (this.#hooks?.hasCustomLoadHooks) {
163+
if (this.#hooks?.hasCustomResolveOrLoadHooks) {
164+
// This method of cloning only works so long as import assertions cannot contain objects as values,
165+
// which they currently cannot per spec.
164166
importAssertionsForResolve = {
165167
__proto__: null,
166168
...importAssertions,
167169
};
168170
}
169171

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

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

175178
// CommonJS will set functions for lazy job evaluation.
176179
if (typeof job === 'function') {
177180
this.moduleMap.set(url, undefined, job = job());
178181
}
179182

180183
if (job === undefined) {
181-
job = this.#createModuleJob(url, importAssertions, parentURL, format);
184+
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format);
182185
}
183186

184187
return job;
@@ -223,6 +226,7 @@ class ESMLoader {
223226
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
224227
process.send({ 'watch:import': [url] });
225228
}
229+
226230
const ModuleJob = require('internal/modules/esm/module_job');
227231
const job = new ModuleJob(
228232
this,
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
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+
// Clean new import assertions object to ensure that this test isn't passing due to mutation.
17+
result.importAssertions = {
18+
...(result.importAssertions ?? context.importAssertions),
19+
type: 'json',
20+
};
1521
}
16-
return next(url);
22+
23+
return result;
1724
}

0 commit comments

Comments
 (0)