Skip to content

Commit

Permalink
module: add 'webassembly' and 'javascript' types
Browse files Browse the repository at this point in the history
  • Loading branch information
GeoffreyBooth committed Nov 16, 2021
1 parent ba05dd9 commit 4ff88dc
Show file tree
Hide file tree
Showing 13 changed files with 272 additions and 35 deletions.
43 changes: 38 additions & 5 deletions doc/api/esm.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ import fs from 'node:fs/promises';
added: v17.1.0
-->

> Stability: 1 - Experimental
The [Import Assertions proposal][] adds an inline syntax for module import
statements to pass on more information alongside the module specifier.

Expand All @@ -239,11 +241,35 @@ const { default: barData } =
await import('./bar.json', { assert: { type: 'json' } });
```

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

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

Imports of CommonJS and ES module JavaScript files, and Node.js core modules, do
not need an import assertion; but they can be given a `type` of `'javascript'`
if desired:

```js
const url = getUrlOfModuleToBeDynamicallyImported();
const { protocol, pathname } = new URL(url, import.meta.url);

let assertType = 'javascript';
if (protocol === 'data:' ?
pathname.startsWith('application/json') :
pathname.endsWith('.json')) {
assertType = 'json';
} else if (protocol === 'data:' ?
pathname.startsWith('application/wasm') :
pathname.endsWith('.wasm')) {
assertType = 'webassembly';
}

| `type` | Resolves to |
| -------- | ---------------- |
| `'json'` | [JSON modules][] |
const importedModule = await import(url, { assert: { type: assertType } });
```
## Builtin modules
Expand Down Expand Up @@ -554,6 +580,8 @@ 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
Expand All @@ -570,7 +598,7 @@ This integration is in line with the
For example, an `index.mjs` containing:
```js
import * as M from './module.wasm';
import * as M from './module.wasm' assert { type: 'webassembly' };
console.log(M);
```
Expand All @@ -582,6 +610,9 @@ node --experimental-wasm-modules index.mjs
would provide the exports interface for the instantiation of `module.wasm`.
The `assert { type: 'webassembly' }` syntax is mandatory; see
[Import Assertions][].
<i id="esm_experimental_top_level_await"></i>
## Top-level `await`
Expand Down Expand Up @@ -1391,12 +1422,14 @@ 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 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/
[WHATWG JSON modules specification]: https://html.spec.whatwg.org/#creating-a-json-module-script
[Wasm modules]: #wasm-modules
[`"exports"`]: packages.md#exports
[`"type"`]: packages.md#type
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
Expand Down
29 changes: 17 additions & 12 deletions lib/internal/modules/esm/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const {
ObjectCreate,
ObjectValues,
ObjectPrototypeHasOwnProperty,
Symbol,
} = primordials;
const { validateString } = require('internal/validators');

Expand All @@ -15,23 +14,25 @@ const {
ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED,
} = require('internal/errors').codes;

const kImplicitAssertType = Symbol('implicit assert type');
// Per the HTML spec, import statements without an assertion type imply a
// `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 | typeof kImplicitAssertType}
* 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, // Should probably be 'webassembly' per https://github.com/tc39/proposal-import-assertions
'wasm': 'webassembly',
};

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


Expand All @@ -50,25 +51,29 @@ function validateAssertions(url, format,

switch (validType) {
case undefined:
// Ignore assertions for module types we don't recognize, to allow new
// Ignore assertions for module formats 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.
// This case also covers when the implicit type is declared explicitly
// (`assert { type: 'javascript' }`).
process.emitWarning('Import assertions are experimental.',
'ExperimentalWarning');
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.
// This format allows the type to be implied. (If it were declared
// explicitly, it would have been caught already by the previous case.)
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.
// `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);
Expand All @@ -86,7 +91,7 @@ function handleInvalidType(url, type) {
// `type` might have not been a string.
validateString(type, 'type');

// `type` was not one of the types we understand.
// `type` might not have been one of the types we understand.
if (!ArrayPrototypeIncludes(supportedAssertionTypes, type)) {
throw new ERR_IMPORT_ASSERTION_TYPE_UNSUPPORTED(type);
}
Expand Down
9 changes: 3 additions & 6 deletions lib/internal/modules/esm/module_map.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,17 @@ let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
const { ERR_INVALID_ARG_TYPE } = require('internal/errors').codes;
const { validateString } = require('internal/validators');

const validateAssertType = (type) =>
type === kImplicitAssertType || validateString(type, 'type');

// Tracks the state of the loader-level module cache
class ModuleMap extends SafeMap {
constructor(i) { super(i); } // eslint-disable-line no-useless-constructor
get(url, type = kImplicitAssertType) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
return super.get(url)?.[type];
}
set(url, type = kImplicitAssertType, job) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
if (job instanceof ModuleJob !== true &&
typeof job !== 'function') {
throw new ERR_INVALID_ARG_TYPE('job', 'ModuleJob', job);
Expand All @@ -39,7 +36,7 @@ class ModuleMap extends SafeMap {
}
has(url, type = kImplicitAssertType) {
validateString(url, 'url');
validateAssertType(type);
validateString(type, 'type');
return super.get(url)?.[type] !== undefined;
}
}
Expand Down
95 changes: 95 additions & 0 deletions test/es-module/test-esm-assertionless-javascript-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';
const common = require('../common');
const { strictEqual } = require('assert');

async function test() {
{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs'),
import(
'../fixtures/es-modules/default-export.mjs',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import(
'../fixtures/es-modules/default-export.mjs',
{ assert: { type: 'javascript' } }
),
import('../fixtures/es-modules/default-export.mjs'),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs?test'),
import(
'../fixtures/es-modules/default-export.mjs?test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs#test'),
import(
'../fixtures/es-modules/default-export.mjs#test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs?test2#test'),
import(
'../fixtures/es-modules/default-export.mjs?test2#test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('data:text/javascript,export default { ofLife: 42 }'),
import(
'data:text/javascript,export default { ofLife: 42 }',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
}
}

test().then(common.mustCall());
90 changes: 90 additions & 0 deletions test/es-module/test-esm-assertionless-javascript-import.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import '../common/index.mjs';
import { strictEqual } from 'assert';

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs'),
import(
'../fixtures/es-modules/default-export.mjs',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import(
'../fixtures/es-modules/default-export.mjs',
{ assert: { type: 'javascript' } }
),
import('../fixtures/es-modules/default-export.mjs'),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs?test'),
import(
'../fixtures/es-modules/default-export.mjs?test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs#test'),
import(
'../fixtures/es-modules/default-export.mjs#test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('../fixtures/es-modules/default-export.mjs?test2#test'),
import(
'../fixtures/es-modules/default-export.mjs?test2#test',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
strictEqual(secret0.default, secret1.default);
strictEqual(secret0, secret1);
}

{
const [secret0, secret1] = await Promise.all([
import('data:text/javascript,export default { ofLife: 42 }'),
import(
'data:text/javascript,export default { ofLife: 42 }',
{ assert: { type: 'javascript' } }
),
]);

strictEqual(secret0.default.ofLife, 42);
strictEqual(secret1.default.ofLife, 42);
}
Loading

0 comments on commit 4ff88dc

Please sign in to comment.