
























@@ -1,8 +1,9 @@
1-import { readFileSync, readdirSync } from "node:fs";
1+import { spawnSync } from "node:child_process";
2+import fs, { readFileSync } from "node:fs";
23import { dirname, relative, resolve, sep } from "node:path";
34import { fileURLToPath } from "node:url";
45import ts from "typescript";
5-import { describe, expect, it } from "vitest";
6+import { describe, expect, it, vi } from "vitest";
6778const SRC_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
89const REPO_ROOT = resolve(SRC_ROOT, "..");
@@ -120,14 +121,20 @@ type FileFilter = {
120121function listTsFiles(rootRelativePath: string, filter: FileFilter = {}): string[] {
121122const cacheKey = `${rootRelativePath}:${filter.excludeTests ? "exclude-tests" : ""}:${filter.testOnly ? "test-only" : ""}`;
122123const cached = tsFilesCache.get(cacheKey);
123-if (cached) {
124+if (cached !== undefined) {
124125return cached;
125126}
127+const externalFiles = listExternalTsFiles(rootRelativePath, filter);
128+if (externalFiles) {
129+tsFilesCache.set(cacheKey, externalFiles);
130+return externalFiles;
131+}
132+126133const root = resolve(REPO_ROOT, rootRelativePath);
127134const files: string[] = [];
128135129136function walk(directory: string) {
130-for (const entry of readdirSync(directory, { withFileTypes: true })) {
137+for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
131138const fullPath = resolve(directory, entry.name);
132139if (entry.isDirectory()) {
133140if (entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git") {
@@ -156,6 +163,73 @@ function listTsFiles(rootRelativePath: string, filter: FileFilter = {}): string[
156163return sorted;
157164}
158165166+function listExternalTsFiles(rootRelativePath: string, filter: FileFilter): string[] | null {
167+return listGitTrackedTsFiles(rootRelativePath, filter) ?? listFindTsFiles(rootRelativePath, filter);
168+}
169+170+function listGitTrackedTsFiles(rootRelativePath: string, filter: FileFilter): string[] | null {
171+if (!rootRelativePath || rootRelativePath.startsWith("..")) {
172+return null;
173+}
174+const result = spawnSync("git", ["ls-files", "--", rootRelativePath], {
175+cwd: REPO_ROOT,
176+encoding: "utf8",
177+maxBuffer: 16 * 1024 * 1024,
178+stdio: ["ignore", "pipe", "ignore"],
179+});
180+if (result.status !== 0) {
181+return null;
182+}
183+return result.stdout
184+.split("\n")
185+.map((line) => line.trim().replaceAll("\\", "/"))
186+.filter((line) => line.endsWith(".ts"))
187+.filter((line) => !(filter.excludeTests && line.endsWith(".test.ts")))
188+.filter((line) => !(filter.testOnly && !line.endsWith(".test.ts")))
189+.toSorted();
190+}
191+192+function listFindTsFiles(rootRelativePath: string, filter: FileFilter): string[] | null {
193+if (!rootRelativePath || rootRelativePath.startsWith("..")) {
194+return null;
195+}
196+const root = resolve(REPO_ROOT, rootRelativePath);
197+const result = spawnSync(
198+"find",
199+[
200+root,
201+"-type",
202+"f",
203+"-name",
204+"*.ts",
205+"-not",
206+"-path",
207+"*/node_modules/*",
208+"-not",
209+"-path",
210+"*/dist/*",
211+],
212+{
213+cwd: REPO_ROOT,
214+encoding: "utf8",
215+maxBuffer: 16 * 1024 * 1024,
216+stdio: ["ignore", "pipe", "ignore"],
217+},
218+);
219+if (result.status !== 0) {
220+return null;
221+}
222+return result.stdout
223+.split("\n")
224+.map((line) => line.trim())
225+.filter((line) => line.length > 0)
226+.map((line) => relative(REPO_ROOT, line).split(sep).join("/"))
227+.filter((line) => line.endsWith(".ts"))
228+.filter((line) => !(filter.excludeTests && line.endsWith(".test.ts")))
229+.filter((line) => !(filter.testOnly && !line.endsWith(".test.ts")))
230+.toSorted();
231+}
232+159233function readRepoSource(file: string): string {
160234const cached = sourceCache.get(file);
161235if (cached !== undefined) {
@@ -221,6 +295,22 @@ function collectTypedHookNames(source: string): string[] {
221295}
222296223297describe("plugin contract boundary invariants", () => {
298+it("lists boundary invariant source files without walking roots in-process", () => {
299+const readDir = vi.spyOn(fs, "readdirSync");
300+try {
301+tsFilesCache.clear();
302+const files = listTsFiles("src", { excludeTests: true });
303+304+expect(files.length).toBeGreaterThan(0);
305+expect(files.every((file) => file.startsWith("src/") && file.endsWith(".ts"))).toBe(true);
306+expect(files.some((file) => file.endsWith(".test.ts"))).toBe(false);
307+expect(readDir).not.toHaveBeenCalled();
308+} finally {
309+readDir.mockRestore();
310+tsFilesCache.clear();
311+}
312+});
313+224314it("keeps bundled-capability-metadata confined to contract/test inventory", () => {
225315const files = listTsFiles("src");
226316const offenders = files.filter((file) => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。