import fs from "node:fs"; import Module from "node:module"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildPluginLoaderAliasMap, type PluginSdkResolutionPreference } from "./sdk-alias.js"; type ResolveFilename = ( request: string, parent: NodeJS.Module | undefined, isMain: boolean, options?: { paths?: string[] }, ) => string; type ModuleWithResolver = typeof Module & { _resolveFilename?: ResolveFilename; }; type NativeAliasEntry = { parentRoot: string; target: string; }; export type InstallOpenClawPluginSdkNativeResolverOptions = { modulePath?: string; pluginModulePath?: string; allowedParentRoots?: readonly string[]; argv1?: string; moduleUrl?: string; pluginSdkResolution?: PluginSdkResolutionPreference; }; const moduleWithResolver = Module as ModuleWithResolver; const nodeResolveFilenameProperty = "_resolveFilename" as const; const PLUGIN_SDK_PACKAGE_PREFIXES = ["openclaw/plugin-sdk", "@openclaw/plugin-sdk"] as const; const pluginSdkNativeAliases = new Map(); let installed = false; let previousResolveFilename: ResolveFilename | undefined; function resolveLoaderModulePath(options: InstallOpenClawPluginSdkNativeResolverOptions): string { return options.modulePath ?? fileURLToPath(options.moduleUrl ?? import.meta.url); } function isPluginSdkAliasSpecifier(specifier: string): boolean { return PLUGIN_SDK_PACKAGE_PREFIXES.some( (prefix) => specifier === prefix || specifier.startsWith(`${prefix}/`), ); } function isNativeLoadableSdkTarget(targetPath: string): boolean { switch (path.extname(targetPath)) { case ".cjs": case ".js": case ".mjs": return true; default: return false; } } function normalizePathForBoundary(candidate: string): string { try { return fs.realpathSync(candidate); } catch { return path.resolve(candidate); } } function findNearestPackageRoot(modulePath: string): string { let cursor = path.dirname(path.resolve(modulePath)); for (let i = 0; i < 12; i += 1) { if (fs.existsSync(path.join(cursor, "package.json"))) { return cursor; } const parent = path.dirname(cursor); if (parent === cursor) { break; } cursor = parent; } return path.dirname(path.resolve(modulePath)); } function findBundledPluginRoot(modulePath: string): string | undefined { const resolvedModulePath = normalizePathForBoundary(modulePath); const packageRoot = normalizePathForBoundary(resolveLoaderPackageRootFromModulePath(modulePath)); for (const relativeRoot of ["extensions", "dist/extensions", "dist-runtime/extensions"]) { const bundledRoot = path.join(packageRoot, relativeRoot); const relative = path.relative(bundledRoot, resolvedModulePath); if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { continue; } const [pluginId] = relative.split(path.sep); if (pluginId) { return path.join(bundledRoot, pluginId); } } return undefined; } function resolveLoaderPackageRootFromModulePath(modulePath: string): string { let cursor = path.dirname(path.resolve(modulePath)); for (let i = 0; i < 12; i += 1) { const packageJsonPath = path.join(cursor, "package.json"); if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { bin?: unknown; name?: unknown; }; if ( packageJson.name === "openclaw" || (typeof packageJson.bin === "object" && packageJson.bin !== null && typeof (packageJson.bin as { openclaw?: unknown }).openclaw === "string") ) { return cursor; } } catch { // Keep walking; malformed package metadata should not widen alias scope. } } const parent = path.dirname(cursor); if (parent === cursor) { break; } cursor = parent; } return findNearestPackageRoot(modulePath); } function resolveAllowedParentRoot(modulePath: string): string { return findBundledPluginRoot(modulePath) ?? findNearestPackageRoot(modulePath); } function resolveAllowedParentRoots( options: InstallOpenClawPluginSdkNativeResolverOptions, ): string[] { const roots = new Set(); if (options.pluginModulePath) { roots.add(normalizePathForBoundary(resolveAllowedParentRoot(options.pluginModulePath))); } for (const root of options.allowedParentRoots ?? []) { roots.add(normalizePathForBoundary(root)); } return [...roots]; } function isWithinRoot(candidate: string, root: string): boolean { const relative = path.relative(root, normalizePathForBoundary(candidate)); return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); } function resolveAliasTargetForParent( request: string, parent: NodeJS.Module | undefined, ): string | undefined { const entries = pluginSdkNativeAliases.get(request); const parentFilename = parent?.filename; if (!entries || !parentFilename) { return undefined; } return entries.find((entry) => isWithinRoot(parentFilename, entry.parentRoot))?.target; } function listPluginSdkNativeAliases( options: InstallOpenClawPluginSdkNativeResolverOptions, ): Array { const modulePath = options.pluginModulePath ?? resolveLoaderModulePath(options); return Object.entries( buildPluginLoaderAliasMap( modulePath, options.argv1 ?? process.argv[1], options.moduleUrl, // Native require hooks must point at JavaScript artifacts, even when the // plugin loader itself is configured to prefer source imports. "dist", ), ) .filter(([specifier]) => isPluginSdkAliasSpecifier(specifier)) .filter(([, target]) => isNativeLoadableSdkTarget(target)) .flatMap(([specifier, target]) => { if (specifier.endsWith(".js")) { return [[specifier, target]] as Array; } return [ [specifier, target], [`${specifier}.js`, target], ] as Array; }); } function installResolver(): void { if (installed || !moduleWithResolver[nodeResolveFilenameProperty]) { return; } previousResolveFilename = moduleWithResolver[nodeResolveFilenameProperty]; moduleWithResolver[nodeResolveFilenameProperty] = ((request, parent, isMain, options) => { const aliasTarget = resolveAliasTargetForParent(request, parent); if (aliasTarget) { return aliasTarget; } return previousResolveFilename?.(request, parent, isMain, options) ?? request; }) satisfies ResolveFilename; installed = true; } function registerNativeAlias(params: { request: string; target: string; parentRoots: readonly string[]; }): void { const entries = pluginSdkNativeAliases.get(params.request) ?? []; for (const parentRoot of params.parentRoots) { if ( entries.some((entry) => entry.parentRoot === parentRoot && entry.target === params.target) ) { continue; } entries.push({ parentRoot, target: params.target }); } if (entries.length > 0) { pluginSdkNativeAliases.set(params.request, entries); } } export function installOpenClawPluginSdkNativeResolver( options: InstallOpenClawPluginSdkNativeResolverOptions = {}, ): string[] { const parentRoots = resolveAllowedParentRoots(options); for (const [specifier, target] of listPluginSdkNativeAliases(options)) { registerNativeAlias({ request: specifier, target, parentRoots }); } installResolver(); return [...pluginSdkNativeAliases.keys()].toSorted(); } export function resetOpenClawPluginSdkNativeResolverForTest(): void { pluginSdkNativeAliases.clear(); if (installed && previousResolveFilename) { moduleWithResolver[nodeResolveFilenameProperty] = previousResolveFilename; } previousResolveFilename = undefined; installed = false; }