1use crate::result::{ProbarError, ProbarResult};
7use serde::{Deserialize, Serialize};
8
9pub const MAX_JS_LINES: usize = 20;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GeneratedJs {
15 pub content: String,
17 pub line_count: usize,
19 pub functions: Vec<String>,
21}
22
23impl GeneratedJs {
24 #[must_use]
26 pub fn within_limit(&self) -> bool {
27 self.line_count <= MAX_JS_LINES
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct WasmConfig {
34 pub path: String,
36 pub memory_initial: u32,
38 pub memory_maximum: u32,
40}
41
42impl Default for WasmConfig {
43 fn default() -> Self {
44 Self {
45 path: "app.wasm".to_string(),
46 memory_initial: 256, memory_maximum: 1024, }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct JsBuilder {
55 wasm_path: String,
56 canvas_id: String,
57 memory_initial: u32,
58 memory_maximum: u32,
59 entry_point: String,
60}
61
62impl JsBuilder {
63 #[must_use]
65 pub fn new(wasm_path: &str, canvas_id: &str) -> Self {
66 Self {
67 wasm_path: wasm_path.to_string(),
68 canvas_id: canvas_id.to_string(),
69 memory_initial: 256,
70 memory_maximum: 1024,
71 entry_point: "main".to_string(),
72 }
73 }
74
75 #[must_use]
77 pub fn memory(mut self, initial: u32, maximum: u32) -> Self {
78 self.memory_initial = initial;
79 self.memory_maximum = maximum;
80 self
81 }
82
83 #[must_use]
85 pub fn entry_point(mut self, name: &str) -> Self {
86 self.entry_point = name.to_string();
87 self
88 }
89
90 pub fn build(self) -> ProbarResult<GeneratedJs> {
96 let content = format!(
98 r#"(async()=>{{
99const c=document.getElementById('{canvas_id}');
100const m=new WebAssembly.Memory({{initial:{mem_init},maximum:{mem_max}}});
101const i={{env:{{memory:m,canvas:c}}}};
102const{{instance:w}}=await WebAssembly.instantiateStreaming(fetch('{wasm_path}'),i);
103w.exports.{entry}();
104}})();"#,
105 canvas_id = self.canvas_id,
106 wasm_path = self.wasm_path,
107 mem_init = self.memory_initial,
108 mem_max = self.memory_maximum,
109 entry = self.entry_point,
110 );
111
112 let line_count = content.lines().count();
113
114 if line_count > MAX_JS_LINES {
116 return Err(ProbarError::JsGeneration(format!(
117 "Generated JavaScript exceeds {MAX_JS_LINES} line limit: {line_count} lines"
118 )));
119 }
120
121 Ok(GeneratedJs {
122 content,
123 line_count,
124 functions: vec!["main".to_string()],
125 })
126 }
127}
128
129#[allow(dead_code)]
131#[derive(Debug, Clone)]
132pub struct ExtendedJsBuilder {
133 base: JsBuilder,
134 error_handler: bool,
135 loading_indicator: bool,
136}
137
138#[allow(dead_code)]
139impl ExtendedJsBuilder {
140 #[must_use]
142 pub fn new(wasm_path: &str, canvas_id: &str) -> Self {
143 Self {
144 base: JsBuilder::new(wasm_path, canvas_id),
145 error_handler: false,
146 loading_indicator: false,
147 }
148 }
149
150 #[must_use]
152 pub fn with_error_handler(mut self) -> Self {
153 self.error_handler = true;
154 self
155 }
156
157 #[must_use]
159 pub fn with_loading_indicator(mut self) -> Self {
160 self.loading_indicator = true;
161 self
162 }
163
164 #[must_use]
166 pub fn memory(mut self, initial: u32, maximum: u32) -> Self {
167 self.base = self.base.memory(initial, maximum);
168 self
169 }
170
171 pub fn build(self) -> ProbarResult<GeneratedJs> {
177 let mut lines = Vec::new();
178
179 lines.push("(async()=>{".to_string());
180
181 if self.loading_indicator {
183 lines.push("const l=document.querySelector('.loading');".to_string());
184 }
185
186 lines.push(format!(
187 "const c=document.getElementById('{}');",
188 self.base.canvas_id
189 ));
190 lines.push(format!(
191 "const m=new WebAssembly.Memory({{initial:{},maximum:{}}});",
192 self.base.memory_initial, self.base.memory_maximum
193 ));
194
195 if self.error_handler {
197 lines.push("try{".to_string());
198 }
199
200 lines.push(format!(
201 "const{{instance:w}}=await WebAssembly.instantiateStreaming(fetch('{}'),{{env:{{memory:m,canvas:c}}}});",
202 self.base.wasm_path
203 ));
204
205 if self.loading_indicator {
207 lines.push("if(l)l.style.display='none';".to_string());
208 }
209
210 lines.push(format!("w.exports.{}();", self.base.entry_point));
211
212 if self.error_handler {
214 lines.push("}catch(e){console.error('WASM Error:',e);}".to_string());
215 }
216
217 lines.push("})();".to_string());
218
219 let content = lines.join("\n");
220 let line_count = lines.len();
221
222 if line_count > MAX_JS_LINES {
224 return Err(ProbarError::JsGeneration(format!(
225 "Generated JavaScript exceeds {MAX_JS_LINES} line limit: {line_count} lines"
226 )));
227 }
228
229 let mut functions = vec!["main".to_string()];
230 if self.error_handler {
231 functions.push("error_handler".to_string());
232 }
233
234 Ok(GeneratedJs {
235 content,
236 line_count,
237 functions,
238 })
239 }
240}
241
242#[cfg(test)]
243#[allow(clippy::unwrap_used, clippy::expect_used)]
244mod tests {
245 use super::*;
246
247 #[test]
252 fn h0_js_01_builder_new() {
253 let builder = JsBuilder::new("app.wasm", "canvas");
254 assert_eq!(builder.wasm_path, "app.wasm");
255 assert_eq!(builder.canvas_id, "canvas");
256 }
257
258 #[test]
259 fn h0_js_02_builder_memory() {
260 let builder = JsBuilder::new("app.wasm", "c").memory(512, 2048);
261 assert_eq!(builder.memory_initial, 512);
262 assert_eq!(builder.memory_maximum, 2048);
263 }
264
265 #[test]
266 fn h0_js_03_builder_entry_point() {
267 let builder = JsBuilder::new("app.wasm", "c").entry_point("start");
268 assert_eq!(builder.entry_point, "start");
269 }
270
271 #[test]
276 fn h0_js_04_build_success() {
277 let js = JsBuilder::new("test.wasm", "canvas").build().unwrap();
278
279 assert!(js.within_limit());
280 assert!(js.line_count <= MAX_JS_LINES);
281 }
282
283 #[test]
284 fn h0_js_05_contains_wasm_loading() {
285 let js = JsBuilder::new("game.wasm", "game").build().unwrap();
286
287 assert!(js.content.contains("WebAssembly.instantiateStreaming"));
288 assert!(js.content.contains("fetch('game.wasm')"));
289 }
290
291 #[test]
292 fn h0_js_06_contains_canvas_reference() {
293 let js = JsBuilder::new("app.wasm", "myCanvas").build().unwrap();
294
295 assert!(js.content.contains("getElementById('myCanvas')"));
296 }
297
298 #[test]
299 fn h0_js_07_contains_memory_config() {
300 let js = JsBuilder::new("app.wasm", "c")
301 .memory(128, 512)
302 .build()
303 .unwrap();
304
305 assert!(js.content.contains("initial:128"));
306 assert!(js.content.contains("maximum:512"));
307 }
308
309 #[test]
310 fn h0_js_08_contains_entry_point() {
311 let js = JsBuilder::new("app.wasm", "c")
312 .entry_point("init")
313 .build()
314 .unwrap();
315
316 assert!(js.content.contains(".init()"));
317 }
318
319 #[test]
324 fn h0_js_09_under_20_lines() {
325 let js = JsBuilder::new("app.wasm", "canvas").build().unwrap();
326
327 assert!(
328 js.line_count <= 20,
329 "JS must be under 20 lines, got {}",
330 js.line_count
331 );
332 }
333
334 #[test]
335 fn h0_js_10_within_limit_check() {
336 let js = GeneratedJs {
337 content: "test".to_string(),
338 line_count: 10,
339 functions: vec![],
340 };
341 assert!(js.within_limit());
342
343 let over_limit = GeneratedJs {
344 content: "test".to_string(),
345 line_count: 25,
346 functions: vec![],
347 };
348 assert!(!over_limit.within_limit());
349 }
350
351 #[test]
356 fn h0_js_11_extended_builder() {
357 let js = ExtendedJsBuilder::new("app.wasm", "canvas")
358 .build()
359 .unwrap();
360
361 assert!(js.within_limit());
362 }
363
364 #[test]
365 fn h0_js_12_extended_with_error_handler() {
366 let js = ExtendedJsBuilder::new("app.wasm", "canvas")
367 .with_error_handler()
368 .build()
369 .unwrap();
370
371 assert!(js.content.contains("try{"));
372 assert!(js.content.contains("catch(e)"));
373 assert!(js.within_limit());
374 }
375
376 #[test]
377 fn h0_js_13_extended_with_loading() {
378 let js = ExtendedJsBuilder::new("app.wasm", "canvas")
379 .with_loading_indicator()
380 .build()
381 .unwrap();
382
383 assert!(js.content.contains(".loading"));
384 assert!(js.content.contains("display='none'"));
385 assert!(js.within_limit());
386 }
387
388 #[test]
389 fn h0_js_14_extended_all_features() {
390 let js = ExtendedJsBuilder::new("app.wasm", "canvas")
391 .with_error_handler()
392 .with_loading_indicator()
393 .memory(256, 1024)
394 .build()
395 .unwrap();
396
397 assert!(js.content.contains("try{"));
398 assert!(js.content.contains(".loading"));
399 assert!(js.within_limit(), "Must stay under {} lines", MAX_JS_LINES);
400 }
401
402 #[test]
407 fn h0_js_15_wasm_config_default() {
408 let config = WasmConfig::default();
409 assert_eq!(config.path, "app.wasm");
410 assert_eq!(config.memory_initial, 256);
411 assert_eq!(config.memory_maximum, 1024);
412 }
413
414 #[test]
419 fn h0_js_16_generated_js_fields() {
420 let js = JsBuilder::new("test.wasm", "c").build().unwrap();
421
422 assert!(!js.content.is_empty());
423 assert!(js.line_count > 0);
424 assert!(!js.functions.is_empty());
425 }
426}