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

[v16.x Backport] Import assertions related PRs #41776

Closed
Closed
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
30 changes: 30 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
@@ -1679,6 +1679,36 @@ is set for the `Http2Stream`.

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

<a id="ERR_IMPORT_ASSERTION_TYPE_FAILED"></a>

### `ERR_IMPORT_ASSERTION_TYPE_FAILED`

<!-- YAML
added: REPLACEME
-->

An import assertion has failed, preventing the specified module to be imported.

<a id="ERR_IMPORT_ASSERTION_TYPE_MISSING"></a>

### `ERR_IMPORT_ASSERTION_TYPE_MISSING`

<!-- YAML
added: REPLACEME
-->

An import assertion is missing, preventing the specified module to be imported.

<a id="ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED"></a>

### `ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED`

<!-- YAML
added: REPLACEME
-->

An import assertion is not supported by this version of Node.js.

<a id="ERR_INCOMPATIBLE_OPTION_PAIR"></a>

### `ERR_INCOMPATIBLE_OPTION_PAIR`
55 changes: 48 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,9 @@
<!-- YAML
added: v8.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/40250
description: Add support for import assertions.
- version:
- v16.12.0
pr-url: https://github.com/nodejs/node/pull/37468
@@ -216,6 +219,31 @@ absolute URL strings.
import fs from 'node:fs/promises';
```

## Import assertions

<!-- YAML
added: REPLACEME
-->

> Stability: 1 - Experimental

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

```js
import fooData from './foo.json' assert { type: 'json' };

const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
```

Node.js supports the following `type` values, for which the assertion is
mandatory:

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

## Builtin modules

[Core modules][] provide named exports of their public API. A
@@ -511,10 +539,8 @@ same path.

Assuming an `index.mjs` with

<!-- eslint-skip -->

```js
import packageConfig from './package.json';
import packageConfig from './package.json' assert { type: 'json' };
```

The `--experimental-json-modules` flag is needed for the module
@@ -525,18 +551,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:

@@ -602,12 +630,20 @@ CommonJS modules loaded.

#### `resolve(specifier, context, defaultResolve)`

<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/40250
description: Add support for import assertions.
-->

> Note: The loaders API is being redesigned. This hook may disappear or its
> signature may change. Do not rely on the API described below.

* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]}
* `importAssertions` {Object}
* `parentURL` {string|undefined}
* `defaultResolve` {Function} The Node.js default resolver.
* Returns: {Object}
@@ -684,13 +720,15 @@ export async function resolve(specifier, context, defaultResolve) {
* `context` {Object}
* `format` {string|null|undefined} The format optionally supplied by the
`resolve` hook.
* `importAssertions` {Object}
* `defaultLoad` {Function}
* Returns: {Object}
* `format` {string}
* `source` {string|ArrayBuffer|TypedArray}

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

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

@@ -1372,7 +1410,10 @@ 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
[Terminology]: #terminology
[URL]: https://url.spec.whatwg.org/
6 changes: 6 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
@@ -1083,6 +1083,12 @@ E('ERR_HTTP_SOCKET_ENCODING',
E('ERR_HTTP_TRAILER_INVALID',
'Trailers are invalid with this transfer encoding', Error);
E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError);
E('ERR_IMPORT_ASSERTION_TYPE_FAILED',
'Module "%s" is not of type "%s"', TypeError);
E('ERR_IMPORT_ASSERTION_TYPE_MISSING',
'Module "%s" needs an import assertion of type "%s"', TypeError);
E('ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
'Import assertion type "%s" is unsupported', TypeError);
E('ERR_INCOMPATIBLE_OPTION_PAIR',
'Option "%s" cannot be used in combination with option "%s"', TypeError);
E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' +
10 changes: 6 additions & 4 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
@@ -1021,9 +1021,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: async (specifier) => {
importModuleDynamically: async (specifier, _, importAssertions) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});
}
@@ -1036,9 +1037,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
'__dirname',
], {
filename,
importModuleDynamically(specifier) {
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename));
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
},
});
} catch (err) {
110 changes: 110 additions & 0 deletions lib/internal/modules/esm/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use strict';

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

const {
ERR_IMPORT_ASSERTION_TYPE_FAILED,
ERR_IMPORT_ASSERTION_TYPE_MISSING,
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
} = require('internal/errors').codes;

// 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>}
*/
const formatTypeMap = {
'__proto__': null,
'builtin': kImplicitAssertType,
'commonjs': kImplicitAssertType,
'json': 'json',
'module': kImplicitAssertType,
'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
};

/**
* 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);


/**
* Test a module's import assertions.
* @param {string} url The URL of the imported module, for error reporting.
* @param {string} format One of Node's supported translators
* @param {Record<string, string>} importAssertions Validations for the
* module import.
* @returns {true}
* @throws {TypeError} If the format and assertion type are incompatible.
*/
function validateAssertions(url, format,
importAssertions = ObjectCreate(null)) {
const validType = formatTypeMap[format];

switch (validType) {
case undefined:
// Ignore assertions for module formats we don't recognize, to allow new
// formats in the future.
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.
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
return true;
}
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` might not have been it.
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
// `type` wasn't specified at all.
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
}
handleInvalidType(url, importAssertions.type);
}
}

/**
* Throw the correct error depending on what's wrong with the type assertion.
* @param {string} url The resolved URL for the module to be imported
* @param {string} type The value of the import assertion `type` property
*/
function handleInvalidType(url, type) {
// `type` might have not been a string.
validateString(type, 'type');

// `type` might not have been one of the types we understand.
if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
}

// `type` was the wrong value for this format.
throw new ERR_IMPORT_ASSERTION_TYPE_FAILED(url, type);
}


module.exports = {
kImplicitAssertType,
validateAssertions,
};
14 changes: 13 additions & 1 deletion lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
@@ -3,14 +3,26 @@
const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { defaultGetSource } = require('internal/modules/esm/get_source');
const { translators } = require('internal/modules/esm/translators');
const { validateAssertions } = require('internal/modules/esm/assert');

/**
* Node.js default load hook.
* @param {string} url
* @param {object} context
* @returns {object}
*/
async function defaultLoad(url, context) {
let {
format,
source,
} = context;
const { importAssertions } = context;

if (!translators.has(format)) format = defaultGetFormat(url);
if (!format || !translators.has(format)) {
format = defaultGetFormat(url);
}

validateAssertions(url, format, importAssertions);

if (
format === 'builtin' ||
Loading