From 84e4f72350480ea5dbc77625c97c9f3238e65766 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 23:16:05 +0100 Subject: [PATCH] refactor: drop config metadata node_modules isolation --- scripts/load-channel-config-surface.ts | 146 +----------------- .../load-channel-config-surface.test.ts | 50 ------ 2 files changed, 1 insertion(+), 195 deletions(-) diff --git a/scripts/load-channel-config-surface.ts b/scripts/load-channel-config-surface.ts index d729a0ef8eb..966ba9ffa92 100644 --- a/scripts/load-channel-config-surface.ts +++ b/scripts/load-channel-config-surface.ts @@ -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([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 { - const discovered = new Set(); - 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 { - 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) { diff --git a/src/config/load-channel-config-surface.test.ts b/src/config/load-channel-config-surface.test.ts index ad1b4ede15c..e23c015a507 100644 --- a/src/config/load-channel-config-surface.test.ts +++ b/src/config/load-channel-config-surface.test.ts @@ -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"), - ); - }); - }); });