























@@ -1,7 +1,8 @@
1-import { existsSync, readdirSync, readFileSync } from "node:fs";
1+import { spawnSync } from "node:child_process";
2+import fs from "node:fs";
23import { basename, dirname, resolve } from "node:path";
34import { fileURLToPath } from "node:url";
4-import { describe, expect, it } from "vitest";
5+import { describe, expect, it, vi } from "vitest";
56import { classifyBundledExtensionSourcePath } from "../../../../scripts/lib/extension-source-classifier.mjs";
67import { GUARDED_EXTENSION_PUBLIC_SURFACE_BASENAMES } from "../../../plugin-sdk/test-helpers/public-artifacts.js";
78import { loadPluginManifestRegistry } from "../../../plugins/manifest-registry.js";
@@ -48,7 +49,7 @@ const GUARDED_CHANNEL_EXTENSIONS = new Set([
48494950function resolveBundledPluginSourceRoot(rootDir: string): string {
5051const sourceRoot = resolve(REPO_ROOT, BUNDLED_PLUGIN_ROOT_DIR, basename(rootDir));
51-return existsSync(sourceRoot) ? sourceRoot : rootDir;
52+return fs.existsSync(sourceRoot) ? sourceRoot : rootDir;
5253}
53545455function bundledPluginFile(pluginId: string, relativePath: string): string {
@@ -261,6 +262,7 @@ const RE_EXPORT_STAR_RE =
261262const RE_EXPORT_NAMED_RE = /^\s*export\s+(?:type\s+)?\{[^}]*\}\s+from\s*["']([^"']+)["']/gmu;
262263const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/gmu;
263264const REQUIRE_RE = /\brequire\s*\(\s*["']([^"']+)["']\s*\)/gmu;
265+const trackedSourceFilesByRoot = new Map<string, readonly string[] | null>();
264266265267type SourceFileCollectorOptions = {
266268rootDir: string;
@@ -274,7 +276,7 @@ function readSource(path: string): string {
274276if (cached !== undefined) {
275277return cached;
276278}
277-const text = readFileSync(fullPath, "utf8");
279+const text = fs.readFileSync(fullPath, "utf8");
278280sourceTextCache.set(fullPath, text);
279281return text;
280282}
@@ -283,21 +285,78 @@ function normalizePath(path: string): string {
283285return path.replaceAll("\\", "/");
284286}
285287288+function repoRelativePath(path: string): string {
289+const normalizedRepoRoot = normalizePath(REPO_ROOT);
290+const normalizedPath = normalizePath(path);
291+return normalizedPath.startsWith(normalizedRepoRoot)
292+ ? normalizedPath.slice(normalizedRepoRoot.length + 1)
293+ : normalizedPath;
294+}
295+296+function listTrackedSourceFiles(options: SourceFileCollectorOptions): string[] | null {
297+const relativeRoot = repoRelativePath(options.rootDir);
298+if (!relativeRoot || relativeRoot.startsWith("..")) {
299+return null;
300+}
301+if (trackedSourceFilesByRoot.has(relativeRoot)) {
302+const files = trackedSourceFilesByRoot.get(relativeRoot);
303+return files ? [...files] : null;
304+}
305+const result = spawnSync("git", ["ls-files", "--", relativeRoot], {
306+cwd: REPO_ROOT,
307+encoding: "utf8",
308+stdio: ["ignore", "pipe", "ignore"],
309+});
310+if (result.status !== 0) {
311+trackedSourceFilesByRoot.set(relativeRoot, null);
312+return null;
313+}
314+const files = result.stdout
315+.split("\n")
316+.map((line) => line.trim().replaceAll("\\", "/"))
317+.filter((line) => {
318+if (!/\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(line) || line.endsWith(".d.ts")) {
319+return false;
320+}
321+const parts = line.split("/");
322+return !parts.some(
323+(part) => part === "node_modules" || part === "dist" || part === "coverage",
324+);
325+})
326+.map((line) => resolve(REPO_ROOT, line))
327+.filter((fullPath) => {
328+const normalizedFullPath = normalizePath(fullPath);
329+const entryName = basename(fullPath);
330+return !(
331+options.shouldSkipPath?.(normalizedFullPath) ||
332+options.shouldSkipEntry?.({ entryName, normalizedFullPath })
333+);
334+})
335+.toSorted();
336+trackedSourceFilesByRoot.set(relativeRoot, files);
337+return [...files];
338+}
339+286340function collectSourceFiles(
287341cached: string[] | undefined | null,
288342options: SourceFileCollectorOptions,
289343): string[] {
290344if (cached) {
291345return cached;
292346}
347+const trackedFiles = listTrackedSourceFiles(options);
348+if (trackedFiles) {
349+return trackedFiles;
350+}
351+293352const files: string[] = [];
294353const stack = [options.rootDir];
295354while (stack.length > 0) {
296355const current = stack.pop();
297356if (!current) {
298357continue;
299358}
300-for (const entry of readdirSync(current, { withFileTypes: true })) {
359+for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
301360const fullPath = resolve(current, entry.name);
302361const normalizedFullPath = normalizePath(fullPath);
303362if (entry.isDirectory()) {
@@ -529,6 +588,22 @@ function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports:
529588}
530589531590describe("channel import guardrails", () => {
591+it("lists channel import guardrail sources from git without walking roots", () => {
592+const readDir = vi.spyOn(fs, "readdirSync");
593+try {
594+const extensionSources = collectExtensionSourceFiles();
595+const coreSources = collectCoreSourceFiles();
596+const telegramSources = collectExtensionFiles("telegram");
597+598+expect(extensionSources.length).toBeGreaterThan(0);
599+expect(coreSources.length).toBeGreaterThan(0);
600+expect(telegramSources.length).toBeGreaterThan(0);
601+expect(readDir).not.toHaveBeenCalled();
602+} finally {
603+readDir.mockRestore();
604+}
605+});
606+532607it("keeps channel helper modules off their own SDK barrels", () => {
533608for (const source of SAME_CHANNEL_SDK_GUARDS) {
534609const text = readSource(source.path);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。