Skip to main content

fraiseql_cli/commands/
serve.rs

1//! Development server command with hot-reload
2//!
3//! Watches schema.json for changes and auto-recompiles
4
5use std::{path::Path, sync::mpsc::channel, time::Duration};
6
7use anyhow::{Context, Result};
8use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
9use tracing::{error, info};
10
11/// Run the serve command (development server with hot-reload)
12///
13/// # Arguments
14///
15/// * `schema` - Path to schema.json file to watch
16/// * `port` - Port to listen on (for future GraphQL server integration)
17///
18/// # Behavior
19///
20/// 1. Compiles the initial schema
21/// 2. Watches schema.json for file changes
22/// 3. Auto-recompiles on save
23/// 4. Provides compilation feedback
24///
25/// # Future Enhancement
26///
27/// Will integrate with fraiseql-server to provide hot-reload of the GraphQL endpoint
28pub async fn run(schema: &str, port: u16) -> Result<()> {
29    info!("Starting development server");
30    println!("🚀 FraiseQL Dev Server");
31    println!("   Schema: {schema}");
32    println!("   Port:   {port} (GraphQL server integration coming soon)");
33    println!("   Watching for changes...\n");
34
35    // Verify schema file exists
36    let schema_path = Path::new(schema);
37    if !schema_path.exists() {
38        anyhow::bail!("Schema file not found: {schema}");
39    }
40
41    // Compile initial schema
42    println!("📦 Initial compilation:");
43    match compile_schema(schema).await {
44        Ok(()) => println!("   ✓ Schema compiled successfully\n"),
45        Err(e) => {
46            error!("Initial compilation failed: {e}");
47            println!("   ❌ Compilation failed: {e}\n");
48            println!("   Fix errors and save to retry...\n");
49        },
50    }
51
52    // Set up file watcher
53    let (tx, rx) = channel();
54
55    let mut watcher = RecommendedWatcher::new(
56        move |res: Result<Event, notify::Error>| {
57            if let Ok(event) = res {
58                let _ = tx.send(event);
59            }
60        },
61        Config::default(),
62    )
63    .context("Failed to create file watcher")?;
64
65    // Watch the schema file
66    watcher
67        .watch(schema_path, RecursiveMode::NonRecursive)
68        .context("Failed to watch schema file")?;
69
70    // Watch for file changes
71    loop {
72        match rx.recv() {
73            Ok(event) => {
74                // Only recompile on write/modify events
75                if matches!(event.kind, EventKind::Modify(_)) {
76                    info!("Schema file modified, recompiling...");
77                    println!("🔄 Schema changed, recompiling...");
78
79                    // Small delay to ensure file write is complete
80                    tokio::time::sleep(Duration::from_millis(100)).await;
81
82                    match compile_schema(schema).await {
83                        Ok(()) => {
84                            info!("Recompilation successful");
85                            println!("   ✓ Recompiled successfully\n");
86                        },
87                        Err(e) => {
88                            error!("Recompilation failed: {e}");
89                            println!("   ❌ Compilation failed: {e}\n");
90                        },
91                    }
92                }
93            },
94            Err(e) => {
95                error!("Watch error: {e}");
96                anyhow::bail!("File watch error: {e}");
97            },
98        }
99    }
100}
101
102/// Compile schema (used by file watcher)
103async fn compile_schema(input: &str) -> Result<()> {
104    let output = input.replace(".json", ".compiled.json");
105
106    // Use the compile command logic (no database validation for dev server)
107    super::compile::run(input, None, None, Vec::new(), Vec::new(), Vec::new(), &output, false, None)
108        .await
109}