diff options
Diffstat (limited to 'tree-sitter/dsk/dsk-cli/src/commands/build.ts')
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/build.ts | 429 |
1 files changed, 429 insertions, 0 deletions
diff --git a/tree-sitter/dsk/dsk-cli/src/commands/build.ts b/tree-sitter/dsk/dsk-cli/src/commands/build.ts new file mode 100644 index 0000000..811788f --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/build.ts @@ -0,0 +1,429 @@ +/** + * 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<void> { + // 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<void> { + 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<void> { + // 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<void> { + 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<void> { + 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<string> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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<void> { + 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)); + } +} |