Turborepo monorepo with npm workspaces: - apps/web: Next.js 14 frontend with Tailwind v4, SSE progress, doc viewer - apps/worker: BullMQ job processor (clone → parse → LLM generate) - packages/shared: TypeScript types - packages/parser: Babel-based AST parser (JS/TS) + regex (Python) - packages/llm: OpenAI/Anthropic provider abstraction + prompt pipeline - packages/diagrams: Mermaid architecture & dependency graph generators - packages/database: Prisma schema (PostgreSQL) - Docker multi-stage build (web + worker targets) All packages compile successfully with tsc and next build.
151 lines
3.7 KiB
TypeScript
151 lines
3.7 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { dirname, basename } from "node:path";
|
|
import type {
|
|
CodeStructure,
|
|
FileNode,
|
|
ModuleNode,
|
|
DependencyEdge,
|
|
ExportNode,
|
|
} from "@codeboard/shared";
|
|
import { walkFiles } from "./file-walker.js";
|
|
import { typescriptParser } from "./languages/typescript.js";
|
|
import { pythonParser } from "./languages/python.js";
|
|
import type { LanguageParser } from "./languages/base.js";
|
|
|
|
const MAX_FILES = 200;
|
|
|
|
const parsers: LanguageParser[] = [typescriptParser, pythonParser];
|
|
|
|
function getParser(language: string): LanguageParser | null {
|
|
return (
|
|
parsers.find((p) =>
|
|
p.extensions.some((ext) => {
|
|
const langMap: Record<string, string[]> = {
|
|
typescript: [".ts", ".tsx"],
|
|
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
python: [".py"],
|
|
};
|
|
return langMap[language]?.includes(ext);
|
|
})
|
|
) ?? null
|
|
);
|
|
}
|
|
|
|
function buildModules(files: FileNode[]): ModuleNode[] {
|
|
const dirMap = new Map<string, string[]>();
|
|
|
|
for (const file of files) {
|
|
const dir = dirname(file.path);
|
|
const existing = dirMap.get(dir);
|
|
if (existing) {
|
|
existing.push(file.path);
|
|
} else {
|
|
dirMap.set(dir, [file.path]);
|
|
}
|
|
}
|
|
|
|
return Array.from(dirMap.entries()).map(([dirPath, filePaths]) => ({
|
|
name: basename(dirPath) || "root",
|
|
path: dirPath,
|
|
files: filePaths,
|
|
}));
|
|
}
|
|
|
|
function buildDependencies(files: FileNode[]): DependencyEdge[] {
|
|
const edges: DependencyEdge[] = [];
|
|
const filePathSet = new Set(files.map((f) => f.path));
|
|
|
|
for (const file of files) {
|
|
for (const imp of file.imports) {
|
|
let resolved = imp.source;
|
|
|
|
if (resolved.startsWith(".")) {
|
|
const dir = dirname(file.path);
|
|
const candidate = `${dir}/${resolved.replace(/^\.\//, "")}`;
|
|
const extensions = [".ts", ".tsx", ".js", ".jsx", ".py", ""];
|
|
for (const ext of extensions) {
|
|
if (filePathSet.has(candidate + ext)) {
|
|
resolved = candidate + ext;
|
|
break;
|
|
}
|
|
if (filePathSet.has(`${candidate}/index${ext}`)) {
|
|
resolved = `${candidate}/index${ext}`;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
edges.push({
|
|
source: file.path,
|
|
target: resolved,
|
|
type: "import",
|
|
});
|
|
}
|
|
}
|
|
|
|
return edges;
|
|
}
|
|
|
|
function detectEntryPoints(files: FileNode[]): string[] {
|
|
const entryNames = new Set([
|
|
"index",
|
|
"main",
|
|
"app",
|
|
"server",
|
|
"mod",
|
|
"lib",
|
|
"__init__",
|
|
]);
|
|
|
|
return files
|
|
.filter((f) => {
|
|
const name = basename(f.path).replace(/\.[^.]+$/, "");
|
|
return entryNames.has(name);
|
|
})
|
|
.map((f) => f.path);
|
|
}
|
|
|
|
function collectExports(files: FileNode[]): ExportNode[] {
|
|
const allExports: ExportNode[] = [];
|
|
for (const file of files) {
|
|
allExports.push(...file.exports);
|
|
}
|
|
return allExports;
|
|
}
|
|
|
|
export async function analyzeRepository(
|
|
repoPath: string
|
|
): Promise<CodeStructure> {
|
|
const walkedFiles = await walkFiles(repoPath);
|
|
const filesToAnalyze = walkedFiles.slice(0, MAX_FILES);
|
|
|
|
const parsedFiles: FileNode[] = [];
|
|
|
|
for (const walkedFile of filesToAnalyze) {
|
|
const parser = getParser(walkedFile.language);
|
|
if (!parser) continue;
|
|
|
|
try {
|
|
const content = await readFile(walkedFile.absolutePath, "utf-8");
|
|
const fileNode = parser.parse(content, walkedFile.relativePath);
|
|
parsedFiles.push(fileNode);
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const modules = buildModules(parsedFiles);
|
|
const dependencies = buildDependencies(parsedFiles);
|
|
const entryPoints = detectEntryPoints(parsedFiles);
|
|
const exports = collectExports(parsedFiles);
|
|
|
|
return {
|
|
files: parsedFiles,
|
|
modules,
|
|
entryPoints,
|
|
exports,
|
|
dependencies,
|
|
patterns: [],
|
|
};
|
|
}
|