/** * Build Command - Core Build Process * * Compiles grammar.js into C static library and JavaScript package */ import { Command } from 'commander'; import { execa } from 'execa'; import chalk from 'chalk'; import { existsSync, mkdirSync, writeFileSync, copyFileSync, readFileSync, cpSync } from 'fs'; import { join, dirname, basename } from 'path'; import { fileURLToPath } from 'url'; /** * Create the build command */ export function createBuildCommand(): Command { const buildCommand = new Command('build'); buildCommand .description('Build the DSL parser and packages') .option('-v, --verbose', 'Show detailed build output') .option('--skip-c', 'Skip C library generation') .option('--skip-js', 'Skip JavaScript package generation') .action(async (options) => { console.log(chalk.blue('🏗️ Building DSL parser and packages...')); console.log(); try { await runBuildProcess(options); console.log(); console.log(chalk.green('✅ Build completed successfully!')); console.log(); console.log(chalk.blue('Generated artifacts:')); console.log(` ${chalk.gray('•')} C library: generated/c/lib/`); console.log(` ${chalk.gray('•')} C headers: generated/c/include/`); console.log(` ${chalk.gray('•')} JS package: generated/js/`); } catch (error) { console.error(chalk.red('❌ Build failed:'), error instanceof Error ? error.message : error); process.exit(1); } }); return buildCommand; } /** * Run the complete build process */ async function runBuildProcess(options: { verbose?: boolean; skipC?: boolean; skipJs?: boolean }): Promise { // Verify we're in a DSL project directory if (!existsSync('grammar.js')) { throw new Error('No grammar.js found. Are you in a DSL project directory?'); } // Ensure CommonJS semantics locally so tree-sitter can load grammar.js (uses module.exports) ensureCommonJsPackageJson(); // Step 1: Generate parser with Tree-sitter console.log(chalk.blue('1️⃣ Generating parser with Tree-sitter...')); await generateParser(options.verbose); console.log(chalk.green(' ✅ Parser generated')); // Step 2: Build C library (unless skipped) if (!options.skipC) { console.log(chalk.blue('2️⃣ Building C static library...')); await buildCLibrary(options.verbose); console.log(chalk.green(' ✅ C library built')); } else { console.log(chalk.yellow(' ⏭️ Skipping C library build')); } // Step 3: Build JavaScript package (unless skipped) if (!options.skipJs) { console.log(chalk.blue('3️⃣ Building JavaScript package...')); await buildJavaScriptPackage(options.verbose); console.log(chalk.green(' ✅ JavaScript package built')); } else { console.log(chalk.yellow(' ⏭️ Skipping JavaScript package build')); } } /** * Step 1: Generate parser using tree-sitter generate */ async function generateParser(verbose?: boolean): Promise { try { // Check if tree-sitter CLI is available await checkTreeSitterAvailable(); // Run tree-sitter generate const result = await execa('tree-sitter', ['generate'], { stdio: verbose ? 'inherit' : 'pipe' }); if (!verbose && result.stdout) { console.log(chalk.gray(` ${result.stdout.split('\n').slice(-2, -1)[0] || 'Generated successfully'}`)); } } catch (error: any) { if (error.command) { throw new Error(`Tree-sitter generation failed: ${error.message}`); } throw error; } } /** * Step 2: Build C static library */ async function buildCLibrary(verbose?: boolean): Promise { // Create output directories const libDir = 'generated/c/lib'; const includeDir = 'generated/c/include'; mkdirSync(libDir, { recursive: true }); mkdirSync(includeDir, { recursive: true }); // Get project name from grammar.js or directory const projectName = getProjectName(); // Compile parser.c to object file const objectFile = join(libDir, `${projectName}.o`); const libraryFile = join(libDir, `lib${projectName}.a`); try { // Detect C compiler const compiler = await detectCCompiler(); console.log(chalk.gray(` Using compiler: ${compiler}`)); // Compile to object file const compileArgs = [ '-c', // Compile only, don't link '-fPIC', // Position independent code '-O2', // Optimize 'src/parser.c', // Input file '-o', objectFile // Output file ]; await execa(compiler, compileArgs, { stdio: verbose ? 'inherit' : 'pipe' }); // Create static library with ar const arArgs = [ 'rcs', // Create archive, insert files, write symbol table libraryFile, // Output library objectFile // Input object file ]; await execa('ar', arArgs, { stdio: verbose ? 'inherit' : 'pipe' }); // Generate header file await generateHeaderFile(projectName, includeDir); console.log(chalk.gray(` Library: ${libraryFile}`)); console.log(chalk.gray(` Header: ${join(includeDir, projectName + '.h')}`)); } catch (error: any) { throw new Error(`C library build failed: ${error.message}`); } } /** * Step 3: Build JavaScript package */ async function buildJavaScriptPackage(verbose?: boolean): Promise { const jsDir = 'generated/js'; // Create JS package directory mkdirSync(jsDir, { recursive: true }); // Copy JS addon templates await copyJSAddonTemplates(jsDir); // Copy src directory from tree-sitter generation await copySourceFiles(jsDir); // Update binding.gyp based on available files await updateBindingGyp(jsDir); // Update package.json with project name await updateJSPackageJson(jsDir); // Detect runtime and install dependencies const runtime = await detectRuntime(); console.log(chalk.gray(` Using runtime: ${runtime}`)); try { // Install dependencies and build native addon if (runtime === 'bun') { await execa('bun', ['install'], { cwd: jsDir, stdio: verbose ? 'inherit' : 'pipe' }); } else { await execa('npm', ['install'], { cwd: jsDir, stdio: verbose ? 'inherit' : 'pipe' }); } console.log(chalk.gray(` Package: ${jsDir}/`)); } catch (error: any) { throw new Error(`JavaScript package build failed: ${error.message}`); } } /** * Check if tree-sitter CLI is available */ async function checkTreeSitterAvailable(): Promise { try { await execa('tree-sitter', ['--version'], { stdio: 'pipe' }); } catch (error) { throw new Error( 'tree-sitter CLI not found. Please install it:\n' + ' npm install -g tree-sitter-cli\n' + ' # or\n' + ' brew install tree-sitter' ); } } /** * Detect available C compiler */ async function detectCCompiler(): Promise { const compilers = ['clang', 'gcc', 'cc']; for (const compiler of compilers) { try { await execa(compiler, ['--version'], { stdio: 'pipe' }); return compiler; } catch { // Try next compiler } } throw new Error( 'No C compiler found. Please install one:\n' + ' macOS: xcode-select --install\n' + ' Linux: sudo apt install build-essential (Ubuntu) or equivalent' ); } /** * Detect runtime (bun vs npm) */ async function detectRuntime(): Promise<'bun' | 'npm'> { try { await execa('bun', ['--version'], { stdio: 'pipe' }); return 'bun'; } catch { return 'npm'; } } /** * Get project name from grammar.js or directory name */ function getProjectName(): string { try { const grammarContent = readFileSync('grammar.js', 'utf-8'); const nameMatch = grammarContent.match(/name:\s*['"]([^'"]+)['"]/); if (nameMatch) { return nameMatch[1]; } } catch { // Fall back to directory name } return basename(process.cwd()); } /** * Ensure the current directory is treated as CommonJS for Node resolution, * so that tree-sitter can load `grammar.js` (which uses module.exports). * If no local package.json exists, create a minimal one with "type": "commonjs". */ function ensureCommonJsPackageJson(): void { const packageJsonPath = 'package.json'; if (!existsSync(packageJsonPath)) { const projectName = basename(process.cwd()); const minimal = { name: `${projectName}-dsl`, private: true, type: 'commonjs', description: `DSL project for ${projectName} generated by DSK`, license: 'MIT' } as const; writeFileSync(packageJsonPath, JSON.stringify(minimal, null, 2)); console.log(chalk.gray(' Created local package.json with { "type": "commonjs" }')); } } /** * Generate C header file */ async function generateHeaderFile(projectName: string, includeDir: string): Promise { const headerContent = `#ifndef TREE_SITTER_${projectName.toUpperCase()}_H_ #define TREE_SITTER_${projectName.toUpperCase()}_H_ typedef struct TSLanguage TSLanguage; #ifdef __cplusplus extern "C" { #endif const TSLanguage *tree_sitter_${projectName}(void); #ifdef __cplusplus } #endif #endif // TREE_SITTER_${projectName.toUpperCase()}_H_ `; writeFileSync(join(includeDir, `${projectName}.h`), headerContent); } /** * Copy JS addon template files */ async function copyJSAddonTemplates(jsDir: string): Promise { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const templateDir = join(__dirname, '..', '..', 'templates', 'js-addon'); if (!existsSync(templateDir)) { throw new Error(`JS addon template directory not found: ${templateDir}`); } // Copy all template files const { processTemplate } = await import('../utils/template-processor.js'); const projectName = getProjectName(); const templateContext = { architecture: { name: projectName, paradigm: 'mixed' as const, purpose: 'DSL', dataPhilosophy: 'mixed' as const }, features: { controlFlow: [], dataStructures: [], functionTypes: [] }, syntax: { comments: { type: 'line_comment', pattern: '//' }, identifiers: { pattern: '[a-zA-Z_][a-zA-Z0-9_]*', examples: ['identifier'] }, numbers: { pattern: '\\d+', examples: ['42'] }, strings: { pattern: '"[^"]*"', examples: ['"string"'] }, variables: { keyword: 'let', operator: '=', terminator: ';', example: 'let x = 42;' }, paradigmExamples: {} } }; processTemplate(templateDir, jsDir, templateContext); } /** * Copy source files from tree-sitter generation to JS package */ async function copySourceFiles(jsDir: string): Promise { const srcDir = 'src'; const targetSrcDir = join(jsDir, 'src'); if (!existsSync(srcDir)) { throw new Error('src/ directory not found. Run tree-sitter generate first.'); } // Copy the entire src directory cpSync(srcDir, targetSrcDir, { recursive: true }); console.log(chalk.gray(` Copied src files to ${targetSrcDir}`)); } /** * Update binding.gyp based on available source files */ async function updateBindingGyp(jsDir: string): Promise { const bindingGypPath = join(jsDir, 'binding.gyp'); const scannerPath = join(jsDir, 'src', 'scanner.c'); if (existsSync(bindingGypPath)) { let bindingGyp = JSON.parse(readFileSync(bindingGypPath, 'utf-8')); // Add scanner.c if it exists if (existsSync(scannerPath)) { const target = bindingGyp.targets[0]; if (!target.sources.includes('src/scanner.c')) { target.sources.push('src/scanner.c'); console.log(chalk.gray(` Added scanner.c to build`)); } } else { console.log(chalk.gray(` No scanner.c found, skipping`)); } writeFileSync(bindingGypPath, JSON.stringify(bindingGyp, null, 2)); } } /** * Update package.json in JS package */ async function updateJSPackageJson(jsDir: string): Promise { const projectName = getProjectName(); const packageJsonPath = join(jsDir, 'package.json'); if (existsSync(packageJsonPath)) { let packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); // Update name and description packageJson.name = `tree-sitter-${projectName}`; packageJson.description = `Tree-sitter parser for ${projectName}`; // Update keywords if (packageJson.keywords && Array.isArray(packageJson.keywords)) { packageJson.keywords = packageJson.keywords.map((keyword: string) => keyword === '__DSL_NAME__' ? projectName : keyword ); } writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); } }