baobao_codegen_typescript/adapters/
boune.rs1use baobao_codegen::{
4 adapters::{CliAdapter, CliInfo, CommandMeta, Dependency, DispatchInfo, ImportSpec},
5 builder::CodeFragment,
6};
7use baobao_core::ArgType;
8use baobao_manifest::ArgType as ManifestArgType;
9
10use crate::{
11 BOUNE_VERSION,
12 ast::{ArrowFn, JsObject},
13};
14
15#[derive(Debug, Clone, Default)]
17pub struct BouneAdapter;
18
19impl BouneAdapter {
20 pub fn new() -> Self {
21 Self
22 }
23
24 fn convert_arg_type(arg_type: &ManifestArgType) -> ArgType {
26 match arg_type {
27 ManifestArgType::String => ArgType::String,
28 ManifestArgType::Int => ArgType::Int,
29 ManifestArgType::Float => ArgType::Float,
30 ManifestArgType::Bool => ArgType::Bool,
31 ManifestArgType::Path => ArgType::Path,
32 }
33 }
34
35 pub fn build_argument_schema_manifest(
37 &self,
38 arg_type: &ManifestArgType,
39 required: bool,
40 default: Option<&toml::Value>,
41 description: Option<&str>,
42 choices: Option<&[String]>,
43 ) -> String {
44 self.build_argument_schema(
45 Self::convert_arg_type(arg_type),
46 required,
47 default,
48 description,
49 choices,
50 )
51 }
52
53 pub fn build_argument_schema(
57 &self,
58 arg_type: ArgType,
59 required: bool,
60 default: Option<&toml::Value>,
61 description: Option<&str>,
62 choices: Option<&[String]>,
63 ) -> String {
64 let boune_type = self.map_arg_type(arg_type);
65 let mut parts = vec![format!("type: \"{}\"", boune_type)];
66
67 if required && default.is_none() {
68 parts.push("required: true".to_string());
69 }
70
71 if let Some(default) = default {
72 parts.push(format!("default: {}", toml_to_ts_literal(default)));
73 }
74
75 if let Some(desc) = description {
76 parts.push(format!("description: \"{}\"", desc));
77 }
78
79 if let Some(choices) = choices {
80 let choices_array = choices
81 .iter()
82 .map(|c| format!("\"{}\"", c))
83 .collect::<Vec<_>>()
84 .join(", ");
85 parts.push(format!("choices: [{}] as const", choices_array));
86 }
87
88 format!("{{ {} }}", parts.join(", "))
89 }
90
91 pub fn build_option_schema_manifest(
93 &self,
94 flag_type: &ManifestArgType,
95 short: Option<char>,
96 default: Option<&toml::Value>,
97 description: Option<&str>,
98 choices: Option<&[String]>,
99 ) -> String {
100 self.build_option_schema(
101 Self::convert_arg_type(flag_type),
102 short,
103 default,
104 description,
105 choices,
106 )
107 }
108
109 pub fn build_option_schema(
113 &self,
114 flag_type: ArgType,
115 short: Option<char>,
116 default: Option<&toml::Value>,
117 description: Option<&str>,
118 choices: Option<&[String]>,
119 ) -> String {
120 let boune_type = self.map_arg_type(flag_type);
121 let mut parts = vec![format!("type: \"{}\"", boune_type)];
122
123 if let Some(short) = short {
124 parts.push(format!("short: \"{}\"", short));
125 }
126
127 if let Some(default) = default {
128 parts.push(format!("default: {}", toml_to_ts_literal(default)));
129 }
130
131 if let Some(desc) = description {
132 parts.push(format!("description: \"{}\"", desc));
133 }
134
135 if let Some(choices) = choices {
136 let choices_array = choices
137 .iter()
138 .map(|c| format!("\"{}\"", c))
139 .collect::<Vec<_>>()
140 .join(", ");
141 parts.push(format!("choices: [{}] as const", choices_array));
142 }
143
144 format!("{{ {} }}", parts.join(", "))
145 }
146
147 pub fn map_manifest_arg_type(&self, arg_type: &ManifestArgType) -> &'static str {
149 self.map_arg_type(Self::convert_arg_type(arg_type))
150 }
151
152 pub fn build_action_handler(&self, has_args: bool, has_options: bool) -> ArrowFn {
154 let params = match (has_args, has_options) {
156 (true, true) => "{ args, options }",
157 (true, false) => "{ args }",
158 (false, true) => "{ options }",
159 (false, false) => "{}",
160 };
161
162 let run_call = match (has_args, has_options) {
164 (true, true) => "await run(args, options);",
165 (true, false) => "await run(args);",
166 (false, true) => "await run(options);",
167 (false, false) => "await run();",
168 };
169
170 ArrowFn::new(params).async_().body_line(run_call)
171 }
172}
173
174impl CliAdapter for BouneAdapter {
175 fn name(&self) -> &'static str {
176 "boune"
177 }
178
179 fn dependencies(&self) -> Vec<Dependency> {
180 vec![Dependency::new("boune", BOUNE_VERSION)]
181 }
182
183 fn generate_cli(&self, info: &CliInfo) -> Vec<CodeFragment> {
184 let mut schema = JsObject::new()
186 .string("name", &info.name)
187 .string("version", info.version.to_string());
188
189 if let Some(desc) = &info.description {
190 schema = schema.string("description", desc);
191 }
192
193 let mut commands_obj = JsObject::new();
195 for cmd in &info.commands {
196 commands_obj =
197 commands_obj.raw(&cmd.pascal_name, format!("{}Command", cmd.pascal_name));
198 }
199 schema = schema.object("commands", commands_obj);
200
201 let code = format!("const app = defineCli({});", schema.build());
203 vec![CodeFragment::raw(code)]
204 }
205
206 fn generate_command(&self, info: &CommandMeta) -> Vec<CodeFragment> {
207 let action = self.build_action_handler(!info.args.is_empty(), !info.flags.is_empty());
209
210 let schema = JsObject::new()
211 .string("name", &info.name)
212 .string("description", &info.description)
213 .raw_if(!info.args.is_empty(), "arguments", "args")
214 .raw_if(!info.flags.is_empty(), "options", "options")
215 .arrow_fn("action", action);
216
217 let code = format!(
218 "export const {}Command = defineCommand({});",
219 info.pascal_name,
220 schema.build()
221 );
222
223 vec![CodeFragment::raw(code)]
224 }
225
226 fn generate_subcommands(&self, info: &CommandMeta) -> Vec<CodeFragment> {
227 let mut subcommands = JsObject::new();
229 for sub in &info.subcommands {
230 subcommands = subcommands.raw(&sub.pascal_name, format!("{}Command", sub.pascal_name));
231 }
232
233 let schema = JsObject::new()
234 .string("name", &info.name)
235 .string("description", &info.description)
236 .object("subcommands", subcommands);
237
238 let code = format!(
239 "export const {}Command = defineCommand({});",
240 info.pascal_name,
241 schema.build()
242 );
243
244 vec![CodeFragment::raw(code)]
245 }
246
247 fn generate_dispatch(&self, _info: &DispatchInfo) -> Vec<CodeFragment> {
248 Vec::new()
251 }
252
253 fn imports(&self) -> Vec<ImportSpec> {
254 vec![ImportSpec::new("boune").symbol("defineCli")]
255 }
256
257 fn command_imports(&self, info: &CommandMeta) -> Vec<ImportSpec> {
258 let mut imports = vec![ImportSpec::new("boune").symbol("defineCommand")];
259
260 if !info.args.is_empty() {
263 imports.push(ImportSpec::new("boune").symbol("InferArgs").type_only());
264 }
265
266 if !info.flags.is_empty() {
267 imports.push(ImportSpec::new("boune").symbol("InferOpts").type_only());
268 }
269
270 imports
271 }
272
273 fn map_arg_type(&self, arg_type: ArgType) -> &'static str {
274 match arg_type {
275 ArgType::String => "string",
276 ArgType::Int => "number",
277 ArgType::Float => "number",
278 ArgType::Bool => "boolean",
279 ArgType::Path => "string",
280 }
281 }
282
283 fn map_optional_type(&self, arg_type: ArgType) -> String {
284 format!("{} | undefined", self.map_arg_type(arg_type))
285 }
286}
287
288fn toml_to_ts_literal(value: &toml::Value) -> String {
291 match value {
292 toml::Value::String(s) => format!("\"{}\"", s),
293 toml::Value::Integer(i) => i.to_string(),
294 toml::Value::Float(f) => f.to_string(),
295 toml::Value::Boolean(b) => b.to_string(),
296 _ => String::new(),
297 }
298}