fix(test): avoid walking plugin sdk package guardrails

This commit is contained in:
Vincent Koc
2026-05-16 10:38:13 +08:00
parent 069ad23b74
commit f1c8570e0c

View File

@@ -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<string, readonly string[] | null>();
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<string, unknown>;
};
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<string> {
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<string> {
}
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<string, string>;
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<string, string>;
optionalDependencies?: Record<string, string>;
files?: string[];
@@ -193,7 +255,9 @@ function readMatrixPackageJson(): {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
} {
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<string, string>;
optionalDependencies?: Record<string, string>;
};
@@ -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<string>();
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<string, Map<string, number>>();
for (const file of workspaceCodeFiles) {
const source = readFileSync(file, "utf8");
const source = fs.readFileSync(file, "utf8");
const selfCounts = testApiExports.has(file) ? new Map<string, number>() : 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<strin
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 subpath = match[1];
@@ -589,6 +663,22 @@ function collectExtensionProductionSdkSubpathImports(subpaths: ReadonlySet<strin
}
describe("plugin-sdk package contract guardrails", () => {
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<string, number>();
for (const entrypoint of pluginSdkEntrypoints) {