fix(test): avoid scanning bundled build entries

This commit is contained in:
Vincent Koc
2026-05-16 09:14:07 +08:00
parent c320da79ed
commit 7d96d1f41a
2 changed files with 138 additions and 15 deletions

View File

@@ -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,

View File

@@ -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();