From 7d96d1f41adb179c85194e0d5d2bfd64f41dd7cb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 09:14:07 +0800 Subject: [PATCH] fix(test): avoid scanning bundled build entries --- scripts/lib/bundled-plugin-build-entries.mjs | 110 +++++++++++++++--- .../bundled-plugin-build-entries.test.ts | 43 +++++++ 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/scripts/lib/bundled-plugin-build-entries.mjs b/scripts/lib/bundled-plugin-build-entries.mjs index 52be9f0e6b8..35c9e3e5841 100644 --- a/scripts/lib/bundled-plugin-build-entries.mjs +++ b/scripts/lib/bundled-plugin-build-entries.mjs @@ -1,3 +1,4 @@ +import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { @@ -12,8 +13,8 @@ export const NON_PACKAGED_BUNDLED_PLUGIN_DIRS = new Set(["qa-channel", "qa-lab", const EXCLUDED_CORE_BUNDLED_PLUGIN_DIRS = new Set(["qqbot", "whatsapp"]); const toPosixPath = (value) => value.replaceAll("\\", "/"); -function readBundledPluginPackageJson(packageJsonPath) { - if (!fs.existsSync(packageJsonPath)) { +function readBundledPluginPackageJson(packageJsonPath, options = {}) { + if (!(options.hasPackageJson ?? fs.existsSync(packageJsonPath))) { return null; } try { @@ -86,45 +87,124 @@ export function collectTopLevelPublicSurfaceEntries(pluginDir) { .toSorted((left, right) => left.localeCompare(right)); } +function collectTopLevelPublicSurfaceEntriesFromFiles(relativeFiles) { + return relativeFiles + .flatMap((relativeFile) => { + if (relativeFile.includes("/")) { + return []; + } + + const ext = path.extname(relativeFile); + if (!TOP_LEVEL_PUBLIC_SURFACE_EXTENSIONS.has(ext)) { + return []; + } + + const normalizedName = relativeFile.toLowerCase(); + if ( + normalizedName.endsWith(".d.ts") || + /^config-api\.(?:[cm]?[jt]s)$/u.test(normalizedName) || + normalizedName.includes(".test.") || + normalizedName.includes(".spec.") || + normalizedName.includes(".fixture.") || + normalizedName.includes(".snap") + ) { + return []; + } + + return [`./${relativeFile}`]; + }) + .toSorted((left, right) => left.localeCompare(right)); +} + +function collectTrackedBundledPluginFiles(cwd) { + const result = spawnSync("git", ["ls-files", "--", BUNDLED_PLUGIN_ROOT_DIR], { + cwd, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + return null; + } + + const filesByPlugin = new Map(); + for (const rawLine of result.stdout.split("\n")) { + const line = toPosixPath(rawLine.trim()); + const match = new RegExp(`^${BUNDLED_PLUGIN_ROOT_DIR}/([^/]+)/(.+)$`).exec(line); + if (!match) { + continue; + } + const [, dirName, relativeFile] = match; + const files = filesByPlugin.get(dirName) ?? []; + files.push(relativeFile); + filesByPlugin.set(dirName, files); + } + + return filesByPlugin; +} + +function collectBundledPluginCandidates(cwd, extensionsRoot) { + const trackedFiles = collectTrackedBundledPluginFiles(cwd); + if (trackedFiles) { + return [...trackedFiles.entries()] + .map(([dirName, relativeFiles]) => ({ + dirName, + pluginDir: path.join(extensionsRoot, dirName), + relativeFiles, + topLevelPublicSurfaceEntries: collectTopLevelPublicSurfaceEntriesFromFiles(relativeFiles), + })) + .toSorted((left, right) => left.dirName.localeCompare(right.dirName)); + } + + return fs + .readdirSync(extensionsRoot, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => { + const pluginDir = path.join(extensionsRoot, dirent.name); + return { + dirName: dirent.name, + pluginDir, + relativeFiles: null, + topLevelPublicSurfaceEntries: collectTopLevelPublicSurfaceEntries(pluginDir), + }; + }); +} + export function collectBundledPluginBuildEntries(params = {}) { const cwd = params.cwd ?? process.cwd(); const env = params.env ?? process.env; const extensionsRoot = path.join(cwd, BUNDLED_PLUGIN_ROOT_DIR); const entries = []; - for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) { - if (!dirent.isDirectory()) { - continue; - } - - const pluginDir = path.join(extensionsRoot, dirent.name); + for (const candidate of collectBundledPluginCandidates(cwd, extensionsRoot)) { + const { dirName, pluginDir, relativeFiles, topLevelPublicSurfaceEntries } = candidate; const manifestPath = path.join(pluginDir, "openclaw.plugin.json"); - const hasManifest = fs.existsSync(manifestPath); + const hasManifest = relativeFiles?.includes("openclaw.plugin.json") ?? fs.existsSync(manifestPath); const packageJsonPath = path.join(pluginDir, "package.json"); - const packageJson = readBundledPluginPackageJson(packageJsonPath); - const topLevelPublicSurfaceEntries = collectTopLevelPublicSurfaceEntries(pluginDir); + const packageJson = readBundledPluginPackageJson(packageJsonPath, { + hasPackageJson: relativeFiles?.includes("package.json"), + }); if ( !hasManifest && !isManifestlessBundledRuntimeSupportPackage({ - dirName: dirent.name, + dirName, packageJson, topLevelPublicSurfaceEntries, }) ) { continue; } - if (!shouldBuildBundledCluster(dirent.name, env, { packageJson })) { + if (!shouldBuildBundledCluster(dirName, env, { packageJson })) { continue; } if (!shouldBuildBundledDistEntry(packageJson)) { continue; } - if (EXCLUDED_CORE_BUNDLED_PLUGIN_DIRS.has(dirent.name)) { + if (EXCLUDED_CORE_BUNDLED_PLUGIN_DIRS.has(dirName)) { continue; } entries.push({ - id: dirent.name, + id: dirName, hasManifest, hasPackageJson: packageJson !== null, packageJson, diff --git a/test/scripts/bundled-plugin-build-entries.test.ts b/test/scripts/bundled-plugin-build-entries.test.ts index 7bcd58038a7..f15432a8f90 100644 --- a/test/scripts/bundled-plugin-build-entries.test.ts +++ b/test/scripts/bundled-plugin-build-entries.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; @@ -74,6 +75,48 @@ describe("bundled plugin build entries", () => { expect(entries["extensions/telegram/telegram-ingress-worker.runtime"]).toBeUndefined(); }); + it("discovers repo plugin build entries without directory scans", () => { + const output = execFileSync( + process.execPath, + [ + "--input-type=module", + "--eval", + ` + import fs from "node:fs"; + import { syncBuiltinESMExports } from "node:module"; + const counts = { readdirSync: 0 }; + const originalReaddirSync = fs.readdirSync; + fs.readdirSync = (...args) => { + counts.readdirSync += 1; + return originalReaddirSync(...args); + }; + syncBuiltinESMExports(); + const build = await import("./scripts/lib/bundled-plugin-build-entries.mjs"); + const entries = build.listBundledPluginBuildEntries(); + const artifacts = build.listBundledPluginPackArtifacts(); + console.log(JSON.stringify({ + artifacts: artifacts.length, + counts, + entries: Object.keys(entries).length, + })); + `, + ], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + const payload = JSON.parse(output) as { + artifacts: number; + counts: { readdirSync: number }; + entries: number; + }; + + expect(payload.entries).toBeGreaterThan(0); + expect(payload.artifacts).toBeGreaterThan(0); + expect(payload.counts.readdirSync).toBe(0); + }); + it("packs runtime core support packages without requiring plugin manifests", () => { const artifacts = listBundledPluginPackArtifacts();