module: disallow CJS <-> ESM edges in a cycle from require(esm) · nodejs/node@51b88fa
@@ -63,24 +63,33 @@ const {
6363 Symbol,
6464} = primordials;
656566+const { kEvaluated } = internalBinding('module_wrap');
67+6668// Map used to store CJS parsing data or for ESM loading.
67-const cjsSourceCache = new SafeWeakMap();
69+const importedCJSCache = new SafeWeakMap();
6870/**
6971 * Map of already-loaded CJS modules to use.
7072 */
7173const cjsExportsCache = new SafeWeakMap();
74+const requiredESMSourceCache = new SafeWeakMap();
727576+const kIsMainSymbol = Symbol('kIsMainSymbol');
77+const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
78+const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
79+const kIsExecuting = Symbol('kIsExecuting');
7380// Set first due to cycle with ESM loader functions.
7481module.exports = {
7582 cjsExportsCache,
76-cjsSourceCache,
83+importedCJSCache,
7784 initializeCJS,
7885 Module,
7986 wrapSafe,
87+ kIsMainSymbol,
88+ kIsCachedByESMLoader,
89+ kRequiredModuleSymbol,
90+ kIsExecuting,
8091};
819282-const kIsMainSymbol = Symbol('kIsMainSymbol');
83-8493const { BuiltinModule } = require('internal/bootstrap/realm');
8594const {
8695 maybeCacheSourceMap,
@@ -137,6 +146,7 @@ const {
137146codes: {
138147ERR_INVALID_ARG_VALUE,
139148ERR_INVALID_MODULE_SPECIFIER,
149+ERR_REQUIRE_CYCLE_MODULE,
140150ERR_REQUIRE_ESM,
141151ERR_UNKNOWN_BUILTIN_MODULE,
142152},
@@ -942,6 +952,16 @@ const CircularRequirePrototypeWarningProxy = new Proxy({}, {
942952 * @param {Module} module The module instance
943953 */
944954function getExportsForCircularRequire(module) {
955+const requiredESM = module[kRequiredModuleSymbol];
956+if (requiredESM && requiredESM.getStatus() !== kEvaluated) {
957+let message = `Cannot require() ES Module ${module.id} in a cycle.`;
958+const parent = moduleParentCache.get(module);
959+if (parent) {
960+message += ` (from ${parent.filename})`;
961+}
962+throw new ERR_REQUIRE_CYCLE_MODULE(message);
963+}
964+945965if (module.exports &&
946966!isProxy(module.exports) &&
947967ObjectGetPrototypeOf(module.exports) === ObjectPrototype &&
@@ -1009,11 +1029,21 @@ Module._load = function(request, parent, isMain) {
10091029if (cachedModule !== undefined) {
10101030updateChildren(parent, cachedModule, true);
10111031if (!cachedModule.loaded) {
1012-const parseCachedModule = cjsSourceCache.get(cachedModule);
1013-if (!parseCachedModule || parseCachedModule.loaded) {
1032+// If it's not cached by the ESM loader, the loading request
1033+// comes from required CJS, and we can consider it a circular
1034+// dependency when it's cached.
1035+if (!cachedModule[kIsCachedByESMLoader]) {
10141036return getExportsForCircularRequire(cachedModule);
10151037}
1016-parseCachedModule.loaded = true;
1038+// If it's cached by the ESM loader as a way to indirectly pass
1039+// the module in to avoid creating it twice, the loading request
1040+// come from imported CJS. In that case use the importedCJSCache
1041+// to determine if it's loading or not.
1042+const importedCJSMetadata = importedCJSCache.get(cachedModule);
1043+if (importedCJSMetadata.loading) {
1044+return getExportsForCircularRequire(cachedModule);
1045+}
1046+importedCJSMetadata.loading = true;
10171047} else {
10181048return cachedModule.exports;
10191049}
@@ -1027,18 +1057,21 @@ Module._load = function(request, parent, isMain) {
10271057// Don't call updateChildren(), Module constructor already does.
10281058const module = cachedModule || new Module(filename, parent);
102910591030-if (isMain) {
1031-setOwnProperty(process, 'mainModule', module);
1032-setOwnProperty(module.require, 'main', process.mainModule);
1033-module.id = '.';
1034-module[kIsMainSymbol] = true;
1035-} else {
1036-module[kIsMainSymbol] = false;
1037-}
1060+if (!cachedModule) {
1061+if (isMain) {
1062+setOwnProperty(process, 'mainModule', module);
1063+setOwnProperty(module.require, 'main', process.mainModule);
1064+module.id = '.';
1065+module[kIsMainSymbol] = true;
1066+} else {
1067+module[kIsMainSymbol] = false;
1068+}
103810691039-reportModuleToWatchMode(filename);
1070+reportModuleToWatchMode(filename);
1071+Module._cache[filename] = module;
1072+module[kIsCachedByESMLoader] = false;
1073+}
104010741041-Module._cache[filename] = module;
10421075if (parent !== undefined) {
10431076relativeResolveCache[relResolveCacheIdentifier] = filename;
10441077}
@@ -1280,7 +1313,7 @@ function loadESMFromCJS(mod, filename) {
12801313const isMain = mod[kIsMainSymbol];
12811314// TODO(joyeecheung): we may want to invent optional special handling for default exports here.
12821315// For now, it's good enough to be identical to what `import()` returns.
1283-mod.exports = cascadedLoader.importSyncForRequire(filename, source, isMain);
1316+mod.exports = cascadedLoader.importSyncForRequire(mod, filename, source, isMain, moduleParentCache.get(mod));
12841317}
1285131812861319/**
@@ -1366,7 +1399,7 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) {
13661399// Only modules being require()'d really need to avoid TLA.
13671400if (loadAsESM) {
13681401// Pass the source into the .mjs extension handler indirectly through the cache.
1369-cjsSourceCache.set(this, { source: content });
1402+requiredESMSourceCache.set(this, content);
13701403loadESMFromCJS(this, filename);
13711404return;
13721405}
@@ -1407,13 +1440,15 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) {
14071440const module = this;
14081441if (requireDepth === 0) { statCache = new SafeMap(); }
14091442setHasStartedUserCJSExecution();
1443+this[kIsExecuting] = true;
14101444if (inspectorWrapper) {
14111445result = inspectorWrapper(compiledWrapper, thisValue, exports,
14121446require, module, filename, dirname);
14131447} else {
14141448result = ReflectApply(compiledWrapper, thisValue,
14151449[exports, require, module, filename, dirname]);
14161450}
1451+this[kIsExecuting] = false;
14171452if (requireDepth === 0) { statCache = null; }
14181453return result;
14191454};
@@ -1425,15 +1460,15 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) {
14251460 * @returns {string}
14261461 */
14271462function getMaybeCachedSource(mod, filename) {
1428-const cached = cjsSourceCache.get(mod);
1463+const cached = importedCJSCache.get(mod);
14291464let content;
14301465if (cached?.source) {
14311466content = cached.source;
14321467cached.source = undefined;
14331468} else {
14341469// TODO(joyeecheung): we can read a buffer instead to speed up
14351470// compilation.
1436-content = fs.readFileSync(filename, 'utf8');
1471+content = requiredESMSourceCache.get(mod) ?? fs.readFileSync(filename, 'utf8');
14371472}
14381473return content;
14391474}