mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
module: implement module.registerHooks()
PR-URL: https://github.com/nodejs/node/pull/55698 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com>
This commit is contained in:
parent
2960a59540
commit
e85964610c
57 changed files with 2045 additions and 95 deletions
|
@ -102,6 +102,7 @@ const kIsCachedByESMLoader = Symbol('kIsCachedByESMLoader');
|
|||
const kRequiredModuleSymbol = Symbol('kRequiredModuleSymbol');
|
||||
const kIsExecuting = Symbol('kIsExecuting');
|
||||
|
||||
const kURL = Symbol('kURL');
|
||||
const kFormat = Symbol('kFormat');
|
||||
|
||||
// Set first due to cycle with ESM loader functions.
|
||||
|
@ -112,6 +113,9 @@ module.exports = {
|
|||
kModuleCircularVisited,
|
||||
initializeCJS,
|
||||
Module,
|
||||
findLongestRegisteredExtension,
|
||||
resolveForCJSWithHooks,
|
||||
loadSourceForCJSWithHooks: loadSource,
|
||||
wrapSafe,
|
||||
wrapModuleLoad,
|
||||
kIsMainSymbol,
|
||||
|
@ -157,6 +161,15 @@ const {
|
|||
stripBOM,
|
||||
toRealPath,
|
||||
} = require('internal/modules/helpers');
|
||||
const {
|
||||
convertCJSFilenameToURL,
|
||||
convertURLToCJSFilename,
|
||||
loadHooks,
|
||||
loadWithHooks,
|
||||
registerHooks,
|
||||
resolveHooks,
|
||||
resolveWithHooks,
|
||||
} = require('internal/modules/customization_hooks');
|
||||
const { stripTypeScriptModuleTypes } = require('internal/modules/typescript');
|
||||
const packageJsonReader = require('internal/modules/package_json_reader');
|
||||
const { getOptionValue, getEmbedderOptions } = require('internal/options');
|
||||
|
@ -173,6 +186,7 @@ const {
|
|||
ERR_REQUIRE_CYCLE_MODULE,
|
||||
ERR_REQUIRE_ESM,
|
||||
ERR_UNKNOWN_BUILTIN_MODULE,
|
||||
ERR_UNKNOWN_MODULE_FORMAT,
|
||||
},
|
||||
setArrowMessage,
|
||||
} = require('internal/errors');
|
||||
|
@ -585,7 +599,7 @@ function trySelfParentPath(parent) {
|
|||
* @param {string} parentPath The path of the parent module
|
||||
* @param {string} request The module request to resolve
|
||||
*/
|
||||
function trySelf(parentPath, request) {
|
||||
function trySelf(parentPath, request, conditions) {
|
||||
if (!parentPath) { return false; }
|
||||
|
||||
const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath);
|
||||
|
@ -606,7 +620,7 @@ function trySelf(parentPath, request) {
|
|||
const { packageExportsResolve } = require('internal/modules/esm/resolve');
|
||||
return finalizeEsmResolution(packageExportsResolve(
|
||||
pathToFileURL(pkg.path), expansion, pkg.data,
|
||||
pathToFileURL(parentPath), getCjsConditions()), parentPath, pkg.path);
|
||||
pathToFileURL(parentPath), conditions), parentPath, pkg.path);
|
||||
} catch (e) {
|
||||
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
throw createEsmNotFoundErr(request, pkg.path);
|
||||
|
@ -627,7 +641,7 @@ const EXPORTS_PATTERN = /^((?:@[^/\\%]+\/)?[^./\\%][^/\\%]*)(\/.*)?$/;
|
|||
* @param {string} nmPath The path to the module.
|
||||
* @param {string} request The request for the module.
|
||||
*/
|
||||
function resolveExports(nmPath, request) {
|
||||
function resolveExports(nmPath, request, conditions) {
|
||||
// The implementation's behavior is meant to mirror resolution in ESM.
|
||||
const { 1: name, 2: expansion = '' } =
|
||||
RegExpPrototypeExec(EXPORTS_PATTERN, request) || kEmptyObject;
|
||||
|
@ -639,7 +653,7 @@ function resolveExports(nmPath, request) {
|
|||
const { packageExportsResolve } = require('internal/modules/esm/resolve');
|
||||
return finalizeEsmResolution(packageExportsResolve(
|
||||
pathToFileURL(pkgPath + '/package.json'), '.' + expansion, pkg, null,
|
||||
getCjsConditions()), null, pkgPath);
|
||||
conditions), null, pkgPath);
|
||||
} catch (e) {
|
||||
if (e.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
|
||||
|
@ -681,7 +695,7 @@ function getDefaultExtensions() {
|
|||
* @param {boolean} isMain Whether the request is the main app entry point
|
||||
* @returns {string | false}
|
||||
*/
|
||||
Module._findPath = function(request, paths, isMain) {
|
||||
Module._findPath = function(request, paths, isMain, conditions = getCjsConditions()) {
|
||||
const absoluteRequest = path.isAbsolute(request);
|
||||
if (absoluteRequest) {
|
||||
paths = [''];
|
||||
|
@ -736,7 +750,7 @@ Module._findPath = function(request, paths, isMain) {
|
|||
}
|
||||
|
||||
if (!absoluteRequest) {
|
||||
const exportsResolved = resolveExports(curPath, request);
|
||||
const exportsResolved = resolveExports(curPath, request, conditions);
|
||||
if (exportsResolved) {
|
||||
return exportsResolved;
|
||||
}
|
||||
|
@ -1017,6 +1031,153 @@ function getExportsForCircularRequire(module) {
|
|||
return module.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a module request for CommonJS, invoking hooks from module.registerHooks()
|
||||
* if necessary.
|
||||
* @param {string} specifier
|
||||
* @param {Module|undefined} parent
|
||||
* @param {boolean} isMain
|
||||
* @returns {{url?: string, format?: string, parentURL?: string, filename: string}}
|
||||
*/
|
||||
function resolveForCJSWithHooks(specifier, parent, isMain) {
|
||||
let defaultResolvedURL;
|
||||
let defaultResolvedFilename;
|
||||
let format;
|
||||
|
||||
function defaultResolveImpl(specifier, parent, isMain, options) {
|
||||
// For backwards compatibility, when encountering requests starting with node:,
|
||||
// throw ERR_UNKNOWN_BUILTIN_MODULE on failure or return the normalized ID on success
|
||||
// without going into Module._resolveFilename.
|
||||
let normalized;
|
||||
if (StringPrototypeStartsWith(specifier, 'node:')) {
|
||||
normalized = BuiltinModule.normalizeRequirableId(specifier);
|
||||
if (!normalized) {
|
||||
throw new ERR_UNKNOWN_BUILTIN_MODULE(specifier);
|
||||
}
|
||||
defaultResolvedURL = specifier;
|
||||
format = 'builtin';
|
||||
return normalized;
|
||||
}
|
||||
return Module._resolveFilename(specifier, parent, isMain, options).toString();
|
||||
}
|
||||
|
||||
// Fast path: no hooks, just return simple results.
|
||||
if (!resolveHooks.length) {
|
||||
const filename = defaultResolveImpl(specifier, parent, isMain);
|
||||
return { __proto__: null, url: defaultResolvedURL, filename, format };
|
||||
}
|
||||
|
||||
// Slow path: has hooks, do the URL conversions and invoke hooks with contexts.
|
||||
let parentURL;
|
||||
if (parent) {
|
||||
if (!parent[kURL] && parent.filename) {
|
||||
parent[kURL] = convertCJSFilenameToURL(parent.filename);
|
||||
}
|
||||
parentURL = parent[kURL];
|
||||
}
|
||||
|
||||
// This is used as the last nextResolve for the resolve hooks.
|
||||
function defaultResolve(specifier, context) {
|
||||
// TODO(joyeecheung): parent and isMain should be part of context, then we
|
||||
// no longer need to use a different defaultResolve for every resolution.
|
||||
defaultResolvedFilename = defaultResolveImpl(specifier, parent, isMain, {
|
||||
__proto__: null,
|
||||
conditions: context.conditions,
|
||||
});
|
||||
|
||||
defaultResolvedURL = convertCJSFilenameToURL(defaultResolvedFilename);
|
||||
return { __proto__: null, url: defaultResolvedURL };
|
||||
}
|
||||
|
||||
const resolveResult = resolveWithHooks(specifier, parentURL, /* importAttributes */ undefined,
|
||||
getCjsConditions(), defaultResolve);
|
||||
const { url } = resolveResult;
|
||||
format = resolveResult.format;
|
||||
|
||||
let filename;
|
||||
if (url === defaultResolvedURL) { // Not overridden, skip the re-conversion.
|
||||
filename = defaultResolvedFilename;
|
||||
} else {
|
||||
filename = convertURLToCJSFilename(url);
|
||||
}
|
||||
|
||||
return { __proto__: null, url, format, filename, parentURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('internal/modules/customization_hooks').ModuleLoadContext} ModuleLoadContext;
|
||||
* @typedef {import('internal/modules/customization_hooks').ModuleLoadResult} ModuleLoadResult;
|
||||
*/
|
||||
|
||||
/**
|
||||
* Load the source code of a module based on format.
|
||||
* @param {string} filename Filename of the module.
|
||||
* @param {string|undefined|null} format Format of the module.
|
||||
* @returns {string|null}
|
||||
*/
|
||||
function defaultLoadImpl(filename, format) {
|
||||
switch (format) {
|
||||
case undefined:
|
||||
case null:
|
||||
case 'module':
|
||||
case 'commonjs':
|
||||
case 'json':
|
||||
case 'module-typescript':
|
||||
case 'commonjs-typescript':
|
||||
case 'typescript': {
|
||||
return fs.readFileSync(filename, 'utf8');
|
||||
}
|
||||
case 'builtin':
|
||||
return null;
|
||||
default:
|
||||
// URL is not necessarily necessary/available - convert it on the spot for errors.
|
||||
throw new ERR_UNKNOWN_MODULE_FORMAT(format, convertCJSFilenameToURL(filename));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a last nextLoad() for load hooks invoked for the CJS loader.
|
||||
* @param {string} url URL passed from the hook.
|
||||
* @param {string} filename Filename inferred from the URL.
|
||||
* @returns {(url: string, context: ModuleLoadContext) => ModuleLoadResult}
|
||||
*/
|
||||
function getDefaultLoad(url, filename) {
|
||||
return function defaultLoad(urlFromHook, context) {
|
||||
// If the url is the same as the original one, save the conversion.
|
||||
const isLoadingOriginalModule = (urlFromHook === url);
|
||||
const filenameFromHook = isLoadingOriginalModule ? filename : convertURLToCJSFilename(url);
|
||||
const source = defaultLoadImpl(filenameFromHook, context.format);
|
||||
// Format from context is directly returned, because format detection should only be
|
||||
// done after the entire load chain is completed.
|
||||
return { source, format: context.format };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a specified builtin module, invoking load hooks if necessary.
|
||||
* @param {string} id The module ID (without the node: prefix)
|
||||
* @param {string} url The module URL (with the node: prefix)
|
||||
* @param {string} format Format from resolution.
|
||||
* @returns {any} If there are no load hooks or the load hooks do not override the format of the
|
||||
* builtin, load and return the exports of the builtin. Otherwise, return undefined.
|
||||
*/
|
||||
function loadBuiltinWithHooks(id, url, format) {
|
||||
if (loadHooks.length) {
|
||||
url ??= `node:${id}`;
|
||||
// TODO(joyeecheung): do we really want to invoke the load hook for the builtins?
|
||||
const loadResult = loadWithHooks(url, format || 'builtin', /* importAttributes */ undefined,
|
||||
getCjsConditions(), getDefaultLoad(url, id));
|
||||
if (loadResult.format && loadResult.format !== 'builtin') {
|
||||
return undefined; // Format has been overridden, return undefined for the caller to continue loading.
|
||||
}
|
||||
}
|
||||
|
||||
// No hooks or the hooks have not overridden the format. Load it as a builtin module and return the
|
||||
// exports.
|
||||
const mod = loadBuiltinModule(id);
|
||||
return mod.exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a module from cache if it exists, otherwise create a new module instance.
|
||||
* 1. If a module already exists in the cache: return its exports object.
|
||||
|
@ -1051,19 +1212,18 @@ Module._load = function(request, parent, isMain) {
|
|||
}
|
||||
}
|
||||
|
||||
const { url, format, filename } = resolveForCJSWithHooks(request, parent, isMain);
|
||||
|
||||
// For backwards compatibility, if the request itself starts with node:, load it before checking
|
||||
// Module._cache. Otherwise, load it after the check.
|
||||
if (StringPrototypeStartsWith(request, 'node:')) {
|
||||
// Slice 'node:' prefix
|
||||
const id = StringPrototypeSlice(request, 5);
|
||||
|
||||
if (!BuiltinModule.canBeRequiredByUsers(id)) {
|
||||
throw new ERR_UNKNOWN_BUILTIN_MODULE(request);
|
||||
const result = loadBuiltinWithHooks(filename, url, format);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const module = loadBuiltinModule(id, request);
|
||||
return module.exports;
|
||||
// The format of the builtin has been overridden by user hooks. Continue loading.
|
||||
}
|
||||
|
||||
const filename = Module._resolveFilename(request, parent, isMain);
|
||||
const cachedModule = Module._cache[filename];
|
||||
if (cachedModule !== undefined) {
|
||||
updateChildren(parent, cachedModule, true);
|
||||
|
@ -1088,8 +1248,11 @@ Module._load = function(request, parent, isMain) {
|
|||
}
|
||||
|
||||
if (BuiltinModule.canBeRequiredWithoutScheme(filename)) {
|
||||
const mod = loadBuiltinModule(filename, request);
|
||||
return mod.exports;
|
||||
const result = loadBuiltinWithHooks(filename, url, format);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
// The format of the builtin has been overridden by user hooks. Continue loading.
|
||||
}
|
||||
|
||||
// Don't call updateChildren(), Module constructor already does.
|
||||
|
@ -1108,6 +1271,10 @@ Module._load = function(request, parent, isMain) {
|
|||
reportModuleToWatchMode(filename);
|
||||
Module._cache[filename] = module;
|
||||
module[kIsCachedByESMLoader] = false;
|
||||
// If there are resolve hooks, carry the context information into the
|
||||
// load hooks for the module keyed by the (potentially customized) filename.
|
||||
module[kURL] = url;
|
||||
module[kFormat] = format;
|
||||
}
|
||||
|
||||
if (parent !== undefined) {
|
||||
|
@ -1150,11 +1317,13 @@ Module._load = function(request, parent, isMain) {
|
|||
* @param {ResolveFilenameOptions} options Options object
|
||||
* @typedef {object} ResolveFilenameOptions
|
||||
* @property {string[]} paths Paths to search for modules in
|
||||
* @property {string[]} conditions Conditions used for resolution.
|
||||
*/
|
||||
Module._resolveFilename = function(request, parent, isMain, options) {
|
||||
if (BuiltinModule.normalizeRequirableId(request)) {
|
||||
return request;
|
||||
}
|
||||
const conditions = (options?.conditions) || getCjsConditions();
|
||||
|
||||
let paths;
|
||||
|
||||
|
@ -1200,7 +1369,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
|
|||
try {
|
||||
const { packageImportsResolve } = require('internal/modules/esm/resolve');
|
||||
return finalizeEsmResolution(
|
||||
packageImportsResolve(request, pathToFileURL(parentPath), getCjsConditions()),
|
||||
packageImportsResolve(request, pathToFileURL(parentPath), conditions),
|
||||
parentPath,
|
||||
pkg.path,
|
||||
);
|
||||
|
@ -1215,7 +1384,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
|
|||
|
||||
// Try module self resolution first
|
||||
const parentPath = trySelfParentPath(parent);
|
||||
const selfResolved = trySelf(parentPath, request);
|
||||
const selfResolved = trySelf(parentPath, request, conditions);
|
||||
if (selfResolved) {
|
||||
const cacheKey = request + '\x00' +
|
||||
(paths.length === 1 ? paths[0] : ArrayPrototypeJoin(paths, '\x00'));
|
||||
|
@ -1224,7 +1393,7 @@ Module._resolveFilename = function(request, parent, isMain, options) {
|
|||
}
|
||||
|
||||
// Look up the filename first, since that's the cache key.
|
||||
const filename = Module._findPath(request, paths, isMain);
|
||||
const filename = Module._findPath(request, paths, isMain, conditions);
|
||||
if (filename) { return filename; }
|
||||
const requireStack = [];
|
||||
for (let cursor = parent;
|
||||
|
@ -1291,8 +1460,8 @@ Module.prototype.load = function(filename) {
|
|||
debug('load %j for module %j', filename, this.id);
|
||||
|
||||
assert(!this.loaded);
|
||||
this.filename = filename;
|
||||
this.paths = Module._nodeModulePaths(path.dirname(filename));
|
||||
this.filename ??= filename;
|
||||
this.paths ??= Module._nodeModulePaths(path.dirname(filename));
|
||||
|
||||
const extension = findLongestRegisteredExtension(filename);
|
||||
|
||||
|
@ -1572,27 +1741,41 @@ 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. This is used
|
||||
* for TypeScript, JavaScript and JSON loading.
|
||||
* 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 {{source: string, format?: string}}
|
||||
*/
|
||||
function loadSource(mod, filename, formatFromNode) {
|
||||
if (formatFromNode !== undefined) {
|
||||
if (mod[kFormat] === 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.
|
||||
source = fs.readFileSync(filename, 'utf8');
|
||||
// If the module was loaded before, just return.
|
||||
if (mod[kModuleSource] !== undefined) {
|
||||
return { source: mod[kModuleSource], format: mod[kFormat] };
|
||||
}
|
||||
return { source, format };
|
||||
|
||||
// Fast path: no hooks, just load it and return.
|
||||
if (!loadHooks.length) {
|
||||
const source = defaultLoadImpl(filename, formatFromNode);
|
||||
return { source, format: formatFromNode };
|
||||
}
|
||||
|
||||
if (mod[kURL] === undefined) {
|
||||
mod[kURL] = convertCJSFilenameToURL(filename);
|
||||
}
|
||||
|
||||
const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined, getCjsConditions(),
|
||||
getDefaultLoad(mod[kURL], filename));
|
||||
|
||||
// Reset the module properties with load hook results.
|
||||
if (loadResult.format !== undefined) {
|
||||
mod[kFormat] = loadResult.format;
|
||||
}
|
||||
mod[kModuleSource] = loadResult.source;
|
||||
return { source: mod[kModuleSource], format: mod[kFormat] };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1610,7 +1793,6 @@ function loadMTS(mod, filename) {
|
|||
* @param {Module} module CJS module instance
|
||||
* @param {string} filename The file path of the module
|
||||
*/
|
||||
|
||||
function loadCTS(module, filename) {
|
||||
const loadResult = loadSource(module, filename, 'commonjs-typescript');
|
||||
module._compile(loadResult.source, filename, loadResult.format);
|
||||
|
@ -1724,7 +1906,7 @@ Module._extensions['.js'] = function(module, filename) {
|
|||
* @param {string} filename The file path of the module
|
||||
*/
|
||||
Module._extensions['.json'] = function(module, filename) {
|
||||
const content = fs.readFileSync(filename, 'utf8');
|
||||
const { source: content } = loadSource(module, filename, 'json');
|
||||
|
||||
try {
|
||||
setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
|
||||
|
@ -1878,3 +2060,4 @@ ObjectDefineProperty(Module.prototype, 'constructor', {
|
|||
|
||||
// Backwards compatibility
|
||||
Module.Module = Module;
|
||||
Module.registerHooks = registerHooks;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue