mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
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: https://github.com/nodejs/node/pull/55590 Refs: https://github.com/nodejs/loaders/pull/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>
This commit is contained in:
parent
4379dfb1fd
commit
d080f0db1f
@ -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,
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -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']) {
|
||||
@ -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) {
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user