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
@@ -1,4 +1,4 @@
1
- impl __jacJsx(tag: any, props: dict = {}, children: any = []) -> any {
1
+ impl __jacJsx(tag: any, props: dict = {}, children: any = []) -> JsxElement {
2
2
  if tag == None {
3
3
  tag = React.Fragment;
4
4
  }
@@ -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"},
@@ -194,6 +205,17 @@ impl __removeLocalStorage(key: str) -> None {
194
205
  }
195
206
  }
196
207
 
208
+ """Auth guard component for file-based routing. Renders child routes if
209
+ authenticated, otherwise redirects to the login path."""
210
+ impl AuthGuard(redirect: str = "/login") -> any {
211
+ if jacIsLoggedIn() {
212
+ return
213
+ <ReactRouterOutlet />;
214
+ }
215
+ return
216
+ <ReactRouterNavigate to={redirect} replace={True} />;
217
+ }
218
+
197
219
  impl ErrorFallback(error: str, resetErrorBoundary: any) -> any {
198
220
  return
199
221
  <div
@@ -10,7 +10,6 @@ configuration system, registering:
10
10
 
11
11
  import os;
12
12
  import subprocess;
13
- import sys;
14
13
  import json;
15
14
  import from pathlib { Path }
16
15
  import from typing { Any }
@@ -147,9 +146,7 @@ def _load_template(template_name: str) -> dict[str, Any] | None {
147
146
 
148
147
  return data;
149
148
  } except Exception as e {
150
- print(
151
- f"Warning: Could not load {template_name} template: {e}", file=sys.stderr
152
- );
149
+ console.error(f"Warning: Could not load {template_name} template: {e}");
153
150
  return None;
154
151
  }
155
152
  }
@@ -191,10 +188,7 @@ def _post_create_client(project_path: Path, project_name: str) -> None {
191
188
  # Verify jac.toml exists
192
189
  toml_path = project_path / "jac.toml";
193
190
  if not toml_path.exists() {
194
- print(
195
- "Warning: jac.toml not found, skipping package installation",
196
- file=sys.stderr
197
- );
191
+ console.error("Warning: jac.toml not found, skipping package installation");
198
192
  return;
199
193
  }
200
194
 
@@ -213,9 +207,8 @@ def _post_create_client(project_path: Path, project_name: str) -> None {
213
207
  build_package_json = client_dir / 'package.json';
214
208
 
215
209
  if not configs_package_json.exists() {
216
- print(
217
- "Warning: package.json was not generated, skipping package installation",
218
- file=sys.stderr
210
+ console.error(
211
+ "Warning: package.json was not generated, skipping package installation"
219
212
  );
220
213
  return;
221
214
  }
@@ -8,6 +8,7 @@ import from jaclang.runtimelib.client_bundle { ClientBundleError }
8
8
  import from .asset_processor { AssetProcessor }
9
9
  import from .import_processor { ImportProcessor }
10
10
  import from .jac_to_js { JacToJSCompiler }
11
+ import from .route_scanner { RouteScanner, RouteEntry }
11
12
  import from .vite_bundler { ViteBundler }
12
13
 
13
14
  with entry {
@@ -23,12 +24,16 @@ class ViteCompiler {
23
24
  'Route',
24
25
  'Link',
25
26
  'Navigate',
27
+ 'Outlet',
26
28
  'useNavigate',
27
29
  'useLocation',
28
- 'useParams'
30
+ 'useParams',
31
+ 'AuthGuard'
29
32
  ];
33
+ COMPOUND_EXTENSIONS = ['.cl.jac', '.impl.jac', '.test.jac'];
30
34
  }
31
35
 
36
+ def _jac_path_to_js(self: ViteCompiler, rel_str: str) -> str;
32
37
  def init(
33
38
  self: ViteCompiler,
34
39
  vite_package_json: Path,
@@ -60,7 +65,20 @@ class ViteCompiler {
60
65
 
61
66
  def copy_root_assets(self: ViteCompiler) -> None;
62
67
  def create_entry_file(self: ViteCompiler, module_path: Path) -> None;
68
+ def _create_pages_entry_content(self: ViteCompiler, module_path: Path) -> str;
69
+ def _scan_and_compile_pages(
70
+ self: ViteCompiler,
71
+ visited: (set[Path] | None) = None,
72
+ collected_exports: (set[str] | None) = None,
73
+ collected_globals: (dict[(str, Any)] | None) = None
74
+ ) -> bool;
75
+
76
+ def _generate_routes_manifest(self: ViteCompiler) -> None;
63
77
  def compile_and_bundle(
64
78
  self: ViteCompiler, module: ModuleType, module_path: Path
65
79
  ) -> tuple[str, str, list[str], list[str]];
80
+
81
+ def compile(
82
+ self: ViteCompiler, module: ModuleType, module_path: Path
83
+ ) -> tuple[list[str], list[str]];
66
84
  }
@@ -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,18 +12,26 @@ 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)
22
22
  );
23
23
  client_globals_map = self.jac_compiler.extract_globals(module_manifest, module);
24
24
  collected_globals: dict[(str, Any)] = dict(client_globals_map);
25
+ visited: set[Path] = set();
25
26
  self.compile_dependencies_recursively(
26
27
  module_path,
28
+ visited=visited,
29
+ collected_exports=collected_exports,
30
+ collected_globals=collected_globals
31
+ );
32
+ # Scan pages/ directory for file-based routing (compiles page files too)
33
+ self._has_pages = self._scan_and_compile_pages(
34
+ visited=visited,
27
35
  collected_exports=collected_exports,
28
36
  collected_globals=collected_globals
29
37
  );
@@ -32,13 +40,22 @@ impl ViteCompiler.compile_and_bundle(
32
40
  self.compile_runtime_utils();
33
41
  self.copy_root_assets();
34
42
  self.create_entry_file(module_path);
43
+ # Return exports and globals without bundling
44
+ client_exports = sorted(collected_exports);
45
+ client_globals = list(collected_globals.keys());
46
+ return (client_exports, client_globals);
47
+ }
48
+
49
+ """Compile module and dependencies, then bundle with Vite."""
50
+ impl ViteCompiler.compile_and_bundle(
51
+ self: ViteCompiler, module: ModuleType, module_path: Path
52
+ ) -> tuple[str, str, list[str], list[str]] {
53
+ (client_exports, client_globals) = self.compile(module, module_path);
35
54
  # Vite handles JSX/TSX transpilation natively with Bun - no Babel needed
36
55
  # Vite builds directly from compiled/ directory
37
56
  entry_file = self.compiled_dir / '_entry.js';
38
57
  self.vite_bundler.build(entry_file=entry_file);
39
58
  (bundle_code, bundle_hash) = self.vite_bundler.read_bundle();
40
- client_exports = sorted(collected_exports);
41
- client_globals = list(collected_globals.keys());
42
59
  return (bundle_code, bundle_hash, client_exports, client_globals);
43
60
  }
44
61
 
@@ -46,12 +63,110 @@ impl ViteCompiler.compile_and_bundle(
46
63
  impl ViteCompiler.create_entry_file(self: ViteCompiler, module_path: Path) -> None {
47
64
  # Use _entry.js to avoid conflict with compiled modules that may be named main.js
48
65
  entry_file = self.compiled_dir / '_entry.js';
49
- # Derive the app module filename from the entry point (e.g., main.jac -> main.js, app.jac -> app.js)
50
- app_module_name = module_path.stem;
51
- entry_content = f'import React from "react";\nimport {{ createRoot }} from "react-dom/client";\nimport {{ app as App }} from "./{app_module_name}.js";\nimport {{ JacClientErrorBoundary, ErrorFallback }} from "@jac/runtime";\n\nconst root = createRoot(document.getElementById("root"));\nroot.render(\n\tReact.createElement(\n\t\tJacClientErrorBoundary,{{ FallbackComponent: ErrorFallback }},\n\t\tReact.createElement(App, null)\n\t)\n);\n';
66
+ if self._has_pages {
67
+ # File-based routing mode: import from generated _routes.js
68
+ entry_content = self._create_pages_entry_content(module_path);
69
+ } else {
70
+ # Explicit routing mode: import app from entry module (original behavior)
71
+ app_module_name = module_path.stem;
72
+ entry_content = f'import React from "react";\nimport {{ createRoot }} from "react-dom/client";\nimport {{ app as App }} from "./{app_module_name}.js";\nimport {{ JacClientErrorBoundary, ErrorFallback }} from "@jac/runtime";\n\nconst root = createRoot(document.getElementById("root"));\nroot.render(\n\tReact.createElement(\n\t\tJacClientErrorBoundary,{{ FallbackComponent: ErrorFallback }},\n\t\tReact.createElement(App, null)\n\t)\n);\n';
73
+ }
52
74
  entry_file.write_text(entry_content, encoding='utf-8');
53
75
  }
54
76
 
77
+ impl ViteCompiler._create_pages_entry_content(
78
+ self: ViteCompiler, module_path: Path
79
+ ) -> str {
80
+ lines: list[str] = [];
81
+ auth_redirect = "/login";
82
+ try {
83
+ import from ..config_loader { PluginConfig }
84
+ config = PluginConfig.load(self.project_dir);
85
+ if config and config?.routing and config.routing {
86
+ auth_redirect = config.routing.get('auth_redirect', '/login');
87
+ }
88
+ } except Exception { }
89
+ # Check if main.jac exists and exports app (used as wrapper)
90
+ app_module = self.compiled_dir / f"{module_path.stem}.js";
91
+ has_app_wrapper = app_module.exists();
92
+ # --- Imports ---
93
+ lines.append('import React from "react";');
94
+ lines.append('import { createRoot } from "react-dom/client";');
95
+ lines.append('import { BrowserRouter, Routes, Route } from "react-router-dom";');
96
+ lines.append(
97
+ 'import { JacClientErrorBoundary, ErrorFallback, AuthGuard } from "@jac/runtime";'
98
+ );
99
+ lines.append('import { routes, layouts } from "./_routes.js";');
100
+ if has_app_wrapper {
101
+ lines.append(f'import {{ app as AppWrapper }} from "./{module_path.stem}.js";');
102
+ }
103
+ lines.append('');
104
+ # --- Route helpers ---
105
+ lines.append('const publicRoutes = routes.filter(r => !r.auth);');
106
+ lines.append('const authRoutes = routes.filter(r => r.auth);');
107
+ lines.append('const RootLayout = layouts["/"] || null;');
108
+ lines.append('');
109
+ lines.append('function renderRoutes(routeList) {');
110
+ lines.append('\treturn routeList.map(r =>');
111
+ lines.append(
112
+ '\t\tReact.createElement(Route, { key: r.path, path: r.path, element: React.createElement(r.element, null) })'
113
+ );
114
+ lines.append('\t);');
115
+ lines.append('}');
116
+ lines.append('');
117
+ # --- Auth guard routes (reused in both layout and non-layout paths) ---
118
+ # AuthGuard is a runtime function compiled with plain parameters (not React props
119
+ # destructuring), so we wrap it in a component that passes the redirect string directly.
120
+ lines.append(
121
+ f'function _AuthGuardRoute() {{ return AuthGuard("{auth_redirect}"); }}'
122
+ );
123
+ lines.append(
124
+ 'const authGuardElement = React.createElement(_AuthGuardRoute, null);'
125
+ );
126
+ lines.append('');
127
+ # --- Single App component with one BrowserRouter ---
128
+ lines.append('function App() {');
129
+ lines.append('\tconst routeChildren = [');
130
+ lines.append('\t\t...renderRoutes(publicRoutes),');
131
+ lines.append(
132
+ '\t\tReact.createElement(Route, { key: "__auth", element: authGuardElement },'
133
+ );
134
+ lines.append('\t\t\t...renderRoutes(authRoutes)');
135
+ lines.append('\t\t),');
136
+ lines.append('\t];');
137
+ lines.append('');
138
+ lines.append('\tconst routeTree = RootLayout');
139
+ lines.append(
140
+ '\t\t? React.createElement(Route, { element: React.createElement(RootLayout, null) }, ...routeChildren)'
141
+ );
142
+ lines.append('\t\t: routeChildren;');
143
+ lines.append('');
144
+ lines.append('\treturn React.createElement(BrowserRouter, null,');
145
+ lines.append('\t\tReact.createElement(Routes, null,');
146
+ lines.append('\t\t\t...(Array.isArray(routeTree) ? routeTree : [routeTree])');
147
+ lines.append('\t\t)');
148
+ lines.append('\t);');
149
+ lines.append('}');
150
+ lines.append('');
151
+ # --- Root render ---
152
+ if has_app_wrapper {
153
+ lines.append(
154
+ 'const appElement = React.createElement(AppWrapper, null, React.createElement(App, null));'
155
+ );
156
+ } else {
157
+ lines.append('const appElement = React.createElement(App, null);');
158
+ }
159
+ lines.append('');
160
+ lines.append('const root = createRoot(document.getElementById("root"));');
161
+ lines.append('root.render(');
162
+ lines.append(
163
+ '\tReact.createElement(JacClientErrorBoundary, { FallbackComponent: ErrorFallback }, appElement)'
164
+ );
165
+ lines.append(');');
166
+ lines.append('');
167
+ return "\n".join(lines);
168
+ }
169
+
55
170
  """Copy assets from root assets/ folder to compiled/assets/ for @jac-client/assets alias."""
56
171
  impl ViteCompiler.copy_root_assets(self: ViteCompiler) -> None {
57
172
  root_assets_dir = self.project_dir / 'assets';
@@ -162,45 +277,14 @@ impl ViteCompiler.compile_dependencies_recursively(
162
277
  combined_js = self.jac_compiler.add_runtime_imports(module_js);
163
278
  try {
164
279
  relative_path = module_path.relative_to(source_root);
165
- # Handle compound extensions like .cl.jac, .impl.jac -> .js
166
- rel_str = str(relative_path);
167
- for compound_ext in ['.cl.jac', '.impl.jac', '.test.jac'] {
168
- if rel_str.endswith(compound_ext) {
169
- rel_str = rel_str[:-len(compound_ext)] + '.js';
170
- break;
171
- }
172
- } else {
173
- rel_str = str(relative_path.with_suffix('.js'));
174
- }
280
+ rel_str = self._jac_path_to_js(str(relative_path));
175
281
  output_path = self.compiled_dir / rel_str;
176
282
  } except ValueError {
177
- # Handle compound extensions in filename
178
- name = module_path.name;
179
- for compound_ext in ['.cl.jac', '.impl.jac', '.test.jac'] {
180
- if name.endswith(compound_ext) {
181
- name = name[:-len(compound_ext)] + '.js';
182
- break;
183
- }
184
- } else {
185
- name = module_path.stem + '.js';
186
- }
283
+ name = self._jac_path_to_js(module_path.name);
187
284
  output_path = self.compiled_dir / name;
188
285
  }
189
286
  output_path.parent.mkdir(parents=True, exist_ok=True);
190
- # Add source file header comment for better error messages
191
287
  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
288
  output_path.write_text(source_header + combined_js, encoding='utf-8');
205
289
  if (not manifest or not manifest.imports) {
206
290
  return;
@@ -240,34 +324,118 @@ impl ViteCompiler.compile_runtime_utils(self: ViteCompiler) -> tuple[str, list[s
240
324
  runtimeutils_exports_list = self.jac_compiler.extract_exports(
241
325
  runtimeutils_manifest
242
326
  );
243
- # Include both function exports and glob exports (e.g., useState, useEffect)
244
327
  glob_names = list(runtimeutils_manifest.globals) if runtimeutils_manifest else [];
245
328
  all_exports = sorted(
246
329
  set(runtimeutils_exports_list + self.ROUTER_EXPORTS + glob_names)
247
330
  );
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
331
  self.compiled_dir.mkdir(parents=True, exist_ok=True);
267
332
  (self.compiled_dir / 'client_runtime.js').write_text(
268
- combined_runtime_utils_js, encoding='utf-8'
333
+ runtimeutils_js, encoding='utf-8'
269
334
  );
270
- return (combined_runtime_utils_js, all_exports);
335
+ return (runtimeutils_js, all_exports);
336
+ }
337
+
338
+ """Convert a .jac relative path string to .js, handling compound extensions."""
339
+ impl ViteCompiler._jac_path_to_js(self: ViteCompiler, rel_str: str) -> str {
340
+ for compound_ext in self.COMPOUND_EXTENSIONS {
341
+ if rel_str.endswith(compound_ext) {
342
+ return rel_str[:-len(compound_ext)] + '.js';
343
+ }
344
+ }
345
+ return str(Path(rel_str).with_suffix('.js'));
346
+ }
347
+
348
+ """Scan pages/ directory, compile page files, and generate _routes.js manifest.
349
+ Returns True if pages/ exists and routes were generated."""
350
+ impl ViteCompiler._scan_and_compile_pages(
351
+ self: ViteCompiler,
352
+ visited: (set[Path] | None) = None,
353
+ collected_exports: (set[str] | None) = None,
354
+ collected_globals: (dict[(str, Any)] | None) = None
355
+ ) -> bool {
356
+ scanner = RouteScanner(self.project_dir);
357
+ if not scanner.has_pages_dir() {
358
+ return False;
359
+ }
360
+ routes = scanner.scan();
361
+ if not routes and not scanner.get_layouts() {
362
+ return False;
363
+ }
364
+ self._route_scanner = scanner;
365
+ # Compile each page file through the standard pipeline
366
+ source_root = self.project_dir;
367
+ for page_file in scanner.get_page_files() {
368
+ self.compile_dependencies_recursively(
369
+ page_file,
370
+ visited=visited,
371
+ collected_exports=collected_exports,
372
+ collected_globals=collected_globals,
373
+ source_root=source_root
374
+ );
375
+ }
376
+ self._generate_routes_manifest();
377
+ return True;
378
+ }
379
+
380
+ """Generate _routes.js manifest from previously scanned route entries."""
381
+ impl ViteCompiler._generate_routes_manifest(self: ViteCompiler) -> None {
382
+ scanner = self._route_scanner;
383
+ # Use cached results from _scan_and_compile_pages — don't re-scan
384
+ routes = scanner._routes;
385
+ layouts = scanner.get_layouts();
386
+ lines: list[str] = [];
387
+ # Import page components
388
+ for route in routes {
389
+ if route.is_layout {
390
+ continue;
391
+ }
392
+ # Compute relative import path from pages/ to compiled/pages/
393
+ try {
394
+ rel_path = route.file_path.relative_to(self.project_dir);
395
+ } except ValueError {
396
+ rel_path = route.file_path.relative_to(scanner.pages_dir.parent);
397
+ }
398
+ rel_str = self._jac_path_to_js(str(rel_path));
399
+ lines.append(
400
+ f'import {{ page as {route.component_import} }} from "./{rel_str}";'
401
+ );
402
+ }
403
+ # Import layout components
404
+ for (prefix, layout) in layouts.items() {
405
+ try {
406
+ rel_path = layout.file_path.relative_to(self.project_dir);
407
+ } except ValueError {
408
+ rel_path = layout.file_path.relative_to(scanner.pages_dir.parent);
409
+ }
410
+ rel_str = self._jac_path_to_js(str(rel_path));
411
+ lines.append(
412
+ f'import {{ layout as {layout.component_import} }} from "./{rel_str}";'
413
+ );
414
+ }
415
+ lines.append('');
416
+ # Export routes array
417
+ lines.append('export const routes = [');
418
+ for route in routes {
419
+ if route.is_layout {
420
+ continue;
421
+ }
422
+ auth_str = "true" if route.auth_required else "false";
423
+ lines.append(
424
+ f'\t{{ path: "{route.path}", element: {route.component_import}, auth: {auth_str} }},'
425
+ );
426
+ }
427
+ lines.append('];');
428
+ lines.append('');
429
+ # Export layouts object
430
+ lines.append('export const layouts = {');
431
+ for (prefix, layout) in layouts.items() {
432
+ lines.append(f'\t"{prefix}": {layout.component_import},');
433
+ }
434
+ lines.append('};');
435
+ lines.append('');
436
+ manifest_path = self.compiled_dir / '_routes.js';
437
+ manifest_path.parent.mkdir(parents=True, exist_ok=True);
438
+ manifest_path.write_text("\n".join(lines), encoding='utf-8');
271
439
  }
272
440
 
273
441
  """Initialize the Vite compiler."""
@@ -317,4 +485,6 @@ impl ViteCompiler.init(
317
485
  self.import_processor = ImportProcessor();
318
486
  self.asset_processor = AssetProcessor();
319
487
  self.vite_bundler = ViteBundler(self.project_dir, vite_output_dir, vite_minify);
488
+ self._has_pages = False;
489
+ self._route_scanner = None;
320
490
  }
@@ -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();
@@ -40,6 +40,7 @@ impl PackageInstaller.uninstall_package(
40
40
  """Regenerate package.json from jac.toml and run bun install."""
41
41
  impl PackageInstaller._regenerate_and_install(self: PackageInstaller) -> None {
42
42
  import from jac_client.plugin.utils { ensure_bun_available }
43
+ import from jaclang.cli.console { console }
43
44
  # Ensure bun is available before proceeding
44
45
  if not ensure_bun_available() {
45
46
  raise ClientBundleError('Bun is required. Install manually: https://bun.sh') from None ;
@@ -51,14 +52,14 @@ impl PackageInstaller._regenerate_and_install(self: PackageInstaller) -> None {
51
52
  bundler._ensure_root_package_json();
52
53
  try {
53
54
  # Run bun install to actually install the packages
54
- print("\n ⏳ Installing packages...\n", flush=True);
55
+ console.print("\n ⏳ Installing packages...\n");
55
56
  result = subprocess.run(
56
57
  ['bun', 'install'], cwd=self.project_dir, check=False, text=True
57
58
  );
58
59
  if result.returncode != 0 {
59
60
  raise ClientBundleError('Failed to install packages (see output above)') ;
60
61
  }
61
- print("\n ✔ Packages installed", flush=True);
62
+ console.print("\n ✔ Packages installed");
62
63
  } finally {
63
64
  # Always clean up root package.json and move bun.lockb
64
65
  bundler._cleanup_root_package_files();