import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js"; type PluginSdkAliasCandidateKind = "dist" | "src"; export type LoaderModuleResolveParams = { modulePath?: string; argv1?: string; cwd?: string; moduleUrl?: string; }; type PluginSdkPackageJson = { exports?: Record; bin?: string | Record; }; const STARTUP_ARGV1 = process.argv[1]; function resolveLoaderModulePath(params: LoaderModuleResolveParams = {}): string { return params.modulePath ?? fileURLToPath(params.moduleUrl ?? import.meta.url); } function readPluginSdkPackageJson(packageRoot: string): PluginSdkPackageJson | null { try { const pkgRaw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"); return JSON.parse(pkgRaw) as PluginSdkPackageJson; } catch { return null; } } function isSafePluginSdkSubpathSegment(subpath: string): boolean { return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(subpath); } function listPluginSdkSubpathsFromPackageJson(pkg: PluginSdkPackageJson): string[] { return Object.keys(pkg.exports ?? {}) .filter((key) => key.startsWith("./plugin-sdk/")) .map((key) => key.slice("./plugin-sdk/".length)) .filter((subpath) => isSafePluginSdkSubpathSegment(subpath)) .toSorted(); } function hasTrustedOpenClawRootIndicator(params: { packageRoot: string; packageJson: PluginSdkPackageJson; }): boolean { const packageExports = params.packageJson.exports ?? {}; const hasPluginSdkRootExport = Object.prototype.hasOwnProperty.call( packageExports, "./plugin-sdk", ); if (!hasPluginSdkRootExport) { return false; } const hasCliEntryExport = Object.prototype.hasOwnProperty.call(packageExports, "./cli-entry"); const hasOpenClawBin = (typeof params.packageJson.bin === "string" && params.packageJson.bin.toLowerCase().includes("openclaw")) || (typeof params.packageJson.bin === "object" && params.packageJson.bin !== null && typeof params.packageJson.bin.openclaw === "string"); const hasOpenClawEntrypoint = fs.existsSync(path.join(params.packageRoot, "openclaw.mjs")); return hasCliEntryExport || hasOpenClawBin || hasOpenClawEntrypoint; } function readPluginSdkSubpathsFromPackageRoot(packageRoot: string): string[] | null { const pkg = readPluginSdkPackageJson(packageRoot); if (!pkg) { return null; } if (!hasTrustedOpenClawRootIndicator({ packageRoot, packageJson: pkg })) { return null; } const subpaths = listPluginSdkSubpathsFromPackageJson(pkg); return subpaths.length > 0 ? subpaths : null; } function resolveTrustedOpenClawRootFromArgvHint(params: { argv1?: string; cwd: string; }): string | null { if (!params.argv1) { return null; } const packageRoot = resolveOpenClawPackageRootSync({ cwd: params.cwd, argv1: params.argv1, }); if (!packageRoot) { return null; } const packageJson = readPluginSdkPackageJson(packageRoot); if (!packageJson) { return null; } return hasTrustedOpenClawRootIndicator({ packageRoot, packageJson }) ? packageRoot : null; } function findNearestPluginSdkPackageRoot(startDir: string, maxDepth = 12): string | null { let cursor = path.resolve(startDir); for (let i = 0; i < maxDepth; i += 1) { const subpaths = readPluginSdkSubpathsFromPackageRoot(cursor); if (subpaths) { return cursor; } const parent = path.dirname(cursor); if (parent === cursor) { break; } cursor = parent; } return null; } export function resolveLoaderPackageRoot( params: LoaderModuleResolveParams & { modulePath: string }, ): string | null { const cwd = params.cwd ?? path.dirname(params.modulePath); const fromModulePath = resolveOpenClawPackageRootSync({ cwd }); if (fromModulePath) { return fromModulePath; } const argv1 = params.argv1 ?? process.argv[1]; const moduleUrl = params.moduleUrl ?? (params.modulePath ? undefined : import.meta.url); return resolveOpenClawPackageRootSync({ cwd, ...(argv1 ? { argv1 } : {}), ...(moduleUrl ? { moduleUrl } : {}), }); } function resolveLoaderPluginSdkPackageRoot( params: LoaderModuleResolveParams & { modulePath: string }, ): string | null { const cwd = params.cwd ?? path.dirname(params.modulePath); const fromCwd = resolveOpenClawPackageRootSync({ cwd }); const fromExplicitHints = resolveTrustedOpenClawRootFromArgvHint({ cwd, argv1: params.argv1 }) ?? (params.moduleUrl ? resolveOpenClawPackageRootSync({ cwd, moduleUrl: params.moduleUrl, }) : null); return ( fromCwd ?? fromExplicitHints ?? findNearestPluginSdkPackageRoot(path.dirname(params.modulePath)) ?? (params.cwd ? findNearestPluginSdkPackageRoot(params.cwd) : null) ?? findNearestPluginSdkPackageRoot(process.cwd()) ); } export function resolvePluginSdkAliasCandidateOrder(params: { modulePath: string; isProduction: boolean; }): PluginSdkAliasCandidateKind[] { const normalizedModulePath = params.modulePath.replace(/\\/g, "/"); const isDistRuntime = normalizedModulePath.includes("/dist/"); return isDistRuntime || params.isProduction ? ["dist", "src"] : ["src", "dist"]; } export function listPluginSdkAliasCandidates(params: { srcFile: string; distFile: string; modulePath: string; argv1?: string; cwd?: string; moduleUrl?: string; }) { const orderedKinds = resolvePluginSdkAliasCandidateOrder({ modulePath: params.modulePath, isProduction: process.env.NODE_ENV === "production", }); const packageRoot = resolveLoaderPluginSdkPackageRoot(params); if (packageRoot) { const candidateMap = { src: path.join(packageRoot, "src", "plugin-sdk", params.srcFile), dist: path.join(packageRoot, "dist", "plugin-sdk", params.distFile), } as const; return orderedKinds.map((kind) => candidateMap[kind]); } let cursor = path.dirname(params.modulePath); const candidates: string[] = []; for (let i = 0; i < 6; i += 1) { const candidateMap = { src: path.join(cursor, "src", "plugin-sdk", params.srcFile), dist: path.join(cursor, "dist", "plugin-sdk", params.distFile), } as const; for (const kind of orderedKinds) { candidates.push(candidateMap[kind]); } const parent = path.dirname(cursor); if (parent === cursor) { break; } cursor = parent; } return candidates; } export function resolvePluginSdkAliasFile(params: { srcFile: string; distFile: string; modulePath?: string; argv1?: string; cwd?: string; moduleUrl?: string; }): string | null { try { const modulePath = resolveLoaderModulePath(params); for (const candidate of listPluginSdkAliasCandidates({ srcFile: params.srcFile, distFile: params.distFile, modulePath, argv1: params.argv1, cwd: params.cwd, moduleUrl: params.moduleUrl, })) { if (fs.existsSync(candidate)) { return candidate; } } } catch { // ignore } return null; } const cachedPluginSdkExportedSubpaths = new Map(); const cachedPluginSdkScopedAliasMaps = new Map>(); export function listPluginSdkExportedSubpaths( params: { modulePath?: string; argv1?: string; moduleUrl?: string } = {}, ): string[] { const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); const packageRoot = resolveLoaderPluginSdkPackageRoot({ modulePath, argv1: params.argv1, moduleUrl: params.moduleUrl, }); if (!packageRoot) { return []; } const cached = cachedPluginSdkExportedSubpaths.get(packageRoot); if (cached) { return cached; } const subpaths = readPluginSdkSubpathsFromPackageRoot(packageRoot) ?? []; cachedPluginSdkExportedSubpaths.set(packageRoot, subpaths); return subpaths; } export function resolvePluginSdkScopedAliasMap( params: { modulePath?: string; argv1?: string; moduleUrl?: string } = {}, ): Record { const modulePath = params.modulePath ?? fileURLToPath(import.meta.url); const packageRoot = resolveLoaderPluginSdkPackageRoot({ modulePath, argv1: params.argv1, moduleUrl: params.moduleUrl, }); if (!packageRoot) { return {}; } const orderedKinds = resolvePluginSdkAliasCandidateOrder({ modulePath, isProduction: process.env.NODE_ENV === "production", }); const cacheKey = `${packageRoot}::${orderedKinds.join(",")}`; const cached = cachedPluginSdkScopedAliasMaps.get(cacheKey); if (cached) { return cached; } const aliasMap: Record = {}; for (const subpath of listPluginSdkExportedSubpaths({ modulePath, argv1: params.argv1, moduleUrl: params.moduleUrl, })) { const candidateMap = { src: path.join(packageRoot, "src", "plugin-sdk", `${subpath}.ts`), dist: path.join(packageRoot, "dist", "plugin-sdk", `${subpath}.js`), } as const; for (const kind of orderedKinds) { const candidate = candidateMap[kind]; if (fs.existsSync(candidate)) { aliasMap[`openclaw/plugin-sdk/${subpath}`] = candidate; break; } } } cachedPluginSdkScopedAliasMaps.set(cacheKey, aliasMap); return aliasMap; } export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {}): string | null { try { const modulePath = resolveLoaderModulePath(params); const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath }); if (!packageRoot) { return null; } const orderedKinds = resolvePluginSdkAliasCandidateOrder({ modulePath, isProduction: process.env.NODE_ENV === "production", }); const candidateMap = { src: path.join(packageRoot, "src", "extensionAPI.ts"), dist: path.join(packageRoot, "dist", "extensionAPI.js"), } as const; for (const kind of orderedKinds) { const candidate = candidateMap[kind]; if (fs.existsSync(candidate)) { return candidate; } } } catch { // ignore } return null; } export function buildPluginLoaderAliasMap( modulePath: string, argv1: string | undefined = STARTUP_ARGV1, moduleUrl?: string, ): Record { const pluginSdkAlias = resolvePluginSdkAliasFile({ srcFile: "root-alias.cjs", distFile: "root-alias.cjs", modulePath, argv1, moduleUrl, }); const extensionApiAlias = resolveExtensionApiAlias({ modulePath }); return { ...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}), ...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}), ...resolvePluginSdkScopedAliasMap({ modulePath, argv1, moduleUrl }), }; } export function resolvePluginRuntimeModulePath( params: LoaderModuleResolveParams = {}, ): string | null { try { const modulePath = resolveLoaderModulePath(params); const orderedKinds = resolvePluginSdkAliasCandidateOrder({ modulePath, isProduction: process.env.NODE_ENV === "production", }); const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath }); const candidates = packageRoot ? orderedKinds.map((kind) => kind === "src" ? path.join(packageRoot, "src", "plugins", "runtime", "index.ts") : path.join(packageRoot, "dist", "plugins", "runtime", "index.js"), ) : [ path.join(path.dirname(modulePath), "runtime", "index.ts"), path.join(path.dirname(modulePath), "runtime", "index.js"), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } } catch { // ignore } return null; } export function buildPluginLoaderJitiOptions(aliasMap: Record) { return { interopDefault: true, // Prefer Node's native sync ESM loader for built dist/*.js modules so // bundled plugins and plugin-sdk subpaths stay on the canonical module graph. tryNative: true, extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"], ...(Object.keys(aliasMap).length > 0 ? { alias: aliasMap, } : {}), }; } export function shouldPreferNativeJiti(modulePath: string): boolean { switch (path.extname(modulePath).toLowerCase()) { case ".js": case ".mjs": case ".cjs": case ".json": return true; default: return false; } }