1#![warn(missing_docs)]
2
3use nargo_ir::{IRModule, JsExpr, JsStmt, TemplateNodeIR};
4use nargo_types::{CompileMode, Result};
5
6pub mod targets;
7use rayon::prelude::*;
8use std::{
9 collections::{HashMap, HashSet},
10 path::PathBuf,
11 sync::{Arc, Mutex},
12 thread,
13 time::SystemTime,
14};
15use targets::{js::JsBackend, CssBackend, DtsBackend, HtmlBackend};
16use ws::{listen, Handler, Handshake, Message, Result as WsResult};
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub struct FileFingerprint {
21 pub modified_time: SystemTime,
23 pub content_hash: u64,
25}
26
27#[derive(Debug, Clone)]
29pub struct ModuleCache {
30 pub fingerprint: FileFingerprint,
32 pub compiled_code: String,
34 pub dependencies: HashSet<String>,
36 pub features: FeatureSet,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum SplitStrategy {
43 SingleFile,
45 ByRoute,
47 ByComponent,
49 Custom,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum OutputFormat {
56 JavaScript,
58 CSS,
60 HTML,
62 TypeScript,
64 WebAssembly,
66}
67
68#[derive(Debug, Clone)]
70pub struct BuildOutput {
71 pub code: Vec<u8>,
73 pub format: OutputFormat,
75 pub filename: String,
77}
78
79#[derive(Debug, Clone)]
81pub struct BuildOutputs {
82 pub outputs: Vec<BuildOutput>,
84 pub entry_file: String,
86}
87
88#[derive(Debug, Clone, Default)]
90pub struct FeatureSet {
91 pub has_signals: bool,
93 pub has_effects: bool,
95 pub has_vdom: bool,
97 pub has_ssr: bool,
99 pub used_core_functions: HashSet<String>,
101 pub used_dom_functions: HashSet<String>,
103}
104
105impl FeatureSet {
106 pub fn merge(&mut self, other: &FeatureSet) {
108 self.has_signals |= other.has_signals;
109 self.has_effects |= other.has_effects;
110 self.has_vdom |= other.has_vdom;
111 self.has_ssr |= other.has_ssr;
112 self.used_core_functions.extend(other.used_core_functions.iter().cloned());
113 self.used_dom_functions.extend(other.used_dom_functions.iter().cloned());
114 }
115}
116
117struct HmrHandler {
119 connections: Arc<Mutex<Vec<ws::Sender>>>,
121}
122
123impl Handler for HmrHandler {
124 fn on_open(&mut self, out: Handshake) -> WsResult<()> {
125 println!("HMR: Client connected");
126 Ok(())
129 }
130
131 fn on_message(&mut self, msg: Message) -> WsResult<()> {
132 println!("HMR: Received message: {:?}", msg);
133 Ok(())
134 }
135
136 fn on_close(&mut self, _code: ws::CloseCode, _reason: &str) {
137 println!("HMR: Client disconnected");
138 }
140}
141
142pub struct Bundler {
144 pub feature_set: FeatureSet,
146 pub runtime_path: Option<PathBuf>,
148 pub hmr: bool,
150 pub hmr_port: Option<u16>,
152 hmr_running: bool,
154 connections: Arc<Mutex<Vec<ws::Sender>>>,
156 module_cache: Arc<Mutex<HashMap<String, ModuleCache>>>,
158 incremental: bool,
160 split_strategy: SplitStrategy,
162 dynamic_import: bool,
164 lazy_loading: bool,
166}
167
168impl Bundler {
169 pub fn new(runtime_path: Option<PathBuf>) -> Self {
171 Self { feature_set: FeatureSet::default(), runtime_path, hmr: false, hmr_port: None, hmr_running: false, connections: Arc::new(Mutex::new(vec![])), module_cache: Arc::new(Mutex::new(HashMap::new())), incremental: true, split_strategy: SplitStrategy::SingleFile, dynamic_import: false, lazy_loading: false }
172 }
173
174 pub fn with_hmr(mut self, port: Option<u16>) -> Self {
176 self.hmr = true;
177 self.hmr_port = port;
178 self
179 }
180
181 pub fn with_incremental(mut self, incremental: bool) -> Self {
183 self.incremental = incremental;
184 self
185 }
186
187 pub fn with_split_strategy(mut self, strategy: SplitStrategy) -> Self {
189 self.split_strategy = strategy;
190 self
191 }
192
193 pub fn with_dynamic_import(mut self, dynamic_import: bool) -> Self {
195 self.dynamic_import = dynamic_import;
196 self
197 }
198
199 pub fn with_lazy_loading(mut self, lazy_loading: bool) -> Self {
201 self.lazy_loading = lazy_loading;
202 self
203 }
204
205 fn compute_fingerprint(&self, module: &IRModule) -> FileFingerprint {
207 let content = format!("{}{:?}", module.name, module);
209 let content_hash = content.as_bytes().iter().fold(0u64, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as u64));
210 FileFingerprint { modified_time: SystemTime::now(), content_hash }
211 }
212
213 fn analyze_dependencies(&self, module: &IRModule) -> HashSet<String> {
215 let mut dependencies = HashSet::new();
216
217 if let Some(script) = &module.script {
219 for stmt in &script.body {
220 self.analyze_stmt_for_dependencies(stmt, &mut dependencies);
221 }
222 }
223
224 if let Some(script) = &module.script_client {
225 for stmt in &script.body {
226 self.analyze_stmt_for_dependencies(stmt, &mut dependencies);
227 }
228 }
229
230 if let Some(script) = &module.script_server {
231 for stmt in &script.body {
232 self.analyze_stmt_for_dependencies(stmt, &mut dependencies);
233 }
234 }
235
236 dependencies
237 }
238
239 fn analyze_stmt_for_dependencies(&self, stmt: &JsStmt, dependencies: &mut HashSet<String>) {
241 }
244
245 pub fn analyze_all(&mut self, modules: &[IRModule]) {
247 let feature_set = Arc::new(Mutex::new(FeatureSet::default()));
248
249 modules.par_iter().for_each(|module| {
250 let mut local_features = FeatureSet::default();
251 self.analyze_module_into(module, &mut local_features);
252 let mut global_features = feature_set.lock().unwrap();
253 global_features.merge(&local_features);
254 });
255
256 self.feature_set = Arc::try_unwrap(feature_set).unwrap().into_inner().unwrap();
257 }
258
259 fn analyze_module_into(&self, module: &IRModule, features: &mut FeatureSet) {
261 if let Some(template) = &module.template {
263 if !template.nodes.is_empty() {
264 features.has_vdom = true;
265 features.used_dom_functions.insert("h".to_string());
266 }
267 for node in &template.nodes {
268 self.analyze_node_into(node, features);
269 }
270 }
271
272 if let Some(script) = &module.script {
274 for stmt in &script.body {
275 self.analyze_stmt_into(stmt, features);
276 }
277 }
278
279 if let Some(script) = &module.script_client {
280 for stmt in &script.body {
281 self.analyze_stmt_into(stmt, features);
282 }
283 }
284
285 if let Some(script) = &module.script_server {
286 for stmt in &script.body {
287 self.analyze_stmt_into(stmt, features);
288 features.has_ssr = true;
289 }
290 }
291 }
292
293 fn analyze_stmt_into(&self, stmt: &JsStmt, features: &mut FeatureSet) {
295 match stmt {
296 JsStmt::Expr(expr, _, _) => self.analyze_expr_into(expr, features),
297 JsStmt::VariableDecl { init, .. } => {
298 if let Some(expr) = init {
299 self.analyze_expr_into(expr, features);
300 }
301 }
302 JsStmt::Return(expr, _, _) => {
303 if let Some(expr) = expr {
304 self.analyze_expr_into(expr, features);
305 }
306 }
307 JsStmt::If { test, consequent, alternate, .. } => {
308 self.analyze_expr_into(test, features);
309 self.analyze_stmt_into(consequent, features);
310 if let Some(alt) = alternate {
311 self.analyze_stmt_into(alt, features);
312 }
313 }
314 JsStmt::While { test, body, .. } => {
315 self.analyze_expr_into(test, features);
316 self.analyze_stmt_into(body, features);
317 }
318 JsStmt::For { init, test, update, body, .. } => {
319 if let Some(init) = init {
320 self.analyze_stmt_into(init, features);
321 }
322 if let Some(test) = test {
323 self.analyze_expr_into(test, features);
324 }
325 if let Some(update) = update {
326 self.analyze_expr_into(update, features);
327 }
328 self.analyze_stmt_into(body, features);
329 }
330 JsStmt::Block(stmts, _, _) => {
331 for s in stmts {
332 self.analyze_stmt_into(s, features);
333 }
334 }
335 JsStmt::FunctionDecl { body, .. } => {
336 for s in body {
337 self.analyze_stmt_into(s, features);
338 }
339 }
340 JsStmt::Export { declaration, .. } => self.analyze_stmt_into(declaration, features),
341 _ => {}
342 }
343 }
344
345 fn analyze_expr_into(&self, expr: &JsExpr, features: &mut FeatureSet) {
347 match expr {
348 JsExpr::Call { callee, args, .. } => {
349 if let JsExpr::Identifier(name, _, _) = &**callee {
350 match name.as_str() {
351 "signal" | "createSignal" => {
352 features.has_signals = true;
353 features.used_core_functions.insert(name.clone());
354 }
355 "effect" | "createEffect" => {
356 features.has_effects = true;
357 features.used_core_functions.insert(name.clone());
358 }
359 "computed" | "createComputed" => {
360 features.has_signals = true;
361 features.used_core_functions.insert(name.clone());
362 }
363 _ => {
364 features.used_core_functions.insert(name.clone());
365 }
366 }
367 }
368 for arg in args {
369 self.analyze_expr_into(arg, features);
370 }
371 }
372 JsExpr::Binary { left, right, .. } => {
373 self.analyze_expr_into(left, features);
374 self.analyze_expr_into(right, features);
375 }
376 JsExpr::Unary { argument, .. } => {
377 self.analyze_expr_into(argument, features);
378 }
379 JsExpr::Array(exprs, _, _) => {
380 for e in exprs {
381 self.analyze_expr_into(e, features);
382 }
383 }
384 JsExpr::Object(map, _, _) => {
385 for e in map.values() {
386 self.analyze_expr_into(e, features);
387 }
388 }
389 JsExpr::ArrowFunction { body, .. } => {
390 self.analyze_expr_into(body, features);
391 }
392 JsExpr::Conditional { test, consequent, alternate, .. } => {
393 self.analyze_expr_into(test, features);
394 self.analyze_expr_into(consequent, features);
395 self.analyze_expr_into(alternate, features);
396 }
397 JsExpr::TemplateLiteral { expressions, .. } => {
398 for e in expressions {
399 self.analyze_expr_into(e, features);
400 }
401 }
402 _ => {}
403 }
404 }
405
406 fn analyze_node_into(&self, node: &TemplateNodeIR, features: &mut FeatureSet) {
408 match node {
409 TemplateNodeIR::Element(el) => {
410 for attr in &el.attributes {
411 if let Some(ast) = &attr.value_ast {
412 self.analyze_expr_into(ast, features);
413 }
414 if attr.is_directive {
416 features.has_effects = true;
417 features.used_core_functions.insert("createEffect".to_string());
418 }
419 }
420 for child in &el.children {
421 self.analyze_node_into(child, features);
422 }
423 }
424 TemplateNodeIR::Interpolation(expr) => {
425 features.has_effects = true;
426 features.used_core_functions.insert("createEffect".to_string());
427 if let Some(ast) = &expr.ast {
428 self.analyze_expr_into(ast, features);
429 }
430 }
431 _ => {}
432 }
433 }
434
435 pub fn generate_custom_runtime(&self) -> String {
437 let mut runtime = String::new();
438 runtime.push_str("// Nargo Standalone Runtime\n\n");
439
440 runtime.push_str("const runtime = (function() {\n");
441 runtime.push_str(" const queue = [];\n");
442 runtime.push_str(" let isFlushing = false;\n");
443 runtime.push_str(" const p = Promise.resolve();\n");
444 runtime.push_str(" function nextTick(fn) { return fn ? p.then(fn) : p; }\n");
445 runtime.push_str(" function queueJob(job) { if (!queue.includes(job)) { queue.push(job); scheduleFlush(); } }\n");
446 runtime.push_str(" function scheduleFlush() { if (!isFlushing) { isFlushing = true; nextTick(flushJobs); } }\n");
447 runtime.push_str(" function flushJobs() { try { for (let i = 0; i < queue.length; i++) queue[i](); } finally { isFlushing = false; queue.length = 0; } }\n\n");
448
449 runtime.push_str(" let currentEffect = null;\n");
450 runtime.push_str(" function createSignal(v) {\n");
451 runtime.push_str(" let val = v;\n");
452 runtime.push_str(" const subs = new Set();\n");
453 runtime.push_str(" return [\n");
454 runtime.push_str(" () => { if (currentEffect) subs.add(currentEffect); return val; },\n");
455 runtime.push_str(" (n) => { if (!Object.is(val, n)) { val = n; subs.forEach(s => queueJob(s)); } }\n");
456 runtime.push_str(
457 " ];
458",
459 );
460 runtime.push_str(" }\n\n");
461
462 runtime.push_str(" function createEffect(fn) {\n");
463 runtime.push_str(" const effect = () => {\n");
464 runtime.push_str(" const prev = currentEffect;\n");
465 runtime.push_str(" currentEffect = effect;\n");
466 runtime.push_str(" try { fn(); } finally { currentEffect = prev; }\n");
467 runtime.push_str(
468 " };
469",
470 );
471 runtime.push_str(" effect();\n");
472 runtime.push_str(" }\n\n");
473
474 runtime.push_str(" function createComputed(fn) {\n");
475 runtime.push_str(" const [g, s] = createSignal();\n");
476 runtime.push_str(" createEffect(() => s(fn()));\n");
477 runtime.push_str(" return g;\n");
478 runtime.push_str(" }\n\n");
479
480 runtime.push_str(" function h(tag, props, ...children) { return { tag, props, children: children.flat() }; }\n");
481 runtime.push_str(" function createStaticVNode(html) { return { tag: 'div', props: { innerHTML: html }, children: [], isStatic: true }; }\n");
482
483 runtime.push_str(" function mountElement(vnode, container) {\n");
484 runtime.push_str(" if (typeof vnode === 'string' || typeof vnode === 'number') {\n");
485 runtime.push_str(" const el = document.createTextNode(vnode);\n");
486 runtime.push_str(" container.appendChild(el);\n");
487 runtime.push_str(" return el;\n");
488 runtime.push_str(" }\n");
489 runtime.push_str(" if (vnode.isStatic) {\n");
490 runtime.push_str(" const el = document.createElement(vnode.tag);\n");
491 runtime.push_str(" el.innerHTML = vnode.props.innerHTML;\n");
492 runtime.push_str(" const actualRoot = el.firstChild || el;\n");
493 runtime.push_str(" container.appendChild(actualRoot);\n");
494 runtime.push_str(" return actualRoot;\n");
495 runtime.push_str(" }\n");
496 runtime.push_str(" if (typeof vnode.tag === 'object') {\n");
497 runtime.push_str(" return renderComponent(vnode.tag, container);\n");
498 runtime.push_str(" }\n");
499 runtime.push_str(" const el = document.createElement(vnode.tag);\n");
500 runtime.push_str(" if (vnode.props) {\n");
501 runtime.push_str(" for (const [key, value] of Object.entries(vnode.props)) {\n");
502 runtime.push_str(" if (key.startsWith('on')) el.addEventListener(key.toLowerCase().slice(2), value);\n");
503 runtime.push_str(" else el.setAttribute(key, value);\n");
504 runtime.push_str(" }\n");
505 runtime.push_str(" }\n");
506 runtime.push_str(" vnode.children.forEach(child => mountElement(child, el));\n");
507 runtime.push_str(" container.appendChild(el);\n");
508 runtime.push_str(" return el;\n");
509 runtime.push_str(" }\n\n");
510
511 runtime.push_str(" function renderComponent(comp, container) {\n");
512 runtime.push_str(" const setupContext = { i18n: comp.i18n || {} };\n");
513 runtime.push_str(" const state = comp.setup ? comp.setup({}, setupContext) : {};\n");
514 runtime.push_str(" let rootEl = null;\n");
515 runtime.push_str(" createEffect(() => {\n");
516 runtime.push_str(" const vnode = comp.render(state);\n");
517 runtime.push_str(" if (rootEl) { container.removeChild(rootEl); }\n");
518 runtime.push_str(" rootEl = mountElement(vnode, container);\n");
519 runtime.push_str(" });\n");
520 runtime.push_str(" return rootEl;\n");
521 runtime.push_str(" }\n\n");
522
523 runtime.push_str(" function useI18n(i18n) {\n");
524 runtime.push_str(" return { t: (key, params) => {\n");
525 runtime.push_str(" let msg = (i18n.en && i18n.en[key]) || key;\n");
526 runtime.push_str(" if (params) Object.keys(params).forEach(k => msg = msg.replace(`{${k}}`, params[k]));\n");
527 runtime.push_str(" return msg;\n");
528 runtime.push_str(" }};\n");
529 runtime.push_str(" }\n\n");
530
531 runtime.push_str(" function lazy(loader) {\n");
532 runtime.push_str(" let component = null;\n");
533 runtime.push_str(" return {\n");
534 runtime.push_str(" setup() {\n");
535 runtime.push_str(" const [loading, setLoading] = createSignal(true);\n");
536 runtime.push_str(" const [error, setError] = createSignal(null);\n");
537 runtime.push_str(" \n");
538 runtime.push_str(" loader().then(mod => {\n");
539 runtime.push_str(" component = mod.default || mod;\n");
540 runtime.push_str(" setLoading(false);\n");
541 runtime.push_str(" }).catch(err => {\n");
542 runtime.push_str(" setError(err);\n");
543 runtime.push_str(" setLoading(false);\n");
544 runtime.push_str(" });\n");
545 runtime.push_str(" \n");
546 runtime.push_str(" return {\n");
547 runtime.push_str(" loading,\n");
548 runtime.push_str(" error,\n");
549 runtime.push_str(" component\n");
550 runtime.push_str(" };");
551 runtime.push_str(" },\n");
552 runtime.push_str(" render({ loading, error, component }) {\n");
553 runtime.push_str(" if (loading()) {\n");
554 runtime.push_str(" return h('div', null, 'Loading...');\n");
555 runtime.push_str(" }\n");
556 runtime.push_str(" if (error()) {\n");
557 runtime.push_str(" return h('div', null, `Error: ${error().message}`);\n");
558 runtime.push_str(" }\n");
559 runtime.push_str(" if (component) {\n");
560 runtime.push_str(" return h(component);\n");
561 runtime.push_str(" }\n");
562 runtime.push_str(" return null;\n");
563 runtime.push_str(" }\n");
564 runtime.push_str(" };");
565 runtime.push_str(" }\n\n");
566
567 runtime.push_str(
568 " return {
569",
570 );
571 runtime.push_str(
572 " signal: createSignal, createSignal,
573",
574 );
575 runtime.push_str(
576 " effect: createEffect, createEffect,
577",
578 );
579 runtime.push_str(
580 " computed: createComputed, createComputed,
581",
582 );
583 runtime.push_str(
584 " nextTick, h, render: renderComponent, useI18n, createStaticVNode, lazy
585",
586 );
587 runtime.push_str(
588 " };
589",
590 );
591 runtime.push_str("})();\n\n");
592
593 runtime.push_str("const { signal, createSignal, effect, createEffect, computed, createComputed, nextTick, h, render, useI18n, createStaticVNode, lazy } = runtime;\n");
594 runtime.push_str("const t = useI18n({}).t;\n");
595
596 runtime
597 }
598
599 fn generate_hmr_client(&self) -> String {
601 let port = self.hmr_port.unwrap_or(3000);
602 let mut hmr_code = String::new();
603 hmr_code.push_str("// HMR Client\n\n");
604 hmr_code.push_str("// HMR 模块缓存\n");
605 hmr_code.push_str("window.__HMR_MODULES__ = window.__HMR_MODULES__ || {};\n");
606 hmr_code.push_str("window.__HMR_CLIENTS__ = window.__HMR_CLIENTS__ || [];\n");
607 hmr_code.push_str("\n");
608 hmr_code.push_str(&format!("const ws = new WebSocket('ws://localhost:{}');\n", port));
609 hmr_code.push_str("\n");
610 hmr_code.push_str("// 注册模块更新回调\n");
611 hmr_code.push_str("window.hmrRegisterModule = (moduleName, updateCallback) => {\n");
612 hmr_code.push_str(" window.__HMR_MODULES__[moduleName] = updateCallback;\n");
613 hmr_code.push_str("};\n");
614 hmr_code.push_str("\n");
615 hmr_code.push_str("// 发送模块注册信息\n");
616 hmr_code.push_str("window.hmrSendModuleInfo = () => {\n");
617 hmr_code.push_str(" if (ws.readyState === WebSocket.OPEN) {\n");
618 hmr_code.push_str(" ws.send(JSON.stringify({\n");
619 hmr_code.push_str(" type: 'register',\n");
620 hmr_code.push_str(" modules: Object.keys(window.__HMR_MODULES__)\n");
621 hmr_code.push_str(" }));\n");
622 hmr_code.push_str(" }\n");
623 hmr_code.push_str("};\n");
624 hmr_code.push_str("\n");
625 hmr_code.push_str("// 处理模块更新\n");
626 hmr_code.push_str("const handleModuleUpdate = async (moduleName, updateData) => {\n");
627 hmr_code.push_str(" console.log('HMR: Updating module', moduleName);\n");
628 hmr_code.push_str(" \n");
629 hmr_code.push_str(" try {\n");
630 hmr_code.push_str(" // 动态加载更新后的模块\n");
631 hmr_code.push_str(" const response = await fetch(`${moduleName}.js?_t=${Date.now()}`);\n");
632 hmr_code.push_str(" const code = await response.text();\n");
633 hmr_code.push_str(" \n");
634 hmr_code.push_str(" // 创建临时模块执行环境\n");
635 hmr_code.push_str(" const module = { exports: {} };\n");
636 hmr_code.push_str(" const require = (dep) => {\n");
637 hmr_code.push_str(" if (window.__HMR_MODULES__[dep]) {\n");
638 hmr_code.push_str(" return window.__HMR_MODULES__[dep];\n");
639 hmr_code.push_str(" }\n");
640 hmr_code.push_str(" throw new Error(`Module ${dep} not found`);\n");
641 hmr_code.push_str(" };\n");
642 hmr_code.push_str(" \n");
643 hmr_code.push_str(" // 执行模块代码\n");
644 hmr_code.push_str(" const exec = new Function('module', 'exports', 'require', code);\n");
645 hmr_code.push_str(" exec(module, module.exports, require);\n");
646 hmr_code.push_str(" \n");
647 hmr_code.push_str(" // 调用模块的更新回调\n");
648 hmr_code.push_str(" if (window.__HMR_MODULES__[moduleName]) {\n");
649 hmr_code.push_str(" window.__HMR_MODULES__[moduleName](module.exports);\n");
650 hmr_code.push_str(" console.log('HMR: Module updated successfully', moduleName);\n");
651 hmr_code.push_str(" }\n");
652 hmr_code.push_str(" } catch (error) {\n");
653 hmr_code.push_str(" console.error('HMR: Failed to update module', moduleName, error);\n");
654 hmr_code.push_str(" // 如果更新失败,回退到页面刷新\n");
655 hmr_code.push_str(" window.location.reload();\n");
656 hmr_code.push_str(" }\n");
657 hmr_code.push_str("};\n");
658 hmr_code.push_str("\n");
659 hmr_code.push_str("ws.onmessage = (event) => {\n");
660 hmr_code.push_str(" try {\n");
661 hmr_code.push_str(" const data = JSON.parse(event.data);\n");
662 hmr_code.push_str(" \n");
663 hmr_code.push_str(" switch (data.type) {\n");
664 hmr_code.push_str(" case 'update':\n");
665 hmr_code.push_str(" // 处理模块更新\n");
666 hmr_code.push_str(" handleModuleUpdate(data.module, data);\n");
667 hmr_code.push_str(" break;\n");
668 hmr_code.push_str(" case 'reload':\n");
669 hmr_code.push_str(" // 强制页面刷新\n");
670 hmr_code.push_str(" console.log('HMR: Reloading page');\n");
671 hmr_code.push_str(" window.location.reload();\n");
672 hmr_code.push_str(" break;\n");
673 hmr_code.push_str(" default:\n");
674 hmr_code.push_str(" console.log('HMR: Unknown message type', data.type);\n");
675 hmr_code.push_str(" }\n");
676 hmr_code.push_str(" } catch (error) {\n");
677 hmr_code.push_str(" console.error('HMR: Failed to process message', error);\n");
678 hmr_code.push_str(" }\n");
679 hmr_code.push_str("};\n");
680 hmr_code.push_str("\n");
681 hmr_code.push_str("ws.onopen = () => {\n");
682 hmr_code.push_str(" console.log('HMR: Connected');\n");
683 hmr_code.push_str(" // 发送模块注册信息\n");
684 hmr_code.push_str(" window.hmrSendModuleInfo();\n");
685 hmr_code.push_str("};\n");
686 hmr_code.push_str("\n");
687 hmr_code.push_str("ws.onclose = () => {\n");
688 hmr_code.push_str(" console.log('HMR: Connection closed');\n");
689 hmr_code.push_str(" // 尝试重连\n");
690 hmr_code.push_str(" setTimeout(() => {\n");
691 hmr_code.push_str(" console.log('HMR: Attempting to reconnect');\n");
692 hmr_code.push_str(" window.location.reload();\n");
693 hmr_code.push_str(" }, 1000);\n");
694 hmr_code.push_str("};\n");
695 hmr_code.push_str("\n");
696 hmr_code.push_str("ws.onerror = (error) => {\n");
697 hmr_code.push_str(" console.error('HMR: Connection error', error);\n");
698 hmr_code.push_str("};\n");
699 hmr_code
700 }
701
702 pub fn start_hmr_server(&mut self) -> Result<()> {
704 if !self.hmr || self.hmr_running {
705 return Ok(());
706 }
707
708 let port = self.hmr_port.unwrap_or(3000);
709 let connections = self.connections.clone();
710
711 thread::spawn(move || {
712 println!("HMR: Starting server on port {}", port);
713 if let Err(e) = listen(format!("127.0.0.1:{}", port), |_| HmrHandler { connections: connections.clone() }) {
714 println!("HMR: Server error: {:?}", e);
715 }
716 });
717
718 self.hmr_running = true;
719 Ok(())
720 }
721
722 pub fn send_hmr_update(&self, module_name: &str) -> Result<()> {
724 let message = serde_json::json!({"type": "update", "module": module_name});
725 let message_str = message.to_string();
726
727 let mut connections = self.connections.lock().unwrap();
728 connections.retain(|conn| match conn.send(Message::text(message_str.clone())) {
729 Ok(_) => true,
730 Err(e) => {
731 println!("HMR: Failed to send update: {:?}", e);
732 false
733 }
734 });
735
736 Ok(())
737 }
738
739 pub fn bundle_all(&mut self, modules: &[IRModule], format: OutputFormat) -> Result<BuildOutputs> {
741 match format {
742 OutputFormat::JavaScript => self.bundle_javascript(modules),
743 OutputFormat::CSS => self.bundle_css(modules),
744 OutputFormat::HTML => self.bundle_html(modules),
745 OutputFormat::TypeScript => self.bundle_typescript(modules),
746 OutputFormat::WebAssembly => self.bundle_wasm(modules),
747 }
748 }
749
750 fn is_route_component(&self, module: &IRModule) -> bool {
752 if let Some(script) = &module.script {
754 for stmt in &script.body {
755 if self.analyze_stmt_for_route(stmt) {
756 return true;
757 }
758 }
759 }
760 false
761 }
762
763 fn analyze_stmt_for_route(&self, stmt: &JsStmt) -> bool {
765 match stmt {
766 JsStmt::Expr(expr, _, _) => self.analyze_expr_for_route(expr),
767 JsStmt::VariableDecl { init, .. } => {
768 if let Some(expr) = init {
769 self.analyze_expr_for_route(expr)
770 }
771 else {
772 false
773 }
774 }
775 JsStmt::Return(expr, _, _) => {
776 if let Some(expr) = expr {
777 self.analyze_expr_for_route(expr)
778 }
779 else {
780 false
781 }
782 }
783 JsStmt::Block(stmts, _, _) => stmts.iter().any(|s| self.analyze_stmt_for_route(s)),
784 _ => false,
785 }
786 }
787
788 fn analyze_expr_for_route(&self, expr: &JsExpr) -> bool {
790 match expr {
791 JsExpr::Call { callee, .. } => {
792 if let JsExpr::Identifier(name, _, _) = &**callee {
793 matches!(name.as_str(), "useRoute" | "useRouter" | "createRouter" | "createWebHistory" | "createWebHashHistory")
795 }
796 else {
797 false
798 }
799 }
800 JsExpr::Object(map, _, _) => {
801 map.contains_key("path") || map.contains_key("component") || map.contains_key("children")
803 }
804 _ => false,
805 }
806 }
807
808 pub fn bundle_all_default(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
810 self.bundle_all(modules, OutputFormat::JavaScript)
811 }
812
813 fn bundle_javascript(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
815 let updated_feature_set = Arc::new(Mutex::new(FeatureSet::default()));
816 let need_full_rebuild = Arc::new(Mutex::new(false));
817
818 let backend = JsBackend::new(false, false, None, CompileMode::Vue2);
820 let compiled_modules: Vec<Result<(String, String)>> = modules
821 .par_iter()
822 .map(|module| {
823 let mut module_needs_rebuild = false;
824 let mut module_features = FeatureSet::default();
825 let mut processed_code = String::new();
826
827 if self.incremental {
828 let fingerprint = self.compute_fingerprint(module);
829 let module_cache = self.module_cache.lock().unwrap();
830 if let Some(cache) = module_cache.get(&module.name) {
831 if cache.fingerprint == fingerprint {
832 let mut features = updated_feature_set.lock().unwrap();
834 features.merge(&cache.features);
835 return Ok((module.name.clone(), cache.compiled_code.clone()));
836 }
837 }
838 module_needs_rebuild = true;
840 }
841
842 if module_needs_rebuild || !self.incremental {
843 let (code, _) = backend.generate(module)?;
845
846 processed_code = code.replace("import {", "// import {").replace("} from '@nargo/core';", " } = runtime;").replace("} from '@nargo/dom';", " } = runtime;").replace("} from '@nargo/client';", " } = runtime;").replace("export default ", &format!("const {} = ", module.name));
848
849 self.analyze_module_into(module, &mut module_features);
851 let dependencies = self.analyze_dependencies(module);
852
853 if self.incremental {
855 let fingerprint = self.compute_fingerprint(module);
856 let mut cache = self.module_cache.lock().unwrap();
857 cache.insert(module.name.clone(), ModuleCache { fingerprint, compiled_code: processed_code.clone(), dependencies, features: module_features.clone() });
858 }
859
860 let mut features = updated_feature_set.lock().unwrap();
862 features.merge(&module_features);
863
864 let mut rebuild = need_full_rebuild.lock().unwrap();
866 *rebuild = true;
867 }
868
869 Ok((module.name.clone(), processed_code))
870 })
871 .collect();
872
873 let needs_rebuild = *need_full_rebuild.lock().unwrap();
875 if needs_rebuild || !self.incremental {
876 self.feature_set = Arc::try_unwrap(updated_feature_set).unwrap().into_inner().unwrap();
877 }
878
879 let runtime_code = self.generate_custom_runtime();
881
882 let hmr_code = if self.hmr {
884 self.start_hmr_server()?;
885 self.generate_hmr_client()
886 }
887 else {
888 String::new()
889 };
890
891 match self.split_strategy {
893 SplitStrategy::SingleFile => {
894 let mut output = String::new();
896 output.push_str(&runtime_code);
897 output.push_str(&hmr_code);
898 output.push_str("\n// --- Components --\n");
899
900 for res in compiled_modules {
901 let (name, code) = res?;
902 output.push_str(&format!("\n// --- Component: {} --\n{}\n", name, code));
903 }
904
905 if let Some(main) = modules.first() {
906 output.push_str(&format!("\n// --- Mount --\n"));
907 output.push_str(&format!("render(h({}, null), document.getElementById('app') || document.body);\n", main.name));
908 }
909
910 let build_output = BuildOutput { code: output.into_bytes(), format: OutputFormat::JavaScript, filename: "bundle.js".to_string() };
911
912 Ok(BuildOutputs { outputs: vec![build_output], entry_file: "bundle.js".to_string() })
913 }
914 SplitStrategy::ByComponent => {
915 let mut outputs = Vec::new();
917
918 let runtime_output = BuildOutput { code: runtime_code.into_bytes(), format: OutputFormat::JavaScript, filename: "runtime.js".to_string() };
920 outputs.push(runtime_output);
921
922 if !hmr_code.is_empty() {
924 let hmr_output = BuildOutput { code: hmr_code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: "hmr.js".to_string() };
925 outputs.push(hmr_output);
926 }
927
928 for res in compiled_modules {
930 let (name, code) = res?;
931 let component_output = BuildOutput { code: code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: format!("{}.js", name) };
932 outputs.push(component_output);
933 }
934
935 let mut entry_code = String::new();
937 entry_code.push_str("// Entry Point\n");
938 entry_code.push_str("import './runtime.js';\n");
939 if !hmr_code.is_empty() {
940 entry_code.push_str("import './hmr.js';\n");
941 }
942
943 for module in modules {
944 entry_code.push_str(&format!("import {} from './{}.js';\n", module.name, module.name));
945 }
946
947 if let Some(main) = modules.first() {
948 entry_code.push_str(&format!("\n// Mount\n"));
949 entry_code.push_str(&format!("render(h({}, null), document.getElementById('app') || document.body);\n", main.name));
950 }
951
952 let entry_output = BuildOutput { code: entry_code.into_bytes(), format: OutputFormat::JavaScript, filename: "index.js".to_string() };
953 outputs.push(entry_output);
954
955 Ok(BuildOutputs { outputs, entry_file: "index.js".to_string() })
956 }
957 SplitStrategy::ByRoute => {
958 let mut outputs = Vec::new();
960 let mut route_components = Vec::new();
961 let mut regular_components = Vec::new();
962
963 let runtime_output = BuildOutput { code: runtime_code.into_bytes(), format: OutputFormat::JavaScript, filename: "runtime.js".to_string() };
965 outputs.push(runtime_output);
966
967 if !hmr_code.is_empty() {
969 let hmr_output = BuildOutput { code: hmr_code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: "hmr.js".to_string() };
970 outputs.push(hmr_output);
971 }
972
973 for (i, module) in modules.iter().enumerate() {
975 if self.is_route_component(module) {
976 route_components.push((i, module));
977 }
978 else {
979 regular_components.push((i, module));
980 }
981 }
982
983 let mut shared_code = String::new();
985 for (i, module) in ®ular_components {
986 if let Ok((name, code)) = &compiled_modules[*i] {
987 shared_code.push_str(&format!("// Component: {}\n{}\n", name, code));
988 }
989 }
990
991 if !shared_code.is_empty() {
992 let shared_output = BuildOutput { code: shared_code.into_bytes(), format: OutputFormat::JavaScript, filename: "shared.js".to_string() };
993 outputs.push(shared_output);
994 }
995
996 for (i, module) in &route_components {
998 if let Ok((name, code)) = &compiled_modules[*i] {
999 let route_output = BuildOutput { code: code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: format!("route-{}.js", name) };
1000 outputs.push(route_output);
1001 }
1002 }
1003
1004 let mut entry_code = String::new();
1006 entry_code.push_str("// Entry Point\n");
1007 entry_code.push_str("import './runtime.js';\n");
1008 if !hmr_code.is_empty() {
1009 entry_code.push_str("import './hmr.js';\n");
1010 }
1011 if !regular_components.is_empty() {
1012 entry_code.push_str("import './shared.js';\n");
1013 }
1014
1015 for (i, module) in &route_components {
1017 if let Ok((name, _)) = &compiled_modules[*i] {
1018 entry_code.push_str(&format!("const {} = () => import('./route-{}.js');\n", name, name));
1019 }
1020 }
1021
1022 for (i, module) in ®ular_components {
1024 if let Ok((name, _)) = &compiled_modules[*i] {
1025 entry_code.push_str(&format!("import {} from './shared.js';\n", name));
1026 }
1027 }
1028
1029 if let Some(main) = modules.first() {
1030 entry_code.push_str(&format!("\n// Mount\n"));
1031 entry_code.push_str(&format!("render(h({}, null), document.getElementById('app') || document.body);\n", main.name));
1032 }
1033
1034 let entry_output = BuildOutput { code: entry_code.into_bytes(), format: OutputFormat::JavaScript, filename: "index.js".to_string() };
1035 outputs.push(entry_output);
1036
1037 Ok(BuildOutputs { outputs, entry_file: "index.js".to_string() })
1038 }
1039 SplitStrategy::Custom => {
1040 let mut outputs = Vec::new();
1042 let mut large_modules = Vec::new();
1043 let mut small_modules = Vec::new();
1044
1045 let runtime_output = BuildOutput { code: runtime_code.into_bytes(), format: OutputFormat::JavaScript, filename: "runtime.js".to_string() };
1047 outputs.push(runtime_output);
1048
1049 if !hmr_code.is_empty() {
1051 let hmr_output = BuildOutput { code: hmr_code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: "hmr.js".to_string() };
1052 outputs.push(hmr_output);
1053 }
1054
1055 for (i, module) in modules.iter().enumerate() {
1057 let code_size = if let Ok((_, code)) = &compiled_modules[i] { code.len() } else { 0 };
1058
1059 if code_size > 10 * 1024 {
1061 large_modules.push((i, module));
1062 }
1063 else {
1064 small_modules.push((i, module));
1065 }
1066 }
1067
1068 let mut shared_code = String::new();
1070 for (i, module) in &small_modules {
1071 if let Ok((name, code)) = &compiled_modules[*i] {
1072 shared_code.push_str(&format!("// Component: {}\n{}\n", name, code));
1073 }
1074 }
1075
1076 if !shared_code.is_empty() {
1077 let shared_output = BuildOutput { code: shared_code.into_bytes(), format: OutputFormat::JavaScript, filename: "shared.js".to_string() };
1078 outputs.push(shared_output);
1079 }
1080
1081 for (i, module) in &large_modules {
1083 if let Ok((name, code)) = &compiled_modules[*i] {
1084 let large_output = BuildOutput { code: code.clone().into_bytes(), format: OutputFormat::JavaScript, filename: format!("chunk-{}.js", name) };
1085 outputs.push(large_output);
1086 }
1087 }
1088
1089 let mut entry_code = String::new();
1091 entry_code.push_str("// Entry Point\n");
1092 entry_code.push_str("import './runtime.js';\n");
1093 if !hmr_code.is_empty() {
1094 entry_code.push_str("import './hmr.js';\n");
1095 }
1096 if !small_modules.is_empty() {
1097 entry_code.push_str("import './shared.js';\n");
1098 }
1099
1100 for (i, module) in &large_modules {
1102 if let Ok((name, _)) = &compiled_modules[*i] {
1103 entry_code.push_str(&format!("const {} = () => import('./chunk-{}.js');\n", name, name));
1104 }
1105 }
1106
1107 for (i, module) in &small_modules {
1109 if let Ok((name, _)) = &compiled_modules[*i] {
1110 entry_code.push_str(&format!("import {} from './shared.js';\n", name));
1111 }
1112 }
1113
1114 if let Some(main) = modules.first() {
1115 entry_code.push_str(&format!("\n// Mount\n"));
1116 entry_code.push_str(&format!("render(h({}, null), document.getElementById('app') || document.body);\n", main.name));
1117 }
1118
1119 let entry_output = BuildOutput { code: entry_code.into_bytes(), format: OutputFormat::JavaScript, filename: "index.js".to_string() };
1120 outputs.push(entry_output);
1121
1122 Ok(BuildOutputs { outputs, entry_file: "index.js".to_string() })
1123 }
1124 }
1125 }
1126
1127 fn bundle_css(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
1129 let mut output = String::new();
1130 let backend = CssBackend::new(false);
1131
1132 for module in modules {
1133 let css = backend.generate(module)?;
1134 if !css.is_empty() {
1135 output.push_str(&format!("/* Component: {} */\n", module.name));
1136 output.push_str(&css);
1137 output.push_str("\n");
1138 }
1139 }
1140
1141 let build_output = BuildOutput { code: output.into_bytes(), format: OutputFormat::CSS, filename: "bundle.css".to_string() };
1142 Ok(BuildOutputs { outputs: vec![build_output], entry_file: "bundle.css".to_string() })
1143 }
1144
1145 fn bundle_html(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
1147 let mut output = String::new();
1148 let backend = HtmlBackend::new();
1149
1150 for module in modules {
1151 let html = backend.generate(module)?;
1152 if !html.is_empty() {
1153 output.push_str(&format!("<!-- Component: {} -->\n", module.name));
1154 output.push_str(&html);
1155 output.push_str("\n");
1156 }
1157 }
1158
1159 let build_output = BuildOutput { code: output.into_bytes(), format: OutputFormat::HTML, filename: "bundle.html".to_string() };
1160 Ok(BuildOutputs { outputs: vec![build_output], entry_file: "bundle.html".to_string() })
1161 }
1162
1163 fn bundle_typescript(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
1165 let mut output = String::new();
1166 let backend = DtsBackend::new();
1167
1168 for module in modules {
1169 let dts = backend.generate(module)?;
1170 if !dts.is_empty() {
1171 output.push_str(&format!("// Component: {}\n", module.name));
1172 output.push_str(&dts);
1173 output.push_str("\n");
1174 }
1175 }
1176
1177 let build_output = BuildOutput { code: output.into_bytes(), format: OutputFormat::TypeScript, filename: "bundle.d.ts".to_string() };
1178 Ok(BuildOutputs { outputs: vec![build_output], entry_file: "bundle.d.ts".to_string() })
1179 }
1180
1181 fn bundle_wasm(&mut self, modules: &[IRModule]) -> Result<BuildOutputs> {
1183 let build_output = BuildOutput { code: vec![], format: OutputFormat::WebAssembly, filename: "bundle.wasm".to_string() };
1185 Ok(BuildOutputs { outputs: vec![build_output], entry_file: "bundle.wasm".to_string() })
1186 }
1187}
1188
1189impl Default for Bundler {
1190 fn default() -> Self {
1191 Self::new(None)
1192 }
1193}