mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
perf: cache guard inventory checks
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
|
|
||||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||||
const scanRoots = resolveSourceRoots(repoRoot, ["src/plugin-sdk", "src/plugins/runtime"]);
|
const scanRoots = resolveSourceRoots(repoRoot, ["src/plugin-sdk", "src/plugins/runtime"]);
|
||||||
|
let architectureSmellsPromise;
|
||||||
|
|
||||||
function compareEntries(left, right) {
|
function compareEntries(left, right) {
|
||||||
return (
|
return (
|
||||||
@@ -195,6 +196,8 @@ function scanRuntimeServiceLocatorSmells(sourceFile, filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function collectArchitectureSmells() {
|
export async function collectArchitectureSmells() {
|
||||||
|
if (!architectureSmellsPromise) {
|
||||||
|
architectureSmellsPromise = (async () => {
|
||||||
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
|
const files = (await collectTypeScriptFilesFromRoots(scanRoots)).toSorted((left, right) =>
|
||||||
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
|
normalizeRepoPath(repoRoot, left).localeCompare(normalizeRepoPath(repoRoot, right)),
|
||||||
);
|
);
|
||||||
@@ -210,6 +213,15 @@ export async function collectArchitectureSmells() {
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
})();
|
||||||
|
try {
|
||||||
|
return await architectureSmellsPromise;
|
||||||
|
} catch (error) {
|
||||||
|
architectureSmellsPromise = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await architectureSmellsPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInventoryHuman(inventory) {
|
function formatInventoryHuman(inventory) {
|
||||||
|
|||||||
@@ -101,9 +101,12 @@ async function collectParsedExtensionSourceFiles() {
|
|||||||
if (!parsedExtensionSourceFilesPromise) {
|
if (!parsedExtensionSourceFilesPromise) {
|
||||||
parsedExtensionSourceFilesPromise = (async () => {
|
parsedExtensionSourceFilesPromise = (async () => {
|
||||||
const files = await collectExtensionSourceFiles(extensionsRoot);
|
const files = await collectExtensionSourceFiles(extensionsRoot);
|
||||||
return await Promise.all(
|
const parsed = await Promise.all(
|
||||||
files.map(async (filePath) => {
|
files.map(async (filePath) => {
|
||||||
const source = await fs.readFile(filePath, "utf8");
|
const source = await fs.readFile(filePath, "utf8");
|
||||||
|
if (!mayContainModuleSpecifier(source)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const scriptKind =
|
const scriptKind =
|
||||||
filePath.endsWith(".tsx") || filePath.endsWith(".jsx")
|
filePath.endsWith(".tsx") || filePath.endsWith(".jsx")
|
||||||
? ts.ScriptKind.TSX
|
? ts.ScriptKind.TSX
|
||||||
@@ -120,11 +123,20 @@ async function collectParsedExtensionSourceFiles() {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
return parsed.filter(Boolean);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
return await parsedExtensionSourceFilesPromise;
|
return await parsedExtensionSourceFilesPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mayContainModuleSpecifier(source) {
|
||||||
|
return (
|
||||||
|
/\bfrom\s*["']/.test(source) ||
|
||||||
|
/\bimport\s*(?:\(|["']|type\b|[\w*{])/.test(source) ||
|
||||||
|
/\bexport\s*(?:type\s+)?(?:\*|{)[^;\n]*\bfrom\s*["']/.test(source)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveExtensionRoot(filePath) {
|
function resolveExtensionRoot(filePath) {
|
||||||
const relativePath = normalizeRepoPath(repoRoot, filePath);
|
const relativePath = normalizeRepoPath(repoRoot, filePath);
|
||||||
const segments = relativePath.split("/");
|
const segments = relativePath.split("/");
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ const suspiciousPatterns = [
|
|||||||
/id:\s*"firecrawl"/,
|
/id:\s*"firecrawl"/,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
let webFetchProviderViolationsPromise;
|
||||||
|
|
||||||
export async function collectWebFetchProviderBoundaryViolations() {
|
export async function collectWebFetchProviderBoundaryViolations() {
|
||||||
|
if (!webFetchProviderViolationsPromise) {
|
||||||
|
webFetchProviderViolationsPromise = (async () => {
|
||||||
const violations = [];
|
const violations = [];
|
||||||
const files = await collectSourceFileContents({
|
const files = await collectSourceFileContents({
|
||||||
repoRoot,
|
repoRoot,
|
||||||
@@ -66,6 +70,15 @@ export async function collectWebFetchProviderBoundaryViolations() {
|
|||||||
return violations.toSorted(
|
return violations.toSorted(
|
||||||
(left, right) => left.file.localeCompare(right.file) || left.line - right.line,
|
(left, right) => left.file.localeCompare(right.file) || left.line - right.line,
|
||||||
);
|
);
|
||||||
|
})();
|
||||||
|
try {
|
||||||
|
return await webFetchProviderViolationsPromise;
|
||||||
|
} catch (error) {
|
||||||
|
webFetchProviderViolationsPromise = undefined;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await webFetchProviderViolationsPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function main(argv = process.argv.slice(2), io) {
|
export async function main(argv = process.argv.slice(2), io) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { promises as fs } from "node:fs";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
const parsedTypeScriptSourceCache = new Map();
|
const parsedTypeScriptSourceCache = new Map();
|
||||||
|
const sourceTextCache = new Map();
|
||||||
|
|
||||||
export function normalizeRepoPath(repoRoot, filePath) {
|
export function normalizeRepoPath(repoRoot, filePath) {
|
||||||
return path.relative(repoRoot, filePath).split(path.sep).join("/");
|
return path.relative(repoRoot, filePath).split(path.sep).join("/");
|
||||||
@@ -118,7 +119,11 @@ export async function collectTypeScriptInventory(params) {
|
|||||||
const cacheKey = `${scriptKind}:${filePath}`;
|
const cacheKey = `${scriptKind}:${filePath}`;
|
||||||
let sourceFile = parsedTypeScriptSourceCache.get(cacheKey);
|
let sourceFile = parsedTypeScriptSourceCache.get(cacheKey);
|
||||||
if (!sourceFile) {
|
if (!sourceFile) {
|
||||||
const source = await fs.readFile(filePath, "utf8");
|
let source = sourceTextCache.get(filePath);
|
||||||
|
if (source === undefined) {
|
||||||
|
source = await fs.readFile(filePath, "utf8");
|
||||||
|
sourceTextCache.set(filePath, source);
|
||||||
|
}
|
||||||
if (params.shouldParseSource && !params.shouldParseSource(source, filePath)) {
|
if (params.shouldParseSource && !params.shouldParseSource(source, filePath)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { describe, expect, it } from "vitest";
|
|||||||
import { collectArchitectureSmells, main } from "../scripts/check-architecture-smells.mjs";
|
import { collectArchitectureSmells, main } from "../scripts/check-architecture-smells.mjs";
|
||||||
import { createCapturedIo } from "./helpers/captured-io.js";
|
import { createCapturedIo } from "./helpers/captured-io.js";
|
||||||
|
|
||||||
|
const smellsPromise = collectArchitectureSmells();
|
||||||
|
|
||||||
describe("architecture smell inventory", () => {
|
describe("architecture smell inventory", () => {
|
||||||
it("produces stable sorted output", async () => {
|
it("produces stable sorted output", async () => {
|
||||||
const first = await collectArchitectureSmells();
|
const smells = await smellsPromise;
|
||||||
const second = await collectArchitectureSmells();
|
|
||||||
|
|
||||||
expect(second).toEqual(first);
|
|
||||||
expect(
|
expect(
|
||||||
[...first].toSorted(
|
[...smells].toSorted(
|
||||||
(left, right) =>
|
(left, right) =>
|
||||||
left.category.localeCompare(right.category) ||
|
left.category.localeCompare(right.category) ||
|
||||||
left.file.localeCompare(right.file) ||
|
left.file.localeCompare(right.file) ||
|
||||||
@@ -18,7 +18,7 @@ describe("architecture smell inventory", () => {
|
|||||||
left.specifier.localeCompare(right.specifier) ||
|
left.specifier.localeCompare(right.specifier) ||
|
||||||
left.reason.localeCompare(right.reason),
|
left.reason.localeCompare(right.reason),
|
||||||
),
|
),
|
||||||
).toEqual(first);
|
).toEqual(smells);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("script json output matches the collector", async () => {
|
it("script json output matches the collector", async () => {
|
||||||
@@ -27,6 +27,6 @@ describe("architecture smell inventory", () => {
|
|||||||
|
|
||||||
expect(exitCode).toBe(0);
|
expect(exitCode).toBe(0);
|
||||||
expect(captured.readStderr()).toBe("");
|
expect(captured.readStderr()).toBe("");
|
||||||
expect(JSON.parse(captured.readStdout())).toEqual(await collectArchitectureSmells());
|
expect(JSON.parse(captured.readStdout())).toEqual(await smellsPromise);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,14 +16,26 @@ function buildFixtureScope() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fixtureScope = buildFixtureScope();
|
||||||
|
const publicSurfaceEnvelope = analyzeTopology({
|
||||||
|
repoRoot,
|
||||||
|
scope: fixtureScope,
|
||||||
|
report: "public-surface-usage",
|
||||||
|
});
|
||||||
|
const singleOwnerEnvelope = analyzeTopology({
|
||||||
|
repoRoot,
|
||||||
|
scope: fixtureScope,
|
||||||
|
report: "single-owner-shared",
|
||||||
|
});
|
||||||
|
const unusedEnvelope = analyzeTopology({
|
||||||
|
repoRoot,
|
||||||
|
scope: fixtureScope,
|
||||||
|
report: "unused-public-surface",
|
||||||
|
});
|
||||||
|
|
||||||
describe("ts-topology", () => {
|
describe("ts-topology", () => {
|
||||||
it("collapses canonical symbols exported by multiple public subpaths", () => {
|
it("collapses canonical symbols exported by multiple public subpaths", () => {
|
||||||
const envelope = analyzeTopology({
|
const sharedThing = publicSurfaceEnvelope.records.find((record) =>
|
||||||
repoRoot,
|
|
||||||
scope: buildFixtureScope(),
|
|
||||||
report: "public-surface-usage",
|
|
||||||
});
|
|
||||||
const sharedThing = envelope.records.find((record) =>
|
|
||||||
record.exportNames.includes("sharedThing"),
|
record.exportNames.includes("sharedThing"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -38,16 +50,13 @@ describe("ts-topology", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("counts renamed imports, namespace imports, type-only imports, and test-only consumers", () => {
|
it("counts renamed imports, namespace imports, type-only imports, and test-only consumers", () => {
|
||||||
const envelope = analyzeTopology({
|
const aliasedThing = publicSurfaceEnvelope.records.find((record) =>
|
||||||
repoRoot,
|
|
||||||
scope: buildFixtureScope(),
|
|
||||||
report: "public-surface-usage",
|
|
||||||
});
|
|
||||||
const aliasedThing = envelope.records.find((record) =>
|
|
||||||
record.exportNames.includes("aliasedThing"),
|
record.exportNames.includes("aliasedThing"),
|
||||||
);
|
);
|
||||||
const sharedType = envelope.records.find((record) => record.exportNames.includes("SharedType"));
|
const sharedType = publicSurfaceEnvelope.records.find((record) =>
|
||||||
const testOnlyThing = envelope.records.find((record) =>
|
record.exportNames.includes("SharedType"),
|
||||||
|
);
|
||||||
|
const testOnlyThing = publicSurfaceEnvelope.records.find((record) =>
|
||||||
record.exportNames.includes("testOnlyThing"),
|
record.exportNames.includes("testOnlyThing"),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,33 +74,17 @@ describe("ts-topology", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("surfaces single-owner shared and unused reports correctly", () => {
|
it("surfaces single-owner shared and unused reports correctly", () => {
|
||||||
const singleOwner = analyzeTopology({
|
expect(singleOwnerEnvelope.records.map((record) => record.exportNames[0])).toContain(
|
||||||
repoRoot,
|
|
||||||
scope: buildFixtureScope(),
|
|
||||||
report: "single-owner-shared",
|
|
||||||
});
|
|
||||||
const unused = analyzeTopology({
|
|
||||||
repoRoot,
|
|
||||||
scope: buildFixtureScope(),
|
|
||||||
report: "unused-public-surface",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(singleOwner.records.map((record) => record.exportNames[0])).toContain(
|
|
||||||
"singleOwnerHelper",
|
"singleOwnerHelper",
|
||||||
);
|
);
|
||||||
expect(singleOwner.records.map((record) => record.exportNames[0])).not.toContain("sharedThing");
|
expect(singleOwnerEnvelope.records.map((record) => record.exportNames[0])).not.toContain(
|
||||||
expect(unused.records.map((record) => record.exportNames[0])).toEqual(["unusedThing"]);
|
"sharedThing",
|
||||||
|
);
|
||||||
|
expect(unusedEnvelope.records.map((record) => record.exportNames[0])).toEqual(["unusedThing"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders stable text summaries for the public-surface report", () => {
|
it("renders stable text summaries for the public-surface report", () => {
|
||||||
const envelope = analyzeTopology({
|
expect(renderTextReport({ ...publicSurfaceEnvelope, limit: 3 }, 3)).toMatchInlineSnapshot(`
|
||||||
repoRoot,
|
|
||||||
scope: buildFixtureScope(),
|
|
||||||
report: "public-surface-usage",
|
|
||||||
limit: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(renderTextReport(envelope, 3)).toMatchInlineSnapshot(`
|
|
||||||
"Scope: custom
|
"Scope: custom
|
||||||
Public exports analyzed: 6
|
Public exports analyzed: 6
|
||||||
Production-used exports: 4
|
Production-used exports: 4
|
||||||
|
|||||||
Reference in New Issue
Block a user