

























@@ -1,6 +1,7 @@
1-import { existsSync, readdirSync, readFileSync } from "node:fs";
1+import { spawnSync } from "node:child_process";
2+import fs from "node:fs";
23import { relative, resolve } from "node:path";
3-import { describe, expect, it } from "vitest";
4+import { describe, expect, it, vi } from "vitest";
45import {
56collectExtensionsWithTsconfig,
67collectOptInExtensionPackageBoundaries,
@@ -18,6 +19,7 @@ const EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG =
1819"extensions/tsconfig.package-boundary.paths.json" as const;
1920const EXTENSION_PACKAGE_BOUNDARY_BASE_CONFIG =
2021"extensions/tsconfig.package-boundary.base.json" as const;
22+const trackedCodeFilesByRoot = new Map<string, readonly string[] | null>();
21232224type TsConfigJson = {
2325extends?: unknown;
@@ -71,13 +73,41 @@ const MEMORY_HOST_SDK_RUNTIME_ADAPTER_FILES = [
71737274// oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Test helper lets assertions ascribe JSON file shape.
7375function readJsonFile<T>(relativePath: string): T {
74-return JSON.parse(readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T;
76+return JSON.parse(fs.readFileSync(resolve(REPO_ROOT, relativePath), "utf8")) as T;
77+}
78+79+function listTrackedCodeFiles(relativeDir: string): string[] | null {
80+if (trackedCodeFilesByRoot.has(relativeDir)) {
81+const files = trackedCodeFilesByRoot.get(relativeDir);
82+return files ? [...files] : null;
83+}
84+const result = spawnSync("git", ["ls-files", "--", relativeDir], {
85+cwd: REPO_ROOT,
86+encoding: "utf8",
87+stdio: ["ignore", "pipe", "ignore"],
88+});
89+if (result.status !== 0) {
90+trackedCodeFilesByRoot.set(relativeDir, null);
91+return null;
92+}
93+const files = result.stdout
94+.split("\n")
95+.map((line) => line.trim().replaceAll("\\", "/"))
96+.filter((line) => line.length > 0 && /\.(?:[cm]?ts|tsx|mts|cts)$/u.test(line))
97+.toSorted();
98+trackedCodeFilesByRoot.set(relativeDir, files);
99+return [...files];
75100}
7610177102function collectCodeFiles(relativeDir: string): string[] {
103+const trackedFiles = listTrackedCodeFiles(relativeDir);
104+if (trackedFiles) {
105+return trackedFiles;
106+}
107+78108const dir = resolve(REPO_ROOT, relativeDir);
79109const files: string[] = [];
80-for (const entry of readdirSync(dir, { withFileTypes: true })) {
110+for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
81111const nextPath = resolve(dir, entry.name);
82112if (entry.isDirectory()) {
83113files.push(...collectCodeFiles(relative(REPO_ROOT, nextPath).replaceAll("\\", "/")));
@@ -92,19 +122,33 @@ function collectCodeFiles(relativeDir: string): string[] {
9212293123function collectCoreReferenceFiles(relativeDir: string): string[] {
94124return collectCodeFiles(relativeDir).filter((file) => {
95-const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
125+const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
96126return source.includes("../../../../src/") || source.includes("../../../src/");
97127});
98128}
99129100130function collectOpenClawRuntimeDirectImportFiles(relativeDir: string): string[] {
101131return collectCodeFiles(relativeDir).filter((file) => {
102-const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
132+const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
103133return source.includes('"./openclaw-runtime.js"');
104134});
105135}
106136107137describe("opt-in extension package boundaries", () => {
138+it("lists package boundary code files from git without walking package roots", () => {
139+const readDir = vi.spyOn(fs, "readdirSync");
140+try {
141+const memoryHostFiles = collectCodeFiles("packages/memory-host-sdk/src");
142+const packageContractFiles = collectCodeFiles("packages/plugin-package-contract/src");
143+144+expect(memoryHostFiles.length).toBeGreaterThan(0);
145+expect(packageContractFiles.length).toBeGreaterThan(0);
146+expect(readDir).not.toHaveBeenCalled();
147+} finally {
148+readDir.mockRestore();
149+}
150+});
151+108152it("keeps path aliases in a dedicated shared config", () => {
109153const pathsConfig = readJsonFile<TsConfigJson>(EXTENSION_PACKAGE_BOUNDARY_PATHS_CONFIG);
110154expect(pathsConfig.extends).toBe("../tsconfig.json");
@@ -244,7 +288,7 @@ describe("opt-in extension package boundaries", () => {
244288"./dist/src/plugin-sdk/text-runtime.d.ts",
245289);
246290expect(packageJson.exports?.["./zod"]?.types).toBe("./dist/src/plugin-sdk/zod.d.ts");
247-expect(existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe(
291+expect(fs.existsSync(resolve(REPO_ROOT, "packages/plugin-sdk/types/plugin-entry.d.ts"))).toBe(
248292false,
249293);
250294});
@@ -265,7 +309,10 @@ describe("opt-in extension package boundaries", () => {
265309if (!target) {
266310throw new Error(`Missing memory-host-sdk export target for ${exportPath}`);
267311}
268-const source = readFileSync(resolve(REPO_ROOT, "packages/memory-host-sdk", target), "utf8");
312+const source = fs.readFileSync(
313+resolve(REPO_ROOT, "packages/memory-host-sdk", target),
314+"utf8",
315+);
269316expect(source, target).not.toContain("src/memory-host-sdk/");
270317}
271318此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。