jac-client 0.2.12__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 CHANGED
@@ -426,15 +426,15 @@ def _handle_start_target(ctx: HookContext) -> None {
426
426
  ctx.set_data("cancel_return_code", 1);
427
427
  return;
428
428
  }
429
- # Check if --dev flag is set
430
429
  dev_mode = ctx.get_arg("dev", False);
430
+ api_port = ctx.get_arg("port", 8000);
431
431
  try {
432
432
  if dev_mode {
433
433
  # Desktop target with --dev: launch Tauri dev mode (hot reload)
434
- target.dev(entry_path, project_dir);
434
+ target.dev(entry_path, project_dir, api_port=api_port);
435
435
  } else {
436
436
  # Desktop target without --dev: build web bundle and launch Tauri with built bundle
437
- target.start(entry_path, project_dir);
437
+ target.start(entry_path, project_dir, api_port=api_port);
438
438
  }
439
439
  ctx.set_data("cancel_execution", True);
440
440
  ctx.set_data("cancel_return_code", 0);
@@ -4,7 +4,7 @@ import from 'react' { * as React }
4
4
  import from 'react' { useState as reactUseState, useEffect as reactUseEffect }
5
5
  import from 'react-dom/client' { * as ReactDOM }
6
6
  import from 'react-router-dom' {
7
- HashRouter as ReactRouterHashRouter,
7
+ BrowserRouter as ReactRouterBrowserRouter,
8
8
  Routes as ReactRouterRoutes,
9
9
  Route as ReactRouterRoute,
10
10
  Link as ReactRouterLink,
@@ -21,7 +21,7 @@ def:pub __jacJsx(tag: any, props: dict = {}, children: any = []) -> any;
21
21
  glob:
22
22
  pub useState = reactUseState,
23
23
  useEffect = reactUseEffect,
24
- Router = ReactRouterHashRouter,
24
+ Router = ReactRouterBrowserRouter,
25
25
  Routes = ReactRouterRoutes,
26
26
  Route = ReactRouterRoute,
27
27
  Link = ReactRouterLink,
@@ -40,6 +40,7 @@ async def:pub jacSignup(username: str, password: str) -> dict;
40
40
  async def:pub jacLogin(username: str, password: str) -> bool;
41
41
  def:pub jacLogout -> None;
42
42
  def:pub jacIsLoggedIn -> bool;
43
+ def:pub __getApiBaseUrl -> str;
43
44
  def:pub __getLocalStorage(key: str) -> str;
44
45
  def:pub __setLocalStorage(key: str, value: str) -> None;
45
46
  def:pub __removeLocalStorage(key: str) -> None;
@@ -42,14 +42,22 @@ impl useRouter -> dict {
42
42
  }
43
43
 
44
44
  impl navigate(path: str) -> None {
45
- window.location.hash = "#" + path;
45
+ window.history.pushState({}, "", path);
46
+ window.dispatchEvent(Reflect.construct(PopStateEvent, ["popstate"]));
47
+ }
48
+
49
+ impl __getApiBaseUrl -> str {
50
+ # globalThis.__JAC_API_BASE_URL__ is replaced by Vite's define at build time.
51
+ # Falls back to empty string (same-origin) when not configured.
52
+ return globalThis.__JAC_API_BASE_URL__ || "";
46
53
  }
47
54
 
48
55
  impl __jacSpawn(left: str, right: str = "", fields: dict = {}) -> any {
49
56
  token = __getLocalStorage("jac_token");
50
- url = f"/walker/{left}";
57
+ base_url = __getApiBaseUrl();
58
+ url = f"{base_url}/walker/{left}";
51
59
  if right != "" {
52
- url = f"/walker/{left}/{right}";
60
+ url = f"{base_url}/walker/{left}/{right}";
53
61
  }
54
62
  response = await fetch(
55
63
  url,
@@ -78,8 +86,9 @@ impl jacSpawn(left: str, right: str = "", fields: dict = {}) -> any {
78
86
 
79
87
  impl __jacCallFunction(function_name: str, args: dict = {}) -> any {
80
88
  token = __getLocalStorage("jac_token");
89
+ base_url = __getApiBaseUrl();
81
90
  response = await fetch(
82
- f"/function/{function_name}",
91
+ f"{base_url}/function/{function_name}",
83
92
  {
84
93
  "method": "POST",
85
94
  "headers": {
@@ -104,8 +113,9 @@ impl __jacCallFunction(function_name: str, args: dict = {}) -> any {
104
113
  }
105
114
 
106
115
  impl jacSignup(username: str, password: str) -> dict {
116
+ base_url = __getApiBaseUrl();
107
117
  response = await fetch(
108
- "/user/register",
118
+ f"{base_url}/user/register",
109
119
  {
110
120
  "method": "POST",
111
121
  "headers": {"Content-Type": "application/json"},
@@ -140,8 +150,9 @@ impl jacSignup(username: str, password: str) -> dict {
140
150
  }
141
151
 
142
152
  impl jacLogin(username: str, password: str) -> bool {
153
+ base_url = __getApiBaseUrl();
143
154
  response = await fetch(
144
- "/user/login",
155
+ f"{base_url}/user/login",
145
156
  {
146
157
  "method": "POST",
147
158
  "headers": {"Content-Type": "application/json"},
@@ -63,4 +63,8 @@ class ViteCompiler {
63
63
  def compile_and_bundle(
64
64
  self: ViteCompiler, module: ModuleType, module_path: Path
65
65
  ) -> tuple[str, str, list[str], list[str]];
66
+
67
+ def compile(
68
+ self: ViteCompiler, module: ModuleType, module_path: Path
69
+ ) -> tuple[list[str], list[str]];
66
70
  }
@@ -19,6 +19,7 @@ class JacClientConfig(PluginConfigBase) {
19
19
  override def get_default_config(self: JacClientConfig) -> dict[str, Any];
20
20
  override def load(self: JacClientConfig) -> dict[str, Any];
21
21
  def save(self: JacClientConfig) -> None;
22
+ def get_api_config(self: JacClientConfig) -> dict[str, Any];
22
23
  def get_vite_config(self: JacClientConfig) -> dict[str, Any];
23
24
  def get_ts_config(self: JacClientConfig) -> dict[str, Any];
24
25
  def get_configs(self: JacClientConfig) -> dict[str, Any];
@@ -12,10 +12,10 @@ impl ViteCompiler._get_client_dir(self: ViteCompiler) -> Path {
12
12
  return self.project_dir / '.jac' / 'client';
13
13
  }
14
14
 
15
- """Compile module and dependencies, then bundle with Vite."""
16
- impl ViteCompiler.compile_and_bundle(
15
+ """Compile module and dependencies without bundling (for dev mode)."""
16
+ impl ViteCompiler.compile(
17
17
  self: ViteCompiler, module: ModuleType, module_path: Path
18
- ) -> tuple[str, str, list[str], list[str]] {
18
+ ) -> tuple[list[str], list[str]] {
19
19
  (module_js, mod, module_manifest) = self.jac_compiler.compile_module(module_path);
20
20
  collected_exports: set[str] = set(
21
21
  self.jac_compiler.extract_exports(module_manifest)
@@ -32,13 +32,22 @@ impl ViteCompiler.compile_and_bundle(
32
32
  self.compile_runtime_utils();
33
33
  self.copy_root_assets();
34
34
  self.create_entry_file(module_path);
35
+ # Return exports and globals without bundling
36
+ client_exports = sorted(collected_exports);
37
+ client_globals = list(collected_globals.keys());
38
+ return (client_exports, client_globals);
39
+ }
40
+
41
+ """Compile module and dependencies, then bundle with Vite."""
42
+ impl ViteCompiler.compile_and_bundle(
43
+ self: ViteCompiler, module: ModuleType, module_path: Path
44
+ ) -> tuple[str, str, list[str], list[str]] {
45
+ (client_exports, client_globals) = self.compile(module, module_path);
35
46
  # Vite handles JSX/TSX transpilation natively with Bun - no Babel needed
36
47
  # Vite builds directly from compiled/ directory
37
48
  entry_file = self.compiled_dir / '_entry.js';
38
49
  self.vite_bundler.build(entry_file=entry_file);
39
50
  (bundle_code, bundle_hash) = self.vite_bundler.read_bundle();
40
- client_exports = sorted(collected_exports);
41
- client_globals = list(collected_globals.keys());
42
51
  return (bundle_code, bundle_hash, client_exports, client_globals);
43
52
  }
44
53
 
@@ -187,20 +196,7 @@ impl ViteCompiler.compile_dependencies_recursively(
187
196
  output_path = self.compiled_dir / name;
188
197
  }
189
198
  output_path.parent.mkdir(parents=True, exist_ok=True);
190
- # Add source file header comment for better error messages
191
199
  source_header = f"/* Source: {module_path} */\n";
192
- # Add ES module exports so Vite can resolve named imports between modules
193
- module_export_names = sorted(set(exports_list + list(non_root_globals.keys())));
194
- if module_export_names {
195
- import re;
196
- # Strip any existing export declarations to avoid duplicates
197
- clean_combined_js = re.sub(r'\nexport\s*\{[^}]*\}\s*;?\s*', '\n', combined_js);
198
- clean_combined_js = re.sub(
199
- r'\bexport\s+(let|const|var|function|class)\b', r'\1', clean_combined_js
200
- );
201
- export_stmt = f"\nexport {{ {', '.join(module_export_names)} }};\n";
202
- combined_js = clean_combined_js + export_stmt;
203
- }
204
200
  output_path.write_text(source_header + combined_js, encoding='utf-8');
205
201
  if (not manifest or not manifest.imports) {
206
202
  return;
@@ -240,34 +236,15 @@ impl ViteCompiler.compile_runtime_utils(self: ViteCompiler) -> tuple[str, list[s
240
236
  runtimeutils_exports_list = self.jac_compiler.extract_exports(
241
237
  runtimeutils_manifest
242
238
  );
243
- # Include both function exports and glob exports (e.g., useState, useEffect)
244
239
  glob_names = list(runtimeutils_manifest.globals) if runtimeutils_manifest else [];
245
240
  all_exports = sorted(
246
241
  set(runtimeutils_exports_list + self.ROUTER_EXPORTS + glob_names)
247
242
  );
248
- # Strip all existing export statements from compiled JS to avoid duplicates.
249
- # The codegen produces `export let`, `export function`, `export class`, and
250
- # `export { ... }` forms. We remove the export keyword from declarations and
251
- # remove export blocks entirely, then add one comprehensive export at the end.
252
- import re;
253
- clean_js = re.sub(r'\nexport\s*\{[^}]*\}\s*;?\s*', '\n', runtimeutils_js);
254
- clean_js = re.sub(r'\bexport\s+(let|const|var|function|class)\b', r'\1', clean_js);
255
- # Append single comprehensive ES module export for Vite to resolve @jac/runtime
256
- export_names = [
257
- e
258
- for e in all_exports
259
- if not e.startswith('_')
260
- ];
261
- # Also include internal names that are imported by compiled modules
262
- internal_exports = ['__jacJsx', '__jacSpawn', '__jacCallFunction'];
263
- all_export_names = sorted(set(export_names + internal_exports));
264
- export_stmt = f"\nexport {{ {', '.join(all_export_names)} }};\n";
265
- combined_runtime_utils_js = clean_js + export_stmt;
266
243
  self.compiled_dir.mkdir(parents=True, exist_ok=True);
267
244
  (self.compiled_dir / 'client_runtime.js').write_text(
268
- combined_runtime_utils_js, encoding='utf-8'
245
+ runtimeutils_js, encoding='utf-8'
269
246
  );
270
- return (combined_runtime_utils_js, all_exports);
247
+ return (runtimeutils_js, all_exports);
271
248
  }
272
249
 
273
250
  """Initialize the Vite compiler."""
@@ -19,6 +19,7 @@ impl JacClientConfig.get_plugin_name(self: JacClientConfig) -> str {
19
19
  """Get default configuration structure for client."""
20
20
  impl JacClientConfig.get_default_config(self: JacClientConfig) -> dict[str, Any] {
21
21
  return {
22
+ 'api': {'base_url': ''},
22
23
  'vite': {
23
24
  'plugins': [],
24
25
  'lib_imports': [],
@@ -60,6 +61,7 @@ impl JacClientConfig.load(self: JacClientConfig) -> dict[str, Any] {
60
61
  else {};
61
62
  # Build user config in the expected internal format
62
63
  user_config: dict[str, Any] = {
64
+ 'api': client_config.get('api', {}),
63
65
  'vite': client_config.get('vite', {}),
64
66
  'ts': client_config.get('ts', {}),
65
67
  'configs': client_config.get('configs', {}),
@@ -81,6 +83,12 @@ impl JacClientConfig.get_package_config(self: JacClientConfig) -> dict[str, Any]
81
83
  return config.get('package', {});
82
84
  }
83
85
 
86
+ """Get API configuration (base_url for backend)."""
87
+ impl JacClientConfig.get_api_config(self: JacClientConfig) -> dict[str, Any] {
88
+ config = self.load();
89
+ return config.get('api', {});
90
+ }
91
+
84
92
  """Get Vite-specific configuration."""
85
93
  impl JacClientConfig.get_vite_config(self: JacClientConfig) -> dict[str, Any] {
86
94
  config = self.load();
@@ -216,6 +216,19 @@ impl ViteBundler._format_plugin_options(self: ViteBundler, options: dict) -> str
216
216
  return '{ ' + ', '.join(items) + ' }';
217
217
  }
218
218
 
219
+ """Resolve the API base URL using a consistent priority chain.
220
+
221
+ Priority: jac.toml base_url > direct override > env var (desktop start) > "" (same-origin)
222
+ """
223
+ impl ViteBundler._resolve_api_base_url(
224
+ self: ViteBundler, api_base_url_override: str = ""
225
+ ) -> str {
226
+ api_config_data = self.config_loader.get_api_config();
227
+ toml_base_url = api_config_data.get('base_url', '');
228
+ env_override = os.environ.get(API_BASE_URL_ENV_VAR, '');
229
+ return toml_base_url or api_base_url_override or env_override;
230
+ }
231
+
219
232
  """Get a valid JavaScript variable name from plugin module name."""
220
233
  impl ViteBundler._get_plugin_var_name(self: ViteBundler, plugin_name: str) -> str {
221
234
  name = plugin_name.split('/')[-1];
@@ -224,13 +237,20 @@ impl ViteBundler._get_plugin_var_name(self: ViteBundler, plugin_name: str) -> st
224
237
  return name;
225
238
  }
226
239
 
227
- """Create vite.config.js from config.json during bundling."""
228
- impl ViteBundler.create_vite_config(self: ViteBundler, entry_file: Path) -> Path {
240
+ """Create vite.config.js from config.json during bundling.
241
+
242
+ api_base_url_override: overrides jac.toml base_url for desktop targets
243
+ where the CLI knows the backend port at startup time.
244
+ """
245
+ impl ViteBundler.create_vite_config(
246
+ self: ViteBundler, entry_file: Path, api_base_url_override: str = ""
247
+ ) -> Path {
229
248
  build_dir = self._get_client_dir();
230
249
  build_dir.mkdir(parents=True, exist_ok=True);
231
250
  configs_dir = build_dir / 'configs';
232
251
  configs_dir.mkdir(exist_ok=True);
233
252
  vite_config_data = self.config_loader.get_vite_config();
253
+ api_base_url = self._resolve_api_base_url(api_base_url_override);
234
254
  config_path = configs_dir / 'vite.config.js';
235
255
  # TypeScript is always enabled by default
236
256
  try {
@@ -355,6 +375,9 @@ function jacSourceMapper() {{
355
375
  */
356
376
 
357
377
  export default defineConfig({{
378
+ define: {{
379
+ 'globalThis.__JAC_API_BASE_URL__': '"{api_base_url}"',
380
+ }},
358
381
  plugins: [
359
382
  jacSourceMapper(),{(newline + plugins_str + newline + ' ') if plugins_str else ''}],
360
383
  root: buildDir, // base folder (.jac/client/) so vite can find node_modules
@@ -555,15 +578,23 @@ impl ViteBundler.init(
555
578
  self.output_dir = output_dir or (self._get_client_dir() / 'dist');
556
579
  }
557
580
 
558
- """Create a dev-mode vite config with API proxy for HMR."""
581
+ """Create a dev-mode vite config with API proxy for HMR.
582
+
583
+ api_base_url_override: when set, API calls go directly to this URL
584
+ instead of via proxy (used by desktop targets).
585
+ """
559
586
  impl ViteBundler.create_dev_vite_config(
560
- self: ViteBundler, entry_file: Path, api_port: int = 8000
587
+ self: ViteBundler,
588
+ entry_file: Path,
589
+ api_port: int = 8000,
590
+ api_base_url_override: str = ""
561
591
  ) -> Path {
562
592
  build_dir = self._get_client_dir();
563
593
  build_dir.mkdir(parents=True, exist_ok=True);
564
594
  configs_dir = build_dir / 'configs';
565
595
  configs_dir.mkdir(exist_ok=True);
566
596
  config_path = configs_dir / 'vite.dev.config.js';
597
+ api_base_url = self._resolve_api_base_url(api_base_url_override);
567
598
  # Get entry file relative path
568
599
  try {
569
600
  entry_relative = entry_file.relative_to(build_dir).as_posix();
@@ -599,9 +630,13 @@ const projectRoot = path.resolve(__dirname, "../../..");
599
630
  * Proxies API routes to Python server at localhost:{api_port}
600
631
  */
601
632
  export default defineConfig({{
633
+ define: {{
634
+ 'globalThis.__JAC_API_BASE_URL__': '"{api_base_url}"',
635
+ }},
602
636
  plugins: [react()],
603
637
  root: buildDir,
604
638
  publicDir: false,
639
+ appType: 'spa',
605
640
  build: {{
606
641
  sourcemap: true, // Enable source maps for better error messages
607
642
  }},
@@ -11,7 +11,7 @@ Usage:
11
11
 
12
12
  Options:
13
13
  --module-path PATH Path to the .jac module file (default: main.jac)
14
- --port PORT Port to bind the API server (default: 8000)
14
+ --port PORT Port to bind the API server (default: 8000, 0 = auto)
15
15
  --base-path PATH Base path for the project (default: current directory)
16
16
  --host HOST Host to bind to (default: 127.0.0.1)
17
17
  --help Show this help message
@@ -20,10 +20,16 @@ Options:
20
20
  from __future__ import annotations
21
21
 
22
22
  import argparse
23
+ import socket
23
24
  import sys
24
25
  from pathlib import Path
25
26
 
26
- from jaclang.cli.console import console
27
+
28
+ def _find_free_port(host: str = "127.0.0.1") -> int:
29
+ """Find and return a free port on the given host."""
30
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
31
+ s.bind((host, 0))
32
+ return s.getsockname()[1]
27
33
 
28
34
 
29
35
  def main():
@@ -41,7 +47,7 @@ def main():
41
47
  "--port",
42
48
  type=int,
43
49
  default=8000,
44
- help="Port to bind the API server (default: 8000)",
50
+ help="Port to bind the API server (default: 8000, 0 = auto-assign free port)",
45
51
  )
46
52
  parser.add_argument(
47
53
  "--base-path",
@@ -58,6 +64,10 @@ def main():
58
64
 
59
65
  args = parser.parse_args()
60
66
 
67
+ port = args.port
68
+ if port == 0:
69
+ port = _find_free_port(args.host)
70
+
61
71
  # Determine base path
62
72
  if args.base_path:
63
73
  base_path = Path(args.base_path).resolve()
@@ -75,8 +85,9 @@ def main():
75
85
  module_path = base_path / module_path
76
86
 
77
87
  if not module_path.exists():
78
- console.print(f"Error: Module file not found: {module_path}", file=sys.stderr)
79
- console.print(f" Base path: {base_path}", file=sys.stderr)
88
+ # Console not yet available (jaclang not imported)
89
+ sys.stderr.write(f"Error: Module file not found: {module_path}\n")
90
+ sys.stderr.write(f" Base path: {base_path}\n")
80
91
  sys.exit(1)
81
92
 
82
93
  # Extract module name (without .jac extension)
@@ -88,25 +99,25 @@ def main():
88
99
  # Import jaclang (must be installed via pip)
89
100
  from jaclang.pycore.runtime import JacRuntime as Jac
90
101
  except ImportError as e:
91
- console.print(f"Error: Failed to import Jac runtime: {e}", file=sys.stderr)
92
- console.print(
93
- " Make sure jaclang is installed: pip install jaclang", file=sys.stderr
94
- )
102
+ # Console not available (jaclang import failed)
103
+ sys.stderr.write(f"Error: Failed to import Jac runtime: {e}\n")
104
+ sys.stderr.write(" Make sure jaclang is installed: pip install jaclang\n")
95
105
  sys.exit(1)
96
106
 
107
+ # Get the console now that jaclang is available
108
+ from jaclang.cli.console import console
109
+
97
110
  # Initialize Jac runtime
98
111
  try:
99
112
  # Import the module
100
113
  Jac.jac_import(target=module_name, base_path=str(module_base), lng="jac")
101
114
  if Jac.program.errors_had:
102
- console.print("Error: Failed to compile module:", file=sys.stderr)
115
+ console.error("Failed to compile module:")
103
116
  for error in Jac.program.errors_had:
104
- console.print(f" {error}", file=sys.stderr)
117
+ console.print(f" {error}", style="error")
105
118
  sys.exit(1)
106
119
  except Exception as e:
107
- console.print(
108
- f"Error: Failed to load module '{module_name}': {e}", file=sys.stderr
109
- )
120
+ console.error(f"Failed to load module '{module_name}': {e}")
110
121
  import traceback
111
122
 
112
123
  traceback.print_exc()
@@ -117,23 +128,31 @@ def main():
117
128
  # Get server class (allows plugins like jac-scale to provide enhanced server)
118
129
  server_class = Jac.get_api_server_class()
119
130
  server = server_class(
120
- module_name=module_name, port=args.port, base_path=str(base_path)
131
+ module_name=module_name, port=port, base_path=str(base_path)
121
132
  )
122
133
 
123
- console.print("Jac Sidecar starting...")
124
- console.print(f" Module: {module_name}")
125
- console.print(f" Base path: {base_path}")
126
- console.print(f" Server: http://{args.host}:{args.port}")
127
- console.print("\nPress Ctrl+C to stop the server\n")
134
+ # MUST be raw stdout — Tauri host reads this line to discover the port.
135
+ # Cannot use console here; Tauri parses this exact format.
136
+ sys.stdout.write(f"JAC_SIDECAR_PORT={port}\n")
137
+ sys.stdout.flush()
138
+
139
+ # stderr: Tauri drops the stdout pipe after reading the port marker,
140
+ # so any further stdout writes raise BrokenPipeError.
141
+ console.print("Jac Sidecar starting...", style="bold")
142
+ console.print(f" Module: {module_name}", style="muted")
143
+ console.print(f" Base path: {base_path}", style="muted")
144
+ console.print(f" Server: http://{args.host}:{port}", style="muted")
145
+ console.print("")
128
146
 
129
147
  # Start the server (blocks until interrupted)
130
- server.start(dev=False)
148
+ # no_client=True: client bundle is already embedded in the Tauri webview
149
+ server.start(dev=False, no_client=True)
131
150
 
132
151
  except KeyboardInterrupt:
133
- console.print("\nShutting down sidecar...")
152
+ console.print("\nShutting down sidecar...", style="muted")
134
153
  sys.exit(0)
135
154
  except Exception as e:
136
- console.print(f"Error: Server failed to start: {e}", file=sys.stderr)
155
+ console.error(f"Server failed to start: {e}")
137
156
  import traceback
138
157
 
139
158
  traceback.print_exc()
@@ -28,10 +28,12 @@ class DesktopTarget(ClientTarget) {
28
28
  ) -> Path;
29
29
 
30
30
  """Start desktop dev server - start web dev server and launch tauri dev."""
31
- override def dev(self: DesktopTarget, entry_file: Path, project_dir: Path) -> None;
31
+ override def dev(
32
+ self: DesktopTarget, entry_file: Path, project_dir: Path, api_port: int = 8000
33
+ ) -> None;
32
34
 
33
35
  """Start desktop app - build web bundle and launch Tauri with built bundle."""
34
36
  override def start(
35
- self: DesktopTarget, entry_file: Path, project_dir: Path
37
+ self: DesktopTarget, entry_file: Path, project_dir: Path, api_port: int = 8000
36
38
  ) -> None;
37
39
  }