Skip to content

Commit c1e7fa2

Browse files
joyeecheungmarco-ippolito
authored andcommitted
module: handle .mjs in .js handler in CommonJS
This refactors the CommonJS loading a bit to create a center point that handles source loading (`loadSource`) and make format detection more consistent to pave the way for future synchronous hooks. - Handle .mjs in the .js handler, similar to how .cjs has been handled. - Generate the legacy ERR_REQUIRE_ESM in a getRequireESMError() for require(esm) handling (when it's disabled). PR-URL: #55590 Backport-PR-URL: #59504 Refs: nodejs/loaders#198 Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Marco Ippolito <[email protected]> Reviewed-By: Juan José Arboleda <[email protected]> Refs: #52697
1 parent 621b66a commit c1e7fa2

File tree

1 file changed

+86
-79
lines changed

1 file changed

+86
-79
lines changed

lib/internal/modules/cjs/loader.js

Lines changed: 86 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ const kIsMainSymbol = Symbol('kIsMainSymbol');
101101
const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
102102
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
103103
const kIsExecuting = Symbol('kIsExecuting');
104+
105+
const kFormat = Symbol('kFormat');
106+
104107
// Set first due to cycle with ESM loader functions.
105108
module.exports = {
106109
kModuleSource,
@@ -438,10 +441,6 @@ function initializeCJS() {
438441
// TODO(joyeecheung): deprecate this in favor of a proper hook?
439442
Module.runMain =
440443
require('internal/modules/run_main').executeUserEntryPoint;
441-
442-
if (getOptionValue('--experimental-require-module')) {
443-
Module._extensions['.mjs'] = loadESMFromCJS;
444-
}
445444
}
446445

447446
// Given a module name, and a list of paths to test, returns the first
@@ -651,14 +650,7 @@ function resolveExports(nmPath, request) {
651650
// We don't cache this in case user extends the extensions.
652651
function getDefaultExtensions() {
653652
const extensions = ObjectKeys(Module._extensions);
654-
if (!getOptionValue('--experimental-require-module')) {
655-
return extensions;
656-
}
657-
// If the .mjs extension is added by --experimental-require-module,
658-
// remove it from the supported default extensions to maintain
659-
// compatibility.
660-
// TODO(joyeecheung): allow both .mjs and .cjs?
661-
return ArrayPrototypeFilter(extensions, (ext) => ext !== '.mjs' || Module._extensions['.mjs'] !== loadESMFromCJS);
653+
return extensions;
662654
}
663655

664656
/**
@@ -1270,10 +1262,6 @@ Module.prototype.load = function(filename) {
12701262
this.paths = Module._nodeModulePaths(path.dirname(filename));
12711263

12721264
const extension = findLongestRegisteredExtension(filename);
1273-
// allow .mjs to be overridden
1274-
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {
1275-
throw new ERR_REQUIRE_ESM(filename, true);
1276-
}
12771265

12781266
Module._extensions[extension](this, filename);
12791267
this.loaded = true;
@@ -1309,9 +1297,10 @@ let requireModuleWarningMode;
13091297
* Resolve and evaluate it synchronously as ESM if it's ESM.
13101298
* @param {Module} mod CJS module instance
13111299
* @param {string} filename Absolute path of the file.
1300+
* @param {string} format Format of the module. If it had types, this would be what it is after type-stripping.
1301+
* @param {string} source Source the module. If it had types, this would have the type stripped.
13121302
*/
1313-
function loadESMFromCJS(mod, filename) {
1314-
const source = getMaybeCachedSource(mod, filename);
1303+
function loadESMFromCJS(mod, filename, format, source) {
13151304
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13161305
const isMain = mod[kIsMainSymbol];
13171306
if (isMain) {
@@ -1487,7 +1476,9 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
14871476
* `exports`) to the file. Returns exception, if any.
14881477
* @param {string} content The source code of the module
14891478
* @param {string} filename The file path of the module
1490-
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
1479+
* @param {
1480+
* 'module'|'commonjs'|'commonjs-typescript'|'module-typescript'
1481+
* } format Intended format of the module.
14911482
*/
14921483
Module.prototype._compile = function(content, filename, format) {
14931484
let moduleURL;
@@ -1509,9 +1500,7 @@ Module.prototype._compile = function(content, filename, format) {
15091500
}
15101501

15111502
if (format === 'module') {
1512-
// Pass the source into the .mjs extension handler indirectly through the cache.
1513-
this[kModuleSource] = content;
1514-
loadESMFromCJS(this, filename);
1503+
loadESMFromCJS(this, filename, format, content);
15151504
return;
15161505
}
15171506

@@ -1539,22 +1528,72 @@ Module.prototype._compile = function(content, filename, format) {
15391528

15401529
/**
15411530
* Get the source code of a module, using cached ones if it's cached.
1531+
* After this returns, mod[kFormat], mod[kModuleSource] and mod[kURL] will be set.
15421532
* @param {Module} mod Module instance whose source is potentially already cached.
15431533
* @param {string} filename Absolute path to the file of the module.
1544-
* @returns {string}
1534+
* @returns {{source: string, format?: string}}
15451535
*/
1546-
function getMaybeCachedSource(mod, filename) {
1547-
// If already analyzed the source, then it will be cached.
1548-
let content;
1549-
if (mod[kModuleSource] !== undefined) {
1550-
content = mod[kModuleSource];
1536+
function loadSource(mod, filename, formatFromNode) {
1537+
if (formatFromNode !== undefined) {
1538+
mod[kFormat] = formatFromNode;
1539+
}
1540+
const format = mod[kFormat];
1541+
1542+
let source = mod[kModuleSource];
1543+
if (source !== undefined) {
15511544
mod[kModuleSource] = undefined;
15521545
} else {
15531546
// TODO(joyeecheung): we can read a buffer instead to speed up
15541547
// compilation.
1555-
content = fs.readFileSync(filename, 'utf8');
1548+
source = fs.readFileSync(filename, 'utf8');
1549+
}
1550+
return { source, format };
1551+
}
1552+
1553+
function reconstructErrorStack(err, parentPath, parentSource) {
1554+
const errLine = StringPrototypeSplit(
1555+
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1556+
err.stack, ' at ')), '\n', 1)[0];
1557+
const { 1: line, 2: col } =
1558+
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
1559+
if (line && col) {
1560+
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1561+
const frame = `${parentPath}:${line}\n${srcLine}\n${StringPrototypeRepeat(' ', col - 1)}^\n`;
1562+
setArrowMessage(err, frame);
1563+
}
1564+
}
1565+
1566+
/**
1567+
* Generate the legacy ERR_REQUIRE_ESM for the cases where require(esm) is disabled.
1568+
* @param {Module} mod The module being required.
1569+
* @param {undefined|object} pkg Data of the nearest package.json of the module.
1570+
* @param {string} content Source code of the module.
1571+
* @param {string} filename Filename of the module
1572+
* @returns {Error}
1573+
*/
1574+
function getRequireESMError(mod, pkg, content, filename) {
1575+
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1576+
const parent = mod[kModuleParent];
1577+
const parentPath = parent?.filename;
1578+
const packageJsonPath = pkg?.path ? path.resolve(pkg.path, 'package.json') : null;
1579+
const usesEsm = containsModuleSyntax(content, filename);
1580+
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1581+
packageJsonPath);
1582+
// Attempt to reconstruct the parent require frame.
1583+
const parentModule = Module._cache[parentPath];
1584+
if (parentModule) {
1585+
let parentSource;
1586+
try {
1587+
({ source: parentSource } = loadSource(parentModule, parentPath));
1588+
} catch {
1589+
// Continue regardless of error.
1590+
}
1591+
if (parentSource) {
1592+
// TODO(joyeecheung): trim off internal frames from the stack.
1593+
reconstructErrorStack(err, parentPath, parentSource);
1594+
}
15561595
}
1557-
return content;
1596+
return err;
15581597
}
15591598

15601599
/**
@@ -1563,57 +1602,25 @@ function getMaybeCachedSource(mod, filename) {
15631602
* @param {string} filename The file path of the module
15641603
*/
15651604
Module._extensions['.js'] = function(module, filename) {
1566-
// If already analyzed the source, then it will be cached.
1567-
const content = getMaybeCachedSource(module, filename);
1568-
1569-
let format;
1570-
if (StringPrototypeEndsWith(filename, '.js')) {
1571-
const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
1572-
// Function require shouldn't be used in ES modules.
1573-
if (pkg.data?.type === 'module') {
1574-
if (getOptionValue('--experimental-require-module')) {
1575-
module._compile(content, filename, 'module');
1576-
return;
1577-
}
1578-
1579-
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1580-
const parent = module[kModuleParent];
1581-
const parentPath = parent?.filename;
1582-
const packageJsonPath = path.resolve(pkg.path, 'package.json');
1583-
const usesEsm = containsModuleSyntax(content, filename);
1584-
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1585-
packageJsonPath);
1586-
// Attempt to reconstruct the parent require frame.
1587-
if (Module._cache[parentPath]) {
1588-
let parentSource;
1589-
try {
1590-
parentSource = fs.readFileSync(parentPath, 'utf8');
1591-
} catch {
1592-
// Continue regardless of error.
1593-
}
1594-
if (parentSource) {
1595-
const errLine = StringPrototypeSplit(
1596-
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1597-
err.stack, ' at ')), '\n', 1)[0];
1598-
const { 1: line, 2: col } =
1599-
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
1600-
if (line && col) {
1601-
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1602-
const frame = `${parentPath}:${line}\n${srcLine}\n${
1603-
StringPrototypeRepeat(' ', col - 1)}^\n`;
1604-
setArrowMessage(err, frame);
1605-
}
1606-
}
1607-
}
1608-
throw err;
1609-
} else if (pkg.data?.type === 'commonjs') {
1610-
format = 'commonjs';
1611-
}
1612-
} else if (StringPrototypeEndsWith(filename, '.cjs')) {
1605+
let format, pkg;
1606+
if (StringPrototypeEndsWith(filename, '.cjs')) {
16131607
format = 'commonjs';
1608+
} else if (StringPrototypeEndsWith(filename, '.mjs')) {
1609+
format = 'module';
1610+
} else if (StringPrototypeEndsWith(filename, '.js')) {
1611+
pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
1612+
const typeFromPjson = pkg.data?.type;
1613+
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
1614+
format = typeFromPjson;
1615+
}
16141616
}
1615-
1616-
module._compile(content, filename, format);
1617+
const { source, format: loadedFormat } = loadSource(module, filename, format);
1618+
// Function require shouldn't be used in ES modules when require(esm) is disabled.
1619+
if (loadedFormat === 'module' && !getOptionValue('--experimental-require-module')) {
1620+
const err = getRequireESMError(module, pkg, source, filename);
1621+
throw err;
1622+
}
1623+
module._compile(source, filename, loadedFormat);
16171624
};
16181625

16191626
/**

0 commit comments

Comments
 (0)