From f1c8570e0c8d6444a52fe6eaea8fc916d29b247b Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 16 May 2026 10:38:13 +0800 Subject: [PATCH] fix(test): avoid walking plugin sdk package guardrails --- ...in-sdk-package-contract-guardrails.test.ts | 140 ++++++++++++++---- 1 file changed, 115 insertions(+), 25 deletions(-) diff --git a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts index 38fa826f3dc..1aa74e9a02e 100644 --- a/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-package-contract-guardrails.test.ts @@ -1,7 +1,8 @@ -import { existsSync, readdirSync, readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; import { dirname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { deprecatedBarrelPluginSdkEntrypoints, deprecatedPublicPluginSdkEntrypoints, @@ -55,9 +56,57 @@ const MATRIX_RUNTIME_DEPS = [ "matrix-js-sdk", "music-metadata", ] as const; +const trackedFilesByRoot = new Map(); + +function toRepoRelativePath(filePath: string): string { + return relative(REPO_ROOT, filePath).replaceAll("\\", "/"); +} + +function isSkippedTrackedPath(repoRelativePath: string): boolean { + return repoRelativePath + .split("/") + .some((part) => part === "dist" || part === "node_modules" || part === ".git"); +} + +function isCodeFile(filePath: string): boolean { + return /\.(?:[cm]?ts|tsx|mts|cts)$/.test(filePath); +} + +function listTrackedFiles(root: string): string[] | null { + const relativeRoot = toRepoRelativePath(root); + if (!relativeRoot || relativeRoot.startsWith("..")) { + return null; + } + if (trackedFilesByRoot.has(relativeRoot)) { + const files = trackedFilesByRoot.get(relativeRoot); + return files ? [...files] : null; + } + const result = spawnSync("git", ["ls-files", "--", relativeRoot], { + cwd: REPO_ROOT, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + if (result.status !== 0) { + trackedFilesByRoot.set(relativeRoot, null); + return null; + } + const files = result.stdout + .split("\n") + .map((line) => line.trim().replaceAll("\\", "/")) + .filter((line) => line.length > 0 && !isSkippedTrackedPath(line)) + .map((line) => resolve(REPO_ROOT, line)) + .toSorted(); + trackedFilesByRoot.set(relativeRoot, files); + return [...files]; +} + +function listTrackedCodeFiles(root: string): string[] | null { + const files = listTrackedFiles(root); + return files?.filter(isCodeFile) ?? null; +} function collectPluginSdkPackageExports(): string[] { - const packageJson = JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as { + const packageJson = JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as { exports?: Record; }; const exports = packageJson.exports ?? {}; @@ -78,7 +127,7 @@ function collectPluginSdkPackageExports(): string[] { function collectPluginSdkSubpathReferences() { const references: Array<{ file: string; subpath: string }> = []; for (const file of PUBLIC_CONTRACT_REFERENCE_FILES) { - const source = readFileSync(resolve(REPO_ROOT, file), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8"); for (const match of source.matchAll(PLUGIN_SDK_SUBPATH_PATTERN)) { const subpath = match[1]; if (!subpath) { @@ -91,7 +140,7 @@ function collectPluginSdkSubpathReferences() { } function collectDocumentedSdkSubpaths(): Set { - const source = readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, SDK_SUBPATH_DOC_FILE), "utf8"); return new Set( [...source.matchAll(/`plugin-sdk\/([a-z0-9][a-z0-9-]*)`/g)] .map((match) => match[1]) @@ -100,7 +149,20 @@ function collectDocumentedSdkSubpaths(): Set { } function collectBundledPluginIds(): string[] { - return readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true }) + const trackedFiles = listTrackedFiles(resolve(REPO_ROOT, "extensions")); + if (trackedFiles) { + return [ + ...new Set( + trackedFiles + .map((file) => toRepoRelativePath(file).split("/")) + .filter((parts) => parts.length > 2) + .map((parts) => parts[1]) + .filter((pluginId): pluginId is string => Boolean(pluginId)), + ), + ].toSorted((a, b) => b.length - a.length || a.localeCompare(b)); + } + return fs + .readdirSync(resolve(REPO_ROOT, "extensions"), { withFileTypes: true }) .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .toSorted((a, b) => b.length - a.length || a.localeCompare(b)); @@ -142,7 +204,7 @@ function collectBundledFacadeSdkEntrypoints(): string[] { const entrypoints: string[] = []; for (const entrypoint of pluginSdkEntrypoints) { const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`); - const source = readFileSync(filePath, "utf8"); + const source = fs.readFileSync(filePath, "utf8"); if (BUNDLED_PLUGIN_FACADE_LOADER_PATTERN.test(source)) { entrypoints.push(entrypoint); } @@ -154,7 +216,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] { const entrypoints: string[] = []; for (const entrypoint of pluginSdkEntrypoints) { const filePath = resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`); - const source = readFileSync(filePath, "utf8"); + const source = fs.readFileSync(filePath, "utf8"); if (PRIVATE_BUNDLED_SDK_SURFACE_PATTERN.test(source)) { entrypoints.push(entrypoint); } @@ -165,7 +227,7 @@ function collectPrivateBundledSdkSurfaceEntrypoints(): string[] { function collectGenericCoreOwnerNameLeaks(): Array<{ file: string; match: string }> { const leaks: Array<{ file: string; match: string }> = []; for (const file of GENERIC_CORE_HELPER_FILES) { - const source = readFileSync(resolve(REPO_ROOT, file), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, file), "utf8"); for (const match of source.matchAll(GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN)) { const ownerName = match[0]; if (!ownerName) { @@ -182,7 +244,7 @@ function readRootPackageJson(): { optionalDependencies?: Record; files?: string[]; } { - return JSON.parse(readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as { + return JSON.parse(fs.readFileSync(resolve(REPO_ROOT, "package.json"), "utf8")) as { dependencies?: Record; optionalDependencies?: Record; files?: string[]; @@ -193,7 +255,9 @@ function readMatrixPackageJson(): { dependencies?: Record; optionalDependencies?: Record; } { - return JSON.parse(readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8")) as { + return JSON.parse( + fs.readFileSync(resolve(REPO_ROOT, "extensions/matrix/package.json"), "utf8"), + ) as { dependencies?: Record; optionalDependencies?: Record; }; @@ -210,7 +274,12 @@ function collectRuntimeDependencySpecs(packageJson: { } function collectExtensionFiles(dir: string): string[] { - const entries = readdirSync(dir, { withFileTypes: true }); + const trackedFiles = listTrackedCodeFiles(dir); + if (trackedFiles) { + return trackedFiles; + } + + const entries = fs.readdirSync(dir, { withFileTypes: true }); const files: string[] = []; for (const entry of entries) { if (entry.name === "dist" || entry.name === "node_modules") { @@ -252,7 +321,7 @@ function collectExtensionCoreImportLeaks(): Array<{ file: string; specifier: str } const extensionRootMatch = /^(.*?\/extensions\/[^/]+)/.exec(file.replaceAll("\\", "/")); const extensionRoot = extensionRootMatch?.[1]; - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const match of source.matchAll(importPattern)) { const specifier = match[1]; if (!specifier) { @@ -283,7 +352,7 @@ function collectExtensionTestHelperImportLeaks(): Array<{ file: string; specifie if (isExtensionTestOrSupportPath(repoRelativePath)) { continue; } - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const importPattern of importPatterns) { for (const match of source.matchAll(importPattern)) { const specifier = match[1]; @@ -309,7 +378,7 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier ]; for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) { const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/"); - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const importPattern of importPatterns) { for (const match of source.matchAll(importPattern)) { const specifier = match[1]; @@ -327,8 +396,13 @@ function collectDeprecatedExtensionSdkImports(): Array<{ file: string; specifier } function collectCodeFiles(dir: string): string[] { + const trackedFiles = listTrackedCodeFiles(dir); + if (trackedFiles) { + return trackedFiles; + } + const files: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === "dist" || entry.name === "node_modules" || entry.name === ".git") { continue; } @@ -358,7 +432,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier: if (DEPRECATED_TEST_BARREL_ALLOWED_REFERENCE_FILES.has(repoRelativePath)) { continue; } - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const importPattern of importPatterns) { for (const match of source.matchAll(importPattern)) { const specifier = match[1]; @@ -377,7 +451,7 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier: } function collectDeprecatedPackageTestingBridgeDrift(): string[] { - const source = readFileSync( + const source = fs.readFileSync( resolve(REPO_ROOT, "packages/plugin-sdk/src/testing.ts"), "utf8", ).trim(); @@ -422,7 +496,7 @@ function collectWorkspaceCodeFiles(): string[] { const files: string[] = []; for (const root of ["src", "test", "extensions", "packages", "scripts"]) { const dir = resolve(REPO_ROOT, root); - if (existsSync(dir)) { + if (fs.existsSync(dir)) { files.push(...collectCodeFiles(dir)); } } @@ -443,7 +517,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam const exportNames = new Set(); for (const file of testApiFiles) { - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); const namedExports = parseTestApiNamedExports(source); testApiExports.set(file, namedExports); for (const exportName of namedExports) { @@ -463,7 +537,7 @@ function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportNam const selfReferenceCounts = new Map>(); for (const file of workspaceCodeFiles) { - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); const selfCounts = testApiExports.has(file) ? new Map() : undefined; for (const match of source.matchAll(identifierPattern)) { const exportName = match[1]; @@ -510,7 +584,7 @@ function collectCrossOwnerReservedSdkImports(): Array<{ for (const file of collectExtensionFiles(resolve(REPO_ROOT, "extensions"))) { const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/"); const pluginId = repoRelativePath.split("/")[1]; - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const match of source.matchAll(importPattern)) { const subpath = match[1]; if (!subpath || !reserved.has(subpath)) { @@ -541,7 +615,7 @@ function collectReservedSdkSubpathImports(): string[] { for (const root of ["src", "test", "extensions", "packages", "scripts"]) { for (const file of collectCodeFiles(resolve(REPO_ROOT, root))) { - const source = readFileSync(file, "utf8"); + const source = fs.readFileSync(file, "utf8"); for (const importPattern of importPatterns) { for (const match of source.matchAll(importPattern)) { const subpath = match[1]; @@ -557,7 +631,7 @@ function collectReservedSdkSubpathImports(): string[] { } function hasWildcardReexport(entrypoint: string): boolean { - const source = readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8"); + const source = fs.readFileSync(resolve(REPO_ROOT, "src/plugin-sdk", `${entrypoint}.ts`), "utf8"); return /^\s*export\s+(?:type\s+)?\*\s+from\s+["'][^"']+["']/mu.test(source); } @@ -574,7 +648,7 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet { + it("lists package guardrail scan inputs from git without walking roots", () => { + const readDir = vi.spyOn(fs, "readdirSync"); + try { + const pluginIds = collectBundledPluginIds(); + const extensionFiles = collectExtensionFiles(resolve(REPO_ROOT, "extensions")); + const workspaceFiles = collectWorkspaceCodeFiles(); + + expect(pluginIds.length).toBeGreaterThan(0); + expect(extensionFiles.length).toBeGreaterThan(0); + expect(workspaceFiles.length).toBeGreaterThan(extensionFiles.length); + expect(readDir).not.toHaveBeenCalled(); + } finally { + readDir.mockRestore(); + } + }); + it("keeps plugin-sdk entrypoint metadata unique", () => { const counts = new Map(); for (const entrypoint of pluginSdkEntrypoints) {