mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 21:02:54 +00:00
369 lines
12 KiB
TypeScript
369 lines
12 KiB
TypeScript
import fs from "node:fs";
|
|
import Module from "node:module";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } 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;
|
|
registerHooks?: (options: {
|
|
resolve?: (
|
|
specifier: string,
|
|
context: { parentURL?: string | undefined },
|
|
nextResolve: (
|
|
specifier: string,
|
|
context?: { parentURL?: string | undefined },
|
|
) => {
|
|
url: string;
|
|
},
|
|
) => { shortCircuit?: boolean; url: string };
|
|
}) => { deregister: () => void };
|
|
};
|
|
|
|
type NativeAliasEntry = {
|
|
parentRoot: string;
|
|
target: string;
|
|
};
|
|
|
|
export type InstallOpenClawPluginSdkNativeResolverOptions = {
|
|
modulePath?: string;
|
|
pluginModulePath?: string;
|
|
allowedParentRoots?: readonly string[];
|
|
argv1?: string;
|
|
moduleUrl?: string;
|
|
devSourceRoot?: string | null;
|
|
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 INTERNAL_CORE_PACKAGE_ALIASES = [
|
|
{
|
|
packageName: "@openclaw/normalization-core",
|
|
packageDir: "normalization-core",
|
|
subpaths: [
|
|
["", "index.ts"],
|
|
["number-coercion", "number-coercion.ts"],
|
|
["record-coerce", "record-coerce.ts"],
|
|
["string-coerce", "string-coerce.ts"],
|
|
["string-normalization", "string-normalization.ts"],
|
|
],
|
|
},
|
|
] as const;
|
|
const pluginSdkNativeAliases = new Map<string, NativeAliasEntry[]>();
|
|
let installed = false;
|
|
let previousResolveFilename: ResolveFilename | undefined;
|
|
let esmHooks: { deregister: () => void } | 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<string>();
|
|
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 {
|
|
return resolveAliasTargetForParentPath(request, parent?.filename);
|
|
}
|
|
|
|
function resolveAliasTargetForParentUrl(
|
|
request: string,
|
|
parentUrl: string | undefined,
|
|
): string | undefined {
|
|
if (!parentUrl?.startsWith("file:")) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
return resolveAliasTargetForParentPath(request, fileURLToPath(parentUrl));
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function resolveAliasTargetForParentPath(
|
|
request: string,
|
|
parentFilename: string | undefined,
|
|
): string | undefined {
|
|
const entries = pluginSdkNativeAliases.get(request);
|
|
if (!entries || !parentFilename) {
|
|
return undefined;
|
|
}
|
|
return entries.find((entry) => isWithinRoot(parentFilename, entry.parentRoot))?.target;
|
|
}
|
|
|
|
function listPluginSdkNativeAliases(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions,
|
|
): Array<readonly [string, string]> {
|
|
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",
|
|
options.devSourceRoot,
|
|
),
|
|
)
|
|
.filter(([specifier]) => isPluginSdkAliasSpecifier(specifier))
|
|
.filter(([, target]) => isNativeLoadableSdkTarget(target))
|
|
.flatMap(([specifier, target]) => {
|
|
if (specifier.endsWith(".js")) {
|
|
return [[specifier, target]] as Array<readonly [string, string]>;
|
|
}
|
|
return [
|
|
[specifier, target],
|
|
[`${specifier}.js`, target],
|
|
] as Array<readonly [string, string]>;
|
|
});
|
|
}
|
|
|
|
function listInternalCorePackageNativeAliases(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions,
|
|
): Array<{
|
|
request: string;
|
|
target: string;
|
|
parentRoots: string[];
|
|
}> {
|
|
const packageRoot = resolveLoaderPackageRootFromModulePath(resolveLoaderModulePath(options));
|
|
const parentRoots = ["src", "scripts", "packages", "test"]
|
|
.map((segment) => path.join(packageRoot, segment))
|
|
.filter((candidate) => fs.existsSync(candidate))
|
|
.map(normalizePathForBoundary);
|
|
if (parentRoots.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const aliases: Array<{
|
|
request: string;
|
|
target: string;
|
|
parentRoots: string[];
|
|
}> = [];
|
|
for (const entry of INTERNAL_CORE_PACKAGE_ALIASES) {
|
|
for (const [subpath, srcFile] of entry.subpaths) {
|
|
const request = subpath ? `${entry.packageName}/${subpath}` : entry.packageName;
|
|
const target = path.join(packageRoot, "packages", entry.packageDir, "src", srcFile);
|
|
if (fs.existsSync(target)) {
|
|
aliases.push({ request, target, parentRoots });
|
|
}
|
|
}
|
|
}
|
|
return aliases;
|
|
}
|
|
|
|
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;
|
|
esmHooks = moduleWithResolver.registerHooks?.({
|
|
resolve(specifier, context, nextResolve) {
|
|
const aliasTarget = resolveAliasTargetForParentUrl(specifier, context.parentURL);
|
|
if (aliasTarget) {
|
|
return {
|
|
shortCircuit: true,
|
|
url: pathToFileURL(aliasTarget).href,
|
|
};
|
|
}
|
|
return nextResolve(specifier, context);
|
|
},
|
|
});
|
|
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) {
|
|
const existingIndex = entries.findIndex((entry) => entry.parentRoot === parentRoot);
|
|
if (existingIndex !== -1) {
|
|
entries[existingIndex] = { parentRoot, target: params.target };
|
|
continue;
|
|
}
|
|
entries.push({ parentRoot, target: params.target });
|
|
}
|
|
if (entries.length > 0) {
|
|
pluginSdkNativeAliases.set(params.request, entries);
|
|
}
|
|
}
|
|
|
|
function clearNativeAliasesForParentRoots(parentRoots: readonly string[]): void {
|
|
if (parentRoots.length === 0) {
|
|
return;
|
|
}
|
|
const parentRootSet = new Set(parentRoots);
|
|
for (const [request, entries] of pluginSdkNativeAliases) {
|
|
const nextEntries = entries.filter((entry) => !parentRootSet.has(entry.parentRoot));
|
|
if (nextEntries.length === 0) {
|
|
pluginSdkNativeAliases.delete(request);
|
|
} else {
|
|
pluginSdkNativeAliases.set(request, nextEntries);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function installOpenClawPluginSdkNativeResolver(
|
|
options: InstallOpenClawPluginSdkNativeResolverOptions = {},
|
|
): string[] {
|
|
const parentRoots = resolveAllowedParentRoots(options);
|
|
clearNativeAliasesForParentRoots(parentRoots);
|
|
for (const [specifier, target] of listPluginSdkNativeAliases(options)) {
|
|
registerNativeAlias({ request: specifier, target, parentRoots });
|
|
}
|
|
for (const alias of listInternalCorePackageNativeAliases(options)) {
|
|
registerNativeAlias(alias);
|
|
}
|
|
installResolver();
|
|
return [...pluginSdkNativeAliases.keys()].toSorted();
|
|
}
|
|
|
|
export function installOpenClawInternalCorePackageNativeResolver(
|
|
options: Pick<InstallOpenClawPluginSdkNativeResolverOptions, "moduleUrl"> = {},
|
|
): string[] {
|
|
for (const alias of listInternalCorePackageNativeAliases(options)) {
|
|
registerNativeAlias(alias);
|
|
}
|
|
installResolver();
|
|
return [...pluginSdkNativeAliases.keys()].toSorted();
|
|
}
|
|
|
|
export function resetOpenClawPluginSdkNativeResolverForTest(): void {
|
|
pluginSdkNativeAliases.clear();
|
|
esmHooks?.deregister();
|
|
esmHooks = undefined;
|
|
if (installed && previousResolveFilename) {
|
|
moduleWithResolver[nodeResolveFilenameProperty] = previousResolveFilename;
|
|
}
|
|
previousResolveFilename = undefined;
|
|
installed = false;
|
|
}
|