1use kotoba_core::prelude::*;
6use kotoba_core::types::Value;
7use crate::frontend::component_ir::ExecutionEnvironment;
8use crate::frontend::component_ir::*;
9use crate::frontend::route_ir::*;
10use crate::frontend::render_ir::*;
11use crate::frontend::build_ir::*;
12use crate::frontend::api_ir::*;
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17pub struct WebFramework {
19 route_table: Arc<RwLock<RouteTableIR>>,
20 component_registry: Arc<RwLock<ComponentRegistry>>,
21 renderer: ComponentRenderer,
22 config: WebFrameworkConfigIR,
23 current_route: Arc<RwLock<Option<RouteIR>>>,
24}
25
26
27impl WebFramework {
28 pub fn new(config: WebFrameworkConfigIR) -> Result<Self> {
29 let renderer = ComponentRenderer::new();
30
31 Ok(Self {
32 route_table: Arc::new(RwLock::new(RouteTableIR::new())),
33 component_registry: Arc::new(RwLock::new(ComponentRegistry::new())),
34 renderer,
35 config,
36 current_route: Arc::new(RwLock::new(None)),
37 })
38 }
39
40 pub async fn handle_request(&self, path: &str) -> Result<String> {
42 let table = self.route_table.read().await;
44 if let Some((route, params)) = table.find_route(path) {
45 let render_result = self.render_route_with_params(&route, params).await?;
47 let response = self.create_page_response(render_result)?;
48 return Ok(response);
49 }
50
51 let html = r#"<!DOCTYPE html>
53<html>
54<head><title>404 Not Found</title></head>
55<body><h1>404 Not Found</h1></body>
56</html>"#;
57 Ok(html.to_string())
58 }
59
60
61
62 fn create_page_response(&self, render_result: RenderResultIR) -> Result<String> {
64 Ok(render_result.html)
65 }
66
67 pub async fn add_route(&self, route: RouteIR) -> Result<()> {
69 let mut table = self.route_table.write().await;
70 table.add_route(route);
71 Ok(())
72 }
73
74 pub async fn register_component(&self, component: ComponentIR) -> Result<()> {
76 let mut registry = self.component_registry.write().await;
77 registry.register(component);
78 Ok(())
79 }
80
81 pub async fn navigate(&self, path: &str) -> Result<RenderResultIR> {
83 let table = self.route_table.read().await;
84
85 if let Some((route, params)) = table.find_route(path) {
86 self.render_route_with_params(&route, params).await
87 } else {
88 Err(KotobaError::NotFound(format!("Route not found: {}", path)))
89 }
90 }
91
92 async fn render_route_with_params(&self, route: &RouteIR, params: HashMap<String, String>) -> Result<RenderResultIR> {
94 let mut global_props = HashMap::new();
96 for (key, value) in params {
97 global_props.insert(key, Value::String(value));
98 }
99
100 let context = RenderContext {
101 environment: ExecutionEnvironment::Universal,
102 route_params: global_props,
103 query_params: HashMap::new(),
104 global_state: HashMap::new(),
105 is_server_side: true,
106 is_client_side: false,
107 hydration_id: Some(format!("route_{}", uuid::Uuid::new_v4())),
108 };
109
110 let mut current_route = self.current_route.write().await;
112 *current_route = Some(route.clone());
113
114 self.render_route(route, context).await
116 }
117
118 async fn render_route(&self, route: &RouteIR, context: RenderContext) -> Result<RenderResultIR> {
120 let layout_tree = self.build_layout_tree(route).await?;
122
123 self.renderer.render_component_tree(&layout_tree, context).await
125 }
126
127 async fn build_layout_tree(&self, route: &RouteIR) -> Result<ComponentTreeIR> {
129 let mut root_component = if let Some(layout) = &route.components.layout {
130 layout.clone()
131 } else {
132 ComponentIR::new("RootLayout".to_string(), ComponentType::Layout)
134 };
135
136 if let Some(page) = &route.components.page {
138 root_component.add_child(page.clone());
139 }
140
141 if let Some(loading) = &route.components.loading {
143 let mut loading_component = loading.clone();
144 loading_component.add_child(root_component);
145 Ok(ComponentTreeIR::new(loading_component))
146 } else {
147 Ok(ComponentTreeIR::new(root_component))
148 }
149 }
150
151 pub async fn get_current_route(&self) -> Option<RouteIR> {
153 self.current_route.read().await.clone()
154 }
155
156 pub async fn get_route_table(&self) -> RouteTableIR {
158 self.route_table.read().await.clone()
159 }
160
161 pub fn get_config(&self) -> &WebFrameworkConfigIR {
163 &self.config
164 }
165}
166
167pub struct ComponentRegistry {
169 components: HashMap<String, ComponentIR>,
170}
171
172impl ComponentRegistry {
173 pub fn new() -> Self {
174 Self {
175 components: HashMap::new(),
176 }
177 }
178
179 pub fn register(&mut self, component: ComponentIR) {
180 self.components.insert(component.id.clone(), component);
181 }
182
183 pub fn get(&self, id: &str) -> Option<&ComponentIR> {
184 self.components.get(id)
185 }
186
187 pub fn get_by_name(&self, name: &str) -> Option<&ComponentIR> {
188 self.components.values().find(|c| c.name == name)
189 }
190}
191
192pub struct ComponentRenderer {
194 render_engine: RenderEngineIR,
195 component_cache: Arc<RwLock<HashMap<String, RenderResultIR>>>,
196}
197
198impl ComponentRenderer {
199 pub fn new() -> Self {
200 Self {
201 render_engine: RenderEngineIR {
202 strategies: vec![RenderStrategy::SSR],
203 optimizers: vec![RenderOptimizer::TreeShaking],
204 cache_config: RenderCacheConfig {
205 enable_cache: true,
206 cache_strategy: crate::frontend::render_ir::CacheStrategy::LRU,
207 max_cache_size: 100,
208 ttl_seconds: 300,
209 },
210 },
211 component_cache: Arc::new(RwLock::new(HashMap::new())),
212 }
213 }
214
215 pub async fn render_component_tree(
217 &self,
218 tree: &ComponentTreeIR,
219 context: RenderContext,
220 ) -> Result<RenderResultIR> {
221 let cache_key = format!("tree_{}_{}", tree.root.id, context.hydration_id.clone().unwrap_or_default());
222
223 if self.render_engine.cache_config.enable_cache {
225 if let Some(cached) = self.component_cache.read().await.get(&cache_key) {
226 return Ok(cached.clone());
227 }
228 }
229
230 let virtual_dom = self.build_virtual_dom(&tree.root, &context)?;
232
233 let html = self.generate_html(&virtual_dom, &context)?;
235 let hydration_script = self.generate_hydration_script(&tree.root, &context).await?;
236
237 let result = RenderResultIR {
238 html,
239 css: String::new(), js: String::new(), hydration_script: Some(hydration_script),
242 head_elements: Vec::new(), virtual_dom,
244 render_stats: RenderStats {
245 render_time_ms: 0, component_count: self.count_components(&tree.root),
247 dom_node_count: 0, memory_usage_kb: 0, },
250 };
251
252 if self.render_engine.cache_config.enable_cache {
254 self.component_cache.write().await.insert(cache_key, result.clone());
255 }
256
257 Ok(result)
258 }
259
260 fn build_virtual_dom(
262 &self,
263 component: &ComponentIR,
264 context: &RenderContext,
265 ) -> Result<VirtualNodeIR> {
266 match component.component_type {
267 ComponentType::Server | ComponentType::Client => {
268 let mut element = VirtualNodeIR::element("div".to_string());
270 if let VirtualNodeIR::Element(ref mut el) = element {
271 for (key, value) in &component.props {
273 el.add_attribute(key.clone(), value.clone());
274 }
275
276 for child in &component.children {
278 let child_dom = self.build_virtual_dom(child, context)?;
279 match child_dom {
280 VirtualNodeIR::Element(child_el) => {
281 el.add_child(ElementChild::Element(child_el));
282 },
283 VirtualNodeIR::Text(text) => {
284 el.add_child(ElementChild::Text(text));
285 },
286 VirtualNodeIR::Component(comp) => {
287 el.add_child(ElementChild::Component(comp));
288 },
289 VirtualNodeIR::Fragment(_) => {
290 },
292 }
293 }
294 }
295 Ok(element)
296 },
297 ComponentType::Layout => {
298 let mut layout = VirtualNodeIR::element("div".to_string());
300 if let VirtualNodeIR::Element(ref mut el) = layout {
301 el.add_attribute("data-layout".to_string(), Value::String(component.name.clone()));
302
303 for child in &component.children {
304 let child_dom = self.build_virtual_dom(child, context)?;
305 match child_dom {
306 VirtualNodeIR::Element(child_el) => {
307 el.add_child(ElementChild::Element(child_el));
308 },
309 VirtualNodeIR::Text(text) => {
310 el.add_child(ElementChild::Text(text));
311 },
312 VirtualNodeIR::Component(comp) => {
313 el.add_child(ElementChild::Component(comp));
314 },
315 VirtualNodeIR::Fragment(_) => {
316 },
318 }
319 }
320 }
321 Ok(layout)
322 },
323 ComponentType::Page => {
324 let mut page = VirtualNodeIR::element("main".to_string());
326 if let VirtualNodeIR::Element(ref mut el) = page {
327 el.add_attribute("data-page".to_string(), Value::String(component.name.clone()));
328
329 let content = format!("Content of {}", component.name);
331 el.add_child(ElementChild::Text(content));
332 }
333 Ok(page)
334 },
335 _ => {
336 Ok(VirtualNodeIR::text(format!("Component: {}", component.name)))
338 }
339 }
340 }
341
342 fn generate_html(&self, virtual_dom: &VirtualNodeIR, context: &RenderContext) -> Result<String> {
344 match virtual_dom {
345 VirtualNodeIR::Element(element) => {
346 let mut html = format!("<{}", element.tag_name);
347
348 for (key, value) in &element.attributes {
350 if let Value::String(val) = value {
351 html.push_str(&format!(" {}=\"{}\"", key, val));
352 }
353 }
354
355 if context.is_server_side && context.hydration_id.is_some() {
356 html.push_str(&format!(" data-hydrate=\"{}\"", context.hydration_id.as_ref().unwrap()));
357 }
358
359 html.push_str(">");
360
361 for child in &element.children {
363 match child {
364 ElementChild::Text(text) => html.push_str(text),
365 ElementChild::Element(child_element) => {
366 let child_html = self.generate_html(&VirtualNodeIR::Element(child_element.clone()), context)?;
367 html.push_str(&child_html);
368 },
369 ElementChild::Component(_) => {
370 html.push_str("<!-- Component -->");
371 },
372 ElementChild::Expression(_) => {
373 html.push_str("<!-- Expression -->");
374 },
375 }
376 }
377
378 html.push_str(&format!("</{}>", element.tag_name));
379 Ok(html)
380 },
381 VirtualNodeIR::Text(text) => Ok(text.clone()),
382 VirtualNodeIR::Component(_) => Ok("<!-- Component -->".to_string()),
383 VirtualNodeIR::Fragment(children) => {
384 let mut html = String::new();
385 for child in children {
386 html.push_str(&self.generate_html(child, context)?);
387 }
388 Ok(html)
389 },
390 }
391 }
392
393 async fn generate_hydration_script(&self, component: &ComponentIR, context: &RenderContext) -> Result<String> {
395 let hydration_id = context.hydration_id.as_ref()
396 .ok_or_else(|| KotobaError::InvalidArgument("Hydration ID required".to_string()))?;
397
398 let script = format!(
399 r#"
400// Kotoba Hydration Script
401window.Kotoba = window.Kotoba || {{}};
402window.Kotoba.hydrate('{hydration_id}', {{
403 component: '{component_name}',
404 props: {props},
405 route: {route_params}
406}});
407"#,
408 hydration_id = hydration_id,
409 component_name = component.name,
410 props = "{}",
411 route_params = "{}"
412 );
413
414 Ok(script)
415 }
416
417 fn count_components(&self, component: &ComponentIR) -> usize {
419 1 + component.children.iter().map(|child| self.count_components(child)).sum::<usize>()
420 }
421}
422
423pub struct BuildEngine {
425 config: BuildConfigIR,
426 route_table: Arc<RwLock<RouteTableIR>>,
427}
428
429impl BuildEngine {
430 pub fn new(config: BuildConfigIR) -> Self {
431 Self {
432 config,
433 route_table: Arc::new(RwLock::new(RouteTableIR::new())),
434 }
435 }
436
437 pub async fn build(&self) -> Result<BundleResultIR> {
439 println!("🚀 Starting Kotoba frontend build...");
440
441 let mut chunks = Vec::new();
443 let mut assets = Vec::new();
444
445 for entry in &self.config.entry_points {
446 let chunk = self.process_entry_point(entry).await?;
447 chunks.push(chunk);
448 }
449
450 for optimization in &self.config.optimizations {
452 self.apply_optimization(optimization, &mut chunks, &mut assets).await?;
453 }
454
455 let chunk_count = chunks.len();
457 let module_count = chunks.iter().map(|c| c.modules.len()).sum();
458 let asset_count = assets.len();
459
460 let result = BundleResultIR {
461 chunks: chunks.clone(),
462 assets: assets.clone(),
463 stats: BuildStats {
464 build_time_ms: 1000, total_size: 1024000, gzip_size: 256000, brotli_size: 200000, chunk_count,
469 module_count,
470 asset_count,
471 warnings: Vec::new(),
472 errors: Vec::new(),
473 },
474 manifest: BundleManifest {
475 entries: HashMap::new(), chunks: HashMap::new(), modules: HashMap::new(), },
479 };
480
481 println!("✅ Build completed successfully!");
482 println!("📊 Chunks: {}, Assets: {}, Size: {} KB",
483 result.stats.chunk_count,
484 result.stats.asset_count,
485 result.stats.total_size / 1024);
486
487 Ok(result)
488 }
489
490 async fn process_entry_point(&self, entry: &EntryPoint) -> Result<ChunkIR> {
492 let chunk_id = format!("chunk_{}", uuid::Uuid::new_v4());
493
494 Ok(ChunkIR {
495 id: chunk_id.clone(),
496 name: Some(entry.name.clone()),
497 entry: true,
498 initial: true,
499 files: vec![format!("{}.js", entry.name)],
500 hash: ContentHash::sha256([1; 32]),
501 size: 102400, modules: vec![
503 ModuleIR {
504 id: entry.name.clone(),
505 name: entry.path.clone(),
506 size: 102400,
507 dependencies: Vec::new(), is_entry: true,
509 chunks: vec![chunk_id.clone()],
510 }
511 ],
512 })
513 }
514
515 async fn apply_optimization(
517 &self,
518 optimization: &OptimizationIR,
519 chunks: &mut Vec<ChunkIR>,
520 assets: &mut Vec<AssetIR>,
521 ) -> Result<()> {
522 match optimization {
523 OptimizationIR::CodeSplitting { .. } => {
524 println!("📦 Applying code splitting...");
526 },
527 OptimizationIR::Minification { .. } => {
528 println!("🔧 Applying minification...");
530 for chunk in chunks.iter_mut() {
531 chunk.size = (chunk.size as f64 * 0.7) as usize; }
533 },
534 OptimizationIR::Compression { algorithm, .. } => {
535 match algorithm {
537 CompressionAlgorithm::Gzip => {
538 println!("🗜️ Applying gzip compression...");
539 let compressed_asset = AssetIR {
540 name: "app.js.gz".to_string(),
541 path: "dist/app.js.gz".to_string(),
542 size: 256000, content_type: "application/gzip".to_string(),
544 hash: ContentHash::sha256([2; 32]),
545 };
546 assets.push(compressed_asset);
547 },
548 _ => {},
549 }
550 },
551 _ => {},
552 }
553 Ok(())
554 }
555
556 pub async fn start_dev_server(&self, port: u16) -> Result<()> {
558 println!("🚀 Starting Kotoba development server on port {}", port);
559
560 println!("🔥 Hot reload enabled");
562 println!("📁 Watching for file changes...");
563
564 Ok(())
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use crate::frontend::component_ir::ComponentType;
572 use crate::frontend::api_ir::*;
573
574 #[tokio::test]
575 async fn test_web_framework_creation() {
576 let config = WebFrameworkConfigIR {
577 server: crate::frontend::api_ir::ServerConfig {
578 host: "localhost".to_string(),
579 port: 3000,
580 tls: None,
581 workers: 4,
582 max_connections: 1000,
583 },
584 database: None,
585 api_routes: Vec::new(),
586 web_sockets: Vec::new(),
587 graph_ql: None,
588 middlewares: Vec::new(),
589 static_files: Vec::new(),
590 authentication: None,
591 session: None,
592 };
593
594 let framework = WebFramework::new(config).unwrap();
595 assert_eq!(framework.get_config().server.port, 3000);
596 }
597
598
599 #[tokio::test]
600 async fn test_component_rendering() {
601 let renderer = ComponentRenderer::new();
602
603 let component = ComponentIR::new("TestComponent".to_string(), ComponentType::Server);
605 let tree = ComponentTreeIR::new(component);
606
607 let context = RenderContext::server_side();
608 let result = renderer.render_component_tree(&tree, context).await.unwrap();
609
610 assert!(!result.html.is_empty());
611 assert_eq!(result.render_stats.component_count, 1);
612 }
613
614 #[test]
615 fn test_build_engine_creation() {
616 let config = BuildConfigIR::new(BundlerType::Vite);
617 let engine = BuildEngine::new(config);
618
619 assert_eq!(engine.config.bundler, BundlerType::Vite);
621 }
622
623}