◐ Shell
clean mode source ↗

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

*/

7173

const 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.

7481

module.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-8493

const { BuiltinModule } = require('internal/bootstrap/realm');

8594

const {

8695

maybeCacheSourceMap,

@@ -137,6 +146,7 @@ const {

137146

codes: {

138147

ERR_INVALID_ARG_VALUE,

139148

ERR_INVALID_MODULE_SPECIFIER,

149+

ERR_REQUIRE_CYCLE_MODULE,

140150

ERR_REQUIRE_ESM,

141151

ERR_UNKNOWN_BUILTIN_MODULE,

142152

},

@@ -942,6 +952,16 @@ const CircularRequirePrototypeWarningProxy = new Proxy({}, {

942952

* @param {Module} module The module instance

943953

*/

944954

function 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+945965

if (module.exports &&

946966

!isProxy(module.exports) &&

947967

ObjectGetPrototypeOf(module.exports) === ObjectPrototype &&

@@ -1009,11 +1029,21 @@ Module._load = function(request, parent, isMain) {

10091029

if (cachedModule !== undefined) {

10101030

updateChildren(parent, cachedModule, true);

10111031

if (!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]) {

10141036

return 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 {

10181048

return cachedModule.exports;

10191049

}

@@ -1027,18 +1057,21 @@ Module._load = function(request, parent, isMain) {

10271057

// Don't call updateChildren(), Module constructor already does.

10281058

const 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;

10421075

if (parent !== undefined) {

10431076

relativeResolveCache[relResolveCacheIdentifier] = filename;

10441077

}

@@ -1280,7 +1313,7 @@ function loadESMFromCJS(mod, filename) {

12801313

const 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.

13671400

if (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);

13701403

loadESMFromCJS(this, filename);

13711404

return;

13721405

}

@@ -1407,13 +1440,15 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) {

14071440

const module = this;

14081441

if (requireDepth === 0) { statCache = new SafeMap(); }

14091442

setHasStartedUserCJSExecution();

1443+

this[kIsExecuting] = true;

14101444

if (inspectorWrapper) {

14111445

result = inspectorWrapper(compiledWrapper, thisValue, exports,

14121446

require, module, filename, dirname);

14131447

} else {

14141448

result = ReflectApply(compiledWrapper, thisValue,

14151449

[exports, require, module, filename, dirname]);

14161450

}

1451+

this[kIsExecuting] = false;

14171452

if (requireDepth === 0) { statCache = null; }

14181453

return result;

14191454

};

@@ -1425,15 +1460,15 @@ Module.prototype._compile = function(content, filename, loadAsESM = false) {

14251460

* @returns {string}

14261461

*/

14271462

function getMaybeCachedSource(mod, filename) {

1428-

const cached = cjsSourceCache.get(mod);

1463+

const cached = importedCJSCache.get(mod);

14291464

let content;

14301465

if (cached?.source) {

14311466

content = cached.source;

14321467

cached.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

}

14381473

return content;

14391474

}