Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module: Import assertions improvements #40785

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ import fs from 'node:fs/promises';
added: v17.1.0
-->

> Stability: 1 - Experimental

The [Import Assertions proposal][] adds an inline syntax for module import
statements to pass on more information alongside the module specifier.

Expand All @@ -239,11 +241,12 @@ const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
```

Node.js supports the following `type` values:
Node.js supports the following `type` values, for which the assertion is
mandatory:
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

| `type` | Resolves to |
| -------- | ---------------- |
| `'json'` | [JSON modules][] |
| Assertion `type` | Needed for |
| ---------------- | ---------------- |
| `'json'` | [JSON modules][] |

## Builtin modules

Expand Down Expand Up @@ -554,18 +557,20 @@ node index.mjs # fails
node --experimental-json-modules index.mjs # works
```

The `assert { type: 'json' }` syntax is mandatory; see [Import Assertions][].

<i id="esm_experimental_wasm_modules"></i>

## Wasm modules

> Stability: 1 - Experimental

Importing Web Assembly modules is supported under the
Importing WebAssembly modules is supported under the
`--experimental-wasm-modules` flag, allowing any `.wasm` files to be
imported as normal modules while also supporting their module imports.

This integration is in line with the
[ES Module Integration Proposal for Web Assembly][].
[ES Module Integration Proposal for WebAssembly][].

For example, an `index.mjs` containing:

Expand Down Expand Up @@ -1390,7 +1395,8 @@ success!
[Core modules]: modules.md#core-modules
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
[ES Module Integration Proposal for WebAssembly]: https://github.com/webassembly/esm-integration
[Import Assertions]: #import-assertions
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
[JSON modules]: #json-modules
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
Expand Down
38 changes: 23 additions & 15 deletions lib/internal/modules/esm/assert.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use strict';

const {
ArrayPrototypeFilter,
ArrayPrototypeIncludes,
ObjectCreate,
ObjectValues,
ObjectPrototypeHasOwnProperty,
Symbol,
} = primordials;
const { validateString } = require('internal/validators');

Expand All @@ -15,24 +15,32 @@ const {
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
} = require('internal/errors').codes;

const kImplicitAssertType = Symbol('implicit assert type');
// The HTML spec has an implied default type of `'javascript'`.
const kImplicitAssertType = 'javascript';

/**
* Define a map of module formats to import assertion types (the value of `type`
* in `assert { type: 'json' }`).
* @type {Map<string, string | typeof kImplicitAssertType}
* Define a map of module formats to import assertion types (the value of
* `type` in `assert { type: 'json' }`).
* @type {Map<string, string>}
*/
const formatTypeMap = {
'__proto__': null,
'builtin': kImplicitAssertType,
'commonjs': kImplicitAssertType,
'json': 'json',
'module': kImplicitAssertType,
'wasm': kImplicitAssertType, // Should probably be 'webassembly' per https://github.com/tc39/proposal-import-assertions
'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an assertion type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
};

/** @type {Array<string, string | typeof kImplicitAssertType} */
const supportedAssertionTypes = ObjectValues(formatTypeMap);
/**
* The HTML spec disallows the default type to be explicitly specified
* (for now); so `import './file.js'` is okay but
* `import './file.js' assert { type: 'javascript' }` throws.
* @type {Array<string, string>}
*/
const supportedAssertionTypes = ArrayPrototypeFilter(
ObjectValues(formatTypeMap),
(type) => type !== kImplicitAssertType);


