chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const defaultRoots = [
|
||||
"src/shared/components",
|
||||
"src/lib/db",
|
||||
"src/lib/compliance",
|
||||
"open-sse/translator",
|
||||
"open-sse/mcp-server",
|
||||
];
|
||||
const roots = process.argv.slice(2).length > 0 ? process.argv.slice(2) : defaultRoots;
|
||||
const sourceExtensions = [".ts", ".tsx", ".js", ".mjs", ".jsx", ".mts", ".cts"];
|
||||
|
||||
function toPosix(filePath) {
|
||||
return filePath.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function listSourceFiles(rootDir) {
|
||||
const absRoot = path.resolve(cwd, rootDir);
|
||||
if (!fs.existsSync(absRoot)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stack = [absRoot];
|
||||
const files = [];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop();
|
||||
const entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (sourceExtensions.includes(path.extname(entry.name))) {
|
||||
files.push(path.resolve(fullPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function resolveRelativeImport(fromFile, specifier) {
|
||||
const base = path.resolve(path.dirname(fromFile), specifier);
|
||||
const ext = path.extname(base);
|
||||
|
||||
if (ext && fs.existsSync(base) && fs.statSync(base).isFile()) {
|
||||
return path.resolve(base);
|
||||
}
|
||||
|
||||
for (const extension of sourceExtensions) {
|
||||
const candidate = `${base}${extension}`;
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||
return path.resolve(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
for (const extension of sourceExtensions) {
|
||||
const candidate = path.join(base, `index${extension}`);
|
||||
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
||||
return path.resolve(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractImportSpecifiers(fileContents) {
|
||||
const specs = [];
|
||||
const regex = /\b(?:import|export)\s+(?:[^"'`]*?\sfrom\s*)?["'`]([^"'`]+)["'`]/g;
|
||||
let match = regex.exec(fileContents);
|
||||
while (match) {
|
||||
specs.push(match[1]);
|
||||
match = regex.exec(fileContents);
|
||||
}
|
||||
return specs;
|
||||
}
|
||||
|
||||
function buildGraph(files) {
|
||||
const fileSet = new Set(files);
|
||||
const graph = new Map();
|
||||
|
||||
for (const filePath of files) {
|
||||
const code = fs.readFileSync(filePath, "utf8");
|
||||
const dependencies = new Set();
|
||||
const importSpecifiers = extractImportSpecifiers(code);
|
||||
|
||||
for (const specifier of importSpecifiers) {
|
||||
if (!specifier.startsWith(".")) continue;
|
||||
const resolved = resolveRelativeImport(filePath, specifier);
|
||||
if (!resolved) continue;
|
||||
if (!fileSet.has(resolved)) continue;
|
||||
dependencies.add(resolved);
|
||||
}
|
||||
|
||||
graph.set(filePath, dependencies);
|
||||
}
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
function stronglyConnectedComponents(graph) {
|
||||
const indexMap = new Map();
|
||||
const lowLinkMap = new Map();
|
||||
const onStack = new Set();
|
||||
const stack = [];
|
||||
const components = [];
|
||||
let indexCounter = 0;
|
||||
|
||||
function strongConnect(node) {
|
||||
indexMap.set(node, indexCounter);
|
||||
lowLinkMap.set(node, indexCounter);
|
||||
indexCounter += 1;
|
||||
stack.push(node);
|
||||
onStack.add(node);
|
||||
|
||||
for (const neighbor of graph.get(node) || []) {
|
||||
if (!indexMap.has(neighbor)) {
|
||||
strongConnect(neighbor);
|
||||
lowLinkMap.set(node, Math.min(lowLinkMap.get(node), lowLinkMap.get(neighbor)));
|
||||
} else if (onStack.has(neighbor)) {
|
||||
lowLinkMap.set(node, Math.min(lowLinkMap.get(node), indexMap.get(neighbor)));
|
||||
}
|
||||
}
|
||||
|
||||
if (lowLinkMap.get(node) === indexMap.get(node)) {
|
||||
const component = [];
|
||||
while (stack.length > 0) {
|
||||
const candidate = stack.pop();
|
||||
onStack.delete(candidate);
|
||||
component.push(candidate);
|
||||
if (candidate === node) break;
|
||||
}
|
||||
components.push(component);
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of graph.keys()) {
|
||||
if (!indexMap.has(node)) {
|
||||
strongConnect(node);
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
function isSelfCycle(component, graph) {
|
||||
if (component.length !== 1) return false;
|
||||
const [file] = component;
|
||||
return (graph.get(file) || new Set()).has(file);
|
||||
}
|
||||
|
||||
const files = roots.flatMap((root) => listSourceFiles(root));
|
||||
const graph = buildGraph(files);
|
||||
const components = stronglyConnectedComponents(graph);
|
||||
const cycles = components.filter(
|
||||
(component) => component.length > 1 || isSelfCycle(component, graph)
|
||||
);
|
||||
|
||||
if (cycles.length === 0) {
|
||||
console.log(
|
||||
`[cycles] OK - no cycles detected across ${graph.size} files in: ${roots.join(", ")}`
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error(`[cycles] FAIL - detected ${cycles.length} strongly connected component(s):`);
|
||||
for (const component of cycles) {
|
||||
const sorted = [...component].sort((a, b) => a.localeCompare(b));
|
||||
console.error(`\n- SCC (${sorted.length} files)`);
|
||||
for (const filePath of sorted) {
|
||||
console.error(` - ${toPosix(path.relative(cwd, filePath))}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
Reference in New Issue
Block a user