Skip to content

Commit

Permalink
esm: add support for JSON import assertion
Browse files Browse the repository at this point in the history
Remove V8 flag for import assertions, enabling support for the syntax;
require the import assertion syntax for imports of JSON.

Support import assertions in user loaders.

Use both resolved module URL and import assertion type as the key for
caching modules.

Co-authored-by: Geoffrey Booth <webadmin@geoffreybooth.com>

PR-URL: #40250
Reviewed-By: Bradley Farias <bradley.meck@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>
  • Loading branch information
aduh95 authored and GeoffreyBooth committed Nov 4, 2021
1 parent 2e2a6fe commit 2cc7a91
Show file tree
Hide file tree
Showing 52 changed files with 885 additions and 169 deletions.
30 changes: 30 additions & 0 deletions doc/api/errors.md
Expand Up @@ -1689,6 +1689,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
43 changes: 39 additions & 4 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/40250
description: Add support for import assertions.
- version:
- v17.0.0
- v16.12.0
Expand Down Expand Up @@ -220,6 +223,28 @@ absolute URL strings.
import fs from 'node:fs/promises';
```

## Import assertions

<!-- YAML
added: REPLACEME
-->

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:

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

## Builtin modules

[Core modules][] provide named exports of their public API. A
Expand Down Expand Up @@ -517,10 +542,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 Down Expand Up @@ -608,12 +631,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 @@ -690,13 +721,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 @@ -1358,6 +1391,8 @@ success!
[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
[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
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
Expand Up @@ -1015,9 +1015,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 @@ -1030,9 +1031,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
102 changes: 102 additions & 0 deletions lib/internal/modules/esm/assert.js
@@ -0,0 +1,102 @@
'use strict';

const {
ArrayPrototypeIncludes,
ObjectCreate,
ObjectValues,
ObjectPrototypeHasOwnProperty,
Symbol,
} = 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;

const kImplicitAssertType = Symbol('implicit assert type');

/**
* Define a map of module formats to import assertion types (the value of `type`
* in `assert { type: 'json' }`).
* @type {Map<string, string | typeof kImplicitAssertType}
*/
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
};

/** @type {Array<string, string | typeof kImplicitAssertType} */
const supportedAssertionTypes = ObjectValues(formatTypeMap);


/**
* 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 types 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.
if (!ObjectPrototypeHasOwnProperty(importAssertions, 'type')) {
return true;
}
return handleInvalidType(url, importAssertions.type);

default:
// There is an expected type for this format, but the value of
// `importAssertions.type` was not 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` was not 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
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

0 comments on commit 2cc7a91

Please sign in to comment.