Files
openclaw/src/plugins/native-module-require.ts
2026-05-29 00:49:20 +01:00

157 lines
4.5 KiB
TypeScript

import fs from "node:fs";
import { createRequire } from "node:module";
import Module from "node:module";
import path from "node:path";
const nodeRequire = createRequire(import.meta.url);
type ResolveFilename = (
request: string,
parent: NodeJS.Module | undefined,
isMain: boolean,
options?: { paths?: string[] },
) => string;
const moduleWithResolver = Module as typeof Module & {
_resolveFilename?: ResolveFilename;
};
export function isJavaScriptModulePath(modulePath: string): boolean {
return [".js", ".mjs", ".cjs"].includes(path.extname(modulePath).toLowerCase());
}
function isMissingTargetModuleError(
error: { code?: unknown; message?: unknown },
modulePath: string,
): boolean {
if (error.code !== "MODULE_NOT_FOUND" || typeof error.message !== "string") {
return false;
}
const firstLine = error.message.split("\n", 1)[0] ?? "";
return firstLine.includes(`'${modulePath}'`) || firstLine.includes(`"${modulePath}"`);
}
function isSourceTransformFallbackError(error: unknown, modulePath: string): boolean {
if (!error || typeof error !== "object") {
return false;
}
const candidate = error as { code?: unknown; message?: unknown };
const code = candidate.code;
return (
code === "ERR_REQUIRE_ESM" ||
code === "ERR_REQUIRE_ASYNC_MODULE" ||
isMissingTargetModuleError(candidate, modulePath)
);
}
export function tryNativeRequireJavaScriptModule(
modulePath: string,
options: {
allowWindows?: boolean;
aliasMap?: Record<string, string>;
fallbackOnMissingDependency?: boolean;
fallbackOnNativeError?: boolean;
} = {},
): { ok: true; moduleExport: unknown } | { ok: false } {
if (process.platform === "win32" && options.allowWindows !== true) {
return { ok: false };
}
if (!isJavaScriptModulePath(modulePath)) {
return { ok: false };
}
try {
return { ok: true, moduleExport: requireWithOptionalAliases(modulePath, options.aliasMap) };
} catch (error) {
const code =
error && typeof error === "object" ? (error as { code?: unknown }).code : undefined;
if (
isSourceTransformFallbackError(error, modulePath) ||
options.fallbackOnNativeError ||
(options.fallbackOnMissingDependency === true &&
(code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND"))
) {
return { ok: false };
}
throw error;
}
}
export function clearNativeRequireJavaScriptModuleCache(
modulePath: string,
options: { dependencyRoot?: string } = {},
): void {
if (!isJavaScriptModulePath(modulePath)) {
return;
}
try {
const resolved = nodeRequire.resolve(modulePath);
clearRequireCacheSubtree(
resolved,
resolveRequireCachePath(options.dependencyRoot ?? path.dirname(resolved)),
new Set(),
);
} catch {
// Best-effort lifecycle cleanup: unresolved paths were not native-loaded.
}
}
function resolveRequireCachePath(targetPath: string): string {
try {
return fs.realpathSync.native(targetPath);
} catch {
return path.resolve(targetPath);
}
}
function clearRequireCacheSubtree(
resolvedPath: string,
dependencyRoot: string,
seen: Set<string>,
): void {
if (seen.has(resolvedPath)) {
return;
}
seen.add(resolvedPath);
const cached = nodeRequire.cache[resolvedPath];
if (cached) {
for (const child of cached.children) {
if (isPathInsideOrSame(dependencyRoot, child.id)) {
clearRequireCacheSubtree(child.id, dependencyRoot, seen);
}
}
}
delete nodeRequire.cache[resolvedPath];
}
function isPathInsideOrSame(root: string, target: string): boolean {
const relative = path.relative(root, target);
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
}
function requireWithOptionalAliases(
modulePath: string,
aliasMap: Record<string, string> | undefined,
): unknown {
return withNativeRequireAliases(aliasMap, () => nodeRequire(modulePath));
}
export function withNativeRequireAliases<T>(
aliasMap: Record<string, string> | undefined,
run: () => T,
): T {
if (!aliasMap || Object.keys(aliasMap).length === 0 || !moduleWithResolver["_resolveFilename"]) {
return run();
}
const originalResolveFilename = moduleWithResolver["_resolveFilename"];
moduleWithResolver["_resolveFilename"] = ((request, parent, isMain, options) => {
const aliasTarget = aliasMap[request];
if (aliasTarget) {
return aliasTarget;
}
return originalResolveFilename(request, parent, isMain, options);
}) satisfies ResolveFilename;
try {
return run();
} finally {
moduleWithResolver["_resolveFilename"] = originalResolveFilename;
}
}