1use std::path::Path;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::{Arc, mpsc};
4use std::time::Instant;
5
6use anyhow::{Context, Result};
7use colored::Colorize;
8use notify::RecursiveMode;
9use notify_debouncer_mini::{DebouncedEventKind, new_debouncer};
10
11use crate::commands::write_file;
12use crate::config::RpcConfig;
13use crate::{codegen, parser};
14
15#[cfg(not(tarpaulin_include))]
20pub fn run(config: &RpcConfig) -> Result<()> {
21 let running = Arc::new(AtomicBool::new(true));
22 let running_clone = running.clone();
23
24 ctrlc::set_handler(move || {
25 running_clone.store(false, Ordering::SeqCst);
26 })
27 .context("Failed to set Ctrl+C handler")?;
28
29 if config.watch.clear_screen {
30 clear_screen();
31 }
32 print_banner(config);
33
34 if let Err(e) = generate(config) {
36 print_error(&e);
37 }
38
39 let (tx, rx) = mpsc::channel();
41 let debounce_duration = std::time::Duration::from_millis(config.watch.debounce_ms);
42
43 let mut debouncer =
44 new_debouncer(debounce_duration, tx).context("Failed to create file watcher")?;
45
46 debouncer
47 .watcher()
48 .watch(config.input.dir.as_ref(), RecursiveMode::Recursive)
49 .with_context(|| format!("Failed to watch {}", config.input.dir.display()))?;
50
51 println!(
52 " {} for changes in {}\n",
53 "Watching".cyan().bold(),
54 config.input.dir.display().to_string().underline(),
55 );
56
57 while running.load(Ordering::SeqCst) {
58 match rx.recv_timeout(std::time::Duration::from_millis(100)) {
59 Ok(Ok(events)) => {
60 let has_rs_change = events.iter().any(|e| {
61 e.kind == DebouncedEventKind::Any
62 && e.path.extension().is_some_and(|ext| ext == "rs")
63 });
64
65 if has_rs_change {
66 let changed: Vec<&Path> = events
67 .iter()
68 .filter(|e| e.path.extension().is_some_and(|ext| ext == "rs"))
69 .map(|e| e.path.as_path())
70 .collect();
71
72 if config.watch.clear_screen {
73 clear_screen();
74 print_banner(config);
75 }
76
77 print_change(&changed);
78
79 if let Err(e) = generate(config) {
80 print_error(&e);
81 }
82 }
83 }
84 Ok(Err(errs)) => {
85 eprintln!(" {} Watch error: {errs}", "✗".red().bold());
86 }
87 Err(mpsc::RecvTimeoutError::Timeout) => continue,
88 Err(mpsc::RecvTimeoutError::Disconnected) => break,
89 }
90 }
91
92 println!("\n {} Stopped watching.", "●".dimmed());
93 Ok(())
94}
95
96#[cfg(not(tarpaulin_include))]
98fn generate(config: &RpcConfig) -> Result<()> {
99 let start = Instant::now();
100
101 let manifest = parser::scan_directory(&config.input)?;
102
103 let types_content = codegen::typescript::generate_types_file(
104 &manifest,
105 config.codegen.preserve_docs,
106 config.codegen.naming.fields,
107 );
108 write_file(&config.output.types, &types_content)?;
109
110 let client_content = codegen::client::generate_client_file(
111 &manifest,
112 &config.output.imports.types_specifier(),
113 config.codegen.preserve_docs,
114 );
115 write_file(&config.output.client, &client_content)?;
116
117 let elapsed = start.elapsed();
118 let proc_count = manifest.procedures.len();
119 let struct_count = manifest.structs.len();
120
121 println!(
122 " {} Generated {} procedure(s), {} struct(s) in {:.0?}",
123 "✓".green().bold(),
124 proc_count.to_string().bold(),
125 struct_count.to_string().bold(),
126 elapsed,
127 );
128 println!(
129 " {} {}",
130 "→".dimmed(),
131 config.output.types.display().to_string().dimmed(),
132 );
133 println!(
134 " {} {}",
135 "→".dimmed(),
136 config.output.client.display().to_string().dimmed(),
137 );
138
139 Ok(())
140}
141
142#[cfg(not(tarpaulin_include))]
143fn print_banner(config: &RpcConfig) {
144 println!();
145 println!(" {} {}", "vercel-rpc".bold(), "watch mode".cyan(),);
146 println!(" {} {}", "api dir:".dimmed(), config.input.dir.display(),);
147 println!(" {} {}", "types:".dimmed(), config.output.types.display(),);
148 println!(
149 " {} {}",
150 "client:".dimmed(),
151 config.output.client.display(),
152 );
153 println!();
154}
155
156#[cfg(not(tarpaulin_include))]
157fn print_change(paths: &[&Path]) {
158 for p in paths {
159 let name = p
160 .file_name()
161 .map(|n| n.to_string_lossy())
162 .unwrap_or_default();
163 println!("\n {} {}", "↻".yellow().bold(), name);
164 }
165}
166
167#[cfg(not(tarpaulin_include))]
168fn clear_screen() {
169 print!("\x1B[2J\x1B[H");
170}
171
172#[cfg(not(tarpaulin_include))]
173fn print_error(err: &anyhow::Error) {
174 eprintln!(" {} {err:#}", "✗".red().bold());
175 for cause in err.chain().skip(1) {
176 eprintln!(" {} {cause}", "caused by:".dimmed());
177 }
178}