






















@@ -1,9 +1,11 @@
1+import { spawnSync } from "node:child_process";
12import fs from "node:fs";
23import { builtinModules } from "node:module";
34import path from "node:path";
4-import { describe, expect, it } from "vitest";
5+import { describe, expect, it, vi } from "vitest";
5667const EXTENSION_ROOT = "extensions";
8+const REPO_ROOT = path.resolve(import.meta.dirname, "../../..");
79const EXTENSION_RUNTIME_FILE_EXTENSIONS = new Set([".cjs", ".js", ".jsx", ".mjs", ".ts", ".tsx"]);
810const BUILTIN_MODULES = new Set(builtinModules.map((moduleId) => moduleId.replace(/^node:/, "")));
911const OPTIONAL_UNDECLARED_RUNTIME_IMPORTS = new Map<string, Set<string>>([
@@ -53,16 +55,51 @@ type PackageManifest = {
5355optionalDependencies?: Record<string, string>;
5456peerDependencies?: Record<string, string>;
5557};
58+const trackedFilesByRoot = new Map<string, readonly string[] | null>();
56595760function toPosixPath(filePath: string): string {
5861return filePath.split(path.sep).join("/");
5962}
60636164function readPackageManifest(filePath: string): PackageManifest {
62-return JSON.parse(fs.readFileSync(filePath, "utf8")) as PackageManifest;
65+return JSON.parse(fs.readFileSync(path.resolve(REPO_ROOT, filePath), "utf8")) as PackageManifest;
66+}
67+68+function listTrackedFiles(root: string): string[] | null {
69+const relativeRoot = toPosixPath(path.relative(REPO_ROOT, path.resolve(REPO_ROOT, root)));
70+if (!relativeRoot || relativeRoot.startsWith("..")) {
71+return null;
72+}
73+if (trackedFilesByRoot.has(relativeRoot)) {
74+const files = trackedFilesByRoot.get(relativeRoot);
75+return files ? [...files] : null;
76+}
77+const result = spawnSync("git", ["ls-files", "--", relativeRoot], {
78+cwd: REPO_ROOT,
79+encoding: "utf8",
80+stdio: ["ignore", "pipe", "ignore"],
81+});
82+if (result.status !== 0) {
83+trackedFilesByRoot.set(relativeRoot, null);
84+return null;
85+}
86+const files = result.stdout
87+.split("\n")
88+.map((line) => line.trim().replaceAll("\\", "/"))
89+.filter((line) => line.length > 0)
90+.toSorted();
91+trackedFilesByRoot.set(relativeRoot, files);
92+return [...files];
6393}
64946595function listPackageManifests(root: string): string[] {
96+const trackedFiles = listTrackedFiles(root);
97+if (trackedFiles) {
98+return trackedFiles
99+.filter((filePath) => /^extensions\/[^/]+\/package\.json$/u.test(filePath))
100+.toSorted();
101+}
102+66103const entries = fs.readdirSync(root, { withFileTypes: true });
67104const manifests: string[] = [];
68105for (const entry of entries) {
@@ -94,6 +131,17 @@ function shouldSkipRuntimeFile(filePath: string): boolean {
94131}
9513296133function listRuntimeFiles(root: string): string[] {
134+const trackedFiles = listTrackedFiles(root);
135+if (trackedFiles) {
136+return trackedFiles
137+.filter(
138+(filePath) =>
139+EXTENSION_RUNTIME_FILE_EXTENSIONS.has(path.extname(filePath)) &&
140+!shouldSkipRuntimeFile(filePath),
141+)
142+.toSorted();
143+}
144+97145const files: string[] = [];
98146const visit = (dir: string) => {
99147for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -118,7 +166,8 @@ function listRuntimeFiles(root: string): string[] {
118166119167function readManifestText(root: string): string {
120168const manifestPath = path.join(root, "openclaw.plugin.json");
121-return fs.existsSync(manifestPath) ? fs.readFileSync(manifestPath, "utf8") : "";
169+const resolvedManifestPath = path.resolve(REPO_ROOT, manifestPath);
170+return fs.existsSync(resolvedManifestPath) ? fs.readFileSync(resolvedManifestPath, "utf8") : "";
122171}
123172124173function packageNameForSpecifier(specifier: string): string | null {
@@ -156,7 +205,7 @@ function isTypeOnlyClause(clause: string | undefined): boolean {
156205}
157206158207function collectRuntimeImports(filePath: string): string[] {
159-const source = fs.readFileSync(filePath, "utf8");
208+const source = fs.readFileSync(path.resolve(REPO_ROOT, filePath), "utf8");
160209const imports = new Set<string>();
161210const importRegex =
162211/(import|export)\s+([^'";]*?\s+from\s+)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)|require\s*\(\s*["']([^"']+)["']\s*\)/g;
@@ -226,6 +275,20 @@ describe("Discord dependency ownership", () => {
226275});
227276228277describe("extension runtime dependency manifests", () => {
278+it("lists extension dependency inputs from git without walking extension dirs", () => {
279+const readDir = vi.spyOn(fs, "readdirSync");
280+try {
281+const manifests = listPackageManifests(EXTENSION_ROOT);
282+const runtimeFiles = listRuntimeFiles("extensions/discord");
283+284+expect(manifests.length).toBeGreaterThan(0);
285+expect(runtimeFiles.length).toBeGreaterThan(0);
286+expect(readDir).not.toHaveBeenCalled();
287+} finally {
288+readDir.mockRestore();
289+}
290+});
291+229292it("keeps json5 in memory-core for packaged runtime config parsing", () => {
230293const manifest = readPackageManifest("extensions/memory-core/package.json");
231294@@ -271,7 +334,7 @@ describe("extension runtime dependency manifests", () => {
271334].toSorted();
272335const allowedIndirect = INDIRECT_RUNTIME_DEPENDENCIES.get(extensionDir) ?? new Set<string>();
273336const runtimeText = listRuntimeFiles(extensionDir)
274-.map((filePath) => fs.readFileSync(filePath, "utf8"))
337+.map((filePath) => fs.readFileSync(path.resolve(REPO_ROOT, filePath), "utf8"))
275338.concat(readManifestText(extensionDir))
276339.join("\n");
277340此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。