jac-client 0.2.11__py3-none-any.whl → 0.2.13__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.
- jac_client/plugin/cli.jac +3 -3
- jac_client/plugin/client_runtime.cl.jac +3 -2
- jac_client/plugin/impl/client_runtime.impl.jac +17 -6
- jac_client/plugin/src/compiler.jac +4 -0
- jac_client/plugin/src/config_loader.jac +1 -0
- jac_client/plugin/src/impl/compiler.impl.jac +16 -39
- jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +74 -23
- jac_client/plugin/src/targets/desktop/sidecar/main.py +42 -23
- jac_client/plugin/src/targets/desktop_target.jac +4 -2
- jac_client/plugin/src/targets/impl/desktop_target.impl.jac +324 -112
- jac_client/plugin/src/vite_bundler.jac +18 -3
- jac_client/plugin/utils/__init__.jac +1 -0
- jac_client/plugin/utils/client_deps.jac +14 -0
- jac_client/plugin/utils/impl/client_deps.impl.jac +73 -0
- jac_client/templates/client.jacpack +0 -4
- jac_client/templates/fullstack.jacpack +0 -4
- jac_client/tests/test_cli.py +142 -0
- jac_client/tests/test_desktop_api_url.py +854 -0
- jac_client/tests/test_e2e.py +12 -12
- jac_client/tests/test_it.py +209 -11
- {jac_client-0.2.11.dist-info → jac_client-0.2.13.dist-info}/METADATA +2 -2
- {jac_client-0.2.11.dist-info → jac_client-0.2.13.dist-info}/RECORD +26 -23
- {jac_client-0.2.11.dist-info → jac_client-0.2.13.dist-info}/WHEEL +0 -0
- {jac_client-0.2.11.dist-info → jac_client-0.2.13.dist-info}/entry_points.txt +0 -0
- {jac_client-0.2.11.dist-info → jac_client-0.2.13.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
|
-
|
|
467
|
-
|
|
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::
|
|
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("
|
|
605
|
+
}}
|
|
606
|
+
cmd.arg("--port").arg("0"); // OS assigns free port
|
|
562
607
|
cmd.arg("--host").arg("127.0.0.1");
|
|
563
608
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
#
|
|
2072
|
-
dev_config_path = bundler.create_dev_vite_config(
|
|
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:
|
|
2082
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
2298
|
-
|
|
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
|
|
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,
|
|
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)
|