Skip to main content

vercel_rpc_cli/codegen/
common.rs

1//! Shared logic for framework-specific codegen files (React, Svelte, Vue, SolidJS).
2//!
3//! Each framework wrapper follows the same structure: imports, type helpers,
4//! void/non-void key unions, interface definitions, query/mutation overloads,
5//! and implementation blocks. This module extracts all the common patterns
6//! so that individual framework modules only supply their unique constants.
7
8use crate::model::{Manifest, Procedure, ProcedureKind};
9
10/// Header comment included at the top of every generated file.
11pub const GENERATED_HEADER: &str = "\
12// This file is auto-generated by vercel-rpc-cli. Do not edit manually.
13// Re-run `rpc generate` or use `rpc watch` to regenerate.
14";
15
16/// Returns `true` if the procedure takes no input (void).
17pub fn is_void_input(proc: &Procedure) -> bool {
18    proc.input.as_ref().is_none_or(|ty| ty.name == "()")
19}
20
21/// Configuration for generating a framework-specific reactive wrapper file.
22pub struct FrameworkConfig<'a> {
23    /// Framework-specific import line (e.g. `import { useState, ... } from "react";`).
24    /// `None` for Svelte which has no framework import.
25    pub framework_import: Option<&'a str>,
26
27    /// Name of the query function (e.g. `"useQuery"` or `"createQuery"`).
28    pub query_fn_name: &'a str,
29
30    /// Whether non-void query input is a getter `() => QueryInput<K>` (Svelte/Vue/Solid)
31    /// or a direct value `QueryInput<K>` (React).
32    pub input_as_getter: bool,
33
34    /// TypeScript interface constants for queries (options, result).
35    pub query_interfaces: &'a [&'a str],
36
37    /// TypeScript interface constants for mutations (options, result).
38    pub mutation_interfaces: &'a [&'a str],
39
40    /// Query implementation block (TypeScript source).
41    pub query_impl: &'a str,
42
43    /// Mutation implementation block (TypeScript source).
44    pub mutation_impl: &'a str,
45}
46
47/// Generates a complete framework-specific reactive wrapper file.
48///
49/// Returns an empty string when the manifest contains no procedures.
50pub fn generate_framework_file(
51    manifest: &Manifest,
52    client_import_path: &str,
53    types_import_path: &str,
54    _preserve_docs: bool,
55    config: &FrameworkConfig<'_>,
56) -> String {
57    let queries: Vec<_> = manifest
58        .procedures
59        .iter()
60        .filter(|p| p.kind == ProcedureKind::Query)
61        .collect();
62    let mutations: Vec<_> = manifest
63        .procedures
64        .iter()
65        .filter(|p| p.kind == ProcedureKind::Mutation)
66        .collect();
67
68    if queries.is_empty() && mutations.is_empty() {
69        return String::new();
70    }
71
72    let has_queries = !queries.is_empty();
73    let has_mutations = !mutations.is_empty();
74
75    let mut out = String::with_capacity(4096);
76
77    // Header
78    out.push_str(GENERATED_HEADER);
79    out.push('\n');
80
81    // Framework import (if any)
82    if let Some(import) = config.framework_import {
83        emit!(out, "{import}\n");
84    }
85
86    // Client import
87    emit!(
88        out,
89        "import {{ type RpcClient, RpcError, type CallOptions }} from \"{client_import_path}\";\n"
90    );
91
92    // Types import + re-exports
93    let type_names: Vec<&str> = manifest
94        .structs
95        .iter()
96        .map(|s| s.name.as_str())
97        .chain(manifest.enums.iter().map(|e| e.name.as_str()))
98        .collect();
99
100    emit_types_import(&mut out, &type_names, types_import_path);
101    emit_re_exports(&mut out, &type_names);
102
103    // Type helpers
104    emit_type_helpers(&mut out, has_queries, has_mutations);
105    out.push('\n');
106
107    // Void/non-void key unions + MutationArgs
108    emit_key_unions_and_args(&mut out, &queries, &mutations, has_queries, has_mutations);
109    out.push('\n');
110
111    // Interfaces
112    if has_queries {
113        for iface in config.query_interfaces {
114            emit!(out, "{iface}\n");
115        }
116    }
117    if has_mutations {
118        for iface in config.mutation_interfaces {
119            emit!(out, "{iface}\n");
120        }
121    }
122
123    // Query overloads + implementation
124    if has_queries {
125        let void_names: Vec<_> = queries
126            .iter()
127            .filter(|p| is_void_input(p))
128            .map(|p| format!("\"{}\"", p.name))
129            .collect();
130        emit!(
131            out,
132            "const VOID_QUERY_KEYS: Set<QueryKey> = new Set([{}]);\n",
133            void_names.join(", ")
134        );
135        emit_query_overloads(
136            &queries,
137            config.query_fn_name,
138            config.input_as_getter,
139            &mut out,
140        );
141        emit!(out, "{}\n", config.query_impl);
142    }
143
144    // Mutation implementation
145    if has_mutations {
146        emit!(out, "{}\n", config.mutation_impl);
147    }
148
149    out
150}
151
152/// Emits the `import type { Procedures, ... }` line.
153fn emit_types_import(out: &mut String, type_names: &[&str], types_import_path: &str) {
154    if type_names.is_empty() {
155        emit!(
156            out,
157            "import type {{ Procedures }} from \"{types_import_path}\";\n"
158        );
159    } else {
160        let types_csv = type_names.join(", ");
161        emit!(
162            out,
163            "import type {{ Procedures, {types_csv} }} from \"{types_import_path}\";\n"
164        );
165    }
166}
167
168/// Emits re-exports: `export { RpcError }` and `export type { ... }`.
169fn emit_re_exports(out: &mut String, type_names: &[&str]) {
170    emit!(out, "export {{ RpcError }};");
171    if type_names.is_empty() {
172        emit!(
173            out,
174            "export type {{ RpcClient, CallOptions, Procedures }};\n"
175        );
176    } else {
177        let types_csv = type_names.join(", ");
178        emit!(
179            out,
180            "export type {{ RpcClient, CallOptions, Procedures, {types_csv} }};\n"
181        );
182    }
183}
184
185/// Emits QueryKey/MutationKey type aliases and their Input/Output helpers.
186fn emit_type_helpers(out: &mut String, has_queries: bool, has_mutations: bool) {
187    if has_queries {
188        emit!(out, "type QueryKey = keyof Procedures[\"queries\"];");
189        emit!(
190            out,
191            "type QueryInput<K extends QueryKey> = Procedures[\"queries\"][K][\"input\"];"
192        );
193        emit!(
194            out,
195            "type QueryOutput<K extends QueryKey> = Procedures[\"queries\"][K][\"output\"];"
196        );
197    }
198    if has_mutations {
199        emit!(out, "type MutationKey = keyof Procedures[\"mutations\"];");
200        emit!(
201            out,
202            "type MutationInput<K extends MutationKey> = Procedures[\"mutations\"][K][\"input\"];"
203        );
204        emit!(
205            out,
206            "type MutationOutput<K extends MutationKey> = Procedures[\"mutations\"][K][\"output\"];"
207        );
208    }
209}
210
211/// Emits VoidQueryKey/NonVoidQueryKey unions and the MutationArgs conditional type.
212fn emit_key_unions_and_args(
213    out: &mut String,
214    queries: &[&Procedure],
215    mutations: &[&Procedure],
216    has_queries: bool,
217    has_mutations: bool,
218) {
219    if has_queries {
220        let void_queries: Vec<_> = queries.iter().filter(|p| is_void_input(p)).collect();
221        let non_void_queries: Vec<_> = queries.iter().filter(|p| !is_void_input(p)).collect();
222
223        if !void_queries.is_empty() {
224            let names: Vec<_> = void_queries
225                .iter()
226                .map(|p| format!("\"{}\"", p.name))
227                .collect();
228            emit!(out, "type VoidQueryKey = {};", names.join(" | "));
229        }
230        if !non_void_queries.is_empty() {
231            let names: Vec<_> = non_void_queries
232                .iter()
233                .map(|p| format!("\"{}\"", p.name))
234                .collect();
235            emit!(out, "type NonVoidQueryKey = {};", names.join(" | "));
236        }
237    }
238
239    if has_mutations {
240        let void_mutations: Vec<_> = mutations.iter().filter(|p| is_void_input(p)).collect();
241        let non_void_mutations: Vec<_> = mutations.iter().filter(|p| !is_void_input(p)).collect();
242
243        if !void_mutations.is_empty() {
244            let names: Vec<_> = void_mutations
245                .iter()
246                .map(|p| format!("\"{}\"", p.name))
247                .collect();
248            emit!(out, "type VoidMutationKey = {};", names.join(" | "));
249        }
250        if !non_void_mutations.is_empty() {
251            let names: Vec<_> = non_void_mutations
252                .iter()
253                .map(|p| format!("\"{}\"", p.name))
254                .collect();
255            emit!(out, "type NonVoidMutationKey = {};", names.join(" | "));
256        }
257
258        let all_void = non_void_mutations.is_empty();
259        let all_non_void = void_mutations.is_empty();
260        if all_void {
261            emit!(out, "type MutationArgs<K extends MutationKey> = [];");
262        } else if all_non_void {
263            emit!(
264                out,
265                "type MutationArgs<K extends MutationKey> = [input: MutationInput<K>];"
266            );
267        } else {
268            emit!(
269                out,
270                "type MutationArgs<K extends MutationKey> = K extends VoidMutationKey ? [] : [input: MutationInput<K>];"
271            );
272        }
273    }
274}
275
276/// Emits overload signatures for the query function.
277///
278/// `fn_name` is the function name (e.g. `"useQuery"` or `"createQuery"`).
279/// When `input_as_getter` is true, non-void input is `() => QueryInput<K>`.
280fn emit_query_overloads(
281    queries: &[&Procedure],
282    fn_name: &str,
283    input_as_getter: bool,
284    out: &mut String,
285) {
286    let (void_queries, non_void_queries): (Vec<&&Procedure>, Vec<&&Procedure>) =
287        queries.iter().partition(|p| is_void_input(p));
288
289    for proc in &void_queries {
290        emit!(
291            out,
292            "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
293            proc.name,
294        );
295    }
296
297    let input_type = if input_as_getter {
298        "() => QueryInput<K>"
299    } else {
300        "QueryInput<K>"
301    };
302
303    for proc in &non_void_queries {
304        emit!(
305            out,
306            "export function {fn_name}<K extends \"{}\">(client: RpcClient, key: K, input: {input_type}, options?: QueryOptions<K> | (() => QueryOptions<K>)): QueryResult<K>;",
307            proc.name,
308        );
309    }
310}