Skip to content

Commit 5569c58

Browse files
committed
module: ensure successful dynamic import returns the same result
1 parent 1b87cb6 commit 5569c58

7 files changed

Lines changed: 101 additions & 10 deletions

File tree

lib/internal/modules/cjs/loader.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,8 +1147,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
11471147
lineOffset: 0,
11481148
importModuleDynamically: async (specifier, _, importAssertions) => {
11491149
const cascadedLoader = getCascadedLoader();
1150-
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
1151-
importAssertions);
1150+
return cascadedLoader.dynamicImport(specifier, normalizeReferrerURL(filename),
1151+
importAssertions);
11521152
},
11531153
});
11541154

@@ -1173,8 +1173,8 @@ function wrapSafe(filename, content, cjsModuleInstance) {
11731173
filename,
11741174
importModuleDynamically(specifier, _, importAssertions) {
11751175
const cascadedLoader = getCascadedLoader();
1176-
return cascadedLoader.import(specifier, normalizeReferrerURL(filename),
1177-
importAssertions);
1176+
return cascadedLoader.dynamicImport(specifier, normalizeReferrerURL(filename),
1177+
importAssertions);
11781178
},
11791179
});
11801180

lib/internal/modules/esm/loader.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ const {
77
Array,
88
ArrayIsArray,
99
FunctionPrototypeCall,
10+
JSONStringify,
1011
ObjectSetPrototypeOf,
12+
PromisePrototypeThen,
13+
SafeMap,
1114
SafePromiseAllReturnArrayLike,
1215
SafeWeakMap,
1316
} = primordials;
@@ -71,6 +74,8 @@ class ESMLoader {
7174
#defaultLoad;
7275
#importMetaInitializer;
7376

77+
#dynamicImportCache = new SafeMap();
78+
7479
/**
7580
* The conditions for resolving packages if `--conditions` is not used.
7681
*/
@@ -124,7 +129,7 @@ class ESMLoader {
124129
const module = new ModuleWrap(url, undefined, source, 0, 0);
125130
setCallbackForWrap(module, {
126131
importModuleDynamically: (specifier, { url }, importAssertions) => {
127-
return this.import(specifier, url, importAssertions);
132+
return this.dynamicImport(specifier, url, importAssertions);
128133
}
129134
});
130135

@@ -242,6 +247,41 @@ class ESMLoader {
242247
return job;
243248
}
244249

250+
async dynamicImport(specifier, parentURL, importAssertions) {
251+
let cache = this.#dynamicImportCache.get(parentURL);
252+
let specifierCache;
253+
if (cache == null) {
254+
this.#dynamicImportCache.set(parentURL, cache = new SafeMap());
255+
} else {
256+
specifierCache = cache.get(specifier);
257+
}
258+
259+
if (specifierCache == null) {
260+
cache.set(specifier, specifierCache = { __proto__: null });
261+
}
262+
263+
const assertions = JSONStringify(importAssertions);
264+
const removeCache = () => {
265+
delete specifierCache[assertions];
266+
};
267+
if (specifierCache[assertions] != null) {
268+
const fallback = () => {
269+
if (specifierCache[assertions] != null) {
270+
return PromisePrototypeThen(specifierCache[assertions], undefined, fallback);
271+
}
272+
const result = this.import(specifier, parentURL, importAssertions);
273+
specifierCache[assertions] = result;
274+
PromisePrototypeThen(result, undefined, removeCache);
275+
return result;
276+
};
277+
return PromisePrototypeThen(specifierCache[assertions], undefined, fallback);
278+
}
279+
const result = this.import(specifier, parentURL, importAssertions);
280+
specifierCache[assertions] = result;
281+
PromisePrototypeThen(result, undefined, removeCache);
282+
return result;
283+
}
284+
245285
/**
246286
* This method is usually called indirectly as part of the loading processes.
247287
* Internally, it is used directly to add loaders. Use directly with caution.
@@ -267,7 +307,9 @@ class ESMLoader {
267307
// create redundant work for the caller, so it is later popped off the
268308
// internal list.
269309
const wasArr = ArrayIsArray(specifiers);
270-
if (!wasArr) { specifiers = [specifiers]; }
310+
if (!wasArr) {
311+
specifiers = [specifiers];
312+
}
271313

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

lib/internal/modules/esm/translators.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ function errPath(url) {
105105
}
106106

107107
async function importModuleDynamically(specifier, { url }, assertions) {
108-
return asyncESM.esmLoader.import(specifier, url, assertions);
108+
return asyncESM.esmLoader.dynamicImport(specifier, url, assertions);
109109
}
110110

111111
// Strategy for loading a standard JavaScript module.

lib/internal/process/execution.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ function evalScript(name, body, breakFirstLine, print, shouldLoadESM = false) {
8686
[kVmBreakFirstLineSymbol]: !!breakFirstLine,
8787
importModuleDynamically(specifier, _, importAssertions) {
8888
const loader = asyncESM.esmLoader;
89-
return loader.import(specifier, baseUrl, importAssertions);
89+
return loader.dynamicImport(specifier, baseUrl, importAssertions);
9090
}
9191
}));
9292
if (print) {

lib/repl.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@ const {
7373
ObjectKeys,
7474
ObjectSetPrototypeOf,
7575
Promise,
76+
PromisePrototypeThen,
7677
ReflectApply,
7778
RegExp,
7879
RegExpPrototypeExec,
80+
SafeMap,
7981
SafePromiseRace,
8082
SafeSet,
8183
SafeWeakSet,
@@ -455,12 +457,13 @@ function REPLServer(prompt,
455457
// Remove all "await"s and attempt running the script
456458
// in order to detect if error is truly non recoverable
457459
const fallbackCode = SideEffectFreeRegExpPrototypeSymbolReplace(/\bawait\b/g, code, '');
460+
const dynamicImportCache = new SafeMap();
458461
try {
459462
vm.createScript(fallbackCode, {
460463
filename: file,
461464
displayErrors: true,
462465
importModuleDynamically: (specifier, _, importAssertions) => {
463-
return asyncESM.esmLoader.import(specifier, parentURL,
466+
return asyncESM.esmLoader.dynamicImport(specifier, parentURL,
464467
importAssertions);
465468
}
466469
});
@@ -504,7 +507,7 @@ function REPLServer(prompt,
504507
filename: file,
505508
displayErrors: true,
506509
importModuleDynamically: (specifier, _, importAssertions) => {
507-
return asyncESM.esmLoader.import(specifier, parentURL,
510+
return asyncESM.esmLoader.dynamicImport(specifier, parentURL,
508511
importAssertions);
509512
}
510513
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict';
2+
const common = require('../common');
3+
const tmpdir = require('../common/tmpdir');
4+
5+
const assert = require('node:assert');
6+
const fs = require('node:fs/promises');
7+
const { pathToFileURL } = require('node:url');
8+
9+
tmpdir.refresh();
10+
const tmpDir = pathToFileURL(tmpdir.path);
11+
12+
const target = new URL('./target.mjs', tmpDir);
13+
14+
(async () => {
15+
16+
await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' });
17+
18+
await fs.writeFile(target, 'export default "actual target"\n');
19+
20+
const moduleRecord = await import(target);
21+
22+
await fs.rm(target);
23+
24+
assert.strictEqual(await import(target), moduleRecord);
25+
})().then(common.mustCall());
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import '../common/index.mjs';
2+
import tmpdir from '../common/tmpdir.js';
3+
4+
import assert from 'node:assert';
5+
import fs from 'node:fs/promises';
6+
import { pathToFileURL } from 'node:url';
7+
8+
tmpdir.refresh();
9+
const tmpDir = pathToFileURL(tmpdir.path);
10+
11+
const target = new URL('./target.mjs', tmpDir);
12+
13+
await assert.rejects(import(target), { code: 'ERR_MODULE_NOT_FOUND' });
14+
15+
await fs.writeFile(target, 'export default "actual target"\n');
16+
17+
const moduleRecord = await import(target);
18+
19+
await fs.rm(target);
20+
21+
assert.strictEqual(await import(target), moduleRecord);

0 commit comments

Comments
 (0)