



























@@ -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 { dirname, join, relative, 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 {
67deprecatedBarrelPluginSdkEntrypoints,
78deprecatedPublicPluginSdkEntrypoints,
@@ -55,9 +56,57 @@ const MATRIX_RUNTIME_DEPS = [
5556"matrix-js-sdk",
5657"music-metadata",
5758] as const;
59+const trackedFilesByRoot = new Map<string, readonly string[] | null>();
60+61+function toRepoRelativePath(filePath: string): string {
62+return relative(REPO_ROOT, filePath).replaceAll("\\", "/");
63+}
64+65+function isSkippedTrackedPath(repoRelativePath: string): boolean {
66+return repoRelativePath
67+.split("/")
68+.some((part) => part === "dist" || part === "node_modules" || part === ".git");
69+}
70+71+function isCodeFile(filePath: string): boolean {
72+return /\.(?:[cm]?ts|tsx|mts|cts)$/.test(filePath);
73+}
74+75+function listTrackedFiles(root: string): string[] | null {
76+const relativeRoot = toRepoRelativePath(root);
77+if (!relativeRoot || relativeRoot.startsWith("..")) {
78+return null;
79+}
80+if (trackedFilesByRoot.has(relativeRoot)) {
81+const files = trackedFilesByRoot.get(relativeRoot);
82+return files ? [...files] : null;
83+}
84+const result = spawnSync("git", ["ls-files", "--", relativeRoot], {
85+cwd: REPO_ROOT,
86+encoding: "utf8",
87+stdio: ["ignore", "pipe", "ignore"],
88+});
89+if (result.status !== 0) {
90+trackedFilesByRoot.set(relativeRoot, 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 && !isSkippedTrackedPath(line))
97+.map((line) => resolve(REPO_ROOT, line))
98+.toSorted();
99+trackedFilesByRoot.set(relativeRoot, files);
100+return [...files];
101+}
102+103+function listTrackedCodeFiles(root: string): string[] | null {
104+const files = listTrackedFiles(root);
105+return files?.filter(isCodeFile) ?? null;
106+}
5810759108function collectPluginSdkPackageExports(): string[] {
60-const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
109+const packageJson = JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
61110exports?: Record<string, unknown>;
62111};
63112const exports = packageJson.exports ?? {};
@@ -78,7 +127,7 @@ function collectPluginSdkPackageExports(): string[] {
78127function collectPluginSdkSubpathReferences() {
79128const references: Array<{ file: string; subpath: string }> = [];
80129for (const file of PUBLIC_CONTRACT_REFERENCE_FILES) {
81-const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
130+const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
82131for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) {
83132const subpath = match[1];
84133if (!subpath) {
@@ -91,7 +140,7 @@ function collectPluginSdkSubpathReferences() {
91140}
9214193142function collectDocumentedSdkSubpaths(): Set<string> {
94-const source = readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8");
143+const source = fs.readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8");
95144return new Set(
96145[...source.matchAll(/`plugin-sdk\/([a-z0-9][a-z0-9-]*)`/g)]
97146.map((match) => match[1])
@@ -100,7 +149,20 @@ function collectDocumentedSdkSubpaths(): Set<string> {
100149}
101150102151function collectBundledPluginIds(): string[] {
103-return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
152+const trackedFiles = listTrackedFiles(resolve(REPO_ROOT, "extensions"));
153+if (trackedFiles) {
154+return [
155+ ...new Set(
156+trackedFiles
157+.map((file) => toRepoRelativePath(file).split("/"))
158+.filter((parts) => parts.length > 2)
159+.map((parts) => parts[1])
160+.filter((pluginId): pluginId is string => Boolean(pluginId)),
161+),
162+].toSorted((a, b) => b.length - a.length || a.localeCompare(b));
163+}
164+return fs
165+.readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true })
104166.filter((entry) => entry.isDirectory())
105167.map((entry) => entry.name)
106168.toSorted((a, b) => b.length - a.length || a.localeCompare(b));
@@ -142,7 +204,7 @@ function collectBundledFacadeSdkEntrypoints(): string[] {
142204const entrypoints: string[] = [];
143205for (const entrypoint of pluginSdkEntrypoints) {
144206const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
145-const source = readFileSync(filePath, "utf8");
207+const source = fs.readFileSync(filePath, "utf8");
146208if (BUNDLED_PLUGIN_FACADE_LOADER_PATTERN.test(source)) {
147209entrypoints.push(entrypoint);
148210}
@@ -154,7 +216,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] {
154216const entrypoints: string[] = [];
155217for (const entrypoint of pluginSdkEntrypoints) {
156218const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`);
157-const source = readFileSync(filePath, "utf8");
219+const source = fs.readFileSync(filePath, "utf8");
158220if (PRIVATE_BUNDLED_SDK_SURFACE_PATTERN.test(source)) {
159221entrypoints.push(entrypoint);
160222}
@@ -165,7 +227,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] {
165227function collectGenericCoreOwnerNameLeaks(): Array<{ file: string; match: string }> {
166228const leaks: Array<{ file: string; match: string }> = [];
167229for (const file of GENERIC_CORE_HELPER_FILES) {
168-const source = readFileSync(resolve(REPO_ROOT, file), "utf8");
230+const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8");
169231for (const match of source.matchAll(GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN)) {
170232const ownerName = match[0];
171233if (!ownerName) {
@@ -182,7 +244,7 @@ function readRootPackageJson(): {
182244optionalDependencies?: Record<string, string>;
183245files?: string[];
184246} {
185-return JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
247+return JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as {
186248dependencies?: Record<string, string>;
187249optionalDependencies?: Record<string, string>;
188250files?: string[];
@@ -193,7 +255,9 @@ function readMatrixPackageJson(): {
193255dependencies?: Record<string, string>;
194256optionalDependencies?: Record<string, string>;
195257} {
196-return JSON.parse(readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8")) as {
258+return JSON.parse(
259+fs.readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8"),
260+) as {
197261dependencies?: Record<string, string>;
198262optionalDependencies?: Record<string, string>;
199263};
@@ -210,7 +274,12 @@ function collectRuntimeDependencySpecs(packageJson: {
210274}
211275212276function collectExtensionFiles(dir: string): string[] {
213-const entries = readdirSync(dir, { withFileTypes: true });
277+const trackedFiles = listTrackedCodeFiles(dir);
278+if (trackedFiles) {
279+return trackedFiles;
280+}
281+282+const entries = fs.readdirSync(dir, { withFileTypes: true });
214283const files: string[] = [];
215284for (const entry of entries) {
216285if (entry.name === "dist" || entry.name === "node_modules") {
@@ -252,7 +321,7 @@ function collectExtensionCoreImportLeaks(): Array<{ file: string; specifier: str
252321}
253322const extensionRootMatch = /^(.*?\/extensions\/[^/]+)/.exec(file.replaceAll("\\", "/"));
254323const extensionRoot = extensionRootMatch?.[1];
255-const source = readFileSync(file, "utf8");
324+const source = fs.readFileSync(file, "utf8");
256325for (const match of source.matchAll(importPattern)) {
257326const specifier = match[1];
258327if (!specifier) {
@@ -283,7 +352,7 @@ function collectExtensionTestHelperImportLeaks(): Array<{ file: string; specifie
283352if (isExtensionTestOrSupportPath(repoRelativePath)) {
284353continue;
285354}
286-const source = readFileSync(file, "utf8");
355+const source = fs.readFileSync(file, "utf8");
287356for (const importPattern of importPatterns) {
288357for (const match of source.matchAll(importPattern)) {
289358const specifier = match[1];
@@ -309,7 +378,7 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier
309378];
310379for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) {
311380const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
312-const source = readFileSync(file, "utf8");
381+const source = fs.readFileSync(file, "utf8");
313382for (const importPattern of importPatterns) {
314383for (const match of source.matchAll(importPattern)) {
315384const specifier = match[1];
@@ -327,8 +396,13 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier
327396}
328397329398function collectCodeFiles(dir: string): string[] {
399+const trackedFiles = listTrackedCodeFiles(dir);
400+if (trackedFiles) {
401+return trackedFiles;
402+}
403+330404const files: string[] = [];
331-for (const entry of readdirSync(dir, { withFileTypes: true })) {
405+for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
332406if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".git") {
333407continue;
334408}
@@ -358,7 +432,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier:
358432if (DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES.has(repoRelativePath)) {
359433continue;
360434}
361-const source = readFileSync(file, "utf8");
435+const source = fs.readFileSync(file, "utf8");
362436for (const importPattern of importPatterns) {
363437for (const match of source.matchAll(importPattern)) {
364438const specifier = match[1];
@@ -377,7 +451,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier:
377451}
378452379453function collectDeprecatedPackageTestingBridgeDrift(): string[] {
380-const source = readFileSync(
454+const source = fs.readFileSync(
381455resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"),
382456"utf8",
383457).trim();
@@ -422,7 +496,7 @@ function collectWorkspaceCodeFiles(): string[] {
422496const files: string[] = [];
423497for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
424498const dir = resolve(REPO_ROOT, root);
425-if (existsSync(dir)) {
499+if (fs.existsSync(dir)) {
426500files.push(...collectCodeFiles(dir));
427501}
428502}
@@ -443,7 +517,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam
443517const exportNames = new Set<string>();
444518445519for (const file of testApiFiles) {
446-const source = readFileSync(file, "utf8");
520+const source = fs.readFileSync(file, "utf8");
447521const namedExports = parseTestApiNamedExports(source);
448522testApiExports.set(file, namedExports);
449523for (const exportName of namedExports) {
@@ -463,7 +537,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam
463537const selfReferenceCounts = new Map<string, Map<string, number>>();
464538465539for (const file of workspaceCodeFiles) {
466-const source = readFileSync(file, "utf8");
540+const source = fs.readFileSync(file, "utf8");
467541const selfCounts = testApiExports.has(file) ? new Map<string, number>() : undefined;
468542for (const match of source.matchAll(identifierPattern)) {
469543const exportName = match[1];
@@ -510,7 +584,7 @@ function collectCrossOwnerReservedSdkImports(): Array<{
510584for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) {
511585const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
512586const pluginId = repoRelativePath.split("/")[1];
513-const source = readFileSync(file, "utf8");
587+const source = fs.readFileSync(file, "utf8");
514588for (const match of source.matchAll(importPattern)) {
515589const subpath = match[1];
516590if (!subpath || !reserved.has(subpath)) {
@@ -541,7 +615,7 @@ function collectReservedSdkSubpathImports(): string[] {
541615542616for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
543617for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) {
544-const source = readFileSync(file, "utf8");
618+const source = fs.readFileSync(file, "utf8");
545619for (const importPattern of importPatterns) {
546620for (const match of source.matchAll(importPattern)) {
547621const subpath = match[1];
@@ -557,7 +631,7 @@ function collectReservedSdkSubpathImports(): string[] {
557631}
558632559633function hasWildcardReexport(entrypoint: string): boolean {
560-const source = readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8");
634+const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8");
561635return /^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source);
562636}
563637@@ -574,7 +648,7 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet<strin
574648if (isExtensionTestOrSupportPath(repoRelativePath)) {
575649continue;
576650}
577-const source = readFileSync(file, "utf8");
651+const source = fs.readFileSync(file, "utf8");
578652for (const importPattern of importPatterns) {
579653for (const match of source.matchAll(importPattern)) {
580654const subpath = match[1];
@@ -589,6 +663,22 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet<strin
589663}
590664591665describe("plugin-sdk package contract guardrails", () => {
666+it("lists package guardrail scan inputs from git without walking roots", () => {
667+const readDir = vi.spyOn(fs, "readdirSync");
668+try {
669+const pluginIds = collectBundledPluginIds();
670+const extensionFiles = collectExtensionFiles(resolve(REPO_ROOT, "extensions"));
671+const workspaceFiles = collectWorkspaceCodeFiles();
672+673+expect(pluginIds.length).toBeGreaterThan(0);
674+expect(extensionFiles.length).toBeGreaterThan(0);
675+expect(workspaceFiles.length).toBeGreaterThan(extensionFiles.length);
676+expect(readDir).not.toHaveBeenCalled();
677+} finally {
678+readDir.mockRestore();
679+}
680+});
681+592682it("keeps plugin-sdk entrypoint metadata unique", () => {
593683const counts = new Map<string, number>();
594684for (const entrypoint of pluginSdkEntrypoints) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。