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,322 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
import { readdir, readFile, stat } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
export const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
export const removedModuleScanRoots = ["apps", "packages"] as const;
|
||||
|
||||
export const sourceFileExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".md", ".mdx"]);
|
||||
|
||||
export const ignoredDirectoryNames = new Set([
|
||||
".cache",
|
||||
".git",
|
||||
".next",
|
||||
".react-router",
|
||||
".tc",
|
||||
".turbo",
|
||||
".vite",
|
||||
"build",
|
||||
"cache",
|
||||
"coverage",
|
||||
"dist",
|
||||
"generated",
|
||||
"node_modules",
|
||||
"out",
|
||||
"storybook-static",
|
||||
"tmp",
|
||||
"temp",
|
||||
"vendor",
|
||||
]);
|
||||
|
||||
const ignoredFileNames = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]);
|
||||
|
||||
export const forbiddenRemovedModulePatterns = [
|
||||
{ id: "postmark", pattern: /\bpostmark\b/i },
|
||||
{ id: "rewardful", pattern: /\brewardful\b/i },
|
||||
{ id: "pageblocks", pattern: /\bPageBlocks?\b/i },
|
||||
{ id: "site-demo", pattern: /\b(?:Site-Demo|site demo|demo site|SiteDemo|my-site-demo)\b/i },
|
||||
] as const;
|
||||
|
||||
const removedModulePrefilterPatterns = ["postmark", "rewardful", "PageBlock", "Site-Demo", "site demo", "demo site", "SiteDemo", "my-site-demo"] as const;
|
||||
const maxReadConcurrency = 16;
|
||||
const maxPrefilterArgBytes = 100_000;
|
||||
|
||||
export type RemovedModuleGroupId = (typeof forbiddenRemovedModulePatterns)[number]["id"];
|
||||
|
||||
export interface RemovedModuleDiagnostic {
|
||||
id: RemovedModuleGroupId;
|
||||
file: string;
|
||||
line: number;
|
||||
excerpt: string;
|
||||
}
|
||||
|
||||
export interface RemovedModuleScanResult {
|
||||
roots: string[];
|
||||
missingRoots: string[];
|
||||
files: string[];
|
||||
diagnostics: RemovedModuleDiagnostic[];
|
||||
}
|
||||
|
||||
export function normalizeRelativePath(relativePath: string) {
|
||||
return relativePath.split(path.sep).join("/").replace(/^\.\//, "");
|
||||
}
|
||||
|
||||
function toRelativePath(root: string, absolutePath: string) {
|
||||
return normalizeRelativePath(path.relative(root, absolutePath));
|
||||
}
|
||||
|
||||
export function isIgnoredDirectory(name: string, relativePath = name) {
|
||||
const normalized = normalizeRelativePath(relativePath);
|
||||
return ignoredDirectoryNames.has(name) || ignoredDirectoryNames.has(normalized) || name.endsWith(".cache") || normalized.includes("/__generated__/");
|
||||
}
|
||||
|
||||
export function isGeneratedOrIgnoredFile(filePath: string) {
|
||||
const name = path.basename(filePath);
|
||||
return (
|
||||
ignoredFileNames.has(name) ||
|
||||
name.endsWith(".d.ts") ||
|
||||
name.endsWith(".generated.ts") ||
|
||||
name.endsWith(".generated.tsx") ||
|
||||
name.endsWith(".gen.ts") ||
|
||||
name.endsWith(".gen.tsx") ||
|
||||
name.endsWith(".min.js") ||
|
||||
name.endsWith(".map")
|
||||
);
|
||||
}
|
||||
|
||||
export function isTestStoryFixtureOrMockFile(filePath: string) {
|
||||
const normalized = normalizeRelativePath(filePath);
|
||||
const name = path.basename(normalized);
|
||||
return (
|
||||
/(?:^|[._-])(?:test|spec|stories|story|fixture|fixtures|mock|mocks)\.[cm]?[jt]sx?$/.test(name) ||
|
||||
/\.(?:test|spec|stories|story|fixture|fixtures|mock|mocks)\.[cm]?[jt]sx?$/.test(name) ||
|
||||
normalized.includes("/__tests__/") ||
|
||||
normalized.includes("/test/") ||
|
||||
normalized.includes("/tests/") ||
|
||||
normalized.includes("/fixtures/") ||
|
||||
normalized.includes("/fixture/") ||
|
||||
normalized.includes("/mocks/") ||
|
||||
normalized.includes("/__mocks__/")
|
||||
);
|
||||
}
|
||||
|
||||
export function isOptionalPluginOutput(relativePath: string) {
|
||||
const normalized = normalizeRelativePath(relativePath);
|
||||
return normalized === "packages/plugins" || normalized.startsWith("packages/plugins/");
|
||||
}
|
||||
|
||||
export function isScannableSourceFile(filePath: string) {
|
||||
return sourceFileExtensions.has(path.extname(filePath)) && !isGeneratedOrIgnoredFile(filePath);
|
||||
}
|
||||
|
||||
async function pathExists(root: string, relativePath: string) {
|
||||
try {
|
||||
await stat(path.join(root, relativePath));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectRemovedModuleSourceFiles(root = repoRoot, scanRoots: readonly string[] = removedModuleScanRoots) {
|
||||
const roots: string[] = [];
|
||||
const missingRoots: string[] = [];
|
||||
const files: string[] = [];
|
||||
|
||||
async function walk(directory: string) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
const relativePath = toRelativePath(root, absolutePath);
|
||||
if (entry.isDirectory()) {
|
||||
if (isIgnoredDirectory(entry.name, relativePath) || isOptionalPluginOutput(relativePath)) continue;
|
||||
await walk(absolutePath);
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
if (!isScannableSourceFile(entry.name)) continue;
|
||||
if (isOptionalPluginOutput(relativePath) || isTestStoryFixtureOrMockFile(relativePath)) continue;
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
for (const scanRoot of scanRoots) {
|
||||
const normalizedRoot = normalizeRelativePath(scanRoot);
|
||||
if (!(await pathExists(root, normalizedRoot))) {
|
||||
missingRoots.push(normalizedRoot);
|
||||
continue;
|
||||
}
|
||||
roots.push(normalizedRoot);
|
||||
const absoluteRoot = path.join(root, normalizedRoot);
|
||||
const rootStat = await stat(absoluteRoot);
|
||||
if (rootStat.isFile()) {
|
||||
if (isScannableSourceFile(absoluteRoot) && !isTestStoryFixtureOrMockFile(normalizedRoot) && !isOptionalPluginOutput(normalizedRoot)) files.push(absoluteRoot);
|
||||
continue;
|
||||
}
|
||||
await walk(absoluteRoot);
|
||||
}
|
||||
|
||||
files.sort((a, b) => toRelativePath(root, a).localeCompare(toRelativePath(root, b)));
|
||||
return { roots, missingRoots, files };
|
||||
}
|
||||
|
||||
export function findRemovedModuleMatches(sourceText: string, relativeFilePath: string) {
|
||||
const diagnostics: RemovedModuleDiagnostic[] = [];
|
||||
const lines = sourceText.split(/\r?\n/);
|
||||
lines.forEach((line, index) => {
|
||||
for (const { id, pattern } of forbiddenRemovedModulePatterns) {
|
||||
pattern.lastIndex = 0;
|
||||
if (!pattern.test(line)) continue;
|
||||
diagnostics.push({ id, file: normalizeRelativePath(relativeFilePath), line: index + 1, excerpt: line.trim() });
|
||||
}
|
||||
});
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function chunkFilesForPrefilter(files: readonly string[]) {
|
||||
const chunks: string[][] = [];
|
||||
let chunk: string[] = [];
|
||||
let chunkBytes = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const fileBytes = Buffer.byteLength(file) + 1;
|
||||
if (chunk.length > 0 && chunkBytes + fileBytes > maxPrefilterArgBytes) {
|
||||
chunks.push(chunk);
|
||||
chunk = [];
|
||||
chunkBytes = 0;
|
||||
}
|
||||
chunk.push(file);
|
||||
chunkBytes += fileBytes;
|
||||
}
|
||||
|
||||
if (chunk.length > 0) chunks.push(chunk);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
async function runRipgrepPrefilter(root: string, files: readonly string[]) {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const matchingFiles = new Set<string>();
|
||||
const chunks = chunkFilesForPrefilter(files);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const args = [
|
||||
"--files-with-matches",
|
||||
"--fixed-strings",
|
||||
"--ignore-case",
|
||||
"--null",
|
||||
"--no-ignore",
|
||||
"--hidden",
|
||||
"--text",
|
||||
"--no-messages",
|
||||
...removedModulePrefilterPatterns.flatMap((pattern) => ["--regexp", pattern]),
|
||||
"--",
|
||||
...chunk,
|
||||
];
|
||||
|
||||
const { code, output } = await new Promise<{ code: number | null; output: string }>((resolve, reject) => {
|
||||
const child = spawn("rg", args, { cwd: root });
|
||||
const outputChunks: Buffer[] = [];
|
||||
const errorChunks: Buffer[] = [];
|
||||
|
||||
child.stdout.on("data", (data: Buffer) => outputChunks.push(data));
|
||||
child.stderr.on("data", (data: Buffer) => errorChunks.push(data));
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0 && code !== 1) {
|
||||
const details = Buffer.concat(errorChunks).toString("utf8").trim();
|
||||
reject(new Error(`removed-module prefilter failed with exit code ${code}${details ? `: ${details}` : ""}`));
|
||||
return;
|
||||
}
|
||||
resolve({ code, output: Buffer.concat(outputChunks).toString("utf8") });
|
||||
});
|
||||
});
|
||||
|
||||
if (code === 1 || output.length === 0) continue;
|
||||
for (const file of output.split("\0")) {
|
||||
if (file.length === 0) continue;
|
||||
matchingFiles.add(normalizeRelativePath(path.isAbsolute(file) ? path.relative(root, file) : file));
|
||||
}
|
||||
}
|
||||
|
||||
return files.filter((file) => matchingFiles.has(toRelativePath(root, file)));
|
||||
}
|
||||
|
||||
export async function prefilterRemovedModuleCandidateFiles(root: string, files: readonly string[]) {
|
||||
try {
|
||||
return await runRipgrepPrefilter(root, files);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") return [...files];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T, R>(items: readonly T[], limit: number, mapper: (item: T, index: number) => Promise<R>) {
|
||||
const results = new Array<R>(items.length);
|
||||
let nextIndex = 0;
|
||||
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||
while (nextIndex < items.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[index] = await mapper(items[index], index);
|
||||
}
|
||||
});
|
||||
await Promise.all(workers);
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function scanRemovedModules(root = repoRoot, scanRoots: readonly string[] = removedModuleScanRoots): Promise<RemovedModuleScanResult> {
|
||||
const collection = await collectRemovedModuleSourceFiles(root, scanRoots);
|
||||
const candidateFiles = await prefilterRemovedModuleCandidateFiles(root, collection.files);
|
||||
const diagnosticsByFile = await mapWithConcurrency(candidateFiles, maxReadConcurrency, async (file) => {
|
||||
const relativeFilePath = toRelativePath(root, file);
|
||||
const sourceText = await readFile(file, "utf8");
|
||||
return findRemovedModuleMatches(sourceText, relativeFilePath);
|
||||
});
|
||||
const diagnostics = diagnosticsByFile.flat();
|
||||
return { roots: collection.roots, missingRoots: collection.missingRoots, files: collection.files.map((file) => toRelativePath(root, file)), diagnostics };
|
||||
}
|
||||
|
||||
export function renderRemovedModuleScanResult(result: RemovedModuleScanResult) {
|
||||
const lines = [
|
||||
`removed-module scan roots: ${result.roots.join(", ") || "(none)"}`,
|
||||
`removed-module missing roots skipped: ${result.missingRoots.join(", ") || "(none)"}`,
|
||||
`files scanned: ${result.files.length}`,
|
||||
`matches found: ${result.diagnostics.length}`,
|
||||
];
|
||||
|
||||
if (result.diagnostics.length === 0) {
|
||||
lines.unshift("PASS removed-module scan: no forbidden runtime references found");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
lines.unshift("FAIL removed-module scan: forbidden runtime references found");
|
||||
for (const diagnostic of result.diagnostics) {
|
||||
lines.push(`${diagnostic.file}:${diagnostic.line}: [${diagnostic.id}] ${diagnostic.excerpt}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await scanRemovedModules(repoRoot);
|
||||
const output = renderRemovedModuleScanResult(result);
|
||||
if (result.diagnostics.length > 0) {
|
||||
console.error(output);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
console.log(output);
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
void main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user