/**
Expand All @@ -50,14 +58,10 @@ function validateAssertions(url, format,

switch (validType) {
case undefined:
// Ignore assertions for module types we don't recognize, to allow new
// Ignore assertions for module formats we don't recognize, to allow new
// formats in the future.
return true;

case importAssertions.type:
// The asserted type is the valid type for this format.
return true;

case kImplicitAssertType:
// This format doesn't allow an import assertion type, so the property
// must not be set on the import assertions object.
Expand All @@ -66,9 +70,13 @@ function validateAssertions(url, format,
}
return handleInvalidType(url, importAssertions.type);

case importAssertions.type:
// The asserted type is the valid type for this format.
return true;

default:
// There is an expected type for this format, but the value of
// `importAssertions.type` was not it.
// `importAssertions.type` might not have been it.
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
// `type` wasn't specified at all.
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
Expand All @@ -86,7 +94,7 @@ function handleInvalidType(url, type) {
// `type` might have not been a string.
validateString(type, 'type');

// `type` was not one of the types we understand.
// `type` might not have been one of the types we understand.
if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
}
Expand Down
9 changes: 3 additions & 6 deletions lib/internal/modules/esm/module_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,17 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { validateString } = require('internal/validators');

const validateAssertType = (type) =>
type === kImplicitAssertType || validateString(type, 'type');

// Tracks the state of the loader-level module cache
class ModuleMap extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
get(url, type = kImplicitAssertType) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
return super.get(url)?.[type];
}
set(url, type = kImplicitAssertType, job) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
if (job instanceof ModuleJob !== true &&
typeof job !== 'function') {
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
Expand All @@ -39,7 +36,7 @@ class ModuleMap extends SafeMap {
}
has(url, type = kImplicitAssertType) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
return super.get(url)?.[type] !== undefined;
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ translators.set('json', async function jsonStrategy(url, source) {

// Strategy for loading a wasm module
translators.set('wasm', async function(url, source) {
emitExperimentalWarning('Importing Web Assembly modules');
emitExperimentalWarning('Importing WebAssembly modules');

assertBufferSource(source, false, 'load');

Expand Down
2 changes: 1 addition & 1 deletion test/es-module/test-esm-import-assertion-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function test() {
);

await rejects(
import('data:text/javascript,', { assert: { type: 'unsupported' } }),
import(jsModuleDataUrl, { assert: { type: 'unsupported' } }),
{ code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED' }
);

Expand Down
4 changes: 2 additions & 2 deletions test/es-module/test-esm-import-assertion-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ assert.throws(() => validateAssertions(url, 'module', { type: 'json' }), {
code: 'ERR_IMPORT_ASSERTION_TYPE_FAILED',
});

// This should be allowed according to HTML spec. Let's keep it disabled
// until WASM module import is sorted out.
// The HTML spec specifically disallows this for now, while Wasm module import
// and whether it will require a type assertion is still an open question.
assert.throws(() => validateAssertions(url, 'module', { type: 'javascript' }), {
code: 'ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
});
Expand Down
5 changes: 2 additions & 3 deletions test/es-module/test-esm-loader-modulemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ const { strictEqual, throws } = require('assert');
const { ESMLoader } = require('internal/modules/esm/loader');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
const { kImplicitAssertType } = require('internal/modules/esm/assert');
const createDynamicModule = require(
'internal/modules/esm/create_dynamic_module');

Expand Down Expand Up @@ -38,14 +37,14 @@ const jsonModuleJob = new ModuleJob(loader, stubJsonModule.module,
strictEqual(moduleMap.get(jsonModuleDataUrl, 'json'), jsonModuleJob);

strictEqual(moduleMap.has(jsModuleDataUrl), true);
strictEqual(moduleMap.has(jsModuleDataUrl, kImplicitAssertType), true);
strictEqual(moduleMap.has(jsModuleDataUrl, 'javascript'), true);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'json'), true);

strictEqual(moduleMap.has('unknown'), false);

// The types must match
strictEqual(moduleMap.has(jsModuleDataUrl, 'json'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl, kImplicitAssertType), false);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'javascript'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl), false);
strictEqual(moduleMap.has(jsModuleDataUrl, 'unknown'), false);
strictEqual(moduleMap.has(jsonModuleDataUrl, 'unknown'), false);
Expand Down
2 changes: 1 addition & 1 deletion test/es-module/test-esm-wasm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ child.on('close', (code, signal) => {
strictEqual(code, 0);
strictEqual(signal, null);
ok(stderr.toString().includes(
'ExperimentalWarning: Importing Web Assembly modules is ' +
'ExperimentalWarning: Importing WebAssembly modules is ' +
'an experimental feature. This feature could change at any time'
));
});