Skip to main content

jugar_probar/web/
js_builder.rs

1//! Minimal JavaScript Generation (Zero-JavaScript Policy)
2//!
3//! Generates minimal JavaScript for WASM loading ONLY.
4//! Enforces strict limit: under 20 lines of JavaScript.
5
6use crate::result::{ProbarError, ProbarResult};
7use serde::{Deserialize, Serialize};
8
9/// Maximum allowed lines of JavaScript
10pub const MAX_JS_LINES: usize = 20;
11
12/// Generated JavaScript output
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct GeneratedJs {
15    /// JavaScript content
16    pub content: String,
17    /// Number of lines
18    pub line_count: usize,
19    /// Functions defined
20    pub functions: Vec<String>,
21}
22
23impl GeneratedJs {
24    /// Check if JS is within line limit
25    #[must_use]
26    pub fn within_limit(&self) -> bool {
27        self.line_count <= MAX_JS_LINES
28    }
29}
30
31/// WASM module configuration
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct WasmConfig {
34    /// Path to WASM file
35    pub path: String,
36    /// Initial memory pages (64KB each)
37    pub memory_initial: u32,
38    /// Maximum memory pages
39    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,  // 16 MB
47            memory_maximum: 1024, // 64 MB
48        }
49    }
50}
51
52/// Minimal JavaScript builder for WASM loading
53#[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    /// Create a new JS builder
64    #[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    /// Set memory configuration
76    #[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    /// Set entry point function name
84    #[must_use]
85    pub fn entry_point(mut self, name: &str) -> Self {
86        self.entry_point = name.to_string();
87        self
88    }
89
90    /// Build the minimal JavaScript loader
91    ///
92    /// # Errors
93    ///
94    /// Returns error if generated JS exceeds line limit
95    pub fn build(self) -> ProbarResult<GeneratedJs> {
96        // Generate minimal WASM loader
97        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        // Enforce line limit
115        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/// Extended JS builder for additional minimal functionality
130#[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    /// Create from base builder
141    #[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    /// Enable error handling
151    #[must_use]
152    pub fn with_error_handler(mut self) -> Self {
153        self.error_handler = true;
154        self
155    }
156
157    /// Enable loading indicator
158    #[must_use]
159    pub fn with_loading_indicator(mut self) -> Self {
160        self.loading_indicator = true;
161        self
162    }
163
164    /// Set memory configuration
165    #[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    /// Build the JavaScript with optional features
172    ///
173    /// # Errors
174    ///
175    /// Returns error if generated JS exceeds line limit
176    pub fn build(self) -> ProbarResult<GeneratedJs> {
177        let mut lines = Vec::new();
178
179        lines.push("(async()=>{".to_string());
180
181        // Loading indicator
182        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        // Error handler wrapper
196        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        // Hide loading indicator
206        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        // Error handler catch
213        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        // Enforce line limit
223        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    // =========================================================================
248    // H₀-JS-01: JsBuilder creation
249    // =========================================================================
250
251    #[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    // =========================================================================
272    // H₀-JS-04: Generated JavaScript validation
273    // =========================================================================
274
275    #[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    // =========================================================================
320    // H₀-JS-09: Line limit enforcement
321    // =========================================================================
322
323    #[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    // =========================================================================
352    // H₀-JS-11: ExtendedJsBuilder
353    // =========================================================================
354
355    #[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    // =========================================================================
403    // H₀-JS-15: WasmConfig
404    // =========================================================================
405
406    #[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    // =========================================================================
415    // H₀-JS-16: Generated structure
416    // =========================================================================
417
418    #[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}