adding monkeytype
Some checks failed
Mark Stale PRs / stale (push) Has been cancelled

This commit is contained in:
Benjamin Falch
2026-04-23 13:53:44 +02:00
parent e214a2fd35
commit 2bc741fb78
1930 changed files with 7590652 additions and 0 deletions

View File

@@ -0,0 +1,389 @@
import fs from "node:fs";
import path from "node:path";
import ts from "typescript";
const ROOT = path.resolve(import.meta.dirname, "..");
// --- Argument handling ---
const args = process.argv.slice(2);
const maxDepthFlagIdx = args.indexOf("--depth");
let maxDepthLimit = Infinity;
if (maxDepthFlagIdx !== -1) {
maxDepthLimit = Number(args[maxDepthFlagIdx + 1]);
args.splice(maxDepthFlagIdx, 2);
}
const target = args[0];
if (target === undefined || target === "") {
console.log("Usage: pnpm import-tree <file-or-directory> [--depth <n>]");
process.exit(1);
}
const resolved = path.resolve(target);
function collectTsFiles(dir: string): string[] {
const results: string[] = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...collectTsFiles(full));
} else if (/\.tsx?$/.test(entry.name)) {
results.push(full);
}
}
return results;
}
const isDir = fs.statSync(resolved).isDirectory();
const boundary = isDir ? resolved : null;
let entryPoints: string[];
if (isDir) {
entryPoints = collectTsFiles(resolved);
} else {
entryPoints = [resolved];
}
if (entryPoints.length === 0) {
console.log("No .ts/.tsx files found.");
process.exit(1);
}
// --- Import extraction (type-aware) ---
const tsConfig: ts.CompilerOptions = {
module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ESNext,
jsx: ts.JsxEmit.Preserve,
sourceMap: false,
declaration: false,
isolatedModules: true,
};
const JS_IMPORT_RE =
/(?:import|export)\s+(?:(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+)?["']([^"']+)["']/g;
function extractImports(filePath: string): string[] {
const content = fs.readFileSync(filePath, "utf-8");
let outputText: string;
try {
({ outputText } = ts.transpileModule(content, {
compilerOptions: tsConfig,
fileName: filePath,
}));
} catch {
// Some files (e.g. declaration files) can't be transpiled — fall back to
// regex on the original source, which still strips type-only imports.
outputText = content;
}
const specifiers: string[] = [];
for (const match of outputText.matchAll(JS_IMPORT_RE)) {
const spec = match[1];
if (spec !== undefined) specifiers.push(spec);
}
return specifiers;
}
// --- Resolution ---
const EXTENSIONS = [".ts", ".tsx", "/index.ts", "/index.tsx"];
function resolveSpecifier(
specifier: string,
importerDir: string,
): string | null {
if (specifier.startsWith("./") || specifier.startsWith("../")) {
const base = path.resolve(importerDir, specifier);
// exact match
if (fs.existsSync(base) && fs.statSync(base).isFile()) return base;
for (const ext of EXTENSIONS) {
const candidate = base + ext;
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
// @monkeytype packages are treated as leaf nodes (no recursion into them)
if (specifier.startsWith("@monkeytype/")) return specifier;
return null; // third-party / virtual
}
const printed = new Set<string>();
// --- Graph traversal ---
type NodeInfo = {
directImports: string[];
totalReachable: number;
maxDepth: number;
};
const cache = new Map<string, NodeInfo>();
function walk(
filePath: string,
ancestors: Set<string>,
): { reachable: Set<string>; maxDepth: number } {
const cached = cache.get(filePath);
if (cached !== undefined) {
return {
reachable: new Set(getAllReachable(filePath, new Set())),
maxDepth: cached.maxDepth,
};
}
const importerDir = path.dirname(filePath);
const specifiers = extractImports(filePath);
const directImports: string[] = [];
const reachable = new Set<string>();
let maxDepth = 0;
for (const spec of specifiers) {
const resolved = resolveSpecifier(spec, importerDir);
if (resolved === null) continue;
if (directImports.includes(resolved)) continue;
directImports.push(resolved);
if (ancestors.has(resolved)) continue; // circular
reachable.add(resolved);
// @monkeytype packages are leaf nodes — don't recurse
if (resolved.startsWith("@monkeytype/")) {
maxDepth = Math.max(maxDepth, 1);
continue;
}
ancestors.add(resolved);
const sub = walk(resolved, ancestors);
ancestors.delete(resolved);
for (const r of sub.reachable) reachable.add(r);
maxDepth = Math.max(maxDepth, 1 + sub.maxDepth);
}
if (directImports.length > 0 && maxDepth === 0) {
maxDepth = 1;
}
cache.set(filePath, {
directImports,
totalReachable: reachable.size,
maxDepth,
});
return { reachable, maxDepth };
}
function getAllReachable(filePath: string, visited: Set<string>): string[] {
const info = cache.get(filePath);
if (!info) return [];
const result: string[] = [];
for (const dep of info.directImports) {
if (visited.has(dep)) continue;
visited.add(dep);
result.push(dep);
result.push(...getAllReachable(dep, visited));
}
return result;
}
// --- Colors ---
const c = {
reset: "\x1b[0m",
dim: "\x1b[2m",
bold: "\x1b[1m",
cyan: "\x1b[36m",
green: "\x1b[32m",
yellow: "\x1b[33m",
magenta: "\x1b[35m",
red: "\x1b[31m",
blue: "\x1b[34m",
white: "\x1b[37m",
};
const DEPTH_COLORS = [c.cyan, c.green, c.yellow, c.blue, c.magenta, c.white];
function depthColor(depth: number): string {
return DEPTH_COLORS[depth % DEPTH_COLORS.length] ?? c.cyan;
}
// --- Display ---
function leavesFolder(filePath: string): boolean {
if (boundary === null) return false;
if (filePath.startsWith("@monkeytype/")) return true;
return !filePath.startsWith(boundary + "/");
}
function displayPath(filePath: string): string {
if (filePath.startsWith(ROOT + "/")) {
return path.relative(ROOT, filePath);
}
return filePath;
}
function printTree(
filePath: string,
ancestors: Set<string>,
prefix: string,
isLast: boolean,
isRoot: boolean,
depth: number = 0,
): void {
const info = cache.get(filePath);
const dp = displayPath(filePath);
const connector = isRoot ? "" : isLast ? "└── " : "├── ";
const dc = depthColor(depth);
const leaves = !isRoot && leavesFolder(filePath);
const leavesTag = leaves ? ` ${c.red}[↑]${c.reset}` : "";
if (!info) {
// leaf node (e.g. @monkeytype package)
console.log(`${c.dim}${prefix}${connector}${dp}${c.reset}${leavesTag}`);
return;
}
const stats =
info.directImports.length > 0
? ` ${c.dim}(direct: ${info.directImports.length}, total: ${info.totalReachable}, depth: ${info.maxDepth})${c.reset}`
: "";
const nameStyle = isRoot ? c.bold + dc : dc;
const seen = !isRoot && printed.has(filePath);
const seenTag = seen ? ` ${c.dim}[seen above]${c.reset}` : "";
console.log(
`${c.dim}${prefix}${connector}${c.reset}${nameStyle}${dp}${c.reset}${stats}${leavesTag}${seenTag}`,
);
if (seen || depth >= maxDepthLimit) return;
printed.add(filePath);
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "│ ");
const deps = [...info.directImports];
if (depth === 0) {
deps.sort((a, b) => {
const ta = cache.get(a)?.totalReachable ?? 0;
const tb = cache.get(b)?.totalReachable ?? 0;
return tb - ta;
});
}
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
if (dep === undefined) continue;
const last = i === deps.length - 1;
if (ancestors.has(dep)) {
const cc = last ? "└── " : "├── ";
console.log(
`${c.dim}${childPrefix}${cc}${c.reset}${c.red}[circular] ${displayPath(dep)}${c.reset}`,
);
continue;
}
ancestors.add(dep);
printTree(dep, ancestors, childPrefix, last, false, depth + 1);
ancestors.delete(dep);
}
}
// --- Main ---
for (const entry of entryPoints) {
if (!fs.existsSync(entry)) {
console.log(`File not found: ${entry}`);
continue;
}
walk(entry, new Set([entry]));
}
entryPoints.sort((a, b) => {
const ta = cache.get(a)?.totalReachable ?? 0;
const tb = cache.get(b)?.totalReachable ?? 0;
return tb - ta;
});
for (const entry of entryPoints) {
if (!cache.has(entry)) continue;
printTree(entry, new Set([entry]), "", true, true);
if (entryPoints.length > 1) console.log();
}
// --- Summary ---
let totalDirect = 0;
let totalTransitive = 0;
const uniqueDirect = new Set<string>();
const uniqueTransitive = new Set<string>();
let maxDirect = 0;
let maxDirectFile = "";
let maxTransitive = 0;
let maxTransitiveFile = "";
let maxDepthSeen = 0;
let maxDepthFile = "";
for (const entry of entryPoints) {
const info = cache.get(entry);
if (!info) continue;
totalDirect += info.directImports.length;
totalTransitive += info.totalReachable;
for (const dep of info.directImports) {
uniqueDirect.add(dep);
}
for (const dep of getAllReachable(entry, new Set())) {
uniqueTransitive.add(dep);
}
if (info.directImports.length > maxDirect) {
maxDirect = info.directImports.length;
maxDirectFile = entry;
}
if (info.totalReachable > maxTransitive) {
maxTransitive = info.totalReachable;
maxTransitiveFile = entry;
}
if (info.maxDepth > maxDepthSeen) {
maxDepthSeen = info.maxDepth;
maxDepthFile = entry;
}
}
console.log(`${c.dim}───────────────────────────${c.reset}`);
console.log(`Target: ${c.bold}${displayPath(resolved)}${c.reset}`);
console.log(`Total direct: ${c.bold}${totalDirect}${c.reset}`);
console.log(`Total transitive: ${c.bold}${totalTransitive}${c.reset}`);
console.log(`Unique direct: ${c.bold}${uniqueDirect.size}${c.reset}`);
console.log(`Unique transitive: ${c.bold}${uniqueTransitive.size}${c.reset}`);
console.log(
`Max direct: ${c.bold}${maxDirect}${c.reset} ${c.dim}(${displayPath(maxDirectFile)})${c.reset}`,
);
console.log(
`Max transitive: ${c.bold}${maxTransitive}${c.reset} ${c.dim}(${displayPath(maxTransitiveFile)})${c.reset}`,
);
console.log(
`Max depth: ${c.bold}${maxDepthSeen}${c.reset} ${c.dim}(${displayPath(maxDepthFile)})${c.reset}`,
);
if (boundary !== null) {
const externalDirect = new Set<string>();
const externalTransitive = new Set<string>();
for (const entry of entryPoints) {
const info = cache.get(entry);
if (!info) continue;
for (const dep of info.directImports) {
if (leavesFolder(dep)) externalDirect.add(dep);
}
for (const dep of getAllReachable(entry, new Set())) {
if (leavesFolder(dep)) externalTransitive.add(dep);
}
}
console.log(
`Leaves folder ${c.red}[↑]${c.reset}: ${c.bold}${externalDirect.size}${c.reset} direct, ${c.bold}${externalTransitive.size}${c.reset} transitive`,
);
}