Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console */
|
||||
import { readdir, stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { scanApiRoutesFromFile, type ApiRouteKey } from "../tools/api-inventory/scan-api-routes.js";
|
||||
|
||||
export const DEPRECATED_API_ROUTE_KEYS = ["GET /v1/auth/verify-token", "GET /v1/auth/me"] as const satisfies readonly ApiRouteKey[];
|
||||
|
||||
export type DeprecatedApiRouteKey = (typeof DEPRECATED_API_ROUTE_KEYS)[number];
|
||||
|
||||
export type ApiInventoryGuardOptions = {
|
||||
appSourcePath?: string;
|
||||
removedWebApiRoutesRoot?: string;
|
||||
legacyRoutesRoot?: string;
|
||||
};
|
||||
|
||||
export type ApiInventoryViolation = {
|
||||
routeKey: DeprecatedApiRouteKey | "REMOVED /api web route" | "UNRESOLVED mounted router";
|
||||
surface: "hono" | "removed-react-router-api" | "hono-route-mount";
|
||||
sourcePath: string;
|
||||
location?: {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
export type ApiInventoryGuardResult = {
|
||||
ok: boolean;
|
||||
violations: ApiInventoryViolation[];
|
||||
missingRoots: string[];
|
||||
};
|
||||
|
||||
const DEFAULT_APP_SOURCE_PATH = "apps/api/src/app.ts";
|
||||
const DEFAULT_REMOVED_WEB_API_ROUTES_ROOT = "apps/web/app/routes/api";
|
||||
const deprecatedRouteKeys = new Set<ApiRouteKey>(DEPRECATED_API_ROUTE_KEYS);
|
||||
const legacyRouteFileNamesByRouteKey = new Map<DeprecatedApiRouteKey, readonly string[]>([
|
||||
["GET /v1/auth/verify-token", [path.join("auth", "verify-token.ts"), path.join("auth", "verify-token.tsx")]],
|
||||
["GET /v1/auth/me", [path.join("auth", "me.ts"), path.join("auth", "me.tsx")]],
|
||||
]);
|
||||
|
||||
export async function validateApiInventory(options: ApiInventoryGuardOptions = {}): Promise<ApiInventoryGuardResult> {
|
||||
const appSourcePath = options.appSourcePath ?? DEFAULT_APP_SOURCE_PATH;
|
||||
const removedWebApiRoutesRoot = options.removedWebApiRoutesRoot ?? options.legacyRoutesRoot ?? DEFAULT_REMOVED_WEB_API_ROUTES_ROOT;
|
||||
const violations: ApiInventoryViolation[] = [];
|
||||
const missingRoots: string[] = [];
|
||||
|
||||
const scanResult = await scanApiRoutesFromFile(appSourcePath);
|
||||
for (const mountedRouter of scanResult.unresolvedMountedRouters) {
|
||||
violations.push({
|
||||
routeKey: "UNRESOLVED mounted router",
|
||||
surface: "hono-route-mount",
|
||||
sourcePath: appSourcePath,
|
||||
location: mountedRouter.location,
|
||||
detail: `${mountedRouter.prefix} -> ${mountedRouter.receiver}`,
|
||||
});
|
||||
}
|
||||
for (const route of scanResult.routes) {
|
||||
if (!deprecatedRouteKeys.has(route.key)) continue;
|
||||
|
||||
violations.push({
|
||||
routeKey: route.key as DeprecatedApiRouteKey,
|
||||
surface: "hono",
|
||||
sourcePath: appSourcePath,
|
||||
location: route.location,
|
||||
});
|
||||
}
|
||||
|
||||
const removedWebApiRootExists = await pathExists(removedWebApiRoutesRoot);
|
||||
const removedWebApiRouteFiles = removedWebApiRootExists ? await listFiles(removedWebApiRoutesRoot) : [];
|
||||
const normalizedRemovedWebApiRouteFiles = new Set(removedWebApiRouteFiles.map((filePath) => normalizeRouteFilePath(filePath)));
|
||||
|
||||
for (const fileName of removedWebApiRouteFiles) {
|
||||
violations.push({
|
||||
routeKey: "REMOVED /api web route",
|
||||
surface: "removed-react-router-api",
|
||||
sourcePath: path.join(removedWebApiRoutesRoot, fileName),
|
||||
});
|
||||
}
|
||||
|
||||
for (const [routeKey, fileNames] of legacyRouteFileNamesByRouteKey.entries()) {
|
||||
for (const fileName of fileNames) {
|
||||
if (!normalizedRemovedWebApiRouteFiles.has(normalizeRouteFilePath(fileName))) continue;
|
||||
|
||||
violations.push({
|
||||
routeKey,
|
||||
surface: "removed-react-router-api",
|
||||
sourcePath: path.join(removedWebApiRoutesRoot, fileName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: violations.length === 0 && missingRoots.length === 0, violations, missingRoots };
|
||||
}
|
||||
|
||||
export function renderApiInventoryGuardResult(result: ApiInventoryGuardResult): string {
|
||||
if (result.ok) return "PASS api-inventory: no deprecated route keys found";
|
||||
|
||||
return [
|
||||
"FAIL api-inventory: deprecated route keys or removed web API routes found",
|
||||
...result.missingRoots.map((root) => `- missing scan root: ${root}`),
|
||||
...result.violations.map(formatViolation),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function pathExists(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(targetPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isNodeError(error) && error.code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listFiles(root: string, relativeDir = ""): Promise<string[]> {
|
||||
const absoluteDir = path.join(root, relativeDir);
|
||||
const entries = await readdir(absoluteDir, { withFileTypes: true });
|
||||
const files: string[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.join(relativeDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listFiles(root, relativePath)));
|
||||
} else if (entry.isFile()) {
|
||||
files.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function formatViolation(violation: ApiInventoryViolation): string {
|
||||
const location = violation.location ? `:${violation.location.line}:${violation.location.column}` : "";
|
||||
const detail = violation.detail ? ` (${violation.detail})` : "";
|
||||
return `- ${violation.routeKey} [${violation.surface}] ${violation.sourcePath}${location}${detail}`;
|
||||
}
|
||||
|
||||
function normalizeRouteFilePath(filePath: string): string {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
||||
return typeof error === "object" && error !== null && "code" in error;
|
||||
}
|
||||
|
||||
async function main(args = process.argv.slice(2)): Promise<void> {
|
||||
if (args.length > 0) {
|
||||
console.error("FAIL api-inventory: unknown arguments");
|
||||
for (const arg of args) console.error(`- ${arg}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await validateApiInventory();
|
||||
const output = renderApiInventoryGuardResult(result);
|
||||
if (result.ok) {
|
||||
console.log(output);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(output);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
await main();
|
||||
}
|
||||
Reference in new issue
Block a user