Skip to content

Commit 4ff88dc

Browse files
committed
module: add 'webassembly' and 'javascript' types
1 parent ba05dd9 commit 4ff88dc

13 files changed

+272
-35
lines changed

doc/api/esm.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ import fs from 'node:fs/promises';
229229
added: v17.1.0
230230
-->
231231

232+
> Stability: 1 - Experimental
233+
232234
The [Import Assertions proposal][] adds an inline syntax for module import
233235
statements to pass on more information alongside the module specifier.
234236

@@ -239,11 +241,35 @@ const { default: barData } =
239241
await import('./bar.json', { assert: { type: 'json' } });
240242
```
241243

242-
Node.js supports the following `type` values:
244+
Node.js supports the following `type` values, for which the assertion is
245+
mandatory:
246+
247+
| Assertion `type` | Needed for |
248+
| ---------------- | ---------------- |
249+
| `'json'` | [JSON modules][] |
250+
| `'webassembly'` | [Wasm modules][] |
251+
252+
Imports of CommonJS and ES module JavaScript files, and Node.js core modules, do
253+
not need an import assertion; but they can be given a `type` of `'javascript'`
254+
if desired:
255+
256+
```js
257+
const url = getUrlOfModuleToBeDynamicallyImported();
258+
const { protocol, pathname } = new URL(url, import.meta.url);
259+
260+
let assertType = 'javascript';
261+
if (protocol === 'data:' ?
262+
pathname.startsWith('application/json') :
263+
pathname.endsWith('.json')) {
264+
assertType = 'json';
265+
} else if (protocol === 'data:' ?
266+
pathname.startsWith('application/wasm') :
267+
pathname.endsWith('.wasm')) {
268+
assertType = 'webassembly';
269+
}
243270

244-
| `type` | Resolves to |
245-
| -------- | ---------------- |
246-
| `'json'` | [JSON modules][] |
271+
const importedModule = await import(url, { assert: { type: assertType } });
272+
```
247273
248274
## Builtin modules
249275
@@ -554,6 +580,8 @@ node index.mjs # fails
554580
node --experimental-json-modules index.mjs # works
555581
```
556582
583+
The `assert { type: 'json' }` syntax is mandatory; see [Import Assertions][].
584+
557585
<i id="esm_experimental_wasm_modules"></i>
558586
559587
## Wasm modules
@@ -570,7 +598,7 @@ This integration is in line with the
570598
For example, an `index.mjs` containing:
571599
572600
```js
573-
import * as M from './module.wasm';
601+
import * as M from './module.wasm' assert { type: 'webassembly' };
574602
console.log(M);
575603
```
576604
@@ -582,6 +610,9 @@ node --experimental-wasm-modules index.mjs
582610
583611
would provide the exports interface for the instantiation of `module.wasm`.
584612
613+
The `assert { type: 'webassembly' }` syntax is mandatory; see
614+
[Import Assertions][].
615+
585616
<i id="esm_experimental_top_level_await"></i>
586617
587618
## Top-level `await`
@@ -1391,12 +1422,14 @@ success!
13911422
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13921423
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13931424
[ES Module Integration Proposal for WebAssembly]: https://github.com/webassembly/esm-integration
1425+
[Import Assertions]: #import-assertions
13941426
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
13951427
[JSON modules]: #json-modules
13961428
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13971429
[Terminology]: #terminology
13981430
[URL]: https://url.spec.whatwg.org/
13991431
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
1432+
[Wasm modules]: #wasm-modules
14001433
[`"exports"`]: packages.md#exports
14011434
[`"type"`]: packages.md#type
14021435
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer

lib/internal/modules/esm/assert.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ const {
55
ObjectCreate,
66
ObjectValues,
77
ObjectPrototypeHasOwnProperty,
8-
Symbol,
98
} = primordials;
109
const { validateString } = require('internal/validators');
1110

@@ -15,23 +14,25 @@ const {
1514
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
1615
} = require('internal/errors').codes;
1716

18-
const kImplicitAssertType = Symbol('implicit assert type');
17+
// Per the HTML spec, import statements without an assertion type imply a
18+
// `type` of `'javascript'`.
19+
const kImplicitAssertType = 'javascript';
1920

2021
/**
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}
22+
* Define a map of module formats to import assertion types (the value of
23+
* `type` in `assert { type: 'json' }`).
24+
* @type {Map<string, string>}
2425
*/
2526
const formatTypeMap = {
2627
'__proto__': null,
2728
'builtin': kImplicitAssertType,
2829
'commonjs': kImplicitAssertType,
2930
'json': 'json',
3031
'module': kImplicitAssertType,
31-
'wasm': kImplicitAssertType, // Should probably be 'webassembly' per https://github.com/tc39/proposal-import-assertions
32+
'wasm': 'webassembly',
3233
};
3334

