Skip to content

Commit d080f0d

Browse files
authored
module: unify TypeScript and .mjs handling 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 both .mts and require(esm) handling (when it's disabled). PR-URL: #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 4379dfb commit d080f0d

File tree

1 file changed

+129
-105
lines changed

1 file changed

+129
-105
lines changed

lib/internal/modules/cjs/loader.js

+129-105
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,
@@ -436,9 +439,8 @@ function initializeCJS() {
436439
Module._extensions['.ts'] = loadTS;
437440
}
438441
if (getOptionValue('--experimental-require-module')) {
439-
Module._extensions['.mjs'] = loadESMFromCJS;
440442
if (tsEnabled) {
441-
Module._extensions['.mts'] = loadESMFromCJS;
443+
Module._extensions['.mts'] = loadMTS;
442444
}
443445
}
444446
}
@@ -653,8 +655,6 @@ function getDefaultExtensions() {
653655
if (tsEnabled) {
654656
// remove .ts and .cts from the default extensions
655657
// to avoid extensionless require of .ts and .cts files.
656-
// it behaves similarly to how .mjs is handled when --experimental-require-module
657-
// is enabled.
658658
extensions = ArrayPrototypeFilter(extensions, (ext) =>
659659
(ext !== '.ts' || Module._extensions['.ts'] !== loadTS) &&
660660
(ext !== '.cts' || Module._extensions['.cts'] !== loadCTS),
@@ -667,14 +667,10 @@ function getDefaultExtensions() {
667667

668668
if (tsEnabled) {
669669
extensions = ArrayPrototypeFilter(extensions, (ext) =>
670-
ext !== '.mts' || Module._extensions['.mts'] !== loadESMFromCJS,
670+
ext !== '.mts' || Module._extensions['.mts'] !== loadMTS,
671671
);
672672
}
673-
// If the .mjs extension is added by --experimental-require-module,
674-
// remove it from the supported default extensions to maintain
675-
// compatibility.
676-
// TODO(joyeecheung): allow both .mjs and .cjs?
677-
return ArrayPrototypeFilter(extensions, (ext) => ext !== '.mjs' || Module._extensions['.mjs'] !== loadESMFromCJS);
673+
return extensions;
678674
}
679675

680676
/**
@@ -1301,10 +1297,6 @@ Module.prototype.load = function(filename) {
13011297
this.paths = Module._nodeModulePaths(path.dirname(filename));
13021298

13031299
const extension = findLongestRegisteredExtension(filename);
1304-
// allow .mjs to be overridden
1305-
if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) {
1306-
throw new ERR_REQUIRE_ESM(filename, true);
1307-
}
13081300

13091301
if (getOptionValue('--experimental-strip-types')) {
13101302
if (StringPrototypeEndsWith(filename, '.mts') && !Module._extensions['.mts']) {
@@ -1353,12 +1345,10 @@ let hasPausedEntry = false;
13531345
* Resolve and evaluate it synchronously as ESM if it's ESM.
13541346
* @param {Module} mod CJS module instance
13551347
* @param {string} filename Absolute path of the file.
1348+
* @param {string} format Format of the module. If it had types, this would be what it is after type-stripping.
1349+
* @param {string} source Source the module. If it had types, this would have the type stripped.
13561350
*/
1357-
function loadESMFromCJS(mod, filename) {
1358-
let source = getMaybeCachedSource(mod, filename);
1359-
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
1360-
source = stripTypeScriptModuleTypes(source, filename);
1361-
}
1351+
function loadESMFromCJS(mod, filename, format, source) {
13621352
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
13631353
const isMain = mod[kIsMainSymbol];
13641354
if (isMain) {
@@ -1512,9 +1502,30 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
15121502
* `exports`) to the file. Returns exception, if any.
15131503
* @param {string} content The source code of the module
15141504
* @param {string} filename The file path of the module
1515-
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
1505+
* @param {
1506+
* 'module'|'commonjs'|'commonjs-typescript'|'module-typescript'
1507+
* } format Intended format of the module.
15161508
*/
15171509
Module.prototype._compile = function(content, filename, format) {
1510+
if (format === 'commonjs-typescript' || format === 'module-typescript' || format === 'typescript') {
1511+
content = stripTypeScriptModuleTypes(content, filename);
1512+
switch (format) {
1513+
case 'commonjs-typescript': {
1514+
format = 'commonjs';
1515+
break;
1516+
}
1517+
case 'module-typescript': {
1518+
format = 'module';
1519+
break;
1520+
}
1521+
// If the format is still unknown i.e. 'typescript', detect it in
1522+
// wrapSafe using the type-stripped source.
1523+
default:
1524+
format = undefined;
1525+
break;
1526+
}
1527+
}
1528+
15181529
let redirects;
15191530

15201531
let compiledWrapper;
@@ -1527,9 +1538,7 @@ Module.prototype._compile = function(content, filename, format) {
15271538
}
15281539

15291540
if (format === 'module') {
1530-
// Pass the source into the .mjs extension handler indirectly through the cache.
1531-
this[kModuleSource] = content;
1532-
loadESMFromCJS(this, filename);
1541+
loadESMFromCJS(this, filename, format, content);
15331542
return;
15341543
}
15351544

@@ -1582,72 +1591,76 @@ Module.prototype._compile = function(content, filename, format) {
15821591

15831592
/**
15841593
* Get the source code of a module, using cached ones if it's cached.
1594+
* After this returns, mod[kFormat], mod[kModuleSource] and mod[kURL] will be set.
15851595
* @param {Module} mod Module instance whose source is potentially already cached.
15861596
* @param {string} filename Absolute path to the file of the module.
1587-
* @returns {string}
1597+
* @returns {{source: string, format?: string}}
15881598
*/
1589-
function getMaybeCachedSource(mod, filename) {
1590-
// If already analyzed the source, then it will be cached.
1591-
let content;
1592-
if (mod[kModuleSource] !== undefined) {
1593-
content = mod[kModuleSource];
1599+
function loadSource(mod, filename, formatFromNode) {
1600+
if (formatFromNode !== undefined) {
1601+
mod[kFormat] = formatFromNode;
1602+
}
1603+
const format = mod[kFormat];
1604+
1605+
let source = mod[kModuleSource];
1606+
if (source !== undefined) {
15941607
mod[kModuleSource] = undefined;
15951608
} else {
15961609
// TODO(joyeecheung): we can read a buffer instead to speed up
15971610
// compilation.
1598-
content = fs.readFileSync(filename, 'utf8');
1611+
source = fs.readFileSync(filename, 'utf8');
15991612
}
1600-
return content;
1613+
return { source, format };
1614+
}
1615+
1616+
/**
1617+
* Built-in handler for `.mts` files.
1618+
* @param {Module} mod CJS module instance
1619+
* @param {string} filename The file path of the module
1620+
*/
1621+
function loadMTS(mod, filename) {
1622+
const loadResult = loadSource(mod, filename, 'module-typescript');
1623+
mod._compile(loadResult.source, filename, loadResult.format);
16011624
}
16021625

1626+
/**
1627+
* Built-in handler for `.cts` files.
1628+
* @param {Module} module CJS module instance
1629+
* @param {string} filename The file path of the module
1630+
*/
1631+
16031632
function loadCTS(module, filename) {
1604-
const source = getMaybeCachedSource(module, filename);
1605-
const code = stripTypeScriptModuleTypes(source, filename);
1606-
module._compile(code, filename, 'commonjs');
1633+
const loadResult = loadSource(module, filename, 'commonjs-typescript');
1634+
module._compile(loadResult.source, filename, loadResult.format);
16071635
}
16081636

16091637
/**
16101638
* Built-in handler for `.ts` files.
1611-
* @param {Module} module The module to compile
1639+
* @param {Module} module CJS module instance
16121640
* @param {string} filename The file path of the module
16131641
*/
16141642
function loadTS(module, filename) {
1615-
// If already analyzed the source, then it will be cached.
1616-
const source = getMaybeCachedSource(module, filename);
1617-
const content = stripTypeScriptModuleTypes(source, filename);
1618-
let format;
16191643
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
1620-
// Function require shouldn't be used in ES modules.
1621-
if (pkg?.data.type === 'module') {
1622-
if (getOptionValue('--experimental-require-module')) {
1623-
module._compile(content, filename, 'module');
1624-
return;
1625-
}
1644+
const typeFromPjson = pkg?.data.type;
16261645

1627-
const parent = module[kModuleParent];
1628-
const parentPath = parent?.filename;
1629-
const packageJsonPath = pkg.path;
1630-
const usesEsm = containsModuleSyntax(content, filename);
1631-
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1632-
packageJsonPath);
1633-
// Attempt to reconstruct the parent require frame.
1634-
if (Module._cache[parentPath]) {
1635-
let parentSource;
1636-
try {
1637-
parentSource = stripTypeScriptModuleTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
1638-
} catch {
1639-
// Continue regardless of error.
1640-
}
1641-
if (parentSource) {
1642-
reconstructErrorStack(err, parentPath, parentSource);
1643-
}
1644-
}
1646+
let format;
1647+
if (typeFromPjson === 'module') {
1648+
format = 'module-typescript';
1649+
} else if (typeFromPjson === 'commonjs') {
1650+
format = 'commonjs-typescript';
1651+
} else {
1652+
format = 'typescript';
1653+
}
1654+
const loadResult = loadSource(module, filename, format);
1655+
1656+
// Function require shouldn't be used in ES modules when require(esm) is disabled.
1657+
if (typeFromPjson === 'module' && !getOptionValue('--experimental-require-module')) {
1658+
const err = getRequireESMError(module, pkg, loadResult.source, filename);
16451659
throw err;
1646-
} else if (pkg?.data.type === 'commonjs') {
1647-
format = 'commonjs';
16481660
}
16491661

1650-
module._compile(content, filename, format);
1662+
module[kFormat] = loadResult.format;
1663+
module._compile(loadResult.source, filename, loadResult.format);
16511664
};
16521665

16531666
function reconstructErrorStack(err, parentPath, parentSource) {
@@ -1663,53 +1676,64 @@ function reconstructErrorStack(err, parentPath, parentSource) {
16631676
}
16641677
}
16651678

1679+
/**
1680+
* Generate the legacy ERR_REQUIRE_ESM for the cases where require(esm) is disabled.
1681+
* @param {Module} mod The module being required.
1682+
* @param {undefined|object} pkg Data of the nearest package.json of the module.
1683+
* @param {string} content Source code of the module.
1684+
* @param {string} filename Filename of the module
1685+
* @returns {Error}
1686+
*/
1687+
function getRequireESMError(mod, pkg, content, filename) {
1688+
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1689+
const parent = mod[kModuleParent];
1690+
const parentPath = parent?.filename;
1691+
const packageJsonPath = pkg?.path;
1692+
const usesEsm = containsModuleSyntax(content, filename);
1693+
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1694+
packageJsonPath);
1695+
// Attempt to reconstruct the parent require frame.
1696+
const parentModule = Module._cache[parentPath];
1697+
if (parentModule) {
1698+
let parentSource;
1699+
try {
1700+
({ source: parentSource } = loadSource(parentModule, parentPath));
1701+
} catch {
1702+
// Continue regardless of error.
1703+
}
1704+
if (parentSource) {
1705+
// TODO(joyeecheung): trim off internal frames from the stack.
1706+
reconstructErrorStack(err, parentPath, parentSource);
1707+
}
1708+
}
1709+
return err;
1710+
}
1711+
16661712
/**
16671713
* Built-in handler for `.js` files.
16681714
* @param {Module} module The module to compile
16691715
* @param {string} filename The file path of the module
16701716
*/
16711717
Module._extensions['.js'] = function(module, filename) {
1672-
// If already analyzed the source, then it will be cached.
1673-
const content = getMaybeCachedSource(module, filename);
1674-
1675-
let format;
1676-
if (StringPrototypeEndsWith(filename, '.js')) {
1677-
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
1678-
// Function require shouldn't be used in ES modules.
1679-
if (pkg?.data.type === 'module') {
1680-
if (getOptionValue('--experimental-require-module')) {
1681-
module._compile(content, filename, 'module');
1682-
return;
1683-
}
1684-
1685-
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
1686-
const parent = module[kModuleParent];
1687-
const parentPath = parent?.filename;
1688-
const packageJsonPath = pkg.path;
1689-
const usesEsm = containsModuleSyntax(content, filename);
1690-
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
1691-
packageJsonPath);
1692-
// Attempt to reconstruct the parent require frame.
1693-
if (Module._cache[parentPath]) {
1694-
let parentSource;
1695-
try {
1696-
parentSource = fs.readFileSync(parentPath, 'utf8');
1697-
} catch {
1698-
// Continue regardless of error.
1699-
}
1700-
if (parentSource) {
1701-
reconstructErrorStack(err, parentPath, parentSource);
1702-
}
1703-
}
1704-
throw err;
1705-
} else if (pkg?.data.type === 'commonjs') {
1706-
format = 'commonjs';
1707-
}
1708-
} else if (StringPrototypeEndsWith(filename, '.cjs')) {
1718+
let format, pkg;
1719+
if (StringPrototypeEndsWith(filename, '.cjs')) {
17091720
format = 'commonjs';
1721+
} else if (StringPrototypeEndsWith(filename, '.mjs')) {
1722+
format = 'module';
1723+
} else if (StringPrototypeEndsWith(filename, '.js')) {
1724+
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
1725+
const typeFromPjson = pkg?.data.type;
1726+
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
1727+
format = typeFromPjson;
1728+
}
17101729
}
1711-
1712-
module._compile(content, filename, format);
1730+
const { source, format: loadedFormat } = loadSource(module, filename, format);
1731+
// Function require shouldn't be used in ES modules when require(esm) is disabled.
1732+
if (loadedFormat === 'module' && !getOptionValue('--experimental-require-module')) {
1733+
const err = getRequireESMError(module, pkg, source, filename);
1734+
throw err;
1735+
}
1736+
module._compile(source, filename, loadedFormat);
17131737
};
17141738

17151739
/**

0 commit comments

Comments
 (0)