about summary refs log tree commit diff stats
path: root/tree-sitter/dsk/dsk-cli/src/commands/build.ts
diff options
context:
space:
mode:
Diffstat (limited to 'tree-sitter/dsk/dsk-cli/src/commands/build.ts')
-rw-r--r--tree-sitter/dsk/dsk-cli/src/commands/build.ts429
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));
+  }
+}