Skip to content

Commit

Permalink
esm: use import attributes instead of import assertions
Browse files Browse the repository at this point in the history
The old import assertions proposal has been
renamed to "import attributes" with the follwing major changes:

1. The keyword is now `with` instead of `assert`.
2. Unknown assertions cause an error rather than being ignored,

This commit updates the documentation to encourage folks to use the new
syntax, and add aliases for module customization hooks.

PR-URL: nodejs#50140
Fixes: nodejs#50134
Refs: v8/v8@159c82c
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
aduh95 committed Oct 22, 2023
1 parent a0e60c1 commit 2c1745e
Show file tree
Hide file tree
Showing 35 changed files with 279 additions and 213 deletions.
17 changes: 14 additions & 3 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1762,7 +1762,8 @@ added:
- v16.14.0
-->

An import assertion has failed, preventing the specified module to be imported.
An import `type` attribute was provided, but the specified module is of a
different type.

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

Expand All @@ -1774,7 +1775,7 @@ added:
- v16.14.0
-->

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

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

Expand All @@ -1786,7 +1787,17 @@ added:
- v16.14.0
-->

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

<a id="ERR_IMPORT_ATTRIBUTE_UNSUPPORTED"></a>

### `ERR_IMPORT_ATTRIBUTE_UNSUPPORTED`

<!-- YAML
added: REPLACEME
-->

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

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

