Skip to content

Commit 9794d21

Browse files
joyeecheungaduh95
authored andcommitted
module: fix submodules loaded by require() and import()
Previously there is an edge case where submodules loaded by require() may not be loaded by import() again from different intermediate edges in the graph. This patch fixes that, added tests, and added debug logs. Drive-by: make loader a private field so it doesn't show up in logs. PR-URL: #52487 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent d67a9a5 commit 9794d21

File tree

8 files changed

+67
-12
lines changed

8 files changed

+67
-12
lines changed

lib/internal/modules/esm/loader.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ class ModuleLoader {
316316
* @param {string} specifier Specifier of the the imported module.
317317
* @param {string} parentURL Where the import comes from.
318318
* @param {object} importAttributes import attributes from the import statement.
319-
* @returns {ModuleWrap}
319+
* @returns {ModuleJobBase}
320320
*/
321321
getModuleWrapForRequire(specifier, parentURL, importAttributes) {
322322
assert(getOptionValue('--experimental-require-module'));
@@ -355,7 +355,7 @@ class ModuleLoader {
355355
// completed (e.g. the require call is lazy) so it's okay. We will return the
356356
// module now and check asynchronicity of the entire graph later, after the
357357
// graph is instantiated.
358-
return job.module;
358+
return job;
359359
}
360360

361361
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
@@ -403,7 +403,7 @@ class ModuleLoader {
403403
job = new ModuleJobSync(this, url, importAttributes, wrap, isMain, inspectBrk);
404404

405405
this.loadCache.set(url, importAttributes.type, job);
406-
return job.module;
406+
return job;
407407
}
408408

409409
/**

lib/internal/modules/esm/module_job.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ const {
1919
StringPrototypeStartsWith,
2020
globalThis,
2121
} = primordials;
22+
let debug = require('internal/util/debuglog').debuglog('esm', (fn) => {
23+
debug = fn;
24+
});
2225

2326
const { ModuleWrap, kEvaluated } = internalBinding('module_wrap');
2427
const {
@@ -53,8 +56,7 @@ const isCommonJSGlobalLikeNotDefinedError = (errorMessage) =>
5356
);
5457

5558
class ModuleJobBase {
56-
constructor(loader, url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) {
57-
this.loader = loader;
59+
constructor(url, importAttributes, moduleWrapMaybePromise, isMain, inspectBrk) {
5860
this.importAttributes = importAttributes;
5961
this.isMain = isMain;
6062
this.inspectBrk = inspectBrk;
@@ -67,11 +69,13 @@ class ModuleJobBase {
6769
/* A ModuleJob tracks the loading of a single Module, and the ModuleJobs of
6870
* its dependencies, over time. */
6971
class ModuleJob extends ModuleJobBase {
72+
#loader = null;
7073
// `loader` is the Loader instance used for loading dependencies.
7174
constructor(loader, url, importAttributes = { __proto__: null },
7275
moduleProvider, isMain, inspectBrk, sync = false) {
7376
const modulePromise = ReflectApply(moduleProvider, loader, [url, isMain]);
74-
super(loader, url, importAttributes, modulePromise, isMain, inspectBrk);
77+
super(url, importAttributes, modulePromise, isMain, inspectBrk);
78+
this.#loader = loader;
7579
// Expose the promise to the ModuleWrap directly for linking below.
7680
// `this.module` is also filled in below.
7781
this.modulePromise = modulePromise;
@@ -94,7 +98,8 @@ class ModuleJob extends ModuleJobBase {
9498
// these `link` callbacks depending on each other.
9599
const dependencyJobs = [];
96100
const promises = this.module.link(async (specifier, attributes) => {
97-
const job = await this.loader.getModuleJob(specifier, url, attributes);
101+
const job = await this.#loader.getModuleJob(specifier, url, attributes);
102+
debug(`async link() ${this.url} -> ${specifier}`, job);
98103
ArrayPrototypePush(dependencyJobs, job);
99104
return job.modulePromise;
100105
});
@@ -126,6 +131,8 @@ class ModuleJob extends ModuleJobBase {
126131
async _instantiate() {
127132
const jobsInGraph = new SafeSet();
128133
const addJobsToDependencyGraph = async (moduleJob) => {
134+
debug(`async addJobsToDependencyGraph() ${this.url}`, moduleJob);
135+
129136
if (jobsInGraph.has(moduleJob)) {
130137
return;
131138
}
@@ -161,7 +168,7 @@ class ModuleJob extends ModuleJobBase {
161168
const { 1: childSpecifier, 2: name } = RegExpPrototypeExec(
162169
/module '(.*)' does not provide an export named '(.+)'/,
163170
e.message);
164-
const { url: childFileURL } = await this.loader.resolve(
171+
const { url: childFileURL } = await this.#loader.resolve(
165172
childSpecifier,
166173
parentFileUrl,
167174
kEmptyObject,
@@ -172,7 +179,7 @@ class ModuleJob extends ModuleJobBase {
172179
// in the import attributes and some formats require them; but we only
173180
// care about CommonJS for the purposes of this error message.
174181
({ format } =
175-
await this.loader.load(childFileURL));
182+
await this.#loader.load(childFileURL));
176183
} catch {
177184
// Continue regardless of error.
178185
}
@@ -265,18 +272,27 @@ class ModuleJob extends ModuleJobBase {
265272
// All the steps are ensured to be synchronous and it throws on instantiating
266273
// an asynchronous graph.
267274
class ModuleJobSync extends ModuleJobBase {
275+
#loader = null;
268276
constructor(loader, url, importAttributes, moduleWrap, isMain, inspectBrk) {
269-
super(loader, url, importAttributes, moduleWrap, isMain, inspectBrk, true);
277+
super(url, importAttributes, moduleWrap, isMain, inspectBrk, true);
270278
assert(this.module instanceof ModuleWrap);
279+
this.#loader = loader;
271280
const moduleRequests = this.module.getModuleRequestsSync();
281+
const linked = [];
272282
for (let i = 0; i < moduleRequests.length; ++i) {
273283
const { 0: specifier, 1: attributes } = moduleRequests[i];
274-
const wrap = this.loader.getModuleWrapForRequire(specifier, url, attributes);
284+
const job = this.#loader.getModuleWrapForRequire(specifier, url, attributes);
275285
const isLast = (i === moduleRequests.length - 1);
276286
// TODO(joyeecheung): make the resolution callback deal with both promisified
277287
// an raw module wraps, then we don't need to wrap it with a promise here.
278-
this.module.cacheResolvedWrapsSync(specifier, PromiseResolve(wrap), isLast);
288+
this.module.cacheResolvedWrapsSync(specifier, PromiseResolve(job.module), isLast);
289+
ArrayPrototypePush(linked, job);
279290
}
291+
this.linked = linked;
292+
}
293+
294+
get modulePromise() {
295+
return PromiseResolve(this.module);
280296
}
281297

282298
async run() {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
4+
// This tests that previously synchronously loaded submodule can still
5+
// be loaded by dynamic import().
6+
7+
const common = require('../common');
8+
const assert = require('assert');
9+
10+
(async () => {
11+
const required = require('../fixtures/es-modules/require-and-import/load.cjs');
12+
const imported = await import('../fixtures/es-modules/require-and-import/load.mjs');
13+
assert.deepStrictEqual({ ...required }, { ...imported });
14+
})().then(common.mustCall());
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Flags: --experimental-require-module
2+
'use strict';
3+
4+
// This tests that previously asynchronously loaded submodule can still
5+
// be loaded by require().
6+
7+
const common = require('../common');
8+
const assert = require('assert');
9+
10+
(async () => {
11+
const imported = await import('../fixtures/es-modules/require-and-import/load.mjs');
12+
const required = require('../fixtures/es-modules/require-and-import/load.cjs');
13+
assert.deepStrictEqual({ ...required }, { ...imported });
14+
})().then(common.mustCall());
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module.exports = require('dep');
2+
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from 'dep';
2+

test/fixtures/es-modules/require-and-import/node_modules/dep/mod.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-modules/require-and-import/node_modules/dep/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)