Files
codeboard/packages/parser/src/analyzer.ts
Vectry 79dad6124f feat: initial CodeBoard monorepo scaffold
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.
2026-02-09 15:22:50 +00:00

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: [],
};
}