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
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
30 changes: 30 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
55 changes: 48 additions & 7 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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/
Expand Down
6 changes: 6 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ' +
Expand Down
10 changes: 6 additions & 4 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
});
}
Expand All @@ -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) {
Expand Down
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,
ljharb marked this conversation as resolved.
Show resolved Hide resolved
'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
Expand Up @@ -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' ||
Expand Down
Loading