Skip to content

Commit 1b8169a

Browse files
aduh95danielleadams
authored andcommitted
esm: add support for JSON import assertion
Remove V8 flag for import assertions, enabling support for the syntax; require the import assertion syntax for imports of JSON. Support import assertions in user loaders. Use both resolved module URL and import assertion type as the key for caching modules. Co-authored-by: Geoffrey Booth <[email protected]> PR-URL: #40250 Backport-PR-URL: #41776 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 036650e commit 1b8169a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+878
-169
lines changed

doc/api/errors.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,36 @@ is set for the `Http2Stream`.
16791679

16801680
An attempt was made to construct an object using a non-public constructor.
16811681

1682+
<a id="ERR_IMPORT_ASSERTION_TYPE_FAILED"></a>
1683+
1684+
### `ERR_IMPORT_ASSERTION_TYPE_FAILED`
1685+
1686+
<!-- YAML
1687+
added: REPLACEME
1688+
-->
1689+
1690+
An import assertion has failed, preventing the specified module to be imported.
1691+
1692+
<a id="ERR_IMPORT_ASSERTION_TYPE_MISSING"></a>
1693+
1694+
### `ERR_IMPORT_ASSERTION_TYPE_MISSING`
1695+
1696+
<!-- YAML
1697+
added: REPLACEME
1698+
-->
1699+
1700+
An import assertion is missing, preventing the specified module to be imported.
1701+
1702+
<a id="ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED"></a>
1703+
1704+
### `ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED`
1705+
1706+
<!-- YAML
1707+
added: REPLACEME
1708+
-->
1709+
1710+
An import assertion is not supported by this version of Node.js.
1711+
16821712
<a id="ERR_INCOMPATIBLE_OPTION_PAIR"></a>
16831713

16841714
### `ERR_INCOMPATIBLE_OPTION_PAIR`

