Skip to content

Commit 0e24df4

Browse files
committed
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: nodejs#55590 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]>
1 parent fc777a6 commit 0e24df4

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,
@@ -434,10 +437,6 @@ function initializeCJS() {
434437
// TODO(joyeecheung): deprecate this in favor of a proper hook?
435438
Module.runMain =
436439
require('internal/modules/run_main').executeUserEntryPoint;
437-
438-
if (getOptionValue('--experimental-require-module')) {
439-
Module._extensions['.mjs'] = loadESMFromCJS;
440-
}
441440
}
442441

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

660652
/**
@@ -1263,10 +1255,6 @@ Module.prototype.load = function(filename) {
12631255
this.paths = Module._nodeModulePaths(path.dirname(filename));
12641256

12651257
const extension = findLongestRegisteredExtension(filename);
1266-
// allow .mjs to be overridden
1267-
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {
1268-
throw new ERR_REQUIRE_ESM(filename, true);
1269-
}
12701258

12711259
Module._extensions[extension](this, filename);
12721260
this.loaded = true;
@@ -1302,9 +1290,10 @@ let requireModuleWarningMode;
13021290
* Resolve and evaluate it synchronously as ESM if it's ESM.
13031291
* @param {Module} mod CJS module instance
13041292
* @param {string} filename Absolute path of the file.
1293+
* @param {string} format Format of the module. If it had types, this would be what it is after type-stripping.
1294+
* @param {string} source Source the module. If it had types, this would have the type stripped.
13051295
*/
1306-
function loadESMFromCJS(mod, filename) {
1307-
const source = getMaybeCachedSource(mod, filename);
1296+
function loadESMFromCJS(mod, filename, format, source) {
13081297
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13091298
const isMain = mod[kIsMainSymbol];
13101299
if (isMain) {
@@ -1480,7 +1469,9 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
14801469
* `exports`) to the file. Returns exception, if any.
14811470
* @param {string} content The source code of the module
14821471
* @param {string} filename The file path of the module
1483-
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
1472+
* @param {
1473+
* 'module'|'commonjs'|'commonjs-typescript'|'module-typescript'
1474+
* } format Intended format of the module.
14841475
*/
14851476
Module.prototype._compile = function(content, filename, format) {
14861477
let moduleURL;
@@ -1502,9 +1493,7 @@ Module.prototype._compile = function(content, filename, format) {
15021493
}
15031494

15041495
if (format === 'module') {
1505-
// Pass the source into the .mjs extension handler indirectly through the cache.
1506-
this[kModuleSource] = content;
1507-
loadESMFromCJS(this, filename);
1496+
loadESMFromCJS(this, filename, format, content);
15081497
return;
15091498
}
15101499

@@ -1532,22 +1521,72 @@ Module.prototype._compile = function(content, filename, format) {
15321521

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

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

16121619
/**

0 commit comments

Comments
 (0)