Skip to content

Commit

Permalink
module: unflag import assertions
Browse files Browse the repository at this point in the history
Unflag import assertions and remove possibility of importing JSON
modules without using an assertion.

Refs: nodejs#37375 (comment)
  • Loading branch information
aduh95 committed Sep 28, 2021
1 parent 947176d commit 9ee9fdc
Show file tree
Hide file tree
Showing 31 changed files with 351 additions and 45 deletions.
25 changes: 25 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,14 @@ The JS execution context is not associated with a Node.js environment.
This may occur when Node.js is used as an embedded library and some hooks
for the JS engine are not set up properly.

<a id="ERR_FAILED_IMPORT_ASSERTION"></a>
### `ERR_FAILED_IMPORT_ASSERTION`
<!-- YAML
added: REPLACEME
-->

An import assertion has failed, preventing the specified module to be imported.

<a id="ERR_FALSY_VALUE_REJECTION"></a>
### `ERR_FALSY_VALUE_REJECTION`

Expand Down Expand Up @@ -1662,6 +1670,14 @@ for more information.

An invalid HTTP token was supplied.

<a id="ERR_INVALID_IMPORT_ASSERTION"></a>
### `ERR_INVALID_IMPORT_ASSERTION`
<!-- YAML
added: REPLACEME
-->

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

<a id="ERR_INVALID_IP_ADDRESS"></a>
### `ERR_INVALID_IP_ADDRESS`

