Skip to content

Commit

Permalink
module: unify TypeScript and .mjs handling in CommonJS
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
joyeecheung authored and RafaelGSS committed Nov 1, 2024
1 parent 25b1422 commit 56c46ab
Showing 1 changed file with 129 additions and 105 deletions.
234 changes: 129 additions & 105 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ const kIsMainSymbol = Symbol('kIsMainSymbol');
const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
const kIsExecuting = Symbol('kIsExecuting');

const kFormat = Symbol('kFormat');

// Set first due to cycle with ESM loader functions.
module.exports = {
kModuleSource,
Expand Down Expand Up @@ -436,9 +439,8 @@ function initializeCJS() {
Module._extensions['.ts'] = loadTS;
}
if (getOptionValue('--experimental-require-module')) {
Module._extensions['.mjs'] = loadESMFromCJS;
if (tsEnabled) {
Module._extensions['.mts'] = loadESMFromCJS;
Module._extensions['.mts'] = loadMTS;
}
}
}
Expand Down Expand Up @@ -653,8 +655,6 @@ function getDefaultExtensions() {
if (tsEnabled) {
// remove .ts and .cts from the default extensions
// to avoid extensionless require of .ts and .cts files.
// it behaves similarly to how .mjs is handled when --experimental-require-module
// is enabled.
extensions = ArrayPrototypeFilter(extensions, (ext) =>
(ext !== '.ts' || Module._extensions['.ts'] !== loadTS) &&
(ext !== '.cts' || Module._extensions['.cts'] !== loadCTS),
Expand All @@ -667,14 +667,10 @@ function getDefaultExtensions() {

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

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

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

if (getOptionValue('--experimental-strip-types')) {
if (StringPrototypeEndsWith(filename, '.mts') && !Module._extensions['.mts']) {
Expand Down Expand Up @@ -1353,12 +1345,10 @@ let hasPausedEntry = false;
* Resolve and evaluate it synchronously as ESM if it's ESM.
* @param {Module} mod CJS module instance
* @param {string} filename Absolute path of the file.
* @param {string} format Format of the module. If it had types, this would be what it is after type-stripping.
* @param {string} source Source the module. If it had types, this would have the type stripped.
*/
function loadESMFromCJS(mod, filename) {
let source = getMaybeCachedSource(mod, filename);
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
source = stripTypeScriptModuleTypes(source, filename);
}
function loadESMFromCJS(mod, filename, format, source) {
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
const isMain = mod[kIsMainSymbol];
if (isMain) {
Expand Down Expand Up @@ -1512,9 +1502,30 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
* `exports`) to the file. Returns exception, if any.
* @param {string} content The source code of the module
* @param {string} filename The file path of the module
* @param {'module'|'commonjs'|undefined} format Intended format of the module.
* @param {
* 'module'|'commonjs'|'commonjs-typescript'|'module-typescript'
* } format Intended format of the module.
*/
Module.prototype._compile = function(content, filename, format) {
if (format === 'commonjs-typescript' || format === 'module-typescript' || format === 'typescript') {
content = stripTypeScriptModuleTypes(content, filename);
switch (format) {
case 'commonjs-typescript': {
format = 'commonjs';
break;
}
case 'module-typescript': {
format = 'module';
break;
}
// If the format is still unknown i.e. 'typescript', detect it in
// wrapSafe using the type-stripped source.
default:
format = undefined;
break;
}
}

let redirects;

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

if (format === 'module') {
// Pass the source into the .mjs extension handler indirectly through the cache.
this[kModuleSource] = content;
loadESMFromCJS(this, filename);
loadESMFromCJS(this, filename, format, content);
return;
}

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

/**
* Get the source code of a module, using cached ones if it's cached.
* After this returns, mod[kFormat], mod[kModuleSource] and mod[kURL] will be set.
* @param {Module} mod Module instance whose source is potentially already cached.
* @param {string} filename Absolute path to the file of the module.
* @returns {string}
* @returns {{source: string, format?: string}}
*/
function getMaybeCachedSource(mod, filename) {
// If already analyzed the source, then it will be cached.
let content;
if (mod[kModuleSource] !== undefined) {
content = mod[kModuleSource];
function loadSource(mod, filename, formatFromNode) {
if (formatFromNode !== undefined) {
mod[kFormat] = formatFromNode;
}
const format = mod[kFormat];

let source = mod[kModuleSource];
if (source !== undefined) {
mod[kModuleSource] = undefined;
} else {
// TODO(joyeecheung): we can read a buffer instead to speed up
// compilation.
content = fs.readFileSync(filename, 'utf8');
source = fs.readFileSync(filename, 'utf8');
}
return content;
return { source, format };
}

/**
* Built-in handler for `.mts` files.
* @param {Module} mod CJS module instance
* @param {string} filename The file path of the module
*/
function loadMTS(mod, filename) {
const loadResult = loadSource(mod, filename, 'module-typescript');
mod._compile(loadResult.source, filename, loadResult.format);
}

/**
* Built-in handler for `.cts` files.
* @param {Module} module CJS module instance
* @param {string} filename The file path of the module
*/

function loadCTS(module, filename) {
const source = getMaybeCachedSource(module, filename);
const code = stripTypeScriptModuleTypes(source, filename);
module._compile(code, filename, 'commonjs');
const loadResult = loadSource(module, filename, 'commonjs-typescript');
module._compile(loadResult.source, filename, loadResult.format);
}

/**
* Built-in handler for `.ts` files.
* @param {Module} module The module to compile
* @param {Module} module CJS module instance
* @param {string} filename The file path of the module
*/
function loadTS(module, filename) {
// If already analyzed the source, then it will be cached.
const source = getMaybeCachedSource(module, filename);
const content = stripTypeScriptModuleTypes(source, filename);
let format;
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
if (getOptionValue('--experimental-require-module')) {
module._compile(content, filename, 'module');
return;
}
const typeFromPjson = pkg?.data.type;

const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = pkg.path;
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = stripTypeScriptModuleTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
} catch {
// Continue regardless of error.
}
if (parentSource) {
reconstructErrorStack(err, parentPath, parentSource);
}
}
let format;
if (typeFromPjson === 'module') {
format = 'module-typescript';
} else if (typeFromPjson === 'commonjs') {
format = 'commonjs-typescript';
} else {
format = 'typescript';
}
const loadResult = loadSource(module, filename, format);

// Function require shouldn't be used in ES modules when require(esm) is disabled.
if (typeFromPjson === 'module' && !getOptionValue('--experimental-require-module')) {
const err = getRequireESMError(module, pkg, loadResult.source, filename);
throw err;
} else if (pkg?.data.type === 'commonjs') {
format = 'commonjs';
}

module._compile(content, filename, format);
module[kFormat] = loadResult.format;
module._compile(loadResult.source, filename, loadResult.format);
};

function reconstructErrorStack(err, parentPath, parentSource) {
Expand All @@ -1663,53 +1676,64 @@ function reconstructErrorStack(err, parentPath, parentSource) {
}
}

/**
* Generate the legacy ERR_REQUIRE_ESM for the cases where require(esm) is disabled.
* @param {Module} mod The module being required.
* @param {undefined|object} pkg Data of the nearest package.json of the module.
* @param {string} content Source code of the module.
* @param {string} filename Filename of the module
* @returns {Error}
*/
function getRequireESMError(mod, pkg, content, filename) {
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = mod[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = pkg?.path;
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
const parentModule = Module._cache[parentPath];
if (parentModule) {
let parentSource;
try {
({ source: parentSource } = loadSource(parentModule, parentPath));
} catch {
// Continue regardless of error.
}
if (parentSource) {
// TODO(joyeecheung): trim off internal frames from the stack.
reconstructErrorStack(err, parentPath, parentSource);
}
}
return err;
}

/**
* Built-in handler for `.js` files.
* @param {Module} module The module to compile
* @param {string} filename The file path of the module
*/
Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const content = getMaybeCachedSource(module, filename);

let format;
if (StringPrototypeEndsWith(filename, '.js')) {
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
if (pkg?.data.type === 'module') {
if (getOptionValue('--experimental-require-module')) {
module._compile(content, filename, 'module');
return;
}

// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = pkg.path;
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
// Attempt to reconstruct the parent require frame.
if (Module._cache[parentPath]) {
let parentSource;
try {
parentSource = fs.readFileSync(parentPath, 'utf8');
} catch {
// Continue regardless of error.
}
if (parentSource) {
reconstructErrorStack(err, parentPath, parentSource);
}
}
throw err;
} else if (pkg?.data.type === 'commonjs') {
format = 'commonjs';
}
} else if (StringPrototypeEndsWith(filename, '.cjs')) {
let format, pkg;
if (StringPrototypeEndsWith(filename, '.cjs')) {
format = 'commonjs';
} else if (StringPrototypeEndsWith(filename, '.mjs')) {
format = 'module';
} else if (StringPrototypeEndsWith(filename, '.js')) {
pkg = packageJsonReader.getNearestParentPackageJSON(filename);
const typeFromPjson = pkg?.data.type;
if (typeFromPjson === 'module' || typeFromPjson === 'commonjs' || !typeFromPjson) {
format = typeFromPjson;
}
}

module._compile(content, filename, format);
const { source, format: loadedFormat } = loadSource(module, filename, format);
// Function require shouldn't be used in ES modules when require(esm) is disabled.
if (loadedFormat === 'module' && !getOptionValue('--experimental-require-module')) {
const err = getRequireESMError(module, pkg, source, filename);
throw err;
}
module._compile(source, filename, loadedFormat);
};

/**
Expand Down

0 comments on commit 56c46ab

Please sign in to comment.