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 kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
|
||||||
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
|
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
|
||||||
const kIsExecuting = Symbol('kIsExecuting');
|
const kIsExecuting = Symbol('kIsExecuting');
|
||||||
|
|
||||||
|
const kFormat = Symbol('kFormat');
|
||||||
|
|
||||||
// Set first due to cycle with ESM loader functions.
|
// Set first due to cycle with ESM loader functions.
|
||||||
module.exports = {
|
module.exports = {
|
||||||
kModuleSource,
|
kModuleSource,
|
||||||
@ -436,9 +439,8 @@ function initializeCJS() {
|
|||||||
Module._extensions['.ts'] = loadTS;
|
Module._extensions['.ts'] = loadTS;
|
||||||
}
|
}
|
||||||
if (getOptionValue('--experimental-require-module')) {
|
if (getOptionValue('--experimental-require-module')) {
|
||||||
Module._extensions['.mjs'] = loadESMFromCJS;
|
|
||||||
if (tsEnabled) {
|
if (tsEnabled) {
|
||||||
Module._extensions['.mts'] = loadESMFromCJS;
|
Module._extensions['.mts'] = loadMTS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -653,8 +655,6 @@ function getDefaultExtensions() {
|
|||||||
if (tsEnabled) {
|
if (tsEnabled) {
|
||||||
// remove .ts and .cts from the default extensions
|
// remove .ts and .cts from the default extensions
|
||||||
// to avoid extensionless require of .ts and .cts files.
|
// 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) =>
|
extensions = ArrayPrototypeFilter(extensions, (ext) =>
|
||||||
(ext !== '.ts' || Module._extensions['.ts'] !== loadTS) &&
|
(ext !== '.ts' || Module._extensions['.ts'] !== loadTS) &&
|
||||||
(ext !== '.cts' || Module._extensions['.cts'] !== loadCTS),
|
(ext !== '.cts' || Module._extensions['.cts'] !== loadCTS),
|
||||||
@ -667,14 +667,10 @@ function getDefaultExtensions() {
|
|||||||
|
|
||||||
if (tsEnabled) {
|
if (tsEnabled) {
|
||||||
extensions = ArrayPrototypeFilter(extensions, (ext) =>
|
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,
|
return extensions;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1301,10 +1297,6 @@ Module.prototype.load = function(filename) {
|
|||||||
this.paths = Module._nodeModulePaths(path.dirname(filename));
|
this.paths = Module._nodeModulePaths(path.dirname(filename));
|
||||||
|
|
||||||
const extension = findLongestRegisteredExtension(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 (getOptionValue('--experimental-strip-types')) {
|
||||||
if (StringPrototypeEndsWith(filename, '.mts') && !Module._extensions['.mts']) {
|
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.
|
* Resolve and evaluate it synchronously as ESM if it's ESM.
|
||||||
* @param {Module} mod CJS module instance
|
* @param {Module} mod CJS module instance
|
||||||
* @param {string} filename Absolute path of the file.
|
* @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) {
|
function loadESMFromCJS(mod, filename, format, source) {
|
||||||
let source = getMaybeCachedSource(mod, filename);
|
|
||||||
if (getOptionValue('--experimental-strip-types') && path.extname(filename) === '.mts') {
|
|
||||||
source = stripTypeScriptModuleTypes(source, filename);
|
|
||||||
}
|
|
||||||
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
const cascadedLoader = require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
||||||
const isMain = mod[kIsMainSymbol];
|
const isMain = mod[kIsMainSymbol];
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
@ -1512,9 +1502,30 @@ function wrapSafe(filename, content, cjsModuleInstance, format) {
|
|||||||
* `exports`) to the file. Returns exception, if any.
|
* `exports`) to the file. Returns exception, if any.
|
||||||
* @param {string} content The source code of the module
|
* @param {string} content The source code of the module
|
||||||
* @param {string} filename The file path 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) {
|
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 redirects;
|
||||||
|
|
||||||
let compiledWrapper;
|
let compiledWrapper;
|
||||||
@ -1527,9 +1538,7 @@ Module.prototype._compile = function(content, filename, format) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (format === 'module') {
|
if (format === 'module') {
|
||||||
// Pass the source into the .mjs extension handler indirectly through the cache.
|
loadESMFromCJS(this, filename, format, content);
|
||||||
this[kModuleSource] = content;
|
|
||||||
loadESMFromCJS(this, filename);
|
|
||||||
return;
|
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.
|
* 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 {Module} mod Module instance whose source is potentially already cached.
|
||||||
* @param {string} filename Absolute path to the file of the module.
|
* @param {string} filename Absolute path to the file of the module.
|
||||||
* @returns {string}
|
* @returns {{source: string, format?: string}}
|
||||||
*/
|
*/
|
||||||
function getMaybeCachedSource(mod, filename) {
|
function loadSource(mod, filename, formatFromNode) {
|
||||||
// If already analyzed the source, then it will be cached.
|
if (formatFromNode !== undefined) {
|
||||||
let content;
|
mod[kFormat] = formatFromNode;
|
||||||
if (mod[kModuleSource] !== undefined) {
|
}
|
||||||
content = mod[kModuleSource];
|
const format = mod[kFormat];
|
||||||
|
|
||||||
|
let source = mod[kModuleSource];
|
||||||
|
if (source !== undefined) {
|
||||||
mod[kModuleSource] = undefined;
|
mod[kModuleSource] = undefined;
|
||||||
} else {
|
} else {
|
||||||
// TODO(joyeecheung): we can read a buffer instead to speed up
|
// TODO(joyeecheung): we can read a buffer instead to speed up
|
||||||
// compilation.
|
// 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) {
|
function loadCTS(module, filename) {
|
||||||
const source = getMaybeCachedSource(module, filename);
|
const loadResult = loadSource(module, filename, 'commonjs-typescript');
|
||||||
const code = stripTypeScriptModuleTypes(source, filename);
|
module._compile(loadResult.source, filename, loadResult.format);
|
||||||
module._compile(code, filename, 'commonjs');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Built-in handler for `.ts` files.
|
* 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
|
* @param {string} filename The file path of the module
|
||||||
*/
|
*/
|
||||||
function loadTS(module, filename) {
|
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);
|
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
|
||||||
// Function require shouldn't be used in ES modules.
|
const typeFromPjson = pkg?.data.type;
|
||||||
if (pkg?.data.type === 'module') {
|
|
||||||
if (getOptionValue('--experimental-require-module')) {
|
|
||||||
module._compile(content, filename, 'module');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parent = module[kModuleParent];
|
let format;
|
||||||
const parentPath = parent?.filename;
|
if (typeFromPjson === 'module') {
|
||||||
const packageJsonPath = pkg.path;
|
format = 'module-typescript';
|
||||||
const usesEsm = containsModuleSyntax(content, filename);
|
} else if (typeFromPjson === 'commonjs') {
|
||||||
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
|
format = 'commonjs-typescript';
|
||||||
packageJsonPath);
|
} else {
|
||||||
// Attempt to reconstruct the parent require frame.
|
format = 'typescript';
|
||||||
if (Module._cache[parentPath]) {
|
}
|
||||||
let parentSource;
|
const loadResult = loadSource(module, filename, format);
|
||||||
try {
|
|
||||||
parentSource = stripTypeScriptModuleTypes(fs.readFileSync(parentPath, 'utf8'), parentPath);
|
// Function require shouldn't be used in ES modules when require(esm) is disabled.
|
||||||
} catch {
|
if (typeFromPjson === 'module' && !getOptionValue('--experimental-require-module')) {
|
||||||
// Continue regardless of error.
|
const err = getRequireESMError(module, pkg, loadResult.source, filename);
|
||||||
}
|
|
||||||
if (parentSource) {
|
|
||||||
reconstructErrorStack(err, parentPath, parentSource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw err;
|
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) {
|
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.
|
* Built-in handler for `.js` files.
|
||||||
* @param {Module} module The module to compile
|
* @param {Module} module The module to compile
|
||||||
* @param {string} filename The file path of the module
|
* @param {string} filename The file path of the module
|
||||||
*/
|
*/
|
||||||
Module._extensions['.js'] = function(module, filename) {
|
Module._extensions['.js'] = function(module, filename) {
|
||||||
// If already analyzed the source, then it will be cached.
|
let format, pkg;
|
||||||
const content = getMaybeCachedSource(module, filename);
|
if (StringPrototypeEndsWith(filename, '.cjs')) {
|
||||||
|
|
||||||
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')) {
|
|
||||||
format = 'commonjs';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
const { source, format: loadedFormat } = loadSource(module, filename, format);
|
||||||
module._compile(content, 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