import * as fs from "node:fs";
import * as path from "node:path";
import { glob } from "glob";
import type { CireConfig, FileIR } from "../types";
import { logger } from "../utils/logger";
import { NavigationGenerator } from "../utils/navigation-generator";
import { type WorkflowConfig, WorkflowManager } from "./WorkflowManager";
export interface BuildStats {
totalFiles: number;
processedFiles: number;
failedFiles: number;
processingTime: number;
}
export class ProjectBuilder {
private workflowManager: WorkflowManager | null = null;
private cireConfig: CireConfig;
constructor(config: CireConfig) {
this.cireConfig = config;
}
Build the entire project
async buildProject(): Promise<BuildStats> {
const startTime = Date.now();
try {
// 1. Load configuration
const config = this.cireConfig;
if (config.logLevel === "info") {
console.log(`📁 Loaded config for: `);
console.log(`📂 Input root: `);
console.log(`📤 Output directory: `);
}
// 2. Scan source code files
const sourceFiles = await this.scanSourceFiles(config);
if (config.logLevel === "info") {
console.log(`🔍 Found source files`);
}
// 3. Create output directory
const outputDir = config.output.directory;
await this.ensureOutputDirectory(outputDir);
// 4. Initialize WorkflowManager
this.workflowManager = this.createWorkflowManager(config);
// 5. Batch process files
const stats = await this.processFiles(
sourceFiles,
outputDir,
config,
);
// 6. Copy asset files
await this.copyAssets(outputDir);
// 7. Generate navigation index page
await this.generateNavigationIndex(sourceFiles, outputDir, config);
const processingTime = Date.now() - startTime;
return {
...stats,
processingTime,
};
} catch (error) {
throw new Error(`Project build failed: `);
}
}
Scan source code files
private async scanSourceFiles(config: CireConfig): Promise<string[]> {
const { root, include, exclude } = config.input;
const sourceFiles: string[] = [];
// Resolve absolute path
const absoluteRoot = path.resolve(root);
// Find files for each include pattern
for (const pattern of include) {
const fullPattern = path.join(absoluteRoot, pattern);
if (this.cireConfig.description) {
console.log(`🔍 Scanning pattern: `);
}
const files = await glob(fullPattern, {
ignore: exclude?.map((exclude) =>
path.join(absoluteRoot, exclude),
),
absolute: true,
});
sourceFiles.push(...files);
}
// Remove duplicates and sort
return [...new Set(sourceFiles)].sort();
}
Ensure output directory exists
private async ensureOutputDirectory(outputDir: string): Promise<void> {
const absoluteOutputDir = path.resolve(outputDir);
if (!fs.existsSync(absoluteOutputDir)) {
fs.mkdirSync(absoluteOutputDir, { recursive: true });
console.log(`📁 Created output directory: `);
}
}
Create WorkflowManager
private createWorkflowManager(config: CireConfig): WorkflowManager {
const workflowConfig: WorkflowConfig = {
syntaxHighlighting: true,
hoverDocumentation: !!config.lsp?.indexPath,
commentToMarkdown: true,
scipIndexPath: config.lsp?.indexPath,
language: config.input.language,
};
return new WorkflowManager(workflowConfig);
}
Batch process files
private async processFiles(
sourceFiles: string[],
outputDir: string,
config: CireConfig,
verbose: boolean = false,
): Promise<{
processedFiles: number;
failedFiles: number;
totalFiles: number;
}> {
let processedFiles = 0;
let failedFiles = 0;
console.log(`🚀 Processing files...`);
for (const sourceFile of sourceFiles) {
try {
await this.processSingleFile(sourceFile, outputDir, config);
processedFiles++;
if (verbose) {
console.log(
`✅ Processed: `,
);
} else if (
processedFiles % 10 === 0 ||
processedFiles === sourceFiles.length
) {
// Show progress every 10 files or when complete
const progress = Math.round(
(processedFiles / sourceFiles.length) * 100,
);
process.stdout.write(
`\r⏳ Progress: % (/)`,
);
}
} catch (error) {
failedFiles++;
console.error(`\n❌ Failed to process :`, error);
}
}
// Clear progress line
if (!verbose) {
process.stdout.write(`\r\r`);
}
console.log(
`\n✨ Processing complete: success, failed`,
);
return {
processedFiles,
failedFiles,
totalFiles: sourceFiles.length,
};
}
Process a single file
private async processSingleFile(
sourceFile: string,
outputDir: string,
config: CireConfig,
): Promise<void> {
if (!this.workflowManager) {
throw new Error("WorkflowManager not initialized");
}
// Calculate relative path and create FileIR
const projectRoot = config.input.root;
const relativePath = path.relative(projectRoot, sourceFile);
const fileIR: FileIR = {
relativePath,
language: config.input.language,
};
// Process file through WorkflowManager
const html = this.workflowManager.processFile(fileIR, projectRoot);
const outputPath = path.resolve(outputDir, relativePath);
const htmlOutputPath = outputPath.replace(/\.[^.]+$/, ".html");
// Ensure output file directory exists
const outputDirPath = path.dirname(htmlOutputPath);
if (!fs.existsSync(outputDirPath)) {
fs.mkdirSync(outputDirPath, { recursive: true });
}
// Write HTML file
fs.writeFileSync(htmlOutputPath, html, "utf-8");
}
Copy asset files (CSS, images, etc.)
private async copyAssets(
outputDir: string,
verbose: boolean = false,
): Promise<void> {
const templateDir = path.resolve(__dirname, "../../templates");
const cssSourcePath = path.join(templateDir, "default.css");
const cssTargetPath = path.resolve(outputDir, "default.css");
if (fs.existsSync(cssSourcePath)) {
fs.copyFileSync(cssSourcePath, cssTargetPath);
if (verbose) {
console.log(`📋 Copied CSS to: `);
}
} else {
console.warn(`! CSS file not found: `);
}
}
Generate navigation index page
private async generateNavigationIndex(
sourceFiles: string[],
outputDir: string,
config: CireConfig,
): Promise<void> {
try {
console.log("📋 Generating navigation index page...");
// Convert absolute paths to relative paths for navigation display
const relativeFiles = sourceFiles.map((file) =>
path.relative(config.input.root, file),
);
const navigationGenerator = new NavigationGenerator();
const navigationHTML = navigationGenerator.generateNavigationPage(
relativeFiles,
config,
);
await navigationGenerator.saveNavigationPage(
navigationHTML,
outputDir,
);
console.log("✅ Navigation index page generated successfully");
} catch (error) {
console.warn("⚠️ Failed to generate navigation index:", error);
}
}
}
ProjectBuilder - Handles project-level batch building. Responsible for scanning source files, batch processing, and creating output directory structure.