Skip to content

Commit d9f3bf1

Browse files
committed
module: unflag import assertions
Refs: nodejs#37375 (comment)
1 parent 7152df0 commit d9f3bf1

25 files changed

Lines changed: 359 additions & 35 deletions

doc/api/errors.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,14 @@ The JS execution context is not associated with a Node.js environment.
11091109
This may occur when Node.js is used as an embedded library and some hooks
11101110
for the JS engine are not set up properly.
11111111

1112+
<a id="ERR_FAILED_IMPORT_ASSERTION"></a>
1113+
### `ERR_FAILED_IMPORT_ASSERTION`
1114+
<!-- YAML
1115+
added: REPLACEME
1116+
-->
1117+
1118+
An import assertion has failed, preventing the specified module to be imported.
1119+
11121120
<a id="ERR_FALSY_VALUE_REJECTION"></a>
11131121
### `ERR_FALSY_VALUE_REJECTION`
11141122

@@ -1660,6 +1668,14 @@ for more information.
16601668

16611669
An invalid HTTP token was supplied.
16621670

1671+
<a id="ERR_INVALID_IMPORT_ASSERTION"></a>
1672+
### `ERR_INVALID_IMPORT_ASSERTION`
1673+
<!-- YAML
1674+
added: REPLACEME
1675+
-->
1676+
1677+
An import assertion is not supported by this version of Node.js.
1678+
16631679
<a id="ERR_INVALID_IP_ADDRESS"></a>
16641680
### `ERR_INVALID_IP_ADDRESS`
16651681