Expand Down Expand Up @@ -1913,6 +1929,15 @@ strict compliance with the API specification (which in some cases may accept
`func(undefined)` and `func()` are treated identically, and the
[`ERR_INVALID_ARG_TYPE`][] error code may be used instead.

<a id="ERR_MISSING_IMPORT_ASSERTION"></a>
### `ERR_MISSING_IMPORT_ASSERTION`
<!-- YAML
added: REPLACEME
-->

An attempt was made to import a module without an assertion that requires
a specific import assertion to be loaded.

<a id="ERR_MISSING_OPTION"></a>
### `ERR_MISSING_OPTION`

Expand Down
27 changes: 25 additions & 2 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/39921
description: Add support for import assertions.
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/37468
Expand Down Expand Up @@ -232,6 +235,25 @@ 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 json from './foo.json' assert { type: "json" };
await import('foo.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 @@ -522,9 +544,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 @@ -1355,6 +1376,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
9 changes: 9 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@ E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
RangeError);
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
E('ERR_FAILED_IMPORT_ASSERTION', (request, key, expectedValue, actualValue) => {
return `Failed to load module "${request}", expected ${key} to be ${JSONStringify(expectedValue)}, got ${JSONStringify(actualValue)} instead`;
}, TypeError);
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
this.reason = reason;
return 'Promise was rejected with falsy value';
Expand Down Expand Up @@ -1250,6 +1253,9 @@ E('ERR_INVALID_FILE_URL_HOST',
E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError);
E('ERR_INVALID_IMPORT_ASSERTION',
(type, value) => `Invalid ${JSONStringify(type)} import assertion: ${JSONStringify(value)}`,
TypeError);
E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);
E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => {
return `Invalid module "${request}" ${reason}${base ?
Expand Down Expand Up @@ -1394,6 +1400,9 @@ E('ERR_MISSING_ARGS',
}
return `${msg} must be specified`;
}, TypeError);
E('ERR_MISSING_IMPORT_ASSERTION',
'Failed to load %s: Node.js requires modules of format "%s" to be loaded ' +
'using an assertion "%s" with value "%s"', TypeError);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => {
return `Cannot find ${type} '${path}' imported from ${base}`;
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 @@ -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
68 changes: 60 additions & 8 deletions lib/internal/modules/esm/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ require('internal/modules/cjs/loader');
const {
Array,
ArrayIsArray,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypePush,
FunctionPrototypeBind,
FunctionPrototypeCall,
ObjectCreate,
ObjectFreeze,
ObjectSetPrototypeOf,
PromiseAll,
RegExpPrototypeExec,
Expand All @@ -20,11 +22,14 @@ const {
} = primordials;

const {
ERR_FAILED_IMPORT_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_IMPORT_ASSERTION,
ERR_INVALID_MODULE_SPECIFIER,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_MISSING_IMPORT_ASSERTION,
ERR_UNKNOWN_MODULE_FORMAT
} = require('internal/errors').codes;
const { pathToFileURL, isURLInstance } = require('internal/url');
Expand All @@ -44,6 +49,10 @@ const { translators } = require(
'internal/modules/esm/translators');
const { getOptionValue } = require('internal/options');

const importAssertionTypeCache = new SafeWeakMap();
const finalFormatCache = new SafeWeakMap();
const supportedTypes = ObjectFreeze([undefined, 'json']);

/**
* An ESMLoader instance is used as the main entry point for loading ES modules.
* Currently, this is a singleton -- there is only one used for loading
Expand Down Expand Up @@ -202,33 +211,74 @@ class ESMLoader {
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(url, undefined, source, 0, 0);
callbackMap.set(module, {
importModuleDynamically: (specifier, { url }) => {
return this.import(specifier, url);
importModuleDynamically: (specifier, { url }, importAssertions) => {
return this.import(specifier, url, importAssertions);
}
});

return module;
};
const job = new ModuleJob(this, url, evalInstance, false, false);
this.moduleMap.set(url, job);
finalFormatCache.set(job, 'module');
const { module } = await job.run();

return {
namespace: module.getNamespace(),
};
}

async getModuleJob(specifier, parentURL) {
async getModuleJob(specifier, parentURL, importAssertions) {
if (!ArrayPrototypeIncludes(supportedTypes, importAssertions.type)) {
throw new ERR_INVALID_IMPORT_ASSERTION('type', importAssertions.type);
}

const { format, url } = await this.resolve(specifier, parentURL);
let job = this.moduleMap.get(url);
// CommonJS will set functions for lazy job evaluation.
if (typeof job === 'function') this.moduleMap.set(url, job = job());

if (job !== undefined) return job;
if (job != null) {
const currentImportAssertionType = importAssertionTypeCache.get(job);
if (currentImportAssertionType === importAssertions.type) return job;

try {
// To avoid race conditions, wait for previous module to fulfill first.
await job.modulePromise;
} catch {
// If the other job failed with a different `type` assertion, we got
// another chance.
job = undefined;
}

if (job !== undefined) {
const finalFormat = finalFormatCache.get(job);
if (importAssertions.type == null && finalFormat === 'json') {
throw new ERR_MISSING_IMPORT_ASSERTION(url, finalFormat,
'type', 'json');
}
if (
importAssertions.type == null ||
(importAssertions.type === 'json' && finalFormat === 'json')
) return job;
throw new ERR_FAILED_IMPORT_ASSERTION(
url, 'type', importAssertions.type, finalFormat);
}
}

const moduleProvider = async (url, isMain) => {
const { format: finalFormat, source } = await this.load(url, { format });

if (importAssertions.type === 'json' && finalFormat !== 'json') {
throw new ERR_FAILED_IMPORT_ASSERTION(
url, 'type', importAssertions.type, finalFormat);
}
if (importAssertions.type !== 'json' && finalFormat === 'json') {
throw new ERR_MISSING_IMPORT_ASSERTION(url, finalFormat,
'type', 'json');
}
finalFormatCache.set(job, finalFormat);

const translator = translators.get(finalFormat);

if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
Expand All @@ -249,6 +299,7 @@ class ESMLoader {
inspectBrk
);

importAssertionTypeCache.set(job, importAssertions.type);
this.moduleMap.set(url, job);

return job;
Expand All @@ -262,18 +313,19 @@ class ESMLoader {
* loader module.
*
* @param {string | string[]} specifiers Path(s) to the module
* @param {string} [parentURL] Path of the parent importing the module
* @returns {object | object[]} A list of module export(s)
* @param {string} parentURL Path of the parent importing the module
* @param {Record<string, Record<string, string>>} importAssertions
* @returns {Promise<object | object[]>} A list of module export(s)
*/
async import(specifiers, parentURL) {
async import(specifiers, parentURL, importAssertions) {
const wasArr = ArrayIsArray(specifiers);
if (!wasArr) specifiers = [specifiers];

const count = specifiers.length;
const jobs = new Array(count);

for (let i = 0; i < count; i++) {
jobs[i] = this.getModuleJob(specifiers[i], parentURL)
jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
.then((job) => job.run())
.then(({ module }) => module.getNamespace());
}
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ class ModuleJob {
// so that circular dependencies can't cause a deadlock by two of
// these `link` callbacks depending on each other.
const dependencyJobs = [];
const promises = this.module.link(async (specifier) => {
const jobPromise = this.loader.getModuleJob(specifier, url);
const promises = this.module.link(async (specifier, assertions) => {
const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
ArrayPrototypePush(dependencyJobs, jobPromise);
const job = await jobPromise;
return job.modulePromise;
Expand Down
4 changes: 2 additions & 2 deletions lib/internal/modules/esm/translators.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ function errPath(url) {
return url;
}

async function importModuleDynamically(specifier, { url }) {
return asyncESM.esmLoader.import(specifier, url);
async function importModuleDynamically(specifier, { url }, assertions) {
return asyncESM.esmLoader.import(specifier, url, assertions);
}

function createImportMetaResolve(defaultParentUrl) {
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ObjectCreate,
StringPrototypeEndsWith,
} = primordials;
const CJSLoader = require('internal/modules/cjs/loader');
Expand Down Expand Up @@ -46,9 +47,8 @@ function runMainESM(mainPath) {

handleMainPromise(loadESM((esmLoader) => {
const main = path.isAbsolute(mainPath) ?
pathToFileURL(mainPath).href :
mainPath;
return esmLoader.import(main);
pathToFileURL(mainPath).href : mainPath;
return esmLoader.import(main, undefined, ObjectCreate(null));
}));
}

Expand Down
10 changes: 8 additions & 2 deletions lib/internal/process/esm_loader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
'use strict';

const {
ObjectCreate,
} = primordials;

const {
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
} = require('internal/errors').codes;
Expand All @@ -22,13 +26,14 @@ exports.initializeImportMetaObject = function(wrap, meta) {
}
};

exports.importModuleDynamicallyCallback = async function(wrap, specifier) {
exports.importModuleDynamicallyCallback =
async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
const { callbackMap } = internalBinding('module_wrap');
if (callbackMap.has(wrap)) {
const { importModuleDynamically } = callbackMap.get(wrap);
if (importModuleDynamically !== undefined) {
return importModuleDynamically(
specifier, getModuleFromWrap(wrap) || wrap);
specifier, getModuleFromWrap(wrap) || wrap, assertions);
}
}
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
Expand Down Expand Up @@ -69,6 +74,7 @@ async function initializeLoader() {
const exports = await internalEsmLoader.import(
customLoaders,
pathToFileURL(cwd).href,
ObjectCreate(null),
);

// Hooks must then be added to external/public loader
Expand Down
6 changes: 3 additions & 3 deletions lib/internal/process/execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ function evalScript(name, body, breakFirstLine, print) {
filename: name,
displayErrors: true,
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
async importModuleDynamically(specifier) {
const loader = await asyncESM.esmLoader;
return loader.import(specifier, baseUrl);
importModuleDynamically(specifier, _, importAssertions) {
const loader = asyncESM.esmLoader;
return loader.import(specifier, baseUrl, importAssertions);
}
}));
if (print) {
Expand Down
Loading

0 comments on commit 9ee9fdc

Please sign in to comment.