



























@@ -1,4 +1,4 @@
1-import { readdirSync, readFileSync } from "node:fs";
1+import { existsSync, readdirSync, readFileSync } from "node:fs";
22import { dirname, join, relative, resolve } from "node:path";
33import { fileURLToPath } from "node:url";
44import { describe, expect, it } from "vitest";
@@ -361,6 +361,88 @@ function collectDeprecatedTestBarrelImports(): Array<{ file: string; specifier:
361361return leaks;
362362}
363363364+function parseTestApiNamedExports(source: string): string[] {
365+const exports = new Set<string>();
366+const declarationPattern =
367+/\bexport\s+(?:const|function|class|async\s+function|type|interface)\s+([A-Za-z_$][\w$]*)/g;
368+const exportListPattern = /\bexport\s*\{([^}]+)\}/g;
369+370+for (const match of source.matchAll(declarationPattern)) {
371+const exportName = match[1];
372+if (exportName) {
373+exports.add(exportName);
374+}
375+}
376+377+for (const match of source.matchAll(exportListPattern)) {
378+const exportList = match[1];
379+if (!exportList) {
380+continue;
381+}
382+for (const part of exportList.split(",")) {
383+const item = part.trim().replace(/^type\s+/, "");
384+const aliasMatch = /\bas\s+([A-Za-z_$][\w$]*)$/u.exec(item);
385+const nameMatch = /^([A-Za-z_$][\w$]*)/u.exec(item);
386+const exportName = aliasMatch?.[1] ?? nameMatch?.[1];
387+if (exportName && exportName !== "default") {
388+exports.add(exportName);
389+}
390+}
391+}
392+393+return [...exports].toSorted();
394+}
395+396+function collectWorkspaceCodeFiles(): string[] {
397+const files: string[] = [];
398+for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
399+const dir = resolve(REPO_ROOT, root);
400+if (existsSync(dir)) {
401+files.push(...collectCodeFiles(dir));
402+}
403+}
404+return files;
405+}
406+407+function countIdentifierReferences(
408+files: readonly string[],
409+excludedFile: string,
410+name: string,
411+): number {
412+let count = 0;
413+const pattern = new RegExp(`\\b${name}\\b`, "g");
414+for (const file of files) {
415+if (file === excludedFile) {
416+continue;
417+}
418+const source = readFileSync(file, "utf8");
419+count += [...source.matchAll(pattern)].length;
420+}
421+return count;
422+}
423+424+function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportName: string }> {
425+const leaks: Array<{ file: string; exportName: string }> = [];
426+const workspaceCodeFiles = collectWorkspaceCodeFiles();
427+const testApiFiles = collectCodeFiles(resolve(REPO_ROOT, "extensions")).filter((file) =>
428+file.endsWith("/test-api.ts"),
429+);
430+431+for (const file of testApiFiles) {
432+const repoRelativePath = relative(REPO_ROOT, file).replaceAll("\\", "/");
433+const source = readFileSync(file, "utf8");
434+for (const exportName of parseTestApiNamedExports(source)) {
435+if (countIdentifierReferences(workspaceCodeFiles, file, exportName) === 0) {
436+leaks.push({ file: repoRelativePath, exportName });
437+}
438+}
439+}
440+441+return leaks.toSorted(
442+(a, b) => a.file.localeCompare(b.file) || a.exportName.localeCompare(b.exportName),
443+);
444+}
445+364446function collectCrossOwnerReservedSdkImports(): Array<{
365447file: string;
366448specifier: string;
@@ -532,6 +614,10 @@ describe("plugin-sdk package contract guardrails", () => {
532614expect(collectDeprecatedTestBarrelImports()).toEqual([]);
533615});
534616617+it("keeps extension test-api exports consumed", () => {
618+expect(collectUnusedExtensionTestApiExports()).toEqual([]);
619+});
620+535621it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => {
536622expect(collectCrossOwnerReservedSdkImports()).toEqual([]);
537623});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。