baobao_codegen_typescript/
code_file.rs1use std::any::Any;
7
8use baobao_codegen::builder::{CodeBuilder, CodeFragment, Indent, Renderable};
9
10use crate::ast::{Export, Import};
11
12#[derive(Debug, Clone)]
16pub struct Shebang(String);
17
18impl Shebang {
19 pub fn new(shebang: impl Into<String>) -> Self {
21 Self(shebang.into())
22 }
23
24 pub fn bun() -> Self {
26 Self::new("#!/usr/bin/env bun")
27 }
28}
29
30impl Renderable for Shebang {
31 fn to_fragments(&self) -> Vec<CodeFragment> {
32 vec![CodeFragment::Line(self.0.clone())]
33 }
34}
35
36#[derive(Default)]
52pub struct CodeFile {
53 shebang: Option<Shebang>,
54 imports: Vec<Import>,
55 body: Vec<Vec<CodeFragment>>,
56 exports: Vec<Export>,
57}
58
59impl CodeFile {
60 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn shebang(mut self, shebang: Shebang) -> Self {
67 self.shebang = Some(shebang);
68 self
69 }
70
71 pub fn import(mut self, import: Import) -> Self {
73 self.imports.push(import);
74 self
75 }
76
77 pub fn imports(mut self, imports: impl IntoIterator<Item = Import>) -> Self {
79 self.imports.extend(imports);
80 self
81 }
82
83 #[allow(clippy::should_implement_trait)]
87 pub fn add<R: Renderable + Any>(mut self, node: R) -> Self {
88 if let Some(shebang) = (&node as &dyn Any).downcast_ref::<Shebang>() {
89 self.shebang = Some(shebang.clone());
90 } else {
91 self.body.push(node.to_fragments());
92 }
93 self
94 }
95
96 pub fn add_all<R: Renderable>(mut self, nodes: impl IntoIterator<Item = R>) -> Self {
98 for node in nodes {
99 self.body.push(node.to_fragments());
100 }
101 self
102 }
103
104 pub fn export(mut self, export: Export) -> Self {
106 self.exports.push(export);
107 self
108 }
109
110 pub fn exports(mut self, exports: impl IntoIterator<Item = Export>) -> Self {
112 self.exports.extend(exports);
113 self
114 }
115
116 pub fn render(&self) -> String {
118 self.render_with_indent(Indent::TYPESCRIPT)
119 }
120
121 pub fn render_with_indent(&self, indent: Indent) -> String {
123 let mut builder = CodeBuilder::new(indent);
124
125 if let Some(shebang) = &self.shebang {
127 builder.emit(shebang);
128 }
129
130 let has_content =
132 !self.imports.is_empty() || !self.body.is_empty() || !self.exports.is_empty();
133 if self.shebang.is_some() && has_content {
134 builder.push_blank();
135 }
136
137 for import in &self.imports {
139 builder.emit(import);
140 }
141
142 if !self.imports.is_empty() && (!self.body.is_empty() || !self.exports.is_empty()) {
144 builder.push_blank();
145 }
146
147 for (i, fragments) in self.body.iter().enumerate() {
149 if i > 0 {
150 builder.push_blank();
151 }
152 for fragment in fragments {
153 builder.apply_fragment(fragment.clone());
154 }
155 }
156
157 if !self.body.is_empty() && !self.exports.is_empty() {
159 builder.push_blank();
160 }
161
162 for export in &self.exports {
164 builder.emit(export);
165 }
166
167 builder.build()
168 }
169
170 pub fn is_empty(&self) -> bool {
172 self.imports.is_empty() && self.body.is_empty() && self.exports.is_empty()
173 }
174}
175
176#[derive(Debug, Clone)]
180pub struct RawCode(String);
181
182impl RawCode {
183 pub fn new(code: impl Into<String>) -> Self {
185 Self(code.into())
186 }
187
188 pub fn lines(lines: impl IntoIterator<Item = impl Into<String>>) -> Self {
190 Self(
191 lines
192 .into_iter()
193 .map(Into::into)
194 .collect::<Vec<_>>()
195 .join("\n"),
196 )
197 }
198}
199
200impl Renderable for RawCode {
201 fn to_fragments(&self) -> Vec<CodeFragment> {
202 self.0
203 .lines()
204 .map(|line| CodeFragment::Line(line.to_string()))
205 .collect()
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_empty_file() {
215 let file = CodeFile::new();
216 assert!(file.is_empty());
217 assert_eq!(file.render(), "");
218 }
219
220 #[test]
221 fn test_imports_only() {
222 let file = CodeFile::new().import(Import::new("boune").named("defineCommand"));
223 let code = file.render();
224 assert!(code.contains("import { defineCommand } from \"boune\";"));
225 }
226
227 #[test]
228 fn test_raw_code_body() {
229 let file = CodeFile::new().add(RawCode::new("const x = 1;"));
230 let code = file.render();
231 assert_eq!(code, "const x = 1;\n");
232 }
233
234 #[test]
235 fn test_full_file() {
236 let file = CodeFile::new()
237 .import(Import::new("boune").named("defineCommand"))
238 .add(RawCode::new("const cmd = defineCommand({});"))
239 .export(Export::new().named("cmd"));
240
241 let code = file.render();
242 assert!(code.contains("import { defineCommand }"));
243 assert!(code.contains("const cmd = defineCommand"));
244 assert!(code.contains("export { cmd }"));
245 }
246
247 #[test]
248 fn test_blank_lines_between_body() {
249 let file = CodeFile::new()
250 .add(RawCode::new("const a = 1;"))
251 .add(RawCode::new("const b = 2;"));
252
253 let code = file.render();
254 assert!(code.contains("const a = 1;\n\nconst b = 2;"));
255 }
256
257 #[test]
258 fn test_shebang_at_top() {
259 let file = CodeFile::new()
260 .add(Shebang::bun())
261 .import(Import::new("./cli.ts").named("app"))
262 .add(RawCode::new("app.run();"));
263
264 let code = file.render();
265 assert!(code.starts_with("#!/usr/bin/env bun\n"));
266 assert!(code.contains("#!/usr/bin/env bun\n\nimport { app }"));
267 }
268
269 #[test]
270 fn test_shebang_only() {
271 let file = CodeFile::new().shebang(Shebang::bun());
272 let code = file.render();
273 assert_eq!(code, "#!/usr/bin/env bun\n");
274 }
275}