jac-client 0.2.12__py3-none-any.whl → 0.2.14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. jac_client/examples/all-in-one/components/Header.jac +1 -1
  2. jac_client/examples/all-in-one/components/ProfitOverview.jac +1 -1
  3. jac_client/examples/all-in-one/components/Summary.jac +1 -1
  4. jac_client/examples/all-in-one/components/TransactionList.jac +2 -2
  5. jac_client/examples/all-in-one/components/navigation.jac +3 -9
  6. jac_client/examples/all-in-one/context/BudgetContext.jac +1 -1
  7. jac_client/examples/all-in-one/main.jac +5 -386
  8. jac_client/examples/all-in-one/pages/(auth)/index.jac +299 -0
  9. jac_client/examples/all-in-one/pages/{nestedDemo.jac → (auth)/nested.jac} +3 -13
  10. jac_client/examples/all-in-one/pages/{loginPage.jac → (public)/login.jac} +1 -1
  11. jac_client/examples/all-in-one/pages/{signupPage.jac → (public)/signup.jac} +1 -1
  12. jac_client/examples/all-in-one/pages/{notFound.jac → [...notFound].jac} +2 -1
  13. jac_client/examples/all-in-one/pages/budget.jac +11 -0
  14. jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +1 -1
  15. jac_client/examples/all-in-one/pages/features.jac +8 -0
  16. jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +7 -7
  17. jac_client/examples/all-in-one/pages/{LandingPage.jac → landing.jac} +4 -9
  18. jac_client/examples/all-in-one/pages/layout.jac +20 -0
  19. jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +1 -1
  20. jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +1 -1
  21. jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +1 -1
  22. jac_client/plugin/cli.jac +3 -3
  23. jac_client/plugin/client_runtime.cl.jac +7 -4
  24. jac_client/plugin/impl/client_runtime.impl.jac +29 -7
  25. jac_client/plugin/plugin_config.jac +4 -11
  26. jac_client/plugin/src/compiler.jac +19 -1
  27. jac_client/plugin/src/config_loader.jac +1 -0
  28. jac_client/plugin/src/impl/compiler.impl.jac +232 -62
  29. jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
  30. jac_client/plugin/src/impl/package_installer.impl.jac +3 -2
  31. jac_client/plugin/src/impl/route_scanner.impl.jac +201 -0
  32. jac_client/plugin/src/impl/vite_bundler.impl.jac +54 -15
  33. jac_client/plugin/src/route_scanner.jac +44 -0
  34. jac_client/plugin/src/targets/desktop/sidecar/main.py +42 -23
  35. jac_client/plugin/src/targets/desktop_target.jac +4 -2
  36. jac_client/plugin/src/targets/impl/desktop_target.impl.jac +324 -112
  37. jac_client/plugin/src/vite_bundler.jac +18 -3
  38. jac_client/plugin/utils/impl/bun_installer.impl.jac +16 -19
  39. jac_client/plugin/utils/impl/client_deps.impl.jac +12 -16
  40. jac_client/templates/fullstack.jacpack +3 -2
  41. jac_client/tests/test_cli.py +74 -0
  42. jac_client/tests/test_desktop_api_url.py +854 -0
  43. jac_client/tests/test_e2e.py +31 -40
  44. jac_client/tests/test_it.py +209 -11
  45. {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/METADATA +2 -2
  46. {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/RECORD +49 -44
  47. jac_client/examples/all-in-one/pages/BudgetPlanner.jac +0 -140
  48. jac_client/examples/all-in-one/pages/FeaturesTest.jac +0 -157
  49. {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/WHEEL +0 -0
  50. {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/entry_points.txt +0 -0
  51. {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,8 @@ import from pathlib { Path }
3
3
  import from typing { Optional, Any }
4
4
  import from jaclang.cli.console { console }
5
5
  import from jaclang.project.config { get_config }
6
+ import from jac_client.plugin.src.config_loader { JacClientConfig }
7
+ import from jac_client.plugin.src.vite_bundler { API_BASE_URL_ENV_VAR }
6
8
  import subprocess;
7
9
  import json;
8
10
  import shutil;
@@ -10,6 +12,15 @@ import os;
10
12
  import platform;
11
13
  import stat;
12
14
 
15
+ def _make_localhost_url(port: int) -> str {
16
+ return f"http://127.0.0.1:{port}";
17
+ }
18
+
19
+ def _get_toml_api_base_url(project_dir: Path) -> str {
20
+ config_loader = JacClientConfig(project_dir);
21
+ return config_loader.get_api_config().get('base_url', '');
22
+ }
23
+
13
24
  """Setup desktop target - scaffold Tauri project structure."""
14
25
  impl DesktopTarget.setup(self: DesktopTarget, project_dir: Path) -> None {
15
26
  # Define tauri_dir early so we can use it in error handling
@@ -242,20 +253,7 @@ def _generate_tauri_config(
242
253
  "devUrl": "http://localhost:5173",
243
254
  "frontendDist": "../.jac/client/dist"
244
255
  },
245
- "app": {
246
- "windows": [
247
- {
248
- "title": name,
249
- "width": 1200,
250
- "height": 800,
251
- "minWidth": 800,
252
- "minHeight": 600,
253
- "resizable": True,
254
- "fullscreen": False
255
- }
256
- ],
257
- "security": {"csp": None}
258
- },
256
+ "app": {"windows": [], "security": {"csp": None}},
259
257
  "bundle": {"active": True, "targets": "all", "icon": []},
260
258
  "plugins": {}
261
259
  };
@@ -462,154 +460,265 @@ except ImportError:
462
460
  );
463
461
  }
464
462
 
465
- """Generate main.rs."""
466
- def _generate_main_rs(tauri_dir: Path) -> None {
467
- main_rs = '''// Prevents additional console window on Windows in release, DO NOT REMOVE!!
463
+ """Generate main.rs.
464
+
465
+ api_base_url: embed this URL and skip sidecar port discovery.
466
+ Empty string means dynamic discovery at runtime.
467
+ """
468
+ def _generate_main_rs(tauri_dir: Path, api_base_url: str = "") -> None {
469
+ import json;
470
+
471
+ # Read window config from tauri.conf.json (before we clear windows array)
472
+ config_path = tauri_dir / "tauri.conf.json";
473
+ win_title = "Jac App";
474
+ win_width = 1200;
475
+ win_height = 800;
476
+ win_min_width = 800;
477
+ win_min_height = 600;
478
+ win_resizable = "true";
479
+
480
+ if config_path.exists() {
481
+ with open(config_path, "r") as f {
482
+ config = json.load(f);
483
+ }
484
+ windows = config.get("app", {}).get("windows", []);
485
+ if windows {
486
+ win = windows[0];
487
+ win_title = win.get("title", win_title);
488
+ win_width = win.get("width", win_width);
489
+ win_height = win.get("height", win_height);
490
+ win_min_width = win.get("minWidth", win_min_width);
491
+ win_min_height = win.get("minHeight", win_min_height);
492
+ win_resizable = "true" if win.get("resizable", True) else "false";
493
+ } else {
494
+ # No windows in config — fall back to productName for title
495
+ win_title = config.get("productName", win_title);
496
+ }
497
+ }
498
+
499
+ main_rs = f'''// Prevents additional console window on Windows in release, DO NOT REMOVE!!
468
500
  #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
469
501
 
470
- use std::process::{Command, Child};
502
+ use std::io::{{BufRead, BufReader}};
503
+ use std::process::{{Command, Child, Stdio}};
471
504
  use std::sync::Mutex;
472
505
  use tauri::Manager;
473
506
 
474
507
  // Global storage for sidecar process
475
508
  static SIDECAR_PROCESS: Mutex<Option<Child>> = Mutex::new(None);
509
+ static API_BASE_URL: Mutex<Option<String>> = Mutex::new(None);
510
+
511
+ /// User-configured base URL from jac.toml (empty = dynamic discovery)
512
+ const CONFIGURED_BASE_URL: &str = "{api_base_url}";
513
+
514
+ fn find_and_start_sidecar(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {{
515
+ // Skip sidecar launch — user manages their own backend
516
+ if !CONFIGURED_BASE_URL.is_empty() {{
517
+ let mut url = API_BASE_URL.lock().unwrap();
518
+ *url = Some(CONFIGURED_BASE_URL.to_string());
519
+ eprintln!("Using configured API base URL: {{}}", CONFIGURED_BASE_URL);
520
+ return Ok(());
521
+ }}
476
522
 
477
- fn find_and_start_sidecar(app: &tauri::AppHandle) -> Result<(), Box<dyn std::error::Error>> {
478
523
  // Try to find the sidecar in bundled resources
479
524
  let resource_dir = app.path().resource_dir()?;
480
525
 
481
526
  // Possible sidecar names
482
- let sidecar_names = if cfg!(windows) {
527
+ let sidecar_names = if cfg!(windows) {{
483
528
  vec!["binaries/jac-sidecar.exe", "binaries/jac-sidecar.bat"]
484
- } else {
529
+ }} else {{
485
530
  vec!["binaries/jac-sidecar", "binaries/jac-sidecar.sh"]
486
- };
531
+ }};
487
532
 
488
533
  let mut sidecar_path = None;
489
- for name in &sidecar_names {
534
+ for name in &sidecar_names {{
490
535
  let path = resource_dir.join(name);
491
- if path.exists() {
536
+ if path.exists() {{
492
537
  sidecar_path = Some(path);
493
538
  break;
494
- }
495
- }
539
+ }}
540
+ }}
496
541
 
497
542
  // If not found in resources, try relative to executable
498
- if sidecar_path.is_none() {
499
- if let Ok(exe_path) = std::env::current_exe() {
500
- if let Some(exe_dir) = exe_path.parent() {
543
+ if sidecar_path.is_none() {{
544
+ if let Ok(exe_path) = std::env::current_exe() {{
545
+ if let Some(exe_dir) = exe_path.parent() {{
501
546
  let exe_dir = exe_dir.to_path_buf();
502
- for name in &sidecar_names {
547
+ for name in &sidecar_names {{
503
548
  let path = exe_dir.join(name);
504
- if path.exists() {
549
+ if path.exists() {{
505
550
  sidecar_path = Some(path);
506
551
  break;
507
- }
508
- }
509
- }
510
- }
511
- }
552
+ }}
553
+ }}
554
+ }}
555
+ }}
556
+ }}
512
557
 
513
- if let Some(sidecar_path) = sidecar_path {
558
+ if let Some(sidecar_path) = sidecar_path {{
514
559
  // Determine module path (try to find main.jac relative to app)
515
- let module_path = if let Ok(exe_path) = std::env::current_exe() {
516
- if let Some(exe_dir) = exe_path.parent() {
560
+ let module_path = if let Ok(exe_path) = std::env::current_exe() {{
561
+ if let Some(exe_dir) = exe_path.parent() {{
517
562
  // Look for main.jac in parent directories
518
563
  let mut current = exe_dir.to_path_buf();
519
- loop {
564
+ loop {{
520
565
  let main_jac = current.join("main.jac");
521
- if main_jac.exists() {
566
+ if main_jac.exists() {{
522
567
  break Some(main_jac);
523
- }
524
- if !current.pop() {
568
+ }}
569
+ if !current.pop() {{
525
570
  break None;
526
- }
527
- }
528
- } else {
571
+ }}
572
+ }}
573
+ }} else {{
529
574
  None
530
- }
531
- } else {
575
+ }}
576
+ }} else {{
532
577
  None
533
- };
578
+ }};
534
579
 
535
580
  // Build command to start sidecar
536
- let mut cmd = if cfg!(windows) {
537
- if sidecar_path.extension().and_then(|s| s.to_str()) == Some("bat") {
581
+ let mut cmd = if cfg!(windows) {{
582
+ if sidecar_path.extension().and_then(|s| s.to_str()) == Some("bat") {{
538
583
  let mut c = Command::new("cmd");
539
584
  c.arg("/C");
540
585
  c.arg(&sidecar_path);
541
586
  c
542
- } else {
587
+ }} else {{
543
588
  Command::new(&sidecar_path)
544
- }
545
- } else {
546
- if sidecar_path.extension().and_then(|s| s.to_str()) == Some("sh") {
589
+ }}
590
+ }} else {{
591
+ if sidecar_path.extension().and_then(|s| s.to_str()) == Some("sh") {{
547
592
  let mut c = Command::new("sh");
548
593
  c.arg(&sidecar_path);
549
594
  c
550
- } else {
595
+ }} else {{
551
596
  Command::new(&sidecar_path)
552
- }
553
- };
597
+ }}
598
+ }};
554
599
 
555
600
  // Add arguments
556
- if let Some(ref mp) = module_path {
601
+ if let Some(ref mp) = module_path {{
557
602
  cmd.arg("--module-path").arg(mp);
558
- } else {
603
+ }} else {{
559
604
  cmd.arg("--module-path").arg("main.jac");
560
- }
561
- cmd.arg("--port").arg("8000");
605
+ }}
606
+ cmd.arg("--port").arg("0"); // OS assigns free port
562
607
  cmd.arg("--host").arg("127.0.0.1");
563
608
 
564
- // Spawn sidecar process
565
- match cmd.spawn() {
566
- Ok(child) => {
609
+ cmd.stdout(Stdio::piped());
610
+ cmd.stderr(Stdio::inherit());
611
+
612
+ match cmd.spawn() {{
613
+ Ok(mut child) => {{
614
+ let mut discovered_port: Option<u16> = None;
615
+ if let Some(stdout) = child.stdout.take() {{
616
+ let reader = BufReader::new(stdout);
617
+ for line in reader.lines() {{
618
+ match line {{
619
+ Ok(line) => {{
620
+ eprintln!("[sidecar] {{}}", line);
621
+ if let Some(port_str) = line.strip_prefix("JAC_SIDECAR_PORT=") {{
622
+ if let Ok(port) = port_str.trim().parse::<u16>() {{
623
+ discovered_port = Some(port);
624
+ break;
625
+ }}
626
+ }}
627
+ }}
628
+ Err(_) => break,
629
+ }}
630
+ }}
631
+ }}
632
+
567
633
  let mut process = SIDECAR_PROCESS.lock().unwrap();
568
634
  *process = Some(child);
569
- eprintln!("Sidecar started successfully");
635
+
636
+ if let Some(port) = discovered_port {{
637
+ let base_url = format!("http://127.0.0.1:{{}}", port);
638
+ eprintln!("Sidecar started on {{}}", base_url);
639
+ let mut url = API_BASE_URL.lock().unwrap();
640
+ *url = Some(base_url);
641
+ }} else {{
642
+ eprintln!("Error: Sidecar started but did not report its port.");
643
+ eprintln!(" Expected JAC_SIDECAR_PORT=<port> on stdout.");
644
+ return Err("Sidecar port discovery failed".into());
645
+ }}
570
646
  Ok(())
571
- }
572
- Err(e) => {
573
- eprintln!("Failed to start sidecar: {}", e);
647
+ }}
648
+ Err(e) => {{
649
+ eprintln!("Failed to start sidecar: {{}}", e);
574
650
  Err(Box::new(e))
575
- }
576
- }
577
- } else {
651
+ }}
652
+ }}
653
+ }} else {{
578
654
  eprintln!("Sidecar not found in resources, skipping auto-start");
579
655
  Ok(())
580
- }
581
- }
656
+ }}
657
+ }}
582
658
 
583
- fn stop_sidecar() {
659
+ fn stop_sidecar() {{
584
660
  let mut process = SIDECAR_PROCESS.lock().unwrap();
585
- if let Some(mut child) = process.take() {
661
+ if let Some(mut child) = process.take() {{
586
662
  let _ = child.kill();
587
663
  let _ = child.wait();
588
664
  eprintln!("Sidecar stopped");
589
- }
590
- }
665
+ }}
666
+ }}
591
667
 
592
- fn main() {
668
+ fn main() {{
593
669
  tauri::Builder::default()
594
- .setup(|app| {
595
- // Try to start sidecar on app startup
596
- if let Err(e) = find_and_start_sidecar(app.handle()) {
597
- eprintln!("Warning: Could not start sidecar: {}", e);
598
- }
670
+ .setup(|app| {{
671
+ // Start sidecar to discover dynamic port
672
+ if let Err(e) = find_and_start_sidecar(app.handle()) {{
673
+ eprintln!("Warning: Could not start sidecar: {{}}", e);
674
+ }}
675
+
676
+ // Build initialization script with API base URL
677
+ let init_js = {{
678
+ let url = API_BASE_URL.lock().unwrap();
679
+ match *url {{
680
+ Some(ref base_url) => {{
681
+ eprintln!("Injecting API base URL: {{}}", base_url);
682
+ format!(
683
+ "globalThis.__JAC_API_BASE_URL__ = '{{}}';",
684
+ base_url
685
+ )
686
+ }}
687
+ None => String::new(),
688
+ }}
689
+ }};
690
+
691
+ // Create window with initialization_script (runs BEFORE page JS)
692
+ let mut builder = tauri::WebviewWindowBuilder::new(
693
+ app,
694
+ "main",
695
+ tauri::WebviewUrl::App("index.html".into())
696
+ )
697
+ .title("{win_title}")
698
+ .inner_size({win_width}.0, {win_height}.0)
699
+ .min_inner_size({win_min_width}.0, {win_min_height}.0)
700
+ .resizable({win_resizable});
701
+
702
+ if !init_js.is_empty() {{
703
+ builder = builder.initialization_script(&init_js);
704
+ }}
705
+
706
+ builder.build()?;
707
+
599
708
  Ok(())
600
- })
601
- .on_window_event(|_window, event| {
709
+ }})
710
+ .on_window_event(|_window, event| {{
602
711
  // Clean up sidecar when last window closes
603
- if matches!(event, tauri::WindowEvent::CloseRequested { .. }) {
712
+ if matches!(event, tauri::WindowEvent::CloseRequested {{ .. }}) {{
604
713
  stop_sidecar();
605
- }
606
- })
714
+ }}
715
+ }})
607
716
  .run(tauri::generate_context!())
608
717
  .expect("error while running tauri application");
609
718
 
610
719
  // Ensure sidecar is stopped on exit
611
720
  stop_sidecar();
612
- }
721
+ }}
613
722
  ''';
614
723
 
615
724
  main_path = tauri_dir / "src" / "main.rs";
@@ -1423,11 +1532,22 @@ impl DesktopTarget.build(
1423
1532
  if not tauri_dir.exists() {
1424
1533
  raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
1425
1534
  }
1535
+ # Bake TOML base_url into the bundle; otherwise sidecar discovers port at runtime.
1536
+ toml_base_url = _get_toml_api_base_url(project_dir);
1537
+ if toml_base_url {
1538
+ os.environ[API_BASE_URL_ENV_VAR] = toml_base_url;
1539
+ }
1426
1540
  # Step 1: Build web bundle first (reuse existing pipeline)
1427
1541
  console.print(" Step 1: Building web bundle...", style="muted");
1428
1542
  web_target = WebTarget();
1429
- web_bundle_path = web_target.build(entry_file, project_dir, platform);
1543
+ try {
1544
+ web_bundle_path = web_target.build(entry_file, project_dir, platform);
1545
+ } finally {
1546
+ os.environ.pop(API_BASE_URL_ENV_VAR, None);
1547
+ }
1430
1548
  console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
1549
+ # Regenerate main.rs with latest TOML config (base_url may have changed since setup)
1550
+ _generate_main_rs(tauri_dir, api_base_url=toml_base_url);
1431
1551
  # Step 1.5: Bundle sidecar (optional - can be skipped if not needed)
1432
1552
  # This bundles the Jac backend as an executable for local use
1433
1553
  sidecar_bundled = False;
@@ -1653,6 +1773,9 @@ def _update_tauri_config_for_build(
1653
1773
  del config["build"]["beforeBuildCommand"];
1654
1774
  }
1655
1775
 
1776
+ # main.rs creates the window via WebviewWindowBuilder (for initialization_script)
1777
+ config.setdefault("app", {})["windows"] = [];
1778
+
1656
1779
  # Populate icon array if empty or missing
1657
1780
  _populate_icon_array(tauri_dir, config);
1658
1781
 
@@ -2017,7 +2140,7 @@ exec python -m jac_client.plugin.src.targets.desktop.sidecar.main "$@"
2017
2140
 
2018
2141
  """Start desktop dev server - start web dev server and launch tauri dev."""
2019
2142
  impl DesktopTarget.dev(
2020
- self: DesktopTarget, entry_file: Path, project_dir: Path
2143
+ self: DesktopTarget, entry_file: Path, project_dir: Path, api_port: int = 8000
2021
2144
  ) -> None {
2022
2145
  import from jac_client.plugin.src.vite_bundler { ViteBundler }
2023
2146
  import signal;
@@ -2068,8 +2191,12 @@ impl DesktopTarget.dev(
2068
2191
  console.print(" ✔ Module compiled for dev mode", style="success");
2069
2192
  # Step 2: Start web dev server
2070
2193
  console.print(" Starting web dev server...", style="muted");
2071
- # Create dev vite config
2072
- dev_config_path = bundler.create_dev_vite_config(entry_file, api_port=8000);
2194
+ # Desktop webview needs an explicit backend URL (same-origin doesn't work)
2195
+ dev_config_path = bundler.create_dev_vite_config(
2196
+ entry_file,
2197
+ api_port=api_port,
2198
+ api_base_url_override=_make_localhost_url(api_port)
2199
+ );
2073
2200
  # Start Vite dev server
2074
2201
  vite_process = bundler.start_dev_server(port=vite_port);
2075
2202
  if not vite_process {
@@ -2078,8 +2205,22 @@ impl DesktopTarget.dev(
2078
2205
  console.print(
2079
2206
  f" ✔ Web dev server running on http://localhost:{vite_port}", style="success"
2080
2207
  );
2081
- # Step 3: Launch tauri dev
2082
- console.print(" Launching Tauri dev window...", style="muted");
2208
+ # Step 3: Start backend API server
2209
+ toml_base_url = _get_toml_api_base_url(project_dir);
2210
+ server_port = _resolve_server_port(toml_base_url, api_port);
2211
+ # Regenerate main.rs with the actual backend URL so sidecar is skipped
2212
+ _generate_main_rs(
2213
+ tauri_dir, api_base_url=toml_base_url or _make_localhost_url(server_port)
2214
+ );
2215
+ console.print(
2216
+ f" Step 3: Starting backend server on port {server_port}...", style="muted"
2217
+ );
2218
+ server_process = _start_backend_server(entry_file, project_dir, server_port);
2219
+ console.print(
2220
+ f" ✔ Backend server starting on port {server_port}", style="success"
2221
+ );
2222
+ # Step 4: Launch tauri dev
2223
+ console.print(" Step 4: Launching Tauri dev window...", style="muted");
2083
2224
  console.print(" (Press Ctrl+C to stop)", style="muted");
2084
2225
  # Setup signal handlers for cleanup
2085
2226
  def cleanup -> None {
@@ -2092,6 +2233,14 @@ impl DesktopTarget.dev(
2092
2233
  vite_process.kill();
2093
2234
  }
2094
2235
  }
2236
+ if server_process {
2237
+ try {
2238
+ server_process.terminate();
2239
+ server_process.wait(timeout=5);
2240
+ } except Exception {
2241
+ server_process.kill();
2242
+ }
2243
+ }
2095
2244
  console.print(" ✔ Dev servers stopped", style="success");
2096
2245
  }
2097
2246
  def signal_handler(signum: int, frame: Any) -> None {
@@ -2155,6 +2304,9 @@ def _update_tauri_config_for_dev(tauri_dir: Path) -> None {
2155
2304
  del config["build"]["beforeBuildCommand"];
2156
2305
  }
2157
2306
 
2307
+ # main.rs creates the window via WebviewWindowBuilder (for initialization_script)
2308
+ config.setdefault("app", {})["windows"] = [];
2309
+
2158
2310
  # Populate icon array if empty or missing
2159
2311
  _populate_icon_array(tauri_dir, config);
2160
2312
 
@@ -2273,33 +2425,104 @@ def _run_tauri_dev(tauri_dir: Path) -> subprocess.Popen {
2273
2425
  }
2274
2426
  }
2275
2427
 
2428
+ """Start backend API server as a subprocess, returning the Popen handle."""
2429
+ def _start_backend_server(entry_file: Path, project_dir: Path, port: int) -> Any {
2430
+ import subprocess;
2431
+ import sys;
2432
+ server_process = subprocess.Popen(
2433
+ [
2434
+ sys.executable,
2435
+ "-m",
2436
+ "jaclang",
2437
+ "start",
2438
+ str(entry_file),
2439
+ "--port",
2440
+ str(port),
2441
+ "--no_client"
2442
+ ],
2443
+ cwd=str(project_dir),
2444
+ stdout=None,
2445
+ stderr=None
2446
+ );
2447
+ return server_process;
2448
+ }
2449
+
2450
+ """Resolve the API server port from toml_base_url or fall back to api_port."""
2451
+ def _resolve_server_port(toml_base_url: str, api_port: int) -> int {
2452
+ if toml_base_url {
2453
+ import from urllib.parse { urlparse }
2454
+ parsed = urlparse(toml_base_url);
2455
+ return parsed.port or api_port;
2456
+ }
2457
+ return api_port;
2458
+ }
2459
+
2276
2460
  """Start desktop app - build web bundle and launch Tauri with built bundle."""
2277
2461
  impl DesktopTarget.start(
2278
- self: DesktopTarget, entry_file: Path, project_dir: Path
2462
+ self: DesktopTarget, entry_file: Path, project_dir: Path, api_port: int = 8000
2279
2463
  ) -> None {
2280
2464
  import from jac_client.plugin.src.targets.web_target { WebTarget }
2281
2465
  import signal;
2282
2466
  import sys;
2467
+ import os;
2283
2468
  console.print("\n🖥️ Starting desktop app (Tauri)", style="bold");
2284
2469
  # Check if setup has been run
2285
2470
  tauri_dir = project_dir / "src-tauri";
2286
2471
  if not tauri_dir.exists() {
2287
2472
  raise RuntimeError("Desktop target not set up. Run 'jac setup desktop' first.") ;
2288
2473
  }
2289
- # Step 1: Build web bundle first
2474
+ # Step 1: Build web bundle with explicit backend URL for desktop webview.
2475
+ toml_base_url = _get_toml_api_base_url(project_dir);
2476
+ if not toml_base_url {
2477
+ os.environ[API_BASE_URL_ENV_VAR] = _make_localhost_url(api_port);
2478
+ }
2290
2479
  console.print(" Step 1: Building web bundle...", style="muted");
2291
2480
  web_target = WebTarget();
2292
- web_bundle_path = web_target.build(entry_file, project_dir, None);
2481
+ try {
2482
+ web_bundle_path = web_target.build(entry_file, project_dir, None);
2483
+ } finally {
2484
+ os.environ.pop(API_BASE_URL_ENV_VAR, None);
2485
+ }
2293
2486
  console.print(f" ✔ Web bundle built: {web_bundle_path}", style="success");
2294
2487
  # Step 2: Update tauri.conf.json to point to web bundle
2295
2488
  console.print(" Step 2: Updating Tauri configuration...", style="muted");
2296
2489
  _update_tauri_config_for_build(tauri_dir, project_dir, web_bundle_path);
2297
- # Step 3: Launch tauri dev (which will use the built bundle)
2298
- console.print(" Step 3: Launching Tauri app...", style="muted");
2490
+ # Step 3: Start backend API server
2491
+ server_port = _resolve_server_port(toml_base_url, api_port);
2492
+ # Regenerate main.rs with the actual backend URL so sidecar is skipped
2493
+ _generate_main_rs(
2494
+ tauri_dir, api_base_url=toml_base_url or _make_localhost_url(server_port)
2495
+ );
2496
+ console.print(
2497
+ f" Step 3: Starting backend server on port {server_port}...", style="muted"
2498
+ );
2499
+ server_process = _start_backend_server(entry_file, project_dir, server_port);
2500
+ console.print(
2501
+ f" ✔ Backend server starting on port {server_port}", style="success"
2502
+ );
2503
+ # Step 4: Launch tauri dev (which will use the built bundle)
2504
+ console.print(" Step 4: Launching Tauri app...", style="muted");
2299
2505
  console.print(" (Press Ctrl+C to stop)", style="muted");
2300
2506
  # Setup signal handlers for cleanup
2507
+ tauri_process = None;
2301
2508
  def cleanup -> None {
2302
2509
  console.print("\n Stopping app...", style="muted");
2510
+ if tauri_process {
2511
+ try {
2512
+ tauri_process.terminate();
2513
+ tauri_process.wait(timeout=5);
2514
+ } except Exception {
2515
+ tauri_process.kill();
2516
+ }
2517
+ }
2518
+ if server_process {
2519
+ try {
2520
+ server_process.terminate();
2521
+ server_process.wait(timeout=5);
2522
+ } except Exception {
2523
+ server_process.kill();
2524
+ }
2525
+ }
2303
2526
  console.print(" ✔ App stopped", style="success");
2304
2527
  }
2305
2528
  def signal_handler(signum: int, frame: Any) -> None {
@@ -2308,7 +2531,6 @@ impl DesktopTarget.start(
2308
2531
  }
2309
2532
  signal.signal(signal.SIGINT, signal_handler);
2310
2533
  signal.signal(signal.SIGTERM, signal_handler);
2311
- tauri_process = None;
2312
2534
  try {
2313
2535
  # Run tauri dev (it will use the built bundle from distDir)
2314
2536
  tauri_process = _run_tauri_dev(tauri_dir);
@@ -2327,16 +2549,6 @@ impl DesktopTarget.start(
2327
2549
  console.print(
2328
2550
  "\n Keyboard interrupt detected. Stopping app...", style="muted"
2329
2551
  );
2330
- if tauri_process {
2331
- try {
2332
- tauri_process.terminate();
2333
- tauri_process.wait(timeout=5);
2334
- } except Exception {
2335
- if tauri_process {
2336
- tauri_process.kill();
2337
- }
2338
- }
2339
- }
2340
2552
  } except Exception as e {
2341
2553
  console.error(f" Error starting desktop app: {e}");
2342
2554
  import traceback;
@@ -2,6 +2,7 @@
2
2
  import hashlib;
3
3
  import json;
4
4
  import logging;
5
+ import os;
5
6
  import shutil;
6
7
  import subprocess;
7
8
  import from pathlib { Path }
@@ -9,7 +10,9 @@ import from typing { Any, Optional }
9
10
  import from jaclang.runtimelib.client_bundle { ClientBundleError }
10
11
  import from .config_loader { JacClientConfig }
11
12
 
12
- glob logger = logging.getLogger(__name__);
13
+ glob logger = logging.getLogger(__name__),
14
+ API_BASE_URL_ENV_VAR = "JAC_CLIENT_API_BASE_URL";
15
+
13
16
  """Handles Vite bundling operations."""
14
17
  class ViteBundler {
15
18
  def init(
@@ -26,7 +29,14 @@ class ViteBundler {
26
29
  def find_css(self: ViteBundler) -> Optional[Path];
27
30
  def read_bundle(self: ViteBundler) -> tuple[str, str];
28
31
  def _has_typescript_support(self: ViteBundler) -> bool;
29
- def create_vite_config(self: ViteBundler, entry_file: Path) -> Path;
32
+ def _resolve_api_base_url(
33
+ self: ViteBundler, api_base_url_override: str = ""
34
+ ) -> str;
35
+
36
+ def create_vite_config(
37
+ self: ViteBundler, entry_file: Path, api_base_url_override: str = ""
38
+ ) -> Path;
39
+
30
40
  def _get_plugin_var_name(self: ViteBundler, plugin_name: str) -> str;
31
41
  def _format_plugin_options(self: ViteBundler, options: dict) -> str;
32
42
  def _format_config_object(self: ViteBundler, config: dict, indent: int = 0) -> str;
@@ -42,9 +52,14 @@ class ViteBundler {
42
52
 
43
53
  """Create a dev-mode vite config with API proxy for HMR."""
44
54
  def create_dev_vite_config(
45
- self: ViteBundler, entry_file: Path, api_port: int = 8000
55
+ self: ViteBundler,
56
+ entry_file: Path,
57
+ api_port: int = 8000,
58
+ api_base_url_override: str = ""
46
59
  ) -> Path;
47
60
 
48
61
  """Start Vite dev server as a subprocess."""
49
62
  def start_dev_server(self: ViteBundler, port: int = 3000) -> Any;
50
63
  }
64
+ # Env var used to pass API base URL through deep call chains
65
+ # (e.g., DesktopTarget.start -> WebTarget.build -> ViteBundler.create_vite_config)