mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-22 00:40:22 +02:00
feat: Add comprehensive ANTLR build integration and watch capabilities
�� Major ANTLR System Enhancements: ## New Features: - ✅ Generic ANTLR generation system (scripts/antlr-generate.mts) - ✅ Dedicated watch command for grammar development (scripts/antlr-watch.mts) - ✅ Build pipeline integration with postinstall hooks - ✅ Development server watch integration for .g4 files - ✅ Sequence diagram ANTLR parser implementation ## Build Integration: - 🏗️ Added ANTLR generation to build process (.esbuild/build.ts) - 📦 Added postinstall hooks to package.json files - 🔄 Integrated ANTLR generation with dev server (.esbuild/server-antlr.ts) - 🎯 Smart path detection for root vs package directory execution ## New Commands: - `pnpm antlr:generate` - Generic generation for all diagrams - `pnpm antlr:watch` - Grammar development with file watching - Auto-generation during `pnpm install` and `pnpm build` ## Documentation: - 📖 Consolidated all ANTLR docs into ANTLR_SETUP.md - 🗑️ Removed duplicate ANTLR_GENERATION.md - 📋 Added comprehensive troubleshooting and usage guides - 🎯 Updated with build integration and watch functionality ## Parser Implementations: - 🔄 Enhanced sequence diagram ANTLR parser with dual-pattern support - 🛠️ Added SequenceListener, SequenceVisitor, SequenceParserCore - ⚡ Improved flowchart parser integration and error handling ## Benefits: - 🔄 Zero manual steps - ANTLR files always generated automatically - ⚡ Fast grammar development with watch mode - 🎯 Unified workflow for all diagram types - 🛡️ CI/CD ready with build integration - 📊 Clear feedback and comprehensive logging This establishes a complete, production-ready ANTLR development workflow!
This commit is contained in:
220
scripts/antlr-generate.mts
Normal file
220
scripts/antlr-generate.mts
Normal file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
/* cspell:disable */
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
||||
import { join, dirname, basename } from 'path';
|
||||
|
||||
/**
|
||||
* Generic ANTLR generator script that finds all .g4 files and generates parsers
|
||||
* Automatically creates generated folders and runs antlr4ng for each diagram type
|
||||
*/
|
||||
|
||||
interface GrammarInfo {
|
||||
lexerFile: string;
|
||||
parserFile: string;
|
||||
outputDir: string;
|
||||
diagramType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all .g4 files in a directory
|
||||
*/
|
||||
function findG4Files(dir: string): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
if (!existsSync(dir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const entries = readdirSync(dir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...findG4Files(fullPath));
|
||||
} else if (entry.endsWith('.g4')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all ANTLR grammar files in the diagrams directory
|
||||
*/
|
||||
function findGrammarFiles(): GrammarInfo[] {
|
||||
const grammarFiles: GrammarInfo[] = [];
|
||||
|
||||
// Determine the correct path based on current working directory
|
||||
const cwd = process.cwd();
|
||||
let diagramsPath: string;
|
||||
|
||||
if (cwd.endsWith('/packages/mermaid')) {
|
||||
// Running from mermaid package directory
|
||||
diagramsPath = 'src/diagrams';
|
||||
} else {
|
||||
// Running from project root
|
||||
diagramsPath = 'packages/mermaid/src/diagrams';
|
||||
}
|
||||
|
||||
// Find all .g4 files
|
||||
const g4Files = findG4Files(diagramsPath);
|
||||
|
||||
// Group by directory (each diagram should have a Lexer and Parser pair)
|
||||
const grammarDirs = new Map<string, string[]>();
|
||||
|
||||
for (const file of g4Files) {
|
||||
const dir = dirname(file);
|
||||
if (!grammarDirs.has(dir)) {
|
||||
grammarDirs.set(dir, []);
|
||||
}
|
||||
grammarDirs.get(dir)!.push(file);
|
||||
}
|
||||
|
||||
// Process each directory
|
||||
for (const [dir, files] of grammarDirs) {
|
||||
const lexerFile = files.find((f) => f.includes('Lexer.g4'));
|
||||
const parserFile = files.find((f) => f.includes('Parser.g4'));
|
||||
|
||||
if (lexerFile && parserFile) {
|
||||
// Extract diagram type from path
|
||||
const pathParts = dir.split('/');
|
||||
const diagramIndex = pathParts.indexOf('diagrams');
|
||||
const diagramType = diagramIndex >= 0 ? pathParts[diagramIndex + 1] : 'unknown';
|
||||
|
||||
grammarFiles.push({
|
||||
lexerFile,
|
||||
parserFile,
|
||||
outputDir: join(dir, 'generated'),
|
||||
diagramType,
|
||||
});
|
||||
} else {
|
||||
console.warn(`⚠️ Incomplete grammar pair in ${dir}:`);
|
||||
console.warn(` Lexer: ${lexerFile ?? 'MISSING'}`);
|
||||
console.warn(` Parser: ${parserFile ?? 'MISSING'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return grammarFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the generated directory
|
||||
*/
|
||||
function cleanGeneratedDir(outputDir: string): void {
|
||||
try {
|
||||
execSync(`rimraf "${outputDir}"`, { stdio: 'inherit' });
|
||||
console.log(`🧹 Cleaned: ${outputDir}`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Failed to clean ${outputDir}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the generated directory if it doesn't exist
|
||||
*/
|
||||
function ensureGeneratedDir(outputDir: string): void {
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
console.log(`📁 Created: ${outputDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate ANTLR files for a grammar pair
|
||||
*/
|
||||
function generateAntlrFiles(grammar: GrammarInfo): void {
|
||||
const { lexerFile, parserFile, outputDir, diagramType } = grammar;
|
||||
|
||||
console.log(`\n🎯 Generating ANTLR files for ${diagramType} diagram...`);
|
||||
console.log(` Lexer: ${basename(lexerFile)}`);
|
||||
console.log(` Parser: ${basename(parserFile)}`);
|
||||
console.log(` Output: ${outputDir}`);
|
||||
|
||||
try {
|
||||
// Clean and create output directory
|
||||
cleanGeneratedDir(outputDir);
|
||||
ensureGeneratedDir(outputDir);
|
||||
|
||||
// Generate ANTLR files
|
||||
const command = [
|
||||
'antlr-ng',
|
||||
'-Dlanguage=TypeScript',
|
||||
'-l',
|
||||
'-v',
|
||||
`-o "${outputDir}"`,
|
||||
`"${lexerFile}"`,
|
||||
`"${parserFile}"`,
|
||||
].join(' ');
|
||||
|
||||
console.log(` Command: ${command}`);
|
||||
execSync(command, { stdio: 'inherit' });
|
||||
|
||||
console.log(`✅ Successfully generated ANTLR files for ${diagramType}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to generate ANTLR files for ${diagramType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main(): void {
|
||||
console.log('🚀 ANTLR Generator - Finding and generating all grammar files...\n');
|
||||
|
||||
try {
|
||||
// Find all grammar files
|
||||
const grammarFiles = findGrammarFiles();
|
||||
|
||||
if (grammarFiles.length === 0) {
|
||||
console.log('ℹ️ No ANTLR grammar files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📋 Found ${grammarFiles.length} diagram(s) with ANTLR grammars:`);
|
||||
for (const grammar of grammarFiles) {
|
||||
console.log(` • ${grammar.diagramType}`);
|
||||
}
|
||||
|
||||
// Generate files for each grammar
|
||||
let successCount = 0;
|
||||
let failureCount = 0;
|
||||
|
||||
for (const grammar of grammarFiles) {
|
||||
try {
|
||||
generateAntlrFiles(grammar);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
failureCount++;
|
||||
console.error(`Failed to process ${grammar.diagramType}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n📊 Generation Summary:');
|
||||
console.log(` ✅ Successful: ${successCount}`);
|
||||
console.log(` ❌ Failed: ${failureCount}`);
|
||||
console.log(` 📁 Total: ${grammarFiles.length}`);
|
||||
|
||||
if (failureCount > 0) {
|
||||
console.error('\n❌ Some ANTLR generations failed. Check the errors above.');
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n🎉 All ANTLR files generated successfully!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Fatal error during ANTLR generation:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
122
scripts/antlr-watch.mts
Normal file
122
scripts/antlr-watch.mts
Normal file
@@ -0,0 +1,122 @@
|
||||
#!/usr/bin/env tsx
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import chokidar from 'chokidar';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
/**
|
||||
* ANTLR Watch Script
|
||||
*
|
||||
* This script generates ANTLR files and then watches for changes to .g4 grammar files,
|
||||
* automatically regenerating the corresponding parsers when changes are detected.
|
||||
*
|
||||
* Features:
|
||||
* - Initial generation of all ANTLR files
|
||||
* - Watch .g4 files for changes
|
||||
* - Debounced regeneration to avoid multiple builds
|
||||
* - Clear logging and progress reporting
|
||||
* - Graceful shutdown handling
|
||||
*/
|
||||
|
||||
let isGenerating = false;
|
||||
let timeoutID: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Generate ANTLR parser files from grammar files
|
||||
*/
|
||||
function generateAntlr(): void {
|
||||
if (isGenerating) {
|
||||
console.log('⏳ ANTLR generation already in progress, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isGenerating = true;
|
||||
console.log('🎯 ANTLR: Generating parser files...');
|
||||
execSync('tsx scripts/antlr-generate.mts', { stdio: 'inherit' });
|
||||
console.log('✅ ANTLR: Parser files generated successfully\n');
|
||||
} catch (error) {
|
||||
console.error('❌ ANTLR: Failed to generate parser files:', error);
|
||||
} finally {
|
||||
isGenerating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file change events with debouncing
|
||||
*/
|
||||
function handleFileChange(path: string): void {
|
||||
if (timeoutID !== undefined) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
|
||||
console.log(`🎯 Grammar file changed: ${path}`);
|
||||
|
||||
// Debounce file changes to avoid multiple regenerations
|
||||
timeoutID = setTimeout(() => {
|
||||
console.log('🔄 Regenerating ANTLR files...\n');
|
||||
generateAntlr();
|
||||
timeoutID = undefined;
|
||||
}, 500); // 500ms debounce
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup graceful shutdown
|
||||
*/
|
||||
function setupGracefulShutdown(): void {
|
||||
const shutdown = () => {
|
||||
console.log('\n🛑 Shutting down ANTLR watch...');
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
function main(): void {
|
||||
console.log('🚀 ANTLR Watch - Generate and watch grammar files for changes\n');
|
||||
|
||||
// Setup graceful shutdown
|
||||
setupGracefulShutdown();
|
||||
|
||||
// Initial generation
|
||||
generateAntlr();
|
||||
|
||||
// Setup file watcher
|
||||
console.log('👀 Watching for .g4 file changes...');
|
||||
console.log('📁 Pattern: **/src/**/parser/antlr/*.g4');
|
||||
console.log('🛑 Press Ctrl+C to stop watching\n');
|
||||
|
||||
const watcher = chokidar.watch('**/src/**/parser/antlr/*.g4', {
|
||||
ignoreInitial: true,
|
||||
ignored: [/node_modules/, /dist/, /docs/, /coverage/],
|
||||
persistent: true,
|
||||
});
|
||||
|
||||
watcher
|
||||
.on('change', handleFileChange)
|
||||
.on('add', handleFileChange)
|
||||
.on('error', (error) => {
|
||||
console.error('❌ Watcher error:', error);
|
||||
})
|
||||
.on('ready', () => {
|
||||
console.log('✅ Watcher ready - monitoring grammar files for changes...\n');
|
||||
});
|
||||
|
||||
// Keep the process alive
|
||||
process.stdin.resume();
|
||||
}
|
||||
|
||||
// Run the script
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main().catch((error) => {
|
||||
console.error('❌ Fatal error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user