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: #50140
Fixes: #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 14, 2023
1 parent f447a46 commit d1ef6aa
Show file tree
Hide file tree
Showing 60 changed files with 472 additions and 353 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Expand Up @@ -9,5 +9,7 @@ tools/github_reporter
benchmark/tmp
benchmark/fixtures
doc/**/*.js
doc/changelogs/CHANGELOG_v1*.md
!doc/changelogs/CHANGELOG_v18.md
!doc/api_assets/*.js
!.eslintrc.js
4 changes: 2 additions & 2 deletions .eslintrc.js
Expand Up @@ -18,7 +18,7 @@ const hacks = [
'eslint-plugin-jsdoc',
'eslint-plugin-markdown',
'@babel/eslint-parser',
'@babel/plugin-syntax-import-assertions',
'@babel/plugin-syntax-import-attributes',
];
Module._findPath = (request, paths, isMain) => {
const r = ModuleFindPath(request, paths, isMain);
Expand All @@ -44,7 +44,7 @@ module.exports = {
parserOptions: {
babelOptions: {
plugins: [
Module._findPath('@babel/plugin-syntax-import-assertions'),
Module._findPath('@babel/plugin-syntax-import-attributes'),
],
},
requireConfigFile: false,
Expand Down
17 changes: 14 additions & 3 deletions doc/api/errors.md
Expand Up @@ -1759,7 +1759,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 @@ -1771,7 +1772,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 @@ -1783,7 +1784,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
39 changes: 26 additions & 13 deletions doc/api/esm.md
Expand Up @@ -7,6 +7,9 @@
<!-- YAML
added: v8.5.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/50140
description: Add experimental support for import attributes.
- version: v20.0.0
pr-url: https://github.com/nodejs/node/pull/44710
description: Module customization hooks are executed off the main thread.
Expand All @@ -19,7 +22,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 @@ -203,7 +206,7 @@ added: v12.10.0

```js
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' };
import _ from 'data:application/json,"world!"' with { type: 'json' };
```

`data:` URLs only resolve [bare specifiers][Terminology] for builtin modules
Expand Down Expand Up @@ -235,30 +238,40 @@ 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
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/50140
description: Switch from Import Assertions to Import Attributes.
-->

> Stability: 1 - Experimental
> Stability: 1.1 - Active development
> This feature was previously named "Import assertions", and using the `assert`
> keyword instead of `with`. Any uses in code of the prior `assert` keyword
> should be updated to use `with` instead.
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
import fooData from './foo.json' assert { type: 'json' };
import fooData from './foo.json' with { type: 'json' };

const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
await import('./bar.json', { with: { 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 @@ -545,10 +558,10 @@ separate cache.
JSON files can be referenced by `import`:

```js
import packageConfig from './package.json' assert { type: 'json' };
import packageConfig from './package.json' with { type: 'json' };
```
The `assert { type: 'json' }` syntax is mandatory; see [Import Assertions][].
The `with { 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 @@ -1055,8 +1068,8 @@ resolution for ESM specifiers is [commonjs-extension-resolution-loader][].
[Determining module system]: packages.md#determining-module-system
[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
[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
[Module customization hooks]: module.md#customization-hooks
[Node.js Module Resolution And Loading Algorithm]: #resolution-algorithm-specification
Expand Down
19 changes: 12 additions & 7 deletions doc/api/module.md
Expand Up @@ -458,6 +458,11 @@ register('./path-to-my-hooks.js', {
<!-- 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
- v16.17.0
Expand All @@ -477,8 +482,8 @@ changes:
* `specifier` {string}
* `context` {Object}
* `conditions` {string\[]} Export conditions of the relevant `package.json`
* `importAssertions` {Object} An object whose key-value pairs represent the
assertions for the module to import
* `importAttributes` {Object} An object whose key-value pairs represent the
attributes for the module to import
* `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 All @@ -489,7 +494,7 @@ changes:
* `format` {string|null|undefined} A hint to the load hook (it might be
ignored)
`'builtin' | 'commonjs' | 'json' | 'module' | 'wasm'`
* `importAssertions` {Object|undefined} The import assertions to use when
* `importAttributes` {Object|undefined} The import attributes to use when
caching the module (optional; if excluded the input will be used)
* `shortCircuit` {undefined|boolean} A signal that this hook intends to
terminate the chain of `resolve` hooks. **Default:** `false`
Expand All @@ -506,10 +511,10 @@ the final `format` value (and it is free to ignore the hint provided by
`resolve`); if `resolve` provides a `format`, a custom `load` hook is required
even if only to pass the value to the Node.js default `load` hook.
Import type assertions are part of the cache key for saving loaded modules into
Import type attributes are part of the cache key for saving loaded modules into
the internal module cache. The `resolve` hook is responsible for returning an
`importAssertions` object if the module should be cached with different
assertions than were present in the source code.
`importAttributes` object if the module should be cached with different
attributes than were present in the source code.
The `conditions` property in `context` is an array of conditions for
[package exports conditions][Conditional exports] that apply to this resolution
Expand Down Expand Up @@ -575,7 +580,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 Down
9 changes: 7 additions & 2 deletions lib/internal/errors.js
Expand Up @@ -1280,12 +1280,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
Expand Up @@ -1258,10 +1258,10 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
const script = new Script(wrapper, {
filename,
lineOffset: 0,
importModuleDynamically: async (specifier, _, importAssertions) => {
importModuleDynamically: async (specifier, _, importAttributes) => {
const cascadedLoader = getCascadedLoader();
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
importAttributes);
},
});

Expand All @@ -1285,10 +1285,10 @@ function wrapSafe(filename, content, cjsModuleInstance, codeCache) {
], {
filename,
cachedData: codeCache,
importModuleDynamically(specifier, _, importAssertions) {
importModuleDynamically(specifier, _, importAttributes) {
const cascadedLoader = getCascadedLoader();
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
importAssertions);
importAttributes);
},
});

Expand Down
53 changes: 24 additions & 29 deletions lib/internal/modules/esm/assert.js
Expand Up @@ -13,16 +13,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 @@ -31,13 +30,13 @@ 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
};

/**
* 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.
* `import './file.js' with { type: 'javascript' }` throws.
* @type {Array<string, string>}
*/
const supportedAssertionTypes = ArrayPrototypeFilter(
Expand All @@ -46,54 +45,50 @@ const supportedAssertionTypes = ArrayPrototypeFilter(


/**
* 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 = { __proto__: 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 @@ -118,5 +113,5 @@ function handleInvalidType(url, type) {

module.exports = {
kImplicitAssertType,
validateAssertions,
validateAttributes,
};

0 comments on commit d1ef6aa

Please sign in to comment.