Skip to content

Commit 95e4d29

Browse files
aduh95targos
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 Reviewed-By: Bradley Farias <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Geoffrey Booth <[email protected]>
1 parent c29658f commit 95e4d29

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

+885
-169
lines changed

doc/api/errors.md

+30
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,36 @@ is set for the `Http2Stream`.
16891689

16901690
An attempt was made to construct an object using a non-public constructor.
16911691

1692+
<a id="ERR_IMPORT_ASSERTION_TYPE_FAILED"></a>
1693+
1694+
### `ERR_IMPORT_ASSERTION_TYPE_FAILED`
1695+
1696+
<!-- YAML
1697+
added: REPLACEME
1698+
-->
1699+
1700+
An import assertion has failed, preventing the specified module to be imported.
1701+
1702+
<a id="ERR_IMPORT_ASSERTION_TYPE_MISSING"></a>
1703+
1704+
### `ERR_IMPORT_ASSERTION_TYPE_MISSING`
1705+
1706+
<!-- YAML
1707+
added: REPLACEME
1708+
-->
1709+
1710+
An import assertion is missing, preventing the specified module to be imported.
1711+
1712+
<a id="ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED"></a>
1713+
1714+
### `ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED`
1715+
1716+
<!-- YAML
1717+
added: REPLACEME
1718+
-->
1719+
1720+
An import assertion is not supported by this version of Node.js.
1721+
16921722
<a id="ERR_INCOMPATIBLE_OPTION_PAIR"></a>
16931723

16941724
### `ERR_INCOMPATIBLE_OPTION_PAIR`

doc/api/esm.md

+39-4
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
- v17.0.0
1215
pr-url: https://github.com/nodejs/node/pull/37468
@@ -219,6 +222,28 @@ absolute URL strings.
219222
import fs from 'node:fs/promises';
220223
```
221224

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

224249
[Core modules][] provide named exports of their public API. A
@@ -516,10 +541,8 @@ same path.
516541
517542
Assuming an `index.mjs` with
518543
519-
<!-- eslint-skip -->
520-
521544
```js
522-
import packageConfig from './package.json';
545+
import packageConfig from './package.json' assert { type: 'json' };
523546
```
524547
525548
The `--experimental-json-modules` flag is needed for the module
@@ -607,12 +630,20 @@ CommonJS modules loaded.
607630
608631
#### `resolve(specifier, context, defaultResolve)`
609632
633+
<!-- YAML
634+
changes:
635+
- version: REPLACEME
636+
pr-url: https://github.com/nodejs/node/pull/40250
637+
description: Add support for import assertions.
638+
-->
639+
610640
> Note: The loaders API is being redesigned. This hook may disappear or its
611641
> signature may change. Do not rely on the API described below.
612642
613643
* `specifier` {string}
614644
* `context` {Object}
615645
* `conditions` {string\[]}
646+
* `importAssertions` {Object}
616647
* `parentURL` {string|undefined}
617648
* `defaultResolve` {Function} The Node.js default resolver.
618649
* Returns: {Object}
@@ -689,13 +720,15 @@ export async function resolve(specifier, context, defaultResolve) {
689720
* `context` {Object}
690721
* `format` {string|null|undefined} The format optionally supplied by the
691722
`resolve` hook.
723+
* `importAssertions` {Object}
692724
* `defaultLoad` {Function}
693725
* Returns: {Object}
694726
* `format` {string}
695727
* `source` {string|ArrayBuffer|TypedArray}
696728

697729
The `load` hook provides a way to define a custom method of determining how
698-
a URL should be interpreted, retrieved, and parsed.
730+
a URL should be interpreted, retrieved, and parsed. It is also in charge of
731+
validating the import assertion.
699732

700733
The final value of `format` must be one of the following:
701734

@@ -1357,6 +1390,8 @@ success!
13571390
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13581391
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13591392
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
1393+
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
1394+
[JSON modules]: #json-modules
13601395
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13611396
[Terminology]: #terminology
13621397
[URL]: https://url.spec.whatwg.org/

lib/internal/errors.js

+6
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

+6-4
Original file line numberDiff line numberDiff line change
@@ -1015,9 +1015,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10151015
filename,
10161016
lineOffset: 0,
10171017
displayErrors: true,
1018-
importModuleDynamically: async (specifier) => {
1018+
importModuleDynamically: async (specifier, _, importAssertions) => {
10191019
const loader = asyncESM.esmLoader;
1020-
return loader.import(specifier, normalizeReferrerURL(filename));
1020+
return loader.import(specifier, normalizeReferrerURL(filename),
1021+
importAssertions);
10211022
},
10221023
});
10231024
}
@@ -1030,9 +1031,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10301031
'__dirname',
10311032
], {
10321033
filename,
1033-
importModuleDynamically(specifier) {
1034+
importModuleDynamically(specifier, _, importAssertions) {
10341035
const loader = asyncESM.esmLoader;
1035-
return loader.import(specifier, normalizeReferrerURL(filename));
1036+
return loader.import(specifier, normalizeReferrerURL(filename),
1037+
importAssertions);
10361038
},
10371039
});
10381040
} catch (err) {

lib/internal/modules/esm/assert.js

+102
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

+13-1
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)