[v20.x] backport require(esm) bugfixes and improvements by joyeecheung · Pull Request #59504 · nodejs/node · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions lib/internal/errors.js
165 changes: 86 additions & 79 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,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 @@ -438,10 +441,6 @@ function initializeCJS() {
// TODO(joyeecheung): deprecate this in favor of a proper hook?
Module.runMain =
require('internal/modules/run_main').executeUserEntryPoint;

if (getOptionValue('--experimental-require-module')) {
Module._extensions['.mjs'] = loadESMFromCJS;
}
}

// Given a module name, and a list of paths to test, returns the first
Expand Down Expand Up @@ -651,14 +650,7 @@ function resolveExports(nmPath, request) {
// We don't cache this in case user extends the extensions.
function getDefaultExtensions() {
const extensions = ObjectKeys(Module._extensions);
if (!getOptionValue('--experimental-require-module')) {
return extensions;
}
// 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 @@ -1270,10 +1262,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);
}

Module._extensions[extension](this, filename);
this.loaded = true;
Expand Down Expand Up @@ -1309,9 +1297,10 @@ let requireModuleWarningMode;
* 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) {
const source = getMaybeCachedSource(mod, 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 @@ -1487,7 +1476,9 @@ 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) {
let moduleURL;
Expand All @@ -1509,9 +1500,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 @@ -1539,22 +1528,72 @@ 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 { source, format };
}

function reconstructErrorStack(err, parentPath, parentSource) {
const errLine = StringPrototypeSplit(
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
err.stack, ' at ')), '\n', 1)[0];
const { 1: line, 2: col } =
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
if (line && col) {
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
const frame = `${parentPath}:${line}\n${srcLine}\n${StringPrototypeRepeat(' ', col - 1)}^\n`;
setArrowMessage(err, frame);
}
}

/**
* 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 ? path.resolve(pkg.path, 'package.json') : null;
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 content;
return err;
}

/**
Expand All @@ -1563,57 +1602,25 @@ function getMaybeCachedSource(mod, filename) {
* @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.readPackageScope(filename) || { __proto__: null };
// 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 = path.resolve(pkg.path, 'package.json');
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) {
const errLine = StringPrototypeSplit(
StringPrototypeSlice(err.stack, StringPrototypeIndexOf(
err.stack, ' at ')), '\n', 1)[0];
const { 1: line, 2: col } =
RegExpPrototypeExec(/(\d+):(\d+)\)/, errLine) || [];
if (line && col) {
const srcLine = StringPrototypeSplit(parentSource, '\n')[line - 1];
const frame = `${parentPath}:${line}\n${srcLine}\n${
StringPrototypeRepeat(' ', col - 1)}^\n`;
setArrowMessage(err, frame);
}
}
}
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.readPackageScope(filename) || { __proto__: null };
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
88 changes: 77 additions & 11 deletions lib/internal/modules/esm/loader.js
Loading
Loading