Skip to main content

vercel_rpc_cli/
commands.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result};
5
6use crate::config::RpcConfig;
7use crate::model::Manifest;
8use crate::{codegen, parser};
9
10/// Scans the configured directory and prints discovered RPC procedures, structs,
11/// and enums to stdout, followed by a JSON manifest.
12pub fn cmd_scan(config: &RpcConfig) -> Result<()> {
13    let manifest = parser::scan_directory(&config.input)?;
14
15    println!(
16        "Discovered {} procedure(s), {} struct(s), {} enum(s):\n",
17        manifest.procedures.len(),
18        manifest.structs.len(),
19        manifest.enums.len(),
20    );
21
22    for proc in &manifest.procedures {
23        let input_str = proc
24            .input
25            .as_ref()
26            .map(|t| t.to_string())
27            .unwrap_or_else(|| "()".to_string());
28        let output_str = proc
29            .output
30            .as_ref()
31            .map(|t| t.to_string())
32            .unwrap_or_else(|| "()".to_string());
33
34        println!(
35            "  {:?} {} ({}) -> {}  [{}]",
36            proc.kind,
37            proc.name,
38            input_str,
39            output_str,
40            proc.source_file.display(),
41        );
42    }
43
44    for s in &manifest.structs {
45        println!("\n  struct {} {{", s.name);
46        for field in &s.fields {
47            println!("    {}: {},", field.name, field.ty);
48        }
49        println!("  }}");
50    }
51
52    for e in &manifest.enums {
53        let variants: Vec<&str> = e.variants.iter().map(|v| v.name.as_str()).collect();
54        println!("\n  enum {} {{ {} }}", e.name, variants.join(", "));
55    }
56
57    // Also output raw JSON for tooling consumption
58    println!("\n--- JSON manifest ---");
59    println!("{}", serde_json::to_string_pretty(&manifest)?);
60
61    Ok(())
62}
63
64/// Generates TypeScript type definitions and a typed RPC client from the
65/// configured directory, writing the results to the configured output paths.
66pub fn cmd_generate(config: &RpcConfig) -> Result<()> {
67    let manifest = generate_all(config)?;
68
69    println!(
70        "Generated {} procedure(s), {} struct(s), {} enum(s)",
71        manifest.procedures.len(),
72        manifest.structs.len(),
73        manifest.enums.len(),
74    );
75    println!("  → {}", config.output.types.display());
76    println!("  → {}", config.output.client.display());
77    if let Some(path) = &config.output.svelte {
78        println!("  → {}", path.display());
79    }
80    if let Some(path) = &config.output.react {
81        println!("  → {}", path.display());
82    }
83    if let Some(path) = &config.output.vue {
84        println!("  → {}", path.display());
85    }
86    if let Some(path) = &config.output.solid {
87        println!("  → {}", path.display());
88    }
89
90    Ok(())
91}
92
93/// Scans the source directory and generates all configured TypeScript output files.
94///
95/// Returns the manifest so callers can use it for logging/reporting.
96pub fn generate_all(config: &RpcConfig) -> Result<Manifest> {
97    let manifest = parser::scan_directory(&config.input)?;
98
99    let types_content = codegen::typescript::generate_types_file(
100        &manifest,
101        config.codegen.preserve_docs,
102        config.codegen.naming.fields,
103    );
104    write_file(&config.output.types, &types_content)?;
105
106    let client_content = codegen::client::generate_client_file(
107        &manifest,
108        &config.output.imports.types_specifier(),
109        config.codegen.preserve_docs,
110    );
111    write_file(&config.output.client, &client_content)?;
112
113    write_framework_files(config, &manifest)?;
114
115    Ok(manifest)
116}
117
118/// Computes the client import specifier from config (e.g. `"./rpc-client"`).
119fn client_import_path(config: &RpcConfig) -> String {
120    let client_stem = config
121        .output
122        .client
123        .file_stem()
124        .unwrap_or_default()
125        .to_string_lossy();
126    format!("./{client_stem}{}", config.output.imports.extension)
127}
128
129/// A framework codegen entry: optional output path paired with its generator function.
130type FrameworkEntry<'a> = (
131    Option<&'a PathBuf>,
132    fn(&Manifest, &str, &str, bool) -> String,
133);
134
135/// Generates and writes all optional framework wrapper files (Svelte, React, Vue, Solid).
136fn write_framework_files(config: &RpcConfig, manifest: &Manifest) -> Result<()> {
137    let client_import = client_import_path(config);
138    let types_specifier = config.output.imports.types_specifier();
139
140    let frameworks: [FrameworkEntry<'_>; 4] = [
141        (
142            config.output.svelte.as_ref(),
143            codegen::svelte::generate_svelte_file,
144        ),
145        (
146            config.output.react.as_ref(),
147            codegen::react::generate_react_file,
148        ),
149        (config.output.vue.as_ref(), codegen::vue::generate_vue_file),
150        (
151            config.output.solid.as_ref(),
152            codegen::solid::generate_solid_file,
153        ),
154    ];
155
156    for (path_opt, generator) in &frameworks {
157        if let Some(path) = path_opt {
158            let content = generator(
159                manifest,
160                &client_import,
161                &types_specifier,
162                config.codegen.preserve_docs,
163            );
164            if !content.is_empty() {
165                write_file(path, &content)?;
166            }
167        }
168    }
169
170    Ok(())
171}
172
173/// Writes content to a file, creating parent directories as needed.
174pub fn write_file(path: &Path, content: &str) -> Result<()> {
175    if let Some(parent) = path.parent() {
176        fs::create_dir_all(parent)
177            .with_context(|| format!("Failed to create directory {}", parent.display()))?;
178    }
179    fs::write(path, content).with_context(|| format!("Failed to write {}", path.display()))?;
180    Ok(())
181}
182
183/// Formats byte count in a human-readable way.
184pub fn bytecount(s: &str) -> String {
185    let bytes = s.len();
186    if bytes < 1024 {
187        format!("{bytes} bytes")
188    } else {
189        format!("{:.1} KB", bytes as f64 / 1024.0)
190    }
191}