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.
- jac_client/examples/all-in-one/components/Header.jac +1 -1
- jac_client/examples/all-in-one/components/ProfitOverview.jac +1 -1
- jac_client/examples/all-in-one/components/Summary.jac +1 -1
- jac_client/examples/all-in-one/components/TransactionList.jac +2 -2
- jac_client/examples/all-in-one/components/navigation.jac +3 -9
- jac_client/examples/all-in-one/context/BudgetContext.jac +1 -1
- jac_client/examples/all-in-one/main.jac +5 -386
- jac_client/examples/all-in-one/pages/(auth)/index.jac +299 -0
- jac_client/examples/all-in-one/pages/{nestedDemo.jac → (auth)/nested.jac} +3 -13
- jac_client/examples/all-in-one/pages/{loginPage.jac → (public)/login.jac} +1 -1
- jac_client/examples/all-in-one/pages/{signupPage.jac → (public)/signup.jac} +1 -1
- jac_client/examples/all-in-one/pages/{notFound.jac → [...notFound].jac} +2 -1
- jac_client/examples/all-in-one/pages/budget.jac +11 -0
- jac_client/examples/all-in-one/pages/budget_planner_ui.cl.jac +1 -1
- jac_client/examples/all-in-one/pages/features.jac +8 -0
- jac_client/examples/all-in-one/pages/features_test_ui.cl.jac +7 -7
- jac_client/examples/all-in-one/pages/{LandingPage.jac → landing.jac} +4 -9
- jac_client/examples/all-in-one/pages/layout.jac +20 -0
- jac_client/examples/nested-folders/nested-advance/src/ButtonRoot.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/src/level1/ButtonSecondL.jac +1 -1
- jac_client/examples/nested-folders/nested-advance/src/level1/level2/ButtonThirdL.jac +1 -1
- jac_client/plugin/cli.jac +3 -3
- jac_client/plugin/client_runtime.cl.jac +7 -4
- jac_client/plugin/impl/client_runtime.impl.jac +29 -7
- jac_client/plugin/plugin_config.jac +4 -11
- jac_client/plugin/src/compiler.jac +19 -1
- jac_client/plugin/src/config_loader.jac +1 -0
- jac_client/plugin/src/impl/compiler.impl.jac +232 -62
- jac_client/plugin/src/impl/config_loader.impl.jac +8 -0
- jac_client/plugin/src/impl/package_installer.impl.jac +3 -2
- jac_client/plugin/src/impl/route_scanner.impl.jac +201 -0
- jac_client/plugin/src/impl/vite_bundler.impl.jac +54 -15
- jac_client/plugin/src/route_scanner.jac +44 -0
- 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/impl/bun_installer.impl.jac +16 -19
- jac_client/plugin/utils/impl/client_deps.impl.jac +12 -16
- jac_client/templates/fullstack.jacpack +3 -2
- jac_client/tests/test_cli.py +74 -0
- jac_client/tests/test_desktop_api_url.py +854 -0
- jac_client/tests/test_e2e.py +31 -40
- jac_client/tests/test_it.py +209 -11
- {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/METADATA +2 -2
- {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/RECORD +49 -44
- jac_client/examples/all-in-one/pages/BudgetPlanner.jac +0 -140
- jac_client/examples/all-in-one/pages/FeaturesTest.jac +0 -157
- {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/WHEEL +0 -0
- {jac_client-0.2.12.dist-info → jac_client-0.2.14.dist-info}/entry_points.txt +0 -0
- {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 = []) ->
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
16
|
-
impl ViteCompiler.
|
|
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[
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
+
runtimeutils_js, encoding='utf-8'
|
|
269
334
|
);
|
|
270
|
-
return (
|
|
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"
|
|
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"
|
|
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();
|