doc/api/esm.md

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<!-- YAML
88
added: v8.5.0
99
changes:
10+
- version: REPLACEME
11+
pr-url: https://github.com/nodejs/node/pull/40250
12+
description: Add support for import assertions.
1013
- version:
1114
- v16.12.0
1215
pr-url: https://github.com/nodejs/node/pull/37468
@@ -216,6 +219,28 @@ absolute URL strings.
216219
import fs from 'node:fs/promises';
217220
```
218221

222+
## Import assertions
223+
224+
<!-- YAML
225+
added: REPLACEME
226+
-->
227+
228+
The [Import Assertions proposal][] adds an inline syntax for module import
229+
statements to pass on more information alongside the module specifier.
230+
231+
```js
232+
import fooData from './foo.json' assert { type: 'json' };
233+
234+
const { default: barData } =
235+
await import('./bar.json', { assert: { type: 'json' } });
236+
```
237+
238+
Node.js supports the following `type` values:
239+
240+
| `type` | Resolves to |
241+
| -------- | ---------------- |
242+
| `'json'` | [JSON modules][] |
243+
219244
## Builtin modules
220245

221246
[Core modules][] provide named exports of their public API. A
@@ -511,10 +536,8 @@ same path.
511536
512537
Assuming an `index.mjs` with
513538
514-
<!-- eslint-skip -->
515-
516539
```js
517-
import packageConfig from './package.json';
540+
import packageConfig from './package.json' assert { type: 'json' };
518541
```
519542
520543
The `--experimental-json-modules` flag is needed for the module
@@ -602,12 +625,20 @@ CommonJS modules loaded.
602625
603626
#### `resolve(specifier, context, defaultResolve)`
604627
628+
<!-- YAML
629+
changes:
630+
- version: REPLACEME
631+
pr-url: https://github.com/nodejs/node/pull/40250
632+
description: Add support for import assertions.
633+
-->
634+
605635
> Note: The loaders API is being redesigned. This hook may disappear or its
606636
> signature may change. Do not rely on the API described below.
607637
608638
* `specifier` {string}
609639
* `context` {Object}
610640
* `conditions` {string\[]}
641+
* `importAssertions` {Object}
611642
* `parentURL` {string|undefined}
612643
* `defaultResolve` {Function} The Node.js default resolver.
613644
* Returns: {Object}
@@ -684,13 +715,15 @@ export async function resolve(specifier, context, defaultResolve) {
684715
* `context` {Object}
685716
* `format` {string|null|undefined} The format optionally supplied by the
686717
`resolve` hook.
718+
* `importAssertions` {Object}
687719
* `defaultLoad` {Function}
688720
* Returns: {Object}
689721
* `format` {string}
690722
* `source` {string|ArrayBuffer|TypedArray}
691723

692724
The `load` hook provides a way to define a custom method of determining how
693-
a URL should be interpreted, retrieved, and parsed.
725+
a URL should be interpreted, retrieved, and parsed. It is also in charge of
726+
validating the import assertion.
694727

695728
The final value of `format` must be one of the following:
696729

@@ -1373,6 +1406,8 @@ success!
13731406
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13741407
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13751408
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
1409+
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
1410+
[JSON modules]: #json-modules
13761411
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13771412
[Terminology]: #terminology
13781413
[URL]: https://url.spec.whatwg.org/

lib/internal/errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,12 @@ E('ERR_HTTP_SOCKET_ENCODING',
10831083
E('ERR_HTTP_TRAILER_INVALID',
10841084
'Trailers are invalid with this transfer encoding', Error);
10851085
E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError);
1086+
E('ERR_IMPORT_ASSERTION_TYPE_FAILED',
1087+
'Module "%s" is not of type "%s"', TypeError);
1088+
E('ERR_IMPORT_ASSERTION_TYPE_MISSING',
1089+
'Module "%s" needs an import assertion of type "%s"', TypeError);
1090+
E('ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
1091+
'Import assertion type "%s" is unsupported', TypeError);
10861092
E('ERR_INCOMPATIBLE_OPTION_PAIR',
10871093
'Option "%s" cannot be used in combination with option "%s"', TypeError);
10881094
E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' +

lib/internal/modules/cjs/loader.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,9 +1021,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10211021
filename,
10221022
lineOffset: 0,
10231023
displayErrors: true,
1024-
importModuleDynamically: async (specifier) => {
1024+
importModuleDynamically: async (specifier, _, importAssertions) => {
10251025
const loader = asyncESM.esmLoader;
1026-
return loader.import(specifier, normalizeReferrerURL(filename));
1026+
return loader.import(specifier, normalizeReferrerURL(filename),
1027+
importAssertions);
10271028
},
10281029
});
10291030
}
@@ -1036,9 +1037,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10361037
'__dirname',
10371038
], {
10381039
filename,
1039-
importModuleDynamically(specifier) {
1040+
importModuleDynamically(specifier, _, importAssertions) {
10401041
const loader = asyncESM.esmLoader;
1041-
return loader.import(specifier, normalizeReferrerURL(filename));
1042+
return loader.import(specifier, normalizeReferrerURL(filename),
1043+
importAssertions);
10421044
},
10431045
});
10441046
} catch (err) {

lib/internal/modules/esm/assert.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use strict';
2+
3+
const {
4+
ArrayPrototypeIncludes,
5+
ObjectCreate,
6+
ObjectValues,
7+
ObjectPrototypeHasOwnProperty,
8+
Symbol,
9+
} = primordials;
10+
const { validateString } = require('internal/validators');
11+
12+
const {
13+
ERR_IMPORT_ASSERTION_TYPE_FAILED,
14+
ERR_IMPORT_ASSERTION_TYPE_MISSING,
15+
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
16+
} = require('internal/errors').codes;
17+
18+
const kImplicitAssertType = Symbol('implicit assert type');
19+
20+
/**
21+
* Define a map of module formats to import assertion types (the value of `type`
22+
* in `assert { type: 'json' }`).
23+
* @type {Map<string, string | typeof kImplicitAssertType}
24+
*/
25+
const formatTypeMap = {
26+
'__proto__': null,
27+
'builtin': kImplicitAssertType,
28+
'commonjs': kImplicitAssertType,
29+
'json': 'json',
30+
'module': kImplicitAssertType,
31+
'wasm': kImplicitAssertType, // Should probably be 'webassembly' per https://github.com/tc39/proposal-import-assertions
32+
};
33+
34+
/** @type {Array<string, string | typeof kImplicitAssertType} */
35+
const supportedAssertionTypes = ObjectValues(formatTypeMap);
36+
37+
38+
/**
39+
* Test a module's import assertions.
40+
* @param {string} url The URL of the imported module, for error reporting.
41+
* @param {string} format One of Node's supported translators
42+
* @param {Record<string, string>} importAssertions Validations for the
43+
* module import.
44+
* @returns {true}
45+
* @throws {TypeError} If the format and assertion type are incompatible.
46+
*/
47+
function validateAssertions(url, format,
48+
importAssertions = ObjectCreate(null)) {
49+
const validType = formatTypeMap[format];
50+
51+
switch (validType) {
52+
case undefined:
53+
// Ignore assertions for module types we don't recognize, to allow new
54+
// formats in the future.
55+
return true;
56+
57+
case importAssertions.type:
58+
// The asserted type is the valid type for this format.
59+
return true;
60+
61+
case kImplicitAssertType:
62+
// This format doesn't allow an import assertion type, so the property
63+
// must not be set on the import assertions object.
64+
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
65+
return true;
66+
}
67+
return handleInvalidType(url, importAssertions.type);
68+
69+
default:
70+
// There is an expected type for this format, but the value of
71+
// `importAssertions.type` was not it.
72+
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
73+
// `type` wasn't specified at all.
74+
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
75+
}
76+
handleInvalidType(url, importAssertions.type);
77+
}
78+
}
79+
80+
/**
81+
* Throw the correct error depending on what's wrong with the type assertion.
82+
* @param {string} url The resolved URL for the module to be imported
83+
* @param {string} type The value of the import assertion `type` property
84+
*/
85+
function handleInvalidType(url, type) {
86+
// `type` might have not been a string.
87+
validateString(type, 'type');
88+
89+
// `type` was not one of the types we understand.
90+
if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
91+
throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
92+
}
93+
94+
// `type` was the wrong value for this format.
95+
throw new ERR_IMPORT_ASSERTION_TYPE_FAILED(url, type);
96+
}
97+
98+
99+
module.exports = {
100+
kImplicitAssertType,
101+
validateAssertions,
102+
};

lib/internal/modules/esm/load.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,26 @@
33
const { defaultGetFormat } = require('internal/modules/esm/get_format');
44
const { defaultGetSource } = require('internal/modules/esm/get_source');
55
const { translators } = require('internal/modules/esm/translators');
6+
const { validateAssertions } = require('internal/modules/esm/assert');
67

8+
/**
9+
* Node.js default load hook.
10+
* @param {string} url
11+
* @param {object} context
12+
* @returns {object}
13+
*/
714
async function defaultLoad(url, context) {
815
let {
916
format,
1017
source,
1118
} = context;
19+
const { importAssertions } = context;
1220

13-
if (!translators.has(format)) format = defaultGetFormat(url);
21+
if (!format || !translators.has(format)) {
22+
format = defaultGetFormat(url);
23+
}
24+
25+
validateAssertions(url, format, importAssertions);
1426

1527
if (
1628
format === 'builtin' ||

0 commit comments

Comments
 (0)