doc/api/esm.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
<!-- YAML
88
added: v8.5.0
99
changes:
10+
- version: REPLACEME
11+
pr-url: https://github.com/nodejs/node/pull/39921
12+
description: Add support for import assertions.
1013
- version:
1114
- REPLACEME
1215
pr-url: https://github.com/nodejs/node/pull/37468
@@ -230,6 +233,25 @@ absolute URL strings.
230233
import fs from 'node:fs/promises';
231234
```
232235

236+
## Import assertions
237+
<!-- YAML
238+
added: REPLACEME
239+
-->
240+
241+
The [Import Assertions proposal][] adds an inline syntax for module import
242+
statements to pass on more information alongside the module specifier.
243+
244+
```js
245+
import json from './foo.json' assert { type: "json" };
246+
await import('foo.json', { assert: { type: "json" } });
247+
```
248+
249+
Node.js supports the following `type` values:
250+
251+
| `type` | Resolves to |
252+
| -------- | ---------------- |
253+
| `"json"` | [JSON modules][] |
254+
233255
## Builtin modules
234256

235257
[Core modules][] provide named exports of their public API. A
@@ -518,9 +540,8 @@ same path.
518540
519541
Assuming an `index.mjs` with
520542
521-
<!-- eslint-skip -->
522543
```js
523-
import packageConfig from './package.json';
544+
import packageConfig from './package.json' assert { type: 'json' };
524545
```
525546
526547
The `--experimental-json-modules` flag is needed for the module
@@ -1351,6 +1372,8 @@ success!
13511372
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13521373
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13531374
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
1375+
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
1376+
[JSON modules]: #json-modules
13541377
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13551378
[Terminology]: #terminology
13561379
[URL]: https://url.spec.whatwg.org/

lib/internal/errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,9 @@ E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported',
953953
RangeError);
954954
E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error);
955955
E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error);
956+
E('ERR_FAILED_IMPORT_ASSERTION', (request, key, expectedValue, actualValue) => {
957+
return `Failed to load module "${request}", expected ${key} to be ${JSONStringify(expectedValue)}, got ${JSONStringify(actualValue)} instead`;
958+
}, TypeError);
956959
E('ERR_FALSY_VALUE_REJECTION', function(reason) {
957960
this.reason = reason;
958961
return 'Promise was rejected with falsy value';
@@ -1250,6 +1253,9 @@ E('ERR_INVALID_FILE_URL_HOST',
12501253
E('ERR_INVALID_FILE_URL_PATH', 'File URL path %s', TypeError);
12511254
E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError);
12521255
E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError);
1256+
E('ERR_INVALID_IMPORT_ASSERTION',
1257+
(type, value) => `Invalid ${JSONStringify(type)} import assertion: ${JSONStringify(value)}`,
1258+
TypeError);
12531259
E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError);
12541260
E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => {
12551261
return `Invalid module "${request}" ${reason}${base ?

lib/internal/modules/cjs/loader.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,9 +1015,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10151015
filename,
10161016
lineOffset: 0,
10171017
displayErrors: true,
1018-
importModuleDynamically: async (specifier) => {
1018+
importModuleDynamically: async (specifier, _, import_assertions) => {
10191019
const loader = asyncESM.esmLoader;
1020-
return loader.import(specifier, normalizeReferrerURL(filename));
1020+
return loader.import(specifier, normalizeReferrerURL(filename),
1021+
import_assertions);
10211022
},
10221023
});
10231024
}
@@ -1030,9 +1031,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10301031
'__dirname',
10311032
], {
10321033
filename,
1033-
importModuleDynamically(specifier) {
1034+
importModuleDynamically(specifier, _, import_assertions) {
10341035
const loader = asyncESM.esmLoader;
1035-
return loader.import(specifier, normalizeReferrerURL(filename));
1036+
return loader.import(specifier, normalizeReferrerURL(filename),
1037+
import_assertions);
10361038
},
10371039
});
10381040
} catch (err) {

lib/internal/modules/esm/loader.js

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ require('internal/modules/cjs/loader');
66
const {
77
Array,
88
ArrayIsArray,
9+
ArrayPrototypeIncludes,
910
ArrayPrototypeJoin,
1011
ArrayPrototypePush,
1112
FunctionPrototypeBind,
1213
FunctionPrototypeCall,
1314
ObjectCreate,
15+
ObjectFreeze,
1416
ObjectSetPrototypeOf,
1517
PromiseAll,
1618
RegExpPrototypeExec,
@@ -20,8 +22,10 @@ const {
2022
} = primordials;
2123

2224
const {
25+
ERR_FAILED_IMPORT_ASSERTION,
2326
ERR_INVALID_ARG_TYPE,
2427
ERR_INVALID_ARG_VALUE,
28+
ERR_INVALID_IMPORT_ASSERTION,
2529
ERR_INVALID_MODULE_SPECIFIER,
2630
ERR_INVALID_RETURN_PROPERTY_VALUE,
2731
ERR_INVALID_RETURN_VALUE,
@@ -44,6 +48,10 @@ const { translators } = require(
4448
'internal/modules/esm/translators');
4549
const { getOptionValue } = require('internal/options');
4650

51+
const importAssertionTypeCache = new SafeWeakMap();
52+
const finalFormatCache = new SafeWeakMap();
53+
const supportedTypes = ObjectFreeze([undefined, 'json']);
54+
4755
/**
4856
* An ESMLoader instance is used as the main entry point for loading ES modules.
4957
* Currently, this is a singleton -- there is only one used for loading
@@ -202,33 +210,66 @@ class ESMLoader {
202210
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
203211
const module = new ModuleWrap(url, undefined, source, 0, 0);
204212
callbackMap.set(module, {
205-
importModuleDynamically: (specifier, { url }) => {
206-
return this.import(specifier, url);
213+
importModuleDynamically: (specifier, { url }, import_assertions) => {
214+
return this.import(specifier, url, import_assertions);
207215
}
208216
});
209217

210218
return module;
211219
};
212220
const job = new ModuleJob(this, url, evalInstance, false, false);
213221
this.moduleMap.set(url, job);
222+
finalFormatCache.set(job, 'module');
214223
const { module } = await job.run();
215224

216225
return {
217226
namespace: module.getNamespace(),
218227
};
219228
}
220229

221-
async getModuleJob(specifier, parentURL) {
230+
async getModuleJob(specifier, parentURL, import_assertions) {
231+
if (!ArrayPrototypeIncludes(supportedTypes, import_assertions.type)) {
232+
throw new ERR_INVALID_IMPORT_ASSERTION('type', import_assertions.type);
233+
}
234+
222235
const { format, url } = await this.resolve(specifier, parentURL);
223236
let job = this.moduleMap.get(url);
224237
// CommonJS will set functions for lazy job evaluation.
225238
if (typeof job === 'function') this.moduleMap.set(url, job = job());
226239

227-
if (job !== undefined) return job;
240+
if (job != null) {
241+
const currentImportAssertionType = importAssertionTypeCache.get(job);
242+
if (currentImportAssertionType === import_assertions.type) return job;
243+
244+
try {
245+
// To avoid race conditions, wait for previous module to fulfill first.
246+
await job.modulePromise;
247+
} catch {
248+
// If the other job failed with a different `type` assertion, we got
249+
// another chance.
250+
job = undefined;
251+
}
252+
253+
if (job !== undefined) {
254+
const finalFormat = finalFormatCache.get(job);
255+
if (
256+
import_assertions.type == null ||
257+
(import_assertions.type === 'json' && finalFormat === 'json')
258+
) return job;
259+
throw new ERR_FAILED_IMPORT_ASSERTION(
260+
url, 'type', import_assertions.type, finalFormat);
261+
}
262+
}
228263

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

267+
if (import_assertions.type === 'json' && finalFormat !== 'json') {
268+
throw new ERR_FAILED_IMPORT_ASSERTION(
269+
url, 'type', import_assertions.type, finalFormat);
270+
}
271+
finalFormatCache.set(job, finalFormat);
272+
232273
const translator = translators.get(finalFormat);
233274

234275
if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
@@ -249,6 +290,7 @@ class ESMLoader {
249290
inspectBrk
250291
);
251292

293+
importAssertionTypeCache.set(job, import_assertions.type);
252294
this.moduleMap.set(url, job);
253295

254296
return job;
@@ -262,18 +304,19 @@ class ESMLoader {
262304
* loader module.
263305
*
264306
* @param {string | string[]} specifiers Path(s) to the module
265-
* @param {string} [parentURL] Path of the parent importing the module
266-
* @returns {object | object[]} A list of module export(s)
307+
* @param {string} parentURL Path of the parent importing the module
308+
* @param {Record<string, Record<string, string>>} import_assertions
309+
* @returns {Promise<object | object[]>} A list of module export(s)
267310
*/
268-
async import(specifiers, parentURL) {
311+
async import(specifiers, parentURL, import_assertions) {
269312
const wasArr = ArrayIsArray(specifiers);
270313
if (!wasArr) specifiers = [specifiers];
271314

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

275318
for (let i = 0; i < count; i++) {
276-
jobs[i] = this.getModuleJob(specifiers[i], parentURL)
319+
jobs[i] = this.getModuleJob(specifiers[i], parentURL, import_assertions)
277320
.then((job) => job.run())
278321
.then(({ module }) => module.getNamespace());
279322
}

lib/internal/modules/esm/module_job.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ class ModuleJob {
7272
// so that circular dependencies can't cause a deadlock by two of
7373
// these `link` callbacks depending on each other.
7474
const dependencyJobs = [];
75-
const promises = this.module.link(async (specifier) => {
76-
const jobPromise = this.loader.getModuleJob(specifier, url);
75+
const promises = this.module.link(async (specifier, assertions) => {
76+
const jobPromise = this.loader.getModuleJob(specifier, url, assertions);
7777
ArrayPrototypePush(dependencyJobs, jobPromise);
7878
const job = await jobPromise;
7979
return job.modulePromise;

lib/internal/modules/esm/translators.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ function errPath(url) {
107107
return url;
108108
}
109109

110-
async function importModuleDynamically(specifier, { url }) {
111-
return asyncESM.esmLoader.import(specifier, url);
110+
async function importModuleDynamically(specifier, { url }, assertions) {
111+
return asyncESM.esmLoader.import(specifier, url, assertions);
112112
}
113113

114114
function createImportMetaResolve(defaultParentUrl) {

lib/internal/modules/run_main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ObjectCreate,
45
StringPrototypeEndsWith,
56
} = primordials;
67
const CJSLoader = require('internal/modules/cjs/loader');
@@ -46,9 +47,8 @@ function runMainESM(mainPath) {
4647

4748
handleMainPromise(loadESM((esmLoader) => {
4849
const main = path.isAbsolute(mainPath) ?
49-
pathToFileURL(mainPath).href :
50-
mainPath;
51-
return esmLoader.import(main);
50+
pathToFileURL(mainPath).href : mainPath;
51+
return esmLoader.import(main, undefined, ObjectCreate(null));
5252
}));
5353
}
5454

lib/internal/process/esm_loader.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict';
22

3+
const {
4+
ObjectCreate,
5+
} = primordials;
6+
37
const {
48
ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING,
59
} = require('internal/errors').codes;
@@ -22,13 +26,14 @@ exports.initializeImportMetaObject = function(wrap, meta) {
2226
}
2327
};
2428

25-
exports.importModuleDynamicallyCallback = async function(wrap, specifier) {
29+
exports.importModuleDynamicallyCallback =
30+
async function importModuleDynamicallyCallback(wrap, specifier, assertions) {
2631
const { callbackMap } = internalBinding('module_wrap');
2732
if (callbackMap.has(wrap)) {
2833
const { importModuleDynamically } = callbackMap.get(wrap);
2934
if (importModuleDynamically !== undefined) {
3035
return importModuleDynamically(
31-
specifier, getModuleFromWrap(wrap) || wrap);
36+
specifier, getModuleFromWrap(wrap) || wrap, assertions);
3237
}
3338
}
3439
throw new ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING();
@@ -69,6 +74,7 @@ async function initializeLoader() {
6974
const exports = await internalEsmLoader.import(
7075
customLoaders,
7176
pathToFileURL(cwd).href,
77+
ObjectCreate(null),
7278
);
7379

7480
// Hooks must then be added to external/public loader

lib/internal/process/execution.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ function evalScript(name, body, breakFirstLine, print) {
8282
filename: name,
8383
displayErrors: true,
8484
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
85-
async importModuleDynamically(specifier) {
86-
const loader = await asyncESM.esmLoader;
87-
return loader.import(specifier, baseUrl);
85+
importModuleDynamically(specifier, _, import_assertions) {
86+
const loader = asyncESM.esmLoader;
87+
return loader.import(specifier, baseUrl, import_assertions);
8888
}
8989
}));
9090
if (print) {

0 commit comments

Comments
 (0)