Skip to content

Commit 11f7f0e

Browse files
committed
module: unflag import assertions
Refs: #37375 (comment)
1 parent 481c160 commit 11f7f0e

31 files changed

+351
-45
lines changed

doc/api/errors.md

+25
Original file line numberDiff line numberDiff line change
@@ -1111,6 +1111,14 @@ The JS execution context is not associated with a Node.js environment.
11111111
This may occur when Node.js is used as an embedded library and some hooks
11121112
for the JS engine are not set up properly.
11131113

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

@@ -1662,6 +1670,14 @@ for more information.
16621670

16631671
An invalid HTTP token was supplied.
16641672

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

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

1932+
<a id="ERR_MISSING_IMPORT_ASSERTION"></a>
1933+
### `ERR_MISSING_IMPORT_ASSERTION`
1934+
<!-- YAML
1935+
added: REPLACEME
1936+
-->
1937+
1938+
An attempt was made to import a module without an assertion that requires
1939+
a specific import assertion to be loaded.
1940+
19161941
<a id="ERR_MISSING_OPTION"></a>
19171942
### `ERR_MISSING_OPTION`
19181943

doc/api/esm.md

+25-2
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
@@ -232,6 +235,25 @@ absolute URL strings.
232235
import fs from 'node:fs/promises';
233236
```
234237

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

237259
[Core modules][] provide named exports of their public API. A
@@ -522,9 +544,8 @@ same path.
522544
523545
Assuming an `index.mjs` with
524546
525-
<!-- eslint-skip -->
526547
```js
527-
import packageConfig from './package.json';
548+
import packageConfig from './package.json' assert { type: 'json' };
528549
```
529550
530551
The `--experimental-json-modules` flag is needed for the module
@@ -1355,6 +1376,8 @@ success!
13551376
[Dynamic `import()`]: https://wiki.developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Dynamic_Imports
13561377
[ECMAScript Top-Level `await` proposal]: https://github.com/tc39/proposal-top-level-await/
13571378
[ES Module Integration Proposal for Web Assembly]: https://github.com/webassembly/esm-integration
1379+
[Import Assertions proposal]: https://github.com/tc39/proposal-import-assertions
1380+
[JSON modules]: #json-modules
13581381
[Node.js Module Resolution Algorithm]: #resolver-algorithm-specification
13591382
[Terminology]: #terminology
13601383
[URL]: https://url.spec.whatwg.org/

lib/internal/errors.js

+9
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 ?
@@ -1394,6 +1400,9 @@ E('ERR_MISSING_ARGS',
13941400
}
13951401
return `${msg} must be specified`;
13961402
}, TypeError);
1403+
E('ERR_MISSING_IMPORT_ASSERTION',
1404+
'Failed to load %s: Node.js requires modules of format "%s" to be loaded ' +
1405+
'using an assertion "%s" with value "%s"', TypeError);
13971406
E('ERR_MISSING_OPTION', '%s is required', TypeError);
13981407
E('ERR_MODULE_NOT_FOUND', (path, base, type = 'package') => {
13991408
return `Cannot find ${type} '${path}' imported from ${base}`;

lib/internal/modules/cjs/loader.js

+6-4
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

+60-8
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,11 +22,14 @@ 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,
32+
ERR_MISSING_IMPORT_ASSERTION,
2833
ERR_UNKNOWN_MODULE_FORMAT
2934
} = require('internal/errors').codes;
3035
const { pathToFileURL, isURLInstance } = require('internal/url');
@@ -44,6 +49,10 @@ const { translators } = require(
4449
'internal/modules/esm/translators');
4550
const { getOptionValue } = require('internal/options');
4651

52+
const importAssertionTypeCache = new SafeWeakMap();
53+
const finalFormatCache = new SafeWeakMap();
54+
const supportedTypes = ObjectFreeze([undefined, 'json']);
55+
4756
/**
4857
* An ESMLoader instance is used as the main entry point for loading ES modules.
4958
* Currently, this is a singleton -- there is only one used for loading
@@ -202,33 +211,74 @@ class ESMLoader {
202211
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
203212
const module = new ModuleWrap(url, undefined, source, 0, 0);
204213
callbackMap.set(module, {
205-
importModuleDynamically: (specifier, { url }) => {
206-
return this.import(specifier, url);
214+
importModuleDynamically: (specifier, { url }, import_assertions) => {
215+
return this.import(specifier, url, import_assertions);
207216
}
208217
});
209218

210219
return module;
211220
};
212221
const job = new ModuleJob(this, url, evalInstance, false, false);
213222
this.moduleMap.set(url, job);
223+
finalFormatCache.set(job, 'module');
214224
const { module } = await job.run();
215225

216226
return {
217227
namespace: module.getNamespace(),
218228
};
219229
}
220230

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

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

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

272+
if (import_assertions.type === 'json' && finalFormat !== 'json') {
273+
throw new ERR_FAILED_IMPORT_ASSERTION(
274+
url, 'type', import_assertions.type, finalFormat);
275+
}
276+
if (import_assertions.type !== 'json' && finalFormat === 'json') {
277+
throw new ERR_MISSING_IMPORT_ASSERTION(url, finalFormat,
278+
'type', 'json');
279+
}
280+
finalFormatCache.set(job, finalFormat);
281+
232282
const translator = translators.get(finalFormat);
233283

234284
if (!translator) throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat);
@@ -249,6 +299,7 @@ class ESMLoader {
249299
inspectBrk
250300
);
251301

302+
importAssertionTypeCache.set(job, import_assertions.type);
252303
this.moduleMap.set(url, job);
253304

254305
return job;
@@ -262,18 +313,19 @@ class ESMLoader {
262313
* loader module.
263314
*
264315
* @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)
316+
* @param {string} parentURL Path of the parent importing the module
317+
* @param {Record<string, Record<string, string>>} import_assertions
318+
* @returns {Promise<object | object[]>} A list of module export(s)
267319
*/
268-
async import(specifiers, parentURL) {
320+
async import(specifiers, parentURL, import_assertions) {
269321
const wasArr = ArrayIsArray(specifiers);
270322
if (!wasArr) specifiers = [specifiers];
271323

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

275327
for (let i = 0; i < count; i++) {
276-
jobs[i] = this.getModuleJob(specifiers[i], parentURL)
328+
jobs[i] = this.getModuleJob(specifiers[i], parentURL, import_assertions)
277329
.then((job) => job.run())
278330
.then(({ module }) => module.getNamespace());
279331
}

lib/internal/modules/esm/module_job.js

+2-2
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

+2-2
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

+3-3
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

+8-2
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

+3-3
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)