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.
This commit is contained in:
150
packages/parser/src/analyzer.ts
Normal file
150
packages/parser/src/analyzer.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user