34-
/** @type {Array<string, string | typeof kImplicitAssertType} */
35+
/** @type {string[]} */
3536
const supportedAssertionTypes = ObjectValues(formatTypeMap);
3637

3738

@@ -50,25 +51,29 @@ function validateAssertions(url, format,
5051

5152
switch (validType) {
5253
case undefined:
53-
// Ignore assertions for module types we don't recognize, to allow new
54+
// Ignore assertions for module formats we don't recognize, to allow new
5455
// formats in the future.
5556
return true;
5657

5758
case importAssertions.type:
5859
// The asserted type is the valid type for this format.
60+
// This case also covers when the implicit type is declared explicitly
61+
// (`assert { type: 'javascript' }`).
62+
process.emitWarning('Import assertions are experimental.',
63+
'ExperimentalWarning');
5964
return true;
6065

6166
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.
67+
// This format allows the type to be implied. (If it were declared
68+
// explicitly, it would have been caught already by the previous case.)
6469
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
6570
return true;
6671
}
6772
return handleInvalidType(url, importAssertions.type);
6873

6974
default:
7075
// There is an expected type for this format, but the value of
71-
// `importAssertions.type` was not it.
76+
// `importAssertions.type` might not have been it.
7277
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
7378
// `type` wasn't specified at all.
7479
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
@@ -86,7 +91,7 @@ function handleInvalidType(url, type) {
8691
// `type` might have not been a string.
8792
validateString(type, 'type');
8893

89-
// `type` was not one of the types we understand.
94+
// `type` might not have been one of the types we understand.
9095
if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
9196
throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
9297
}

lib/internal/modules/esm/module_map.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,17 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
1212
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
1313
const { validateString } = require('internal/validators');
1414

15-
const validateAssertType = (type) =>
16-
type === kImplicitAssertType || validateString(type, 'type');
17-
1815
// Tracks the state of the loader-level module cache
1916
class ModuleMap extends SafeMap {
2017
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
2118
get(url, type = kImplicitAssertType) {
2219
validateString(url, 'url');
23-
validateAssertType(type);
20+
validateString(type, 'type');
2421
return super.get(url)?.[type];
2522
}
2623
set(url, type = kImplicitAssertType, job) {
2724
validateString(url, 'url');
28-
validateAssertType(type);
25+
validateString(type, 'type');
2926
if (job instanceof ModuleJob !== true &&
3027
typeof job !== 'function') {
3128
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
@@ -39,7 +36,7 @@ class ModuleMap extends SafeMap {
3936
}
4037
has(url, type = kImplicitAssertType) {
4138
validateString(url, 'url');
42-
validateAssertType(type);
39+
validateString(type, 'type');
4340
return super.get(url)?.[type] !== undefined;
4441
}
4542
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { strictEqual } = require('assert');
4+
5+
async function test() {
6+
{
7+
const [secret0, secret1] = await Promise.all([
8+
import('../fixtures/es-modules/default-export.mjs'),
9+
import(
10+
'../fixtures/es-modules/default-export.mjs',
11+
{ assert: { type: 'javascript' } }
12+
),
13+
]);
14+
15+
strictEqual(secret0.default.ofLife, 42);
16+
strictEqual(secret1.default.ofLife, 42);
17+
strictEqual(secret0.default, secret1.default);
18+
strictEqual(secret0, secret1);
19+
}
20+
21+
{
22+
const [secret0, secret1] = await Promise.all([
23+
import(
24+
'../fixtures/es-modules/default-export.mjs',
25+
{ assert: { type: 'javascript' } }
26+
),
27+
import('../fixtures/es-modules/default-export.mjs'),
28+
]);
29+
30+
strictEqual(secret0.default.ofLife, 42);
31+
strictEqual(secret1.default.ofLife, 42);
32+
strictEqual(secret0.default, secret1.default);
33+
strictEqual(secret0, secret1);
34+
}
35+
36+
{
37+
const [secret0, secret1] = await Promise.all([
38+
import('../fixtures/es-modules/default-export.mjs?test'),
39+
import(
40+
'../fixtures/es-modules/default-export.mjs?test',
41+
{ assert: { type: 'javascript' } }
42+
),
43+
]);
44+
45+
strictEqual(secret0.default.ofLife, 42);
46+
strictEqual(secret1.default.ofLife, 42);
47+
strictEqual(secret0.default, secret1.default);
48+
strictEqual(secret0, secret1);
49+
}
50+
51+
{
52+
const [secret0, secret1] = await Promise.all([
53+
import('../fixtures/es-modules/default-export.mjs#test'),
54+
import(
55+
'../fixtures/es-modules/default-export.mjs#test',
56+
{ assert: { type: 'javascript' } }
57+
),
58+
]);
59+
60+
strictEqual(secret0.default.ofLife, 42);
61+
strictEqual(secret1.default.ofLife, 42);
62+
strictEqual(secret0.default, secret1.default);
63+
strictEqual(secret0, secret1);
64+
}
65+
66+
{
67+
const [secret0, secret1] = await Promise.all([
68+
import('../fixtures/es-modules/default-export.mjs?test2#test'),
69+
import(
70+
'../fixtures/es-modules/default-export.mjs?test2#test',
71+
{ assert: { type: 'javascript' } }
72+
),
73+
]);
74+
75+
strictEqual(secret0.default.ofLife, 42);
76+
strictEqual(secret1.default.ofLife, 42);
77+
strictEqual(secret0.default, secret1.default);
78+
strictEqual(secret0, secret1);
79+
}
80+
81+
{
82+
const [secret0, secret1] = await Promise.all([
83+
import('data:text/javascript,export default { ofLife: 42 }'),
84+
import(
85+
'data:text/javascript,export default { ofLife: 42 }',
86+
{ assert: { type: 'javascript' } }
87+
),
88+
]);
89+
90+
strictEqual(secret0.default.ofLife, 42);
91+
strictEqual(secret1.default.ofLife, 42);
92+
}
93+
}
94+
95+
test().then(common.mustCall());
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import '../common/index.mjs';
2+
import { strictEqual } from 'assert';
3+
4+
{
5+
const [secret0, secret1] = await Promise.all([
6+
import('../fixtures/es-modules/default-export.mjs'),
7+
import(
8+
'../fixtures/es-modules/default-export.mjs',
9+
{ assert: { type: 'javascript' } }
10+
),
11+
]);
12+
13+
strictEqual(secret0.default.ofLife, 42);
14+
strictEqual(secret1.default.ofLife, 42);
15+
strictEqual(secret0.default, secret1.default);
16+
strictEqual(secret0, secret1);
17+
}
18+
19+
{
20+
const [secret0, secret1] = await Promise.all([
21+
import(
22+
'../fixtures/es-modules/default-export.mjs',
23+
{ assert: { type: 'javascript' } }
24+
),
25+
import('../fixtures/es-modules/default-export.mjs'),
26+
]);
27+
28+
strictEqual(secret0.default.ofLife, 42);
29+
strictEqual(secret1.default.ofLife, 42);
30+
strictEqual(secret0.default, secret1.default);
31+
strictEqual(secret0, secret1);
32+
}
33+
34+
{
35+
const [secret0, secret1] = await Promise.all([
36+
import('../fixtures/es-modules/default-export.mjs?test'),
37+
import(
38+
'../fixtures/es-modules/default-export.mjs?test',
39+
{ assert: { type: 'javascript' } }
40+
),
41+
]);
42+
43+
strictEqual(secret0.default.ofLife, 42);
44+
strictEqual(secret1.default.ofLife, 42);
45+
strictEqual(secret0.default, secret1.default);
46+
strictEqual(secret0, secret1);
47+
}
48+
49+
{
50+
const [secret0, secret1] = await Promise.all([
51+
import('../fixtures/es-modules/default-export.mjs#test'),
52+
import(
53+
'../fixtures/es-modules/default-export.mjs#test',
54+
{ assert: { type: 'javascript' } }
55+
),
56+
]);
57+
58+
strictEqual(secret0.default.ofLife, 42);
59+
strictEqual(secret1.default.ofLife, 42);
60+
strictEqual(secret0.default, secret1.default);
61+
strictEqual(secret0, secret1);
62+
}
63+
64+
{
65+
const [secret0, secret1] = await Promise.all([
66+
import('../fixtures/es-modules/default-export.mjs?test2#test'),
67+
import(
68+
'../fixtures/es-modules/default-export.mjs?test2#test',
69+
{ assert: { type: 'javascript' } }
70+
),
71+
]);
72+
73+
strictEqual(secret0.default.ofLife, 42);
74+
strictEqual(secret1.default.ofLife, 42);
75+
strictEqual(secret0.default, secret1.default);
76+
strictEqual(secret0, secret1);
77+
}
78+
79+
{
80+
const [secret0, secret1] = await Promise.all([
81+
import('data:text/javascript,export default { ofLife: 42 }'),
82+
import(
83+
'data:text/javascript,export default { ofLife: 42 }',
84+
{ assert: { type: 'javascript' } }
85+
),
86+
]);
87+
88+
strictEqual(secret0.default.ofLife, 42);
89+
strictEqual(secret1.default.ofLife, 42);
90+
}

0 commit comments

Comments
 (0)