Skip to content

Commit 2cc7a91

Browse files
aduh95GeoffreyBooth
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 2e2a6fe commit 2cc7a91

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
- v16.12.0
@@ -220,6 +223,28 @@ absolute URL strings.
220223
import fs from 'node:fs/promises';
221224
```
222225

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

225250
[Core modules][] provide named exports of their public API. A
@@ -517,10 +542,8 @@ same path.
517542
518543
Assuming an `index.mjs` with
519544
520-
<!-- eslint-skip -->
521-
522545
```js
523-
import packageConfig from './package.json';
546+
import packageConfig from './package.json' assert { type: 'json' };
524547
```
525548
526549
The `--experimental-json-modules` flag is needed for the module
@@ -608,12 +631,20 @@ CommonJS modules loaded.
608631
609632
#### `resolve(specifier, context, defaultResolve)`
610633
634+
<!-- YAML
635+
changes:
636+
- version: REPLACEME
637+
pr-url: https://github.com/nodejs/node/pull/40250
638+
description: Add support for import assertions.
639+
-->
640+
611641
> Note: The loaders API is being redesigned. This hook may disappear or its
612642
> signature may change. Do not rely on the API described below.
613643
614644
* `specifier` {string}
615645
* `context` {Object}
616646
* `conditions` {string\[]}
647+
* `importAssertions` {Object}
617648
* `parentURL` {string|undefined}
618649
* `defaultResolve` {Function} The Node.js default resolver.
619650
* Returns: {Object}
@@ -690,13 +721,15 @@ export async function resolve(specifier, context, defaultResolve) {
690721
* `context` {Object}
691722
* `format` {string|null|undefined} The format optionally supplied by the
692723
`resolve` hook.
724+
* `importAssertions` {Object}
693725
* `defaultLoad` {Function}
694726
* Returns: {Object}
695727
* `format` {string}
696728
* `source` {string|ArrayBuffer|TypedArray}
697729

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

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

@@ -1358,6 +1391,8 @@ success!
13581391
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13591392
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13601393
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
1394+
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
1395+
[JSON modules]: #json-modules
13611396
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13621397
[Terminology]: #terminology
13631398
[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)