Expand Down
36 changes: 24 additions & 12 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ changes:
- v17.1.0
- v16.14.0
pr-url: https://github.com/nodejs/node/pull/40250
description: Add support for import assertions.
description: Add experimental support for import assertions.
- version:
- v17.0.0
- v16.12.0
Expand Down Expand Up @@ -230,17 +230,24 @@ absolute URL strings.
import fs from 'node:fs/promises';
```

## Import assertions
<a id="import-assertions"></a>

## Import attributes

<!-- YAML
added:
- v17.1.0
- v16.14.0
-->

> Stability: 1 - Experimental
> Stability: 1.1 - Active development
> This feature was previously named "Import assertions", and using the `assert`
> keyword instead of `with`. Because the version of V8 on this release line does
> not support the `with` keyword, you need to keep using `assert` to support
> this version of Node.js.
The [Import Assertions proposal][] adds an inline syntax for module import
The [Import Attributes proposal][] adds an inline syntax for module import
statements to pass on more information alongside the module specifier.

```js
Expand All @@ -250,10 +257,10 @@ const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
```

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

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

Expand Down Expand Up @@ -529,7 +536,7 @@ JSON files can be referenced by `import`:
import packageConfig from './package.json' assert { type: 'json' };
```
The `assert { type: 'json' }` syntax is mandatory; see [Import Assertions][].
The `assert { type: 'json' }` syntax is mandatory; see [Import Attributes][].
The imported JSON only exposes a `default` export. There is no support for named
exports. A cache entry is created in the CommonJS cache to avoid duplication.
Expand Down Expand Up @@ -732,6 +739,11 @@ prevent unintentional breaks in the chain.
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/50140
description: The property `context.importAssertions` is replaced with
`context.importAttributes`. Using the old name is still
supported and will emit an experimental warning.
- version: v18.6.0
pr-url: https://github.com/nodejs/node/pull/42623
description: Add support for chaining resolve hooks. Each hook must either
Expand All @@ -750,7 +762,7 @@ changes:
* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]} Export conditions of the relevant `package.json`
* `importAssertions` {Object}
* `importAttributes` {Object}
* `parentURL` {string|undefined} The module importing this one, or undefined
if this is the Node.js entry point
* `nextResolve` {Function} The subsequent `resolve` hook in the chain, or the
Expand Down Expand Up @@ -842,7 +854,7 @@ changes:
* `conditions` {string\[]} Export conditions of the relevant `package.json`
* `format` {string|null|undefined} The format optionally supplied by the
`resolve` hook chain
* `importAssertions` {Object}
* `importAttributes` {Object}
* `nextLoad` {Function} The subsequent `load` hook in the chain, or the
Node.js default `load` hook after the last user-supplied `load` hook
* `specifier` {string}
Expand All @@ -855,7 +867,7 @@ changes:
The `load` hook provides a way to define a custom method of determining how
a URL should be interpreted, retrieved, and parsed. It is also in charge of
validating the import assertion.
validating the import attributes.
The final value of `format` must be one of the following:
Expand Down Expand Up @@ -1578,8 +1590,8 @@ success!
[Dynamic `import()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import
[ES Module Integration Proposal for WebAssembly]: https://github.com/webassembly/esm-integration
[HTTPS and HTTP imports]: #https-and-http-imports
[Import Assertions]: #import-assertions
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
[Import Attributes]: #import-attributes
[Import Attributes proposal]: https://github.com/tc39/proposal-import-attributes
[JSON modules]: #json-modules
[Loaders API]: #loaders
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
Expand Down
9 changes: 7 additions & 2 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1177,12 +1177,17 @@ 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);
// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_FAILED',
'Module "%s" is not of type "%s"', TypeError);
// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_MISSING',
'Module "%s" needs an import assertion of type "%s"', TypeError);
'Module "%s" needs an import attribute of type "%s"', TypeError);
// TODO(aduh95): change the error to mention import attributes instead of import assertions.
E('ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED',
'Import assertion type "%s" is unsupported', TypeError);
'Import attribute type "%s" is unsupported', TypeError);
E('ERR_IMPORT_ATTRIBUTE_UNSUPPORTED',
'Import attribute "%s" with value "%s" is not supported', 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
8 changes: 4 additions & 4 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1157,10 +1157,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
const script = new Script(wrapper, {
filename,
lineOffset: 0,
importModuleDynamically: async (specifier, _, importAssertions) => {
importModuleDynamically: async (specifier, _, importAttributes) => {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
importAttributes);
},
});

Expand All @@ -1183,10 +1183,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
'__dirname',
], {
filename,
importModuleDynamically(specifier, _, importAssertions) {
importModuleDynamically(specifier, _, importAttributes) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
importAttributes);
},
});

Expand Down
55 changes: 25 additions & 30 deletions lib/internal/modules/esm/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,15 @@ const {
ERR_IMPORT_ASSERTION_TYPE_FAILED,
ERR_IMPORT_ASSERTION_TYPE_MISSING,
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
ERR_IMPORT_ATTRIBUTE_UNSUPPORTED,
} = require('internal/errors').codes;

// The HTML spec has an implied default type of `'javascript'`.
const kImplicitAssertType = 'javascript';

let alreadyWarned = false;

/**
* Define a map of module formats to import assertion types (the value of
* `type` in `assert { type: 'json' }`).
* Define a map of module formats to import attributes types (the value of
* `type` in `with { type: 'json' }`).
* @type {Map<string, string>}
*/
const formatTypeMap = {
Expand All @@ -32,7 +31,7 @@ const formatTypeMap = {
'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
'wasm': kImplicitAssertType, // It's unclear whether the HTML spec will require an attribute type or not for Wasm; see https://github.com/WebAssembly/esm-integration/issues/42
};

/**
Expand All @@ -41,60 +40,56 @@ const formatTypeMap = {
* `import './file.js' assert { type: 'javascript' }` throws.
* @type {Array<string, string>}
*/
const supportedAssertionTypes = ArrayPrototypeFilter(
const supportedAttributesTypes = ArrayPrototypeFilter(
ObjectValues(formatTypeMap),
(type) => type !== kImplicitAssertType);


/**
* Test a module's import assertions.
* Test a module's import attributes.
* @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
* @param {Record<string, string>} importAttributes 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];

if (!alreadyWarned && ObjectKeys(importAssertions).length !== 0) {
alreadyWarned = true;
process.emitWarning(
'Import assertions are not a stable feature of the JavaScript language. ' +
'Avoid relying on their current behavior and syntax as those might change ' +
'in a future version of Node.js.',
'ExperimentalWarning',
);
function validateAttributes(url, format,
importAttributes = { __proto__: null }) {
const keys = ObjectKeys(importAttributes);
for (let i = 0; i < keys.length; i++) {
if (keys[i] !== 'type') {
throw new ERR_IMPORT_ATTRIBUTE_UNSUPPORTED(keys[i], importAttributes[keys[i]]);
}
}
const validType = formatTypeMap[format];

switch (validType) {
case undefined:
// Ignore assertions for module formats we don't recognize, to allow new
// Ignore attributes 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')) {
// must not be set on the import attributes object.
if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
return true;
}
return handleInvalidType(url, importAssertions.type);
return handleInvalidType(url, importAttributes.type);

case importAssertions.type:
case importAttributes.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')) {
// `importAttributes.type` might not have been it.
if (!ObjectPrototypeHasOwnProperty(importAttributes, 'type')) {
// `type` wasn't specified at all.
throw new ERR_IMPORT_ASSERTION_TYPE_MISSING(url, validType);
}
return handleInvalidType(url, importAssertions.type);
return handleInvalidType(url, importAttributes.type);
}
}

Expand All @@ -108,7 +103,7 @@ function handleInvalidType(url, type) {
validateString(type, 'type');

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

Expand All @@ -119,5 +114,5 @@ function handleInvalidType(url, type) {

module.exports = {
kImplicitAssertType,
validateAssertions,
validateAttributes,
};
16 changes: 13 additions & 3 deletions lib/internal/modules/esm/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const {
} = primordials;

const { defaultGetFormat } = require('internal/modules/esm/get_format');
const { validateAssertions } = require('internal/modules/esm/assert');
const { validateAttributes, emitImportAssertionWarning } = require('internal/modules/esm/assert');
const { getOptionValue } = require('internal/options');
const { fetchModule } = require('internal/modules/esm/fetch_module');

Expand Down Expand Up @@ -76,19 +76,29 @@ async function getSource(url, context) {
*/
async function defaultLoad(url, context) {
let responseURL = url;
const { importAssertions } = context;
let {
importAttributes,
format,
source,
} = context;

if (importAttributes == null && !('importAttributes' in context) && 'importAssertions' in context) {
emitImportAssertionWarning();
importAttributes = context.importAssertions;
// Alias `importAssertions` to `importAttributes`
context = {
...context,
importAttributes,
};
}

const urlInstance = new URL(url);

throwIfUnsupportedURLScheme(urlInstance, experimentalNetworkImports);

format ??= await defaultGetFormat(urlInstance, context);

validateAssertions(url, format, importAssertions);
validateAttributes(url, format, importAttributes);

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

0 comments on commit 2c1745e

Please sign in to comment.