mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
refactor: drop config metadata node_modules isolation
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
@@ -54,29 +53,6 @@ function resolveRepoRoot(): string {
|
||||
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
}
|
||||
|
||||
function resolvePackageRoot(modulePath: string): string {
|
||||
let cursor = path.dirname(path.resolve(modulePath));
|
||||
while (true) {
|
||||
if (fs.existsSync(path.join(cursor, "package.json"))) {
|
||||
return cursor;
|
||||
}
|
||||
const parent = path.dirname(cursor);
|
||||
if (parent === cursor) {
|
||||
throw new Error(`package root not found for ${modulePath}`);
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldRetryViaIsolatedCopy(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
}
|
||||
const code = "code" in error ? error.code : undefined;
|
||||
const message = "message" in error && typeof error.message === "string" ? error.message : "";
|
||||
return code === "ERR_MODULE_NOT_FOUND" && message.includes(`${path.sep}node_modules${path.sep}`);
|
||||
}
|
||||
|
||||
function isMissingExecutableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") {
|
||||
return false;
|
||||
@@ -84,113 +60,6 @@ function isMissingExecutableError(error: unknown): boolean {
|
||||
return "code" in error && error.code === "ENOENT";
|
||||
}
|
||||
|
||||
const SOURCE_FILE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
||||
|
||||
function resolveImportCandidates(basePath: string): string[] {
|
||||
const extension = path.extname(basePath);
|
||||
const candidates = new Set<string>([basePath]);
|
||||
if (extension) {
|
||||
const stem = basePath.slice(0, -extension.length);
|
||||
for (const sourceExtension of SOURCE_FILE_EXTENSIONS) {
|
||||
candidates.add(`${stem}${sourceExtension}`);
|
||||
}
|
||||
} else {
|
||||
for (const sourceExtension of SOURCE_FILE_EXTENSIONS) {
|
||||
candidates.add(`${basePath}${sourceExtension}`);
|
||||
candidates.add(path.join(basePath, `index${sourceExtension}`));
|
||||
}
|
||||
}
|
||||
return Array.from(candidates);
|
||||
}
|
||||
|
||||
function resolveRelativeImportPath(fromFile: string, specifier: string): string | null {
|
||||
for (const candidate of resolveImportCandidates(
|
||||
path.resolve(path.dirname(fromFile), specifier),
|
||||
)) {
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function collectRelativeImportGraph(entryPath: string): Set<string> {
|
||||
const discovered = new Set<string>();
|
||||
const queue = [path.resolve(entryPath)];
|
||||
const importPattern =
|
||||
/(?:import|export)\s+(?:[^"'`]*?\s+from\s+)?["'`]([^"'`]+)["'`]|import\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
|
||||
|
||||
while (queue.length > 0) {
|
||||
const currentPath = queue.pop();
|
||||
if (!currentPath || discovered.has(currentPath)) {
|
||||
continue;
|
||||
}
|
||||
discovered.add(currentPath);
|
||||
|
||||
const source = fs.readFileSync(currentPath, "utf8");
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1] ?? match[2];
|
||||
if (!specifier?.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
const resolved = resolveRelativeImportPath(currentPath, specifier);
|
||||
if (resolved) {
|
||||
queue.push(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return discovered;
|
||||
}
|
||||
|
||||
function resolveCommonAncestor(paths: Iterable<string>): string {
|
||||
const resolvedPaths = Array.from(paths, (entry) => path.resolve(entry));
|
||||
const [first, ...rest] = resolvedPaths;
|
||||
if (!first) {
|
||||
throw new Error("cannot resolve common ancestor for empty path set");
|
||||
}
|
||||
let ancestor = first;
|
||||
for (const candidate of rest) {
|
||||
while (path.relative(ancestor, candidate).startsWith(`..${path.sep}`)) {
|
||||
const parent = path.dirname(ancestor);
|
||||
if (parent === ancestor) {
|
||||
return ancestor;
|
||||
}
|
||||
ancestor = parent;
|
||||
}
|
||||
}
|
||||
return ancestor;
|
||||
}
|
||||
|
||||
function copyModuleImportGraphWithoutNodeModules(params: {
|
||||
modulePath: string;
|
||||
repoRoot: string;
|
||||
}): {
|
||||
copiedModulePath: string;
|
||||
cleanup: () => void;
|
||||
} {
|
||||
const packageRoot = resolvePackageRoot(params.modulePath);
|
||||
const relativeFiles = collectRelativeImportGraph(params.modulePath);
|
||||
const copyRoot = resolveCommonAncestor([packageRoot, ...relativeFiles]);
|
||||
const relativeModulePath = path.relative(copyRoot, params.modulePath);
|
||||
const tempParent = path.join(params.repoRoot, ".openclaw-config-doc-cache");
|
||||
fs.mkdirSync(tempParent, { recursive: true });
|
||||
const isolatedRoot = fs.mkdtempSync(path.join(tempParent, `${path.basename(packageRoot)}-`));
|
||||
|
||||
for (const sourcePath of relativeFiles) {
|
||||
const relativePath = path.relative(copyRoot, sourcePath);
|
||||
const targetPath = path.join(isolatedRoot, relativePath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
}
|
||||
return {
|
||||
copiedModulePath: path.join(isolatedRoot, relativeModulePath),
|
||||
cleanup: () => {
|
||||
fs.rmSync(isolatedRoot, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadChannelConfigSurfaceModule(
|
||||
modulePath: string,
|
||||
options?: { repoRoot?: string },
|
||||
@@ -300,20 +169,7 @@ export async function loadChannelConfigSurfaceModule(
|
||||
return null;
|
||||
};
|
||||
|
||||
try {
|
||||
return loadFromPath(modulePath);
|
||||
} catch (error) {
|
||||
if (!shouldRetryViaIsolatedCopy(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const isolatedCopy = copyModuleImportGraphWithoutNodeModules({ modulePath, repoRoot });
|
||||
try {
|
||||
return loadFromPath(isolatedCopy.copiedModulePath);
|
||||
} finally {
|
||||
isolatedCopy.cleanup();
|
||||
}
|
||||
}
|
||||
return loadFromPath(modulePath);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
|
||||
@@ -165,54 +165,4 @@ describe("loadChannelConfigSurfaceModule", () => {
|
||||
expect(spawnSync).toHaveBeenCalledWith("bun", expect.any(Array), expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
it("retries from an isolated package copy when extension-local node_modules is broken", async () => {
|
||||
await withTempDir({ prefix: "openclaw-config-surface-" }, async (repoRoot) => {
|
||||
const { packageRoot, modulePath } = createDemoConfigSchemaModule(repoRoot, [
|
||||
"import { z } from 'zod';",
|
||||
"export const DemoChannelConfigSchema = {",
|
||||
" schema: {",
|
||||
" type: 'object',",
|
||||
" properties: { ok: { type: z.object({}).shape ? 'string' : 'string' } },",
|
||||
" },",
|
||||
"};",
|
||||
]);
|
||||
|
||||
fs.mkdirSync(path.join(repoRoot, "node_modules", "zod"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoRoot, "node_modules", "zod", "package.json"),
|
||||
JSON.stringify({
|
||||
name: "zod",
|
||||
type: "module",
|
||||
exports: { ".": "./index.js" },
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(repoRoot, "node_modules", "zod", "index.js"),
|
||||
"export const z = { object: () => ({ shape: {} }) };\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const poisonedStorePackage = path.join(
|
||||
repoRoot,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"zod@0.0.0",
|
||||
"node_modules",
|
||||
"zod",
|
||||
);
|
||||
fs.mkdirSync(poisonedStorePackage, { recursive: true });
|
||||
fs.mkdirSync(path.join(packageRoot, "node_modules"), { recursive: true });
|
||||
fs.symlinkSync(
|
||||
"../../../node_modules/.pnpm/zod@0.0.0/node_modules/zod",
|
||||
path.join(packageRoot, "node_modules", "zod"),
|
||||
"dir",
|
||||
);
|
||||
|
||||
await expect(loadChannelConfigSurfaceModule(modulePath, { repoRoot })).resolves.toMatchObject(
|
||||
expectedOkSchema("string"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user