Skip to content

Commit 7e19cd0

Browse files
committed
esm: populate separate cache for require(esm) in imported CJS
Otherwise if the ESM happens to be cached separately by the ESM loader before it gets loaded with `require(esm)` from within an imported CJS file (which uses a re-invented require() with a couple of quirks, including a separate cache), it won't be able to load the esm properly from the cache. PR-URL: #59679 Refs: #59666 Refs: #52697 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent 0781bd3 commit 7e19cd0

File tree

13 files changed

+239
-78
lines changed

13 files changed

+239
-78
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 80 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ const {
6969
module_export_names_private_symbol,
7070
module_circular_visited_private_symbol,
7171
module_export_private_symbol,
72-
module_parent_private_symbol,
72+
module_first_parent_private_symbol,
73+
module_last_parent_private_symbol,
7374
},
7475
isInsideNodeModules,
7576
} = internalBinding('util');
@@ -94,9 +95,13 @@ const kModuleCircularVisited = module_circular_visited_private_symbol;
9495
*/
9596
const kModuleExport = module_export_private_symbol;
9697
/**
97-
* {@link Module} parent module.
98+
* {@link Module} The first parent module that loads a module with require().
9899
*/
99-
const kModuleParent = module_parent_private_symbol;
100+
const kFirstModuleParent = module_first_parent_private_symbol;
101+
/**
102+
* {@link Module} The last parent module that loads a module with require().
103+
*/
104+
const kLastModuleParent = module_last_parent_private_symbol;
100105

101106
const kIsMainSymbol = Symbol('kIsMainSymbol');
102107
const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
@@ -117,6 +122,7 @@ module.exports = {
117122
findLongestRegisteredExtension,
118123
resolveForCJSWithHooks,
119124
loadSourceForCJSWithHooks: loadSource,
125+
populateCJSExportsFromESM,
120126
wrapSafe,
121127
wrapModuleLoad,
122128
kIsMainSymbol,
@@ -320,7 +326,8 @@ function Module(id = '', parent) {
320326
this.id = id;
321327
this.path = path.dirname(id);
322328
setOwnProperty(this, 'exports', {});
323-
this[kModuleParent] = parent;
329+
this[kFirstModuleParent] = parent;
330+
this[kLastModuleParent] = parent;
324331
updateChildren(parent, this, false);
325332
this.filename = null;
326333
this.loaded = false;
@@ -400,7 +407,7 @@ ObjectDefineProperty(BuiltinModule.prototype, 'isPreloading', isPreloadingDesc);
400407
* @this {Module}
401408
*/
402409
function getModuleParent() {
403-
return this[kModuleParent];
410+
return this[kFirstModuleParent];
404411
}
405412

406413
/**
@@ -409,7 +416,7 @@ function getModuleParent() {
409416
* @param {Module} value
410417
*/
411418
function setModuleParent(value) {
412-
this[kModuleParent] = value;
419+
this[kFirstModuleParent] = value;
413420
}
414421

415422
let debug = debuglog('module', (fn) => {
@@ -972,7 +979,7 @@ function getExportsForCircularRequire(module) {
972979
const requiredESM = module[kRequiredModuleSymbol];
973980
if (requiredESM && requiredESM.getStatus() !== kEvaluated) {
974981
let message = `Cannot require() ES Module ${module.id} in a cycle.`;
975-
const parent = module[kModuleParent];
982+
const parent = module[kLastModuleParent];
976983
if (parent) {
977984
message += ` (from ${parent.filename})`;
978985
}
@@ -1252,6 +1259,8 @@ Module._load = function(request, parent, isMain) {
12521259
// load hooks for the module keyed by the (potentially customized) filename.
12531260
module[kURL] = url;
12541261
module[kFormat] = format;
1262+
} else {
1263+
module[kLastModuleParent] = parent;
12551264
}
12561265

12571266
if (parent !== undefined) {
@@ -1371,7 +1380,8 @@ Module._resolveFilename = function(request, parent, isMain, options) {
13711380
const requireStack = [];
13721381
for (let cursor = parent;
13731382
cursor;
1374-
cursor = cursor[kModuleParent]) {
1383+
// TODO(joyeecheung): it makes more sense to use kLastModuleParent here.
1384+
cursor = cursor[kFirstModuleParent]) {
13751385
ArrayPrototypePush(requireStack, cursor.filename || cursor.id);
13761386
}
13771387
let message = `Cannot find module '${request}'`;
@@ -1485,7 +1495,7 @@ function loadESMFromCJS(mod, filename, format, source) {
14851495
// ESM won't be accessible via process.mainModule.
14861496
setOwnProperty(process, 'mainModule', undefined);
14871497
} else {
1488-
const parent = mod[kModuleParent];
1498+
const parent = mod[kLastModuleParent];
14891499

14901500
requireModuleWarningMode ??= getOptionValue('--trace-require-module');
14911501
if (requireModuleWarningMode) {
@@ -1534,54 +1544,66 @@ function loadESMFromCJS(mod, filename, format, source) {
15341544
wrap,
15351545
namespace,
15361546
} = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, parent);
1537-
// Tooling in the ecosystem have been using the __esModule property to recognize
1538-
// transpiled ESM in consuming code. For example, a 'log' package written in ESM:
1539-
//
1540-
// export default function log(val) { console.log(val); }
1541-
//
1542-
// Can be transpiled as:
1543-
//
1544-
// exports.__esModule = true;
1545-
// exports.default = function log(val) { console.log(val); }
1546-
//
1547-
// The consuming code may be written like this in ESM:
1548-
//
1549-
// import log from 'log'
1550-
//
1551-
// Which gets transpiled to:
1552-
//
1553-
// const _mod = require('log');
1554-
// const log = _mod.__esModule ? _mod.default : _mod;
1555-
//
1556-
// So to allow transpiled consuming code to recognize require()'d real ESM
1557-
// as ESM and pick up the default exports, we add a __esModule property by
1558-
// building a source text module facade for any module that has a default
1559-
// export and add .__esModule = true to the exports. This maintains the
1560-
// enumerability of the re-exported names and the live binding of the exports,
1561-
// without incurring a non-trivial per-access overhead on the exports.
1562-
//
1563-
// The source of the facade is defined as a constant per-isolate property
1564-
// required_module_default_facade_source_string, which looks like this
1565-
//
1566-
// export * from 'original';
1567-
// export { default } from 'original';
1568-
// export const __esModule = true;
1569-
//
1570-
// And the 'original' module request is always resolved by
1571-
// createRequiredModuleFacade() to `wrap` which is a ModuleWrap wrapping
1572-
// over the original module.
1573-
1574-
// We don't do this to modules that are marked as CJS ESM or that
1575-
// don't have default exports to avoid the unnecessary overhead.
1576-
// If __esModule is already defined, we will also skip the extension
1577-
// to allow users to override it.
1578-
if (ObjectHasOwn(namespace, 'module.exports')) {
1579-
mod.exports = namespace['module.exports'];
1580-
} else if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) {
1581-
mod.exports = namespace;
1582-
} else {
1583-
mod.exports = createRequiredModuleFacade(wrap);
1584-
}
1547+
1548+
populateCJSExportsFromESM(mod, wrap, namespace);
1549+
}
1550+
}
1551+
1552+
/**
1553+
* Populate the exports of a CJS module entry from an ESM module's namespace object for
1554+
* require(esm).
1555+
* @param {Module} mod CJS module instance
1556+
* @param {ModuleWrap} wrap ESM ModuleWrap instance.
1557+
* @param {object} namespace The ESM namespace object.
1558+
*/
1559+
function populateCJSExportsFromESM(mod, wrap, namespace) {
1560+
// Tooling in the ecosystem have been using the __esModule property to recognize
1561+
// transpiled ESM in consuming code. For example, a 'log' package written in ESM:
1562+
//
1563+
// export default function log(val) { console.log(val); }
1564+
//
1565+
// Can be transpiled as:
1566+
//
1567+
// exports.__esModule = true;
1568+
// exports.default = function log(val) { console.log(val); }
1569+
//
1570+
// The consuming code may be written like this in ESM:
1571+
//
1572+
// import log from 'log'
1573+
//
1574+
// Which gets transpiled to:
1575+
//
1576+
// const _mod = require('log');
1577+
// const log = _mod.__esModule ? _mod.default : _mod;
1578+
//
1579+
// So to allow transpiled consuming code to recognize require()'d real ESM
1580+
// as ESM and pick up the default exports, we add a __esModule property by
1581+
// building a source text module facade for any module that has a default
1582+
// export and add .__esModule = true to the exports. This maintains the
1583+
// enumerability of the re-exported names and the live binding of the exports,
1584+
// without incurring a non-trivial per-access overhead on the exports.
1585+
//
1586+
// The source of the facade is defined as a constant per-isolate property
1587+
// required_module_default_facade_source_string, which looks like this
1588+
//
1589+
// export * from 'original';
1590+
// export { default } from 'original';
1591+
// export const __esModule = true;
1592+
//
1593+
// And the 'original' module request is always resolved by
1594+
// createRequiredModuleFacade() to `wrap` which is a ModuleWrap wrapping
1595+
// over the original module.
1596+
1597+
// We don't do this to modules that are marked as CJS ESM or that
1598+
// don't have default exports to avoid the unnecessary overhead.
1599+
// If __esModule is already defined, we will also skip the extension
1600+
// to allow users to override it.
1601+
if (ObjectHasOwn(namespace, 'module.exports')) {
1602+
mod.exports = namespace['module.exports'];
1603+
} else if (!ObjectHasOwn(namespace, 'default') || ObjectHasOwn(namespace, '__esModule')) {
1604+
mod.exports = namespace;
1605+
} else {
1606+
mod.exports = createRequiredModuleFacade(wrap);
15851607
}
15861608
}
15871609

@@ -1772,7 +1794,7 @@ function reconstructErrorStack(err, parentPath, parentSource) {
17721794
*/
17731795
function getRequireESMError(mod, pkg, content, filename) {
17741796
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1775-
const parent = mod[kModuleParent];
1797+
const parent = mod[kFirstModuleParent];
17761798
const parentPath = parent?.filename;
17771799
const packageJsonPath = pkg?.path;
17781800
const usesEsm = containsModuleSyntax(content, filename);

lib/internal/modules/esm/loader.js

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const {
1717
const {
1818
kIsExecuting,
1919
kRequiredModuleSymbol,
20+
Module: CJSModule,
2021
} = require('internal/modules/cjs/loader');
2122
const { imported_cjs_symbol } = internalBinding('symbols');
2223

@@ -89,13 +90,18 @@ function newLoadCache() {
8990
return new LoadCache();
9091
}
9192

93+
let _translators;
94+
function lazyLoadTranslators() {
95+
_translators ??= require('internal/modules/esm/translators');
96+
return _translators;
97+
}
98+
9299
/**
93100
* Lazy-load translators to avoid potentially unnecessary work at startup (ex if ESM is not used).
94101
* @returns {import('./translators.js').Translators}
95102
*/
96103
function getTranslators() {
97-
const { translators } = require('internal/modules/esm/translators');
98-
return translators;
104+
return lazyLoadTranslators().translators;
99105
}
100106

101107
/**
@@ -496,7 +502,7 @@ class ModuleLoader {
496502

497503
const { source } = loadResult;
498504
const isMain = (parentURL === undefined);
499-
const wrap = this.#translate(url, finalFormat, source, isMain);
505+
const wrap = this.#translate(url, finalFormat, source, parentURL);
500506
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
501507

502508
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
@@ -532,18 +538,31 @@ class ModuleLoader {
532538
* @param {string} format Format of the module to be translated. This is used to find
533539
* matching translators.
534540
* @param {ModuleSource} source Source of the module to be translated.
535-
* @param {boolean} isMain Whether the module to be translated is the entry point.
541+
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
536542
* @returns {ModuleWrap}
537543
*/
538-
#translate(url, format, source, isMain) {
544+
#translate(url, format, source, parentURL) {
539545
this.validateLoadResult(url, format);
540546
const translator = getTranslators().get(format);
541547

542548
if (!translator) {
543549
throw new ERR_UNKNOWN_MODULE_FORMAT(format, url);
544550
}
545551

546-
const result = FunctionPrototypeCall(translator, this, url, source, isMain);
552+
// Populate the CJS cache with a facade for ESM in case subsequent require(esm) is
553+
// looking it up from the cache. The parent module of the CJS cache entry would be the
554+
// first CJS module that loads it with require(). This is an approximation, because
555+
// ESM caches more and it does not get re-loaded and updated every time an `import` is
556+
// encountered, unlike CJS require(), and we only use the parent entry to provide
557+
// more information in error messages.
558+
if (format === 'module') {
559+
const parentFilename = urlToFilename(parentURL);
560+
const parent = parentFilename ? CJSModule._cache[parentFilename] : undefined;
561+
const cjsModule = lazyLoadTranslators().cjsEmplaceModuleCacheEntryForURL(url, parent);
562+
debug('cjsEmplaceModuleCacheEntryForURL', url, parent, cjsModule);
563+
}
564+
565+
const result = FunctionPrototypeCall(translator, this, url, source, parentURL === undefined);
547566
assert(result instanceof ModuleWrap);
548567
return result;
549568
}
@@ -553,10 +572,10 @@ class ModuleLoader {
553572
* This is run synchronously, and the translator always return a ModuleWrap synchronously.
554573
* @param {string} url URL of the module to be translated.
555574
* @param {object} loadContext See {@link load}
556-
* @param {boolean} isMain Whether the module to be translated is the entry point.
575+
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
557576
* @returns {ModuleWrap}
558577
*/
559-
loadAndTranslateForRequireInImportedCJS(url, loadContext, isMain) {
578+
loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) {
560579
const { format: formatFromLoad, source } = this.#loadSync(url, loadContext);
561580

562581
if (formatFromLoad === 'wasm') { // require(wasm) is not supported.
@@ -577,7 +596,7 @@ class ModuleLoader {
577596
finalFormat = 'require-commonjs-typescript';
578597
}
579598

580-
const wrap = this.#translate(url, finalFormat, source, isMain);
599+
const wrap = this.#translate(url, finalFormat, source, parentURL);
581600
assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`);
582601
return wrap;
583602
}
@@ -587,13 +606,13 @@ class ModuleLoader {
587606
* This may be run asynchronously if there are asynchronous module loader hooks registered.
588607
* @param {string} url URL of the module to be translated.
589608
* @param {object} loadContext See {@link load}
590-
* @param {boolean} isMain Whether the module to be translated is the entry point.
609+
* @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point.
591610
* @returns {Promise<ModuleWrap>|ModuleWrap}
592611
*/
593-
loadAndTranslate(url, loadContext, isMain) {
612+
loadAndTranslate(url, loadContext, parentURL) {
594613
const maybePromise = this.load(url, loadContext);
595614
const afterLoad = ({ format, source }) => {
596-
return this.#translate(url, format, source, isMain);
615+
return this.#translate(url, format, source, parentURL);
597616
};
598617
if (isPromise(maybePromise)) {
599618
return maybePromise.then(afterLoad);
@@ -619,9 +638,9 @@ class ModuleLoader {
619638
const isMain = parentURL === undefined;
620639
let moduleOrModulePromise;
621640
if (isForRequireInImportedCJS) {
622-
moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, isMain);
641+
moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, parentURL);
623642
} else {
624-
moduleOrModulePromise = this.loadAndTranslate(url, context, isMain);
643+
moduleOrModulePromise = this.loadAndTranslate(url, context, parentURL);
625644
}
626645

627646
const inspectBrk = (

0 commit comments

Comments
 (0)