Skip to content

Commit 1c3f307

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 <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Juan José Arboleda <soyjuanarbol@gmail.com>
1 parent 440abda commit 1c3f307

File tree

1 file changed

+86
-79
lines changed

1 file changed

+86
-79
lines changed

lib/internal/modules/cjs/loader.js

+86-79
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ const kIsMainSymbol = Symbol('kIsMainSymbol');
100100
const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
101101
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
102102
const kIsExecuting = Symbol('kIsExecuting');
103+
104+
const kFormat = Symbol('kFormat');
105+
103106
// Set first due to cycle with ESM loader functions.
104107
module.exports = {
105108
kModuleSource,
@@ -433,10 +436,6 @@ function initializeCJS() {
433436
// TODO(joyeecheung): deprecate this in favor of a proper hook?
434437
Module.runMain =
435438
require('internal/modules/run_main').executeUserEntryPoint;
436-
437-
if (getOptionValue('--experimental-require-module')) {
438-
Module._extensions['.mjs'] = loadESMFromCJS;
439-
}
440439
}
441440

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

659651
/**
@@ -1280,10 +1272,6 @@ Module.prototype.load = function(filename) {
12801272
this.paths = Module._nodeModulePaths(path.dirname(filename));
12811273

12821274
const extension = findLongestRegisteredExtension(filename);
1283-
// allow .mjs to be overridden
1284-
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {
1285-
throw new ERR_REQUIRE_ESM(filename, true);
1286-
}
12871275

12881276
Module._extensions[extension](this, filename);
12891277
this.loaded = true;
@@ -1327,9 +1315,10 @@ let hasPausedEntry = false;
13271315
* Resolve and evaluate it synchronously as ESM if it's ESM.
13281316
* @param {Module} mod CJS module instance
13291317
* @param {string} filename Absolute path of the file.
1318+
* @param {string} format Format of the module. If it had types, this would be what it is after type-stripping.
1319+
* @param {string} source Source the module. If it had types, this would have the type stripped.
13301320
*/
1331-
function loadESMFromCJS(mod, filename) {
1332-
const source = getMaybeCachedSource(mod, filename);
1321+
function loadESMFromCJS(mod, filename, format, source) {
13331322
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13341323
const isMain = mod[kIsMainSymbol];
13351324
if (isMain) {
@@ -1483,7 +1472,9 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
14831472
* `exports`) to the file. Returns exception, if any.
14841473
* @param {string} content The source code of the module
14851474
* @param {string} filename The file path of the module
1486-
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
1475+
* @param {
1476+
* 'module'|'commonjs'|'commonjs-typescript'|'module-typescript'
1477+
* } format Intended format of the module.
14871478
*/
14881479
Module.prototype._compile = function(content, filename, format) {
14891480
let moduleURL;
@@ -1505,9 +1496,7 @@ Module.prototype._compile = function(content, filename, format) {
15051496
}
15061497

15071498
if (format === 'module') {
1508-
// Pass the source into the .mjs extension handler indirectly through the cache.
1509-
this[kModuleSource] = content;
1510-
loadESMFromCJS(this, filename);
1499+
loadESMFromCJS(this, filename, format, content);
15111500
return;
15121501
}
15131502

@@ -1560,22 +1549,72 @@ Module.prototype._compile = function(content, filename, format) {
15601549

15611550
/**
15621551
* Get the source code of a module, using cached ones if it's cached.
1552+
* After this returns, mod[kFormat], mod[kModuleSource] and mod[kURL] will be set.
15631553
* @param {Module} mod Module instance whose source is potentially already cached.
15641554
* @param {string} filename Absolute path to the file of the module.
1565-
* @returns {string}
1555+
* @returns {{source: string, format?: string}}
15661556
*/
1567-
function getMaybeCachedSource(mod, filename) {
1568-
// If already analyzed the source, then it will be cached.
1569-
let content;
1570-
if (mod[kModuleSource] !== undefined) {
1571-
content = mod[kModuleSource];
1557+
function loadSource(mod, filename, formatFromNode) {
1558+
if (formatFromNode !== undefined) {
1559+
mod[kFormat] = formatFromNode;
1560+
}
1561+
const format = mod[kFormat];
1562+
1563+
let source = mod[kModuleSource];
1564+
if (source !== undefined) {
15721565
mod[kModuleSource] = undefined;
15731566
} else {
15741567
// TODO(joyeecheung): we can read a buffer instead to speed up
15751568
// compilation.
1576-
content = fs.readFileSync(filename, 'utf8');
1569+
source = fs.readFileSync(filename, 'utf8');
1570+
}
1571+
return { source, format };
1572+
}
1573+
1574+
function reconstructErrorStack(err, parentPath, parentSource) {
1575+
const errLine = StringPrototypeSplit(
1576+
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1577+
err.stack, ' at ')), '\n', 1)[0];
1578+
const { 1: line, 2: col } =
1579+
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
1580+
if (line && col) {
1581+
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1582+
const frame = `${parentPath}:${line}\n${srcLine}\n${StringPrototypeRepeat(' ', col - 1)}^\n`;
1583+
setArrowMessage(err, frame);
1584+
}
1585+
}
1586+
1587+
/**
1588+
* Generate the legacy ERR_REQUIRE_ESM for the cases where require(esm) is disabled.
1589+
* @param {Module} mod The module being required.
1590+
* @param {undefined|object} pkg Data of the nearest package.json of the module.
1591+
* @param {string} content Source code of the module.
1592+
* @param {string} filename Filename of the module
1593+
* @returns {Error}
1594+
*/
1595+
function getRequireESMError(mod, pkg, content, filename) {
1596+
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1597+
const parent = mod[kModuleParent];
1598+
const parentPath = parent?.filename;
1599+
const packageJsonPath = pkg?.path ? path.resolve(pkg.path, 'package.json') : null;
1600+
const usesEsm = containsModuleSyntax(content, filename);
1601+
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1602+
packageJsonPath);
1603+
// Attempt to reconstruct the parent require frame.
1604+
const parentModule = Module._cache[parentPath];
1605+
if (parentModule) {
1606+
let parentSource;
1607+
try {
1608+
({ source: parentSource } = loadSource(parentModule, parentPath));
1609+
} catch {
1610+
// Continue regardless of error.
1611+
}
1612+
if (parentSource) {
1613+
// TODO(joyeecheung): trim off internal frames from the stack.
1614+
reconstructErrorStack(err, parentPath, parentSource);
1615+
}
15771616
}
1578-
return content;
1617+
return err;
15791618
}
15801619

15811620
/**
@@ -1584,57 +1623,25 @@ function getMaybeCachedSource(mod, filename) {
15841623
* @param {string} filename The file path of the module
15851624
*/
15861625
Module._extensions['.js'] = function(module, filename) {
1587-
// If already analyzed the source, then it will be cached.
1588-
const content = getMaybeCachedSource(module, filename);
1589-
1590-
let format;
1591-
if (StringPrototypeEndsWith(filename, '.js')) {
1592-
const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
1593-
// Function require shouldn't be used in ES modules.
1594-
if (pkg.data?.type === 'module') {
1595-
if (getOptionValue('--experimental-require-module')) {
1596-
module._compile(content, filename, 'module');
1597-
return;
1598-
}
1599-
1600-
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1601-
const parent = module[kModuleParent];
1602-
const parentPath = parent?.filename;
1603-
const packageJsonPath = path.resolve(pkg.path, 'package.json');
1604-
const usesEsm = containsModuleSyntax(content, filename);
1605-
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1606-
packageJsonPath);
1607-
// Attempt to reconstruct the parent require frame.
1608-
if (Module._cache[parentPath]) {
1609-
let parentSource;
1610-
try {
1611-
parentSource = fs.readFileSync(parentPath, 'utf8');
1612-
} catch {
1613-
// Continue regardless of error.
1614-
}
1615-
if (parentSource) {
1616-
const errLine = StringPrototypeSplit(
1617-
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
1618-
err.stack, ' at ')), '\n', 1)[0];
1619-
const { 1: line, 2: col } =
1620-
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
1621-
if (line && col) {
1622-
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
1623-
const frame = `${parentPath}:${line}\n${srcLine}\n${
1624-
StringPrototypeRepeat(' ', col - 1)}^\n`;
1625-
setArrowMessage(err, frame);
1626-
}
1627-
}
1628-
}
1629-
throw err;
1630-
} else if (pkg.data?.type === 'commonjs') {
1631-
format = 'commonjs';
1632-
}
1633-
} else if (StringPrototypeEndsWith(filename, '.cjs')) {
1626+
let format, pkg;
1627+
if (StringPrototypeEndsWith(filename, '.cjs')) {
16341628
format = 'commonjs';
1629+
} else if (StringPrototypeEndsWith(filename, '.mjs')) {
1630+
format = 'module';
1631+
} else if (StringPrototypeEndsWith(filename, '.js')) {
1632+
pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
1633+
const typeFromPjson = pkg.data?.type;
1634+
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
1635+
format = typeFromPjson;
1636+
}
16351637
}
1636-
1637-
module._compile(content, filename, format);
1638+
const { source, format: loadedFormat } = loadSource(module, filename, format);
1639+
// Function require shouldn't be used in ES modules when require(esm) is disabled.
1640+
if (loadedFormat === 'module' && !getOptionValue('--experimental-require-module')) {
1641+
const err = getRequireESMError(module, pkg, source, filename);
1642+
throw err;
1643+
}
1644+
module._compile(source, filename, loadedFormat);
16381645
};
16391646

16401647
/**

0 commit comments

Comments
 (0)