Skip to content

Commit 1a9e7d6

Browse files
committed
esm: resolve optionally returns import assertions
1 parent 757c104 commit 1a9e7d6

File tree

5 files changed

+79
-59
lines changed

5 files changed

+79
-59
lines changed

doc/api/esm.md

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -754,34 +754,39 @@ 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
760+
* `parentURL` {string | undefined} The module importing this one, or undefined
759761
if this is the Node.js entry point
760762
* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
761763
Node.js default `resolve` hook after the last user-supplied `resolve` hook
762764
* `specifier` {string}
763765
* `context` {Object}
764766
* Returns: {Object}
765-
* `format` {string|null|undefined} A hint to the load hook (it might be
767+
* `format` {string | null | undefined} A hint to the load hook (it might be
766768
ignored)
767769
`'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
768-
* `shortCircuit` {undefined|boolean} A signal that this hook intends to
770+
* `importAssertions` {Object | undefined} The import assertions to use when
771+
caching the module (optional; if excluded the input will be used)
772+
* `shortCircuit` {undefined | boolean} A signal that this hook intends to
769773
terminate the chain of `resolve` hooks. **Default:** `false`
770774
* `url` {string} The absolute URL to which this input resolves
771775
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.
776+
The `resolve` hook chain is responsible for telling Node.js where to find and
777+
how to cache a given `import` statement or expression. It can optionally return
778+
its format (such as `'module'`) as a hint to the `load` hook. If a format is
779+
specified, the `load` hook is ultimately responsible for providing the final
780+
`format` value (and it is free to ignore the hint provided by `resolve`); if
781+
`resolve` provides a `format`, a custom `load` hook is required even if only to
782+
pass the value to the Node.js default `load` hook.
783+
784+
Import assertions are part of the cache key for saving loaded modules into the
785+
Node.js internal module cache. The `resolve` hook is responsible for returning
786+
an `importAssertions` object if the module should be cached with different
787+
assertions than were present in the source code (for example, if no assertions
788+
were present but the module should be cached with assertions
789+
`{ type: 'json' }`).
785790
786791
The `conditions` property in `context` is an array of conditions for
787792
[package exports conditions][Conditional Exports] that apply to this resolution
@@ -846,7 +851,7 @@ changes:
846851
* `url` {string} The URL returned by the `resolve` chain
847852
* `context` {Object}
848853
* `conditions` {string\[]} Export conditions of the relevant `package.json`
849-
* `format` {string|null|undefined} The format optionally supplied by the
854+
* `format` {string | null | undefined} The format optionally supplied by the
850855
`resolve` hook chain
851856
* `importAssertions` {Object}
852857
* `nextLoad` {Function} The subsequent `load` hook in the chain, or the
@@ -855,9 +860,10 @@ changes:
855860
* `context` {Object}
856861
* Returns: {Object}
857862
* `format` {string}
858-
* `shortCircuit` {undefined|boolean} A signal that this hook intends to
863+
* `shortCircuit` {undefined | boolean} A signal that this hook intends to
859864
terminate the chain of `resolve` hooks. **Default:** `false`
860-
* `source` {string|ArrayBuffer|TypedArray} The source for Node.js to evaluate
865+
* `source` {string | ArrayBuffer | TypedArray} The source for Node.js to
866+
evaluate
861867
862868
The `load` hook provides a way to define a custom method of determining how
863869
a URL should be interpreted, retrieved, and parsed. It is also in charge of

lib/internal/modules/esm/hooks.js

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -316,22 +316,7 @@ class Hooks {
316316
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
317317
}
318318

319-
const {
320-
format,
321-
url,
322-
} = resolution;
323-
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-
}
319+
const { url, importAssertions: resolvedImportAssertions, format } = resolution;
335320

336321
if (typeof url !== 'string') {
337322
// non-strings can be coerced to a URL string
@@ -359,10 +344,35 @@ class Hooks {
359344
}
360345
}
361346

347+
if (
348+
resolvedImportAssertions != null &&
349+
typeof resolvedImportAssertions !== 'object'
350+
) {
351+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
352+
'an object',
353+
hookErrIdentifier,
354+
'importAssertions',
355+
resolvedImportAssertions,
356+
);
357+
}
358+
359+
if (
360+
format != null &&
361+
typeof format !== 'string' // [2]
362+
) {
363+
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
364+
'a string',
365+
hookErrIdentifier,
366+
'format',
367+
format,
368+
);
369+
}
370+
362371
return {
363372
__proto__: null,
364-
format,
365373
url,
374+
importAssertions: resolvedImportAssertions,
375+
format,
366376
};
367377
}
368378

lib/internal/modules/esm/loader.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,27 +158,29 @@ class ESMLoader {
158158
async getModuleJob(specifier, parentURL, importAssertions) {
159159
let importAssertionsForResolve;
160160

161-
// We can skip cloning if there are no user-provided loaders because
162-
// the Node.js default resolve hook does not use import assertions.
163161
if (this.#hooks?.hasCustomLoadHooks) {
164162
importAssertionsForResolve = {
165163
__proto__: null,
166164
...importAssertions,
167165
};
166+
} else {
167+
// We can skip cloning if there are no user-provided loaders.
168+
importAssertionsForResolve = importAssertions;
168169
}
169170

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

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

175177
// CommonJS will set functions for lazy job evaluation.
176178
if (typeof job === 'function') {
177179
this.moduleMap.set(url, undefined, job = job());
178180
}
179181

180182
if (job === undefined) {
181-
job = this.#createModuleJob(url, importAssertions, parentURL, format);
183+
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format);
182184
}
183185

184186
return job;
@@ -223,6 +225,7 @@ class ESMLoader {
223225
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
224226
process.send({ 'watch:import': [url] });
225227
}
228+
226229
const ModuleJob = require('internal/modules/esm/module_job');
227230
const job = new ModuleJob(
228231
this,
@@ -299,11 +302,7 @@ class ESMLoader {
299302
* statement or expression.
300303
* @returns {Promise<{ format: string, url: URL['href'] }>}
301304
*/
302-
async resolve(
303-
originalSpecifier,
304-
parentURL,
305-
importAssertions = { __proto__: null },
306-
) {
305+
async resolve(originalSpecifier, parentURL, importAssertions = { __proto__: null }) {
307306
if (this.#hooks) {
308307
return this.#hooks.resolve(originalSpecifier, parentURL, importAssertions);
309308
}

lib/internal/modules/esm/resolve.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,7 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
966966
}
967967

968968
async function defaultResolve(specifier, context = {}) {
969+
const { importAssertions } = context;
969970
let { parentURL, conditions } = context;
970971
if (parentURL && policy?.manifest) {
971972
const redirects = policy.manifest.getDependencyMapper(parentURL);
@@ -1017,7 +1018,7 @@ async function defaultResolve(specifier, context = {}) {
10171018
)
10181019
)
10191020
) {
1020-
return { __proto__: null, url: parsed.href };
1021+
return { __proto__: null, url: parsed.href, importAssertions };
10211022
}
10221023
} catch {
10231024
// Ignore exception
@@ -1035,7 +1036,9 @@ async function defaultResolve(specifier, context = {}) {
10351036
if (maybeReturn) return maybeReturn;
10361037

10371038
// This must come after checkIfDisallowedImport
1038-
if (parsed && parsed.protocol === 'node:') return { __proto__: null, url: specifier };
1039+
if (parsed && parsed.protocol === 'node:') {
1040+
return { __proto__: null, url: specifier, importAssertions };
1041+
}
10391042

10401043
throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports);
10411044

@@ -1091,6 +1094,7 @@ async function defaultResolve(specifier, context = {}) {
10911094
// Do NOT cast `url` to a string: that will work even when there are real
10921095
// problems, silencing them
10931096
url: url.href,
1097+
importAssertions,
10941098
format: defaultGetFormatWithoutErrors(url, context),
10951099
};
10961100
}

test/fixtures/es-module-loaders/assertionless-json-import.mjs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ const DATA_URL_PATTERN = /^data:application\/json(?:[^,]*?)(;base64)?,([\s\S]*)$
22
const JSON_URL_PATTERN = /\.json(\?[^#]*)?(#.*)?$/;
33

44
export function resolve(url, context, next) {
5+
const resolvedImportAssertions = {}
6+
if (context.importAssertions.type) {
7+
resolvedImportAssertions.type = context.importAssertions.type;
8+
}
9+
10+
if (resolvedImportAssertions.type == null && (DATA_URL_PATTERN.test(url) || JSON_URL_PATTERN.test(url))) {
11+
resolvedImportAssertions.type = 'json';
12+
}
13+
514
// Mutation from resolve hook should be discarded.
615
context.importAssertions.type = 'whatever';
7-
return next(url);
8-
}
916

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';
15-
}
16-
return next(url);
17+
return next(url, { ...context, importAssertions: resolvedImportAssertions });
1718
}

0 commit comments

Comments
 (0)