jac-client 0.2.0__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 (72) hide show
  1. jac_client/docs/README.md +659 -0
  2. jac_client/docs/advanced-state.md +1266 -0
  3. jac_client/docs/assets/pipe_line.png +0 -0
  4. jac_client/docs/guide-example/intro.md +117 -0
  5. jac_client/docs/guide-example/step-01-setup.md +260 -0
  6. jac_client/docs/guide-example/step-02-components.md +416 -0
  7. jac_client/docs/guide-example/step-03-styling.md +478 -0
  8. jac_client/docs/guide-example/step-04-todo-ui.md +477 -0
  9. jac_client/docs/guide-example/step-05-local-state.md +530 -0
  10. jac_client/docs/guide-example/step-06-events.md +750 -0
  11. jac_client/docs/guide-example/step-07-effects.md +469 -0
  12. jac_client/docs/guide-example/step-08-walkers.md +534 -0
  13. jac_client/docs/guide-example/step-09-authentication.md +586 -0
  14. jac_client/docs/guide-example/step-10-routing.md +540 -0
  15. jac_client/docs/guide-example/step-11-final.md +964 -0
  16. jac_client/docs/imports.md +1142 -0
  17. jac_client/docs/lifecycle-hooks.md +774 -0
  18. jac_client/docs/routing.md +660 -0
  19. jac_client/examples/basic/.babelrc +9 -0
  20. jac_client/examples/basic/README.md +16 -0
  21. jac_client/examples/basic/app.jac +16 -0
  22. jac_client/examples/basic/package.json +27 -0
  23. jac_client/examples/basic/vite.config.js +28 -0
  24. jac_client/examples/basic-auth/.babelrc +9 -0
  25. jac_client/examples/basic-auth/README.md +16 -0
  26. jac_client/examples/basic-auth/app.jac +308 -0
  27. jac_client/examples/basic-auth/package.json +27 -0
  28. jac_client/examples/basic-auth/vite.config.js +28 -0
  29. jac_client/examples/basic-auth-with-router/.babelrc +9 -0
  30. jac_client/examples/basic-auth-with-router/README.md +60 -0
  31. jac_client/examples/basic-auth-with-router/app.jac +464 -0
  32. jac_client/examples/basic-auth-with-router/package.json +28 -0
  33. jac_client/examples/basic-auth-with-router/vite.config.js +28 -0
  34. jac_client/examples/basic-full-stack/.babelrc +9 -0
  35. jac_client/examples/basic-full-stack/README.md +18 -0
  36. jac_client/examples/basic-full-stack/app.jac +320 -0
  37. jac_client/examples/basic-full-stack/package.json +28 -0
  38. jac_client/examples/basic-full-stack/vite.config.js +28 -0
  39. jac_client/examples/full-stack-with-auth/.babelrc +9 -0
  40. jac_client/examples/full-stack-with-auth/README.md +16 -0
  41. jac_client/examples/full-stack-with-auth/app.jac +735 -0
  42. jac_client/examples/full-stack-with-auth/package.json +28 -0
  43. jac_client/examples/full-stack-with-auth/vite.config.js +30 -0
  44. jac_client/examples/little-x/app.jac +615 -0
  45. jac_client/examples/little-x/package.json +23 -0
  46. jac_client/examples/little-x/submit-button.jac +8 -0
  47. jac_client/examples/with-router/.babelrc +9 -0
  48. jac_client/examples/with-router/README.md +17 -0
  49. jac_client/examples/with-router/app.jac +323 -0
  50. jac_client/examples/with-router/package.json +28 -0
  51. jac_client/examples/with-router/vite.config.js +28 -0
  52. jac_client/plugin/cli.py +239 -0
  53. jac_client/plugin/client.py +89 -0
  54. jac_client/plugin/client_runtime.jac +234 -0
  55. jac_client/plugin/vite_client_bundle.py +355 -0
  56. jac_client/tests/__init__.py +2 -0
  57. jac_client/tests/fixtures/basic-app/app.jac +18 -0
  58. jac_client/tests/fixtures/client_app_with_antd/app.jac +28 -0
  59. jac_client/tests/fixtures/js_import/app.jac +30 -0
  60. jac_client/tests/fixtures/js_import/utils.js +22 -0
  61. jac_client/tests/fixtures/package-lock.json +329 -0
  62. jac_client/tests/fixtures/package.json +11 -0
  63. jac_client/tests/fixtures/relative_import/app.jac +13 -0
  64. jac_client/tests/fixtures/relative_import/button.jac +6 -0
  65. jac_client/tests/fixtures/spawn_test/app.jac +133 -0
  66. jac_client/tests/fixtures/test_fragments_spread/app.jac +53 -0
  67. jac_client/tests/test_cl.py +476 -0
  68. jac_client/tests/test_create_jac_app.py +139 -0
  69. jac_client-0.2.0.dist-info/METADATA +182 -0
  70. jac_client-0.2.0.dist-info/RECORD +72 -0
  71. jac_client-0.2.0.dist-info/WHEEL +4 -0
  72. jac_client-0.2.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,89 @@
1
+ import html
2
+ import types
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from jaclang.runtimelib.client_bundle import ClientBundle
7
+ from jaclang.runtimelib.machine import (
8
+ JacMachine as Jac,
9
+ hookimpl,
10
+ )
11
+ from jaclang.runtimelib.server import ModuleIntrospector
12
+
13
+ from .vite_client_bundle import ViteClientBundleBuilder
14
+
15
+
16
+ class JacClientModuleIntrospector(ModuleIntrospector):
17
+ """Jac Client Module Introspector."""
18
+
19
+ def render_page(
20
+ self, function_name: str, args: dict[str, Any], username: str
21
+ ) -> dict[str, Any]:
22
+ """Render HTML page for client function using the Vite bundle."""
23
+ self.load()
24
+
25
+ available_exports = set(self._client_manifest.get("exports", [])) or set(
26
+ self.get_client_functions().keys()
27
+ )
28
+ if function_name not in available_exports:
29
+ raise ValueError(f"Client function '{function_name}' not found")
30
+
31
+ bundle_hash = self.ensure_bundle()
32
+
33
+ page = (
34
+ "<!DOCTYPE html>"
35
+ '<html lang="en">'
36
+ "<head>"
37
+ '<meta charset="utf-8"/>'
38
+ f"<title>{html.escape(function_name)}</title>"
39
+ "</head>"
40
+ "<body>"
41
+ '<div id="root"></div>'
42
+ f'<script src="/static/client.js?hash={bundle_hash}" defer></script>'
43
+ "</body>"
44
+ "</html>"
45
+ )
46
+
47
+ return {
48
+ "html": page,
49
+ "bundle_hash": bundle_hash,
50
+ "bundle_code": self._bundle.code,
51
+ }
52
+
53
+
54
+ class JacClient:
55
+ """Jac Client."""
56
+
57
+ @staticmethod
58
+ @hookimpl
59
+ def get_client_bundle_builder() -> ViteClientBundleBuilder:
60
+ """Get the client bundle builder instance."""
61
+ base_path = Path(Jac.base_path_dir)
62
+ package_json_path = base_path / "package.json"
63
+ output_dir = base_path / "dist"
64
+ # Use the plugin's client_runtime.jac file
65
+ runtime_path = Path(__file__).with_name("client_runtime.jac")
66
+ return ViteClientBundleBuilder(
67
+ runtime_path=runtime_path,
68
+ vite_package_json=package_json_path,
69
+ vite_output_dir=output_dir,
70
+ vite_minify=False,
71
+ )
72
+
73
+ @staticmethod
74
+ @hookimpl
75
+ def build_client_bundle(
76
+ module: types.ModuleType,
77
+ force: bool = False,
78
+ ) -> ClientBundle:
79
+ """Build a client bundle for the supplied module."""
80
+ builder = JacClient.get_client_bundle_builder()
81
+ return builder.build(module, force=force)
82
+
83
+ @staticmethod
84
+ @hookimpl
85
+ def get_module_introspector(
86
+ module_name: str, base_path: str | None
87
+ ) -> ModuleIntrospector:
88
+ """Get a module introspector for the supplied module."""
89
+ return JacClientModuleIntrospector(module_name, base_path)
@@ -0,0 +1,234 @@
1
+ """Client-side runtime for Jac JSX and walker interactions."""
2
+
3
+ cl import from 'react' { * as React }
4
+ cl import from 'react-dom/client' { * as ReactDOM }
5
+ cl import from 'react-router-dom' {
6
+ HashRouter as ReactRouterHashRouter,
7
+ Routes as ReactRouterRoutes,
8
+ Route as ReactRouterRoute,
9
+ Link as ReactRouterLink,
10
+ Navigate as ReactRouterNavigate,
11
+ useNavigate as reactRouterUseNavigate,
12
+ useLocation as reactRouterUseLocation,
13
+ useParams as reactRouterUseParams
14
+ }
15
+
16
+ cl {
17
+ # JSX factory function - uses React.createElement
18
+ def __jacJsx(tag: any, props: dict = {}, children: any = []) -> any {
19
+ # Handle fragments: when tag is None/null, use React.Fragment
20
+ if tag == None {
21
+ tag = React.Fragment;
22
+ }
23
+
24
+ childrenArray = [];
25
+ if children != None {
26
+ if Array.isArray(children) {
27
+ childrenArray = children;
28
+ } else {
29
+ childrenArray = [children];
30
+ }
31
+ }
32
+
33
+ # Filter out null/undefined children
34
+ reactChildren = [];
35
+ for child in childrenArray {
36
+ if child != None {
37
+ reactChildren.push(child);
38
+ }
39
+ }
40
+
41
+ if reactChildren.length > 0 {
42
+ args = [tag, props];
43
+ for child in reactChildren {
44
+ args.push(child);
45
+ }
46
+ return React.createElement.apply(React, args);
47
+ } else {
48
+ return React.createElement(tag, props);
49
+ }
50
+ }
51
+
52
+ # ============================================================================
53
+ # React Router Integration (using react-router-dom v6)
54
+ # ============================================================================
55
+ # Direct re-exports of React Router components for seamless integration
56
+ # Router uses HashRouter for hash-based routing (#/path)
57
+ # See: https://reactrouter.com/6.30.1/router-components/hash-router
58
+ let Router = ReactRouterHashRouter;
59
+ let Routes = ReactRouterRoutes;
60
+ let Route = ReactRouterRoute;
61
+ let Link = ReactRouterLink;
62
+ let Navigate = ReactRouterNavigate;
63
+
64
+ # React Router Hooks - wrapped for Jac compatibility
65
+ let useNavigate = reactRouterUseNavigate;
66
+ let useLocation = reactRouterUseLocation;
67
+ let useParams = reactRouterUseParams;
68
+
69
+ # useRouter Hook - convenience hook that combines common router utilities
70
+ def useRouter() -> dict {
71
+ navigate = reactRouterUseNavigate();
72
+ location = reactRouterUseLocation();
73
+ params = reactRouterUseParams();
74
+
75
+ return {
76
+ "navigate": navigate,
77
+ "location": location,
78
+ "params": params,
79
+ "pathname": location.pathname,
80
+ "search": location.search,
81
+ "hash": location.hash
82
+ };
83
+ }
84
+
85
+ # navigate function - programmatic navigation (backward compatibility)
86
+ # Note: Use useNavigate() hook inside components instead
87
+ def navigate(path: str) -> None {
88
+ window.location.hash = "#" + path;
89
+ }
90
+
91
+ # ============================================================================
92
+ # Walker spawn function
93
+ # ============================================================================
94
+ async def __jacSpawn(left: str, right: str = "", fields: dict = {}) -> any {
95
+ token = __getLocalStorage("jac_token");
96
+ url = f"/walker/{left}";
97
+ if right != "" {
98
+ url = f"/walker/{left}/{right}";
99
+ }
100
+ response = await fetch(
101
+ url,
102
+ {
103
+ "method": "POST",
104
+ "accept": "application/json",
105
+ "headers": {
106
+ "Content-Type": "application/json",
107
+ "Authorization": f"Bearer {token}" if token else ""
108
+ },
109
+ "body": JSON.stringify({"fields": fields})
110
+ }
111
+ );
112
+
113
+ if not response.ok {
114
+ error_text = await response.json();
115
+ raise Exception(f"Walker {walker} failed: {error_text}") ;
116
+ }
117
+
118
+ return await response.json();
119
+ }
120
+
121
+ def jacSpawn(left: str, right: str = "", fields: dict = {}) -> any {
122
+ return __jacSpawn(left, right, fields);
123
+ }
124
+
125
+ # Function call function - calls server-side functions from client
126
+ async def __jacCallFunction(function_name: str, args: dict = {}) -> any {
127
+ token = __getLocalStorage("jac_token");
128
+
129
+ response = await fetch(
130
+ f"/function/{function_name}",
131
+ {
132
+ "method": "POST",
133
+ "headers": {
134
+ "Content-Type": "application/json",
135
+ "Authorization": f"Bearer {token}" if token else ""
136
+ },
137
+ "body": JSON.stringify({"args": args})
138
+ }
139
+ );
140
+
141
+ if not response.ok {
142
+ error_text = await response.text();
143
+ raise Exception(f"Function {function_name} failed: {error_text}") ;
144
+ }
145
+
146
+ data = JSON.parse(await response.text());
147
+ return data["result"];
148
+ }
149
+
150
+ # Authentication helpers
151
+ async def jacSignup(username: str, password: str) -> dict {
152
+ response = await fetch(
153
+ "/user/create",
154
+ {
155
+ "method": "POST",
156
+ "headers": {"Content-Type": "application/json"},
157
+ "body": JSON.stringify({"username": username, "password": password})
158
+ }
159
+ );
160
+
161
+ if response.ok {
162
+ data = JSON.parse(await response.text());
163
+ token = data["token"];
164
+ if token {
165
+ __setLocalStorage("jac_token", token);
166
+ return {"success": True, "token": token, "username": username};
167
+ }
168
+ return {"success": False, "error": "No token received"};
169
+ } else {
170
+ error_text = await response.text();
171
+ try {
172
+ error_data = JSON.parse(error_text);
173
+ return {
174
+ "success": False,
175
+ "error": error_data["error"]
176
+ if error_data["error"] != None
177
+ else "Signup failed"
178
+ };
179
+ } except Exception {
180
+ return {"success": False, "error": error_text};
181
+ }
182
+ }
183
+ }
184
+
185
+ async def jacLogin(username: str, password: str) -> bool {
186
+ response = await fetch(
187
+ "/user/login",
188
+ {
189
+ "method": "POST",
190
+ "headers": {"Content-Type": "application/json"},
191
+ "body": JSON.stringify({"username": username, "password": password})
192
+ }
193
+ );
194
+
195
+ if response.ok {
196
+ data = JSON.parse(await response.text());
197
+ token = data["token"];
198
+ if token {
199
+ __setLocalStorage("jac_token", token);
200
+ return True;
201
+ }
202
+ }
203
+ return False;
204
+ }
205
+
206
+ def jacLogout() -> None {
207
+ __removeLocalStorage("jac_token");
208
+ }
209
+
210
+ def jacIsLoggedIn() -> bool {
211
+ token = __getLocalStorage("jac_token");
212
+ return token != None and token != "";
213
+ }
214
+
215
+ # Browser API shims
216
+ def __getLocalStorage(key: str) -> str {
217
+ storage = globalThis.localStorage;
218
+ return storage.getItem(key) if storage else "";
219
+ }
220
+
221
+ def __setLocalStorage(key: str, value: str) -> None {
222
+ storage = globalThis.localStorage;
223
+ if storage {
224
+ storage.setItem(key, value);
225
+ }
226
+ }
227
+
228
+ def __removeLocalStorage(key: str) -> None {
229
+ storage = globalThis.localStorage;
230
+ if storage {
231
+ storage.removeItem(key);
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,355 @@
1
+ """Vite-enhanced client bundle generation for Jac web front-ends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import hashlib
7
+ import shutil
8
+ import subprocess
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ from jaclang.runtimelib.client_bundle import (
14
+ ClientBundle,
15
+ ClientBundleBuilder,
16
+ ClientBundleError,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from jaclang.compiler.codeinfo import ClientManifest
21
+
22
+
23
+ class ViteClientBundleBuilder(ClientBundleBuilder):
24
+ """Enhanced ClientBundleBuilder that uses Vite for optimized bundling."""
25
+
26
+ def __init__(
27
+ self,
28
+ runtime_path: Path | None = None,
29
+ vite_output_dir: Path | None = None,
30
+ vite_package_json: Path | None = None,
31
+ vite_minify: bool = False,
32
+ ) -> None:
33
+ """Initialize the Vite-enhanced bundle builder.
34
+
35
+ Args:
36
+ runtime_path: Path to client runtime file
37
+ vite_output_dir: Output directory for Vite builds (defaults to temp/dist)
38
+ vite_package_json: Path to package.json for Vite (required)
39
+ vite_minify: Whether to enable minification in Vite build
40
+ """
41
+ super().__init__(runtime_path)
42
+ self.vite_output_dir = vite_output_dir
43
+ self.vite_package_json = vite_package_json
44
+ self.vite_minify = vite_minify
45
+
46
+ def _process_imports(
47
+ self, manifest: ClientManifest | None, module_path: Path
48
+ ) -> list[Path | None]: # type: ignore[override]
49
+ """Process client imports for Vite bundling.
50
+
51
+ Only mark modules as bundled when we actually inline their code (.jac files we compile
52
+ and local .js files we embed). Bare package specifiers (e.g., "antd") are left as real
53
+ ES imports so Vite can resolve and bundle them.
54
+ """
55
+ imported_js_modules: list[Path | None] = []
56
+
57
+ if manifest and manifest.imports:
58
+ for _, import_path in manifest.imports.items():
59
+ import_path_obj = Path(import_path)
60
+
61
+ if import_path_obj.suffix == ".js":
62
+ # Inline local JS files and mark as bundled
63
+ try:
64
+
65
+ imported_js_modules.append(import_path_obj)
66
+ except FileNotFoundError:
67
+ imported_js_modules.append(None)
68
+
69
+ elif import_path_obj.suffix == ".jac":
70
+ # Compile .jac imports and include transitive .jac imports
71
+ try:
72
+ imported_js_modules.append(import_path_obj)
73
+ except ClientBundleError:
74
+ imported_js_modules.append(None)
75
+
76
+ else:
77
+ # Non .jac/.js entries (likely bare specifiers) should be handled by Vite.
78
+ # Do not inline or mark as bundled so their import lines are preserved.
79
+ pass
80
+
81
+ return imported_js_modules
82
+
83
+ def _compile_dependencies_recursively(
84
+ self,
85
+ module_path: Path,
86
+ visited: set[Path] | None = None,
87
+ collected_exports: set[str] | None = None,
88
+ collected_globals: dict[str, Any] | None = None,
89
+ ) -> None:
90
+ """Recursively compile/copy .jac/.js imports to temp, skipping bundling.
91
+
92
+ Only prepares dependency JS artifacts for Vite by writing compiled JS (.jac)
93
+ or copying local JS (.js) into the temp directory. Bare specifiers are left
94
+ untouched for Vite to resolve.
95
+ """
96
+ if visited is None:
97
+ visited = set()
98
+ if collected_exports is None:
99
+ collected_exports = set()
100
+ if collected_globals is None:
101
+ collected_globals = {}
102
+
103
+ module_path = module_path.resolve()
104
+ if module_path in visited:
105
+ return
106
+ visited.add(module_path)
107
+ manifest = None
108
+
109
+ # Compile current module to JS and append registration
110
+ module_js, mod = self._compile_to_js(module_path)
111
+ manifest = mod.gen.client_manifest if mod else None
112
+
113
+ # Extract exports from manifest
114
+ exports_list = self._extract_client_exports(manifest)
115
+ collected_exports.update(exports_list)
116
+
117
+ # Build globals map using manifest.globals_values only for non-root
118
+ non_root_globals: dict[str, Any] = {}
119
+ if manifest:
120
+ for name in manifest.globals:
121
+ non_root_globals[name] = manifest.globals_values.get(name)
122
+ collected_globals.update(non_root_globals)
123
+ export_block = (
124
+ f"export {{ {', '.join(exports_list)} }};\n" if exports_list else ""
125
+ )
126
+
127
+ # inport jacJsx from client_runtime_utils.jac
128
+ jac_jsx_path = 'import {__jacJsx, __jacSpawn} from "@jac-client/utils";'
129
+
130
+ combined_js = f"{jac_jsx_path}\n{module_js}\n{export_block}"
131
+ if self.vite_package_json is not None:
132
+ (
133
+ self.vite_package_json.parent / "src" / f"{module_path.stem}.js"
134
+ ).write_text(combined_js, encoding="utf-8")
135
+
136
+ if not manifest or not manifest.imports:
137
+ return
138
+
139
+ for _name, import_path in manifest.imports.items():
140
+ path_obj = Path(import_path).resolve()
141
+ # Avoid re-processing
142
+ if path_obj in visited:
143
+ continue
144
+ if path_obj.suffix == ".jac":
145
+ # Recurse into transitive deps
146
+ self._compile_dependencies_recursively(
147
+ path_obj,
148
+ visited,
149
+ collected_exports=collected_exports,
150
+ collected_globals=collected_globals,
151
+ )
152
+ elif path_obj.suffix == ".js":
153
+ try:
154
+ js_code = path_obj.read_text(encoding="utf-8")
155
+ if self.vite_package_json is not None:
156
+ (
157
+ self.vite_package_json.parent / "src" / path_obj.name
158
+ ).write_text(js_code, encoding="utf-8")
159
+ except FileNotFoundError:
160
+ pass
161
+ else:
162
+ # Bare specifiers or other assets handled by Vite
163
+ continue
164
+
165
+ def _compile_bundle(
166
+ self,
167
+ module: ModuleType,
168
+ module_path: Path,
169
+ ) -> ClientBundle:
170
+ """Override to use Vite bundling instead of simple concatenation."""
171
+
172
+ # Check if package.json exists before proceeding
173
+ if not self.vite_package_json or not self.vite_package_json.exists():
174
+ raise ClientBundleError(
175
+ "Vite package.json not found. Set vite_package_json when using ViteClientBundleBuilder"
176
+ )
177
+
178
+ # client_runtime for jac client utils
179
+ runtime_utils_path = self.runtime_path.parent / "client_runtime.jac"
180
+ runtimeutils_js, mod = self._compile_to_js(runtime_utils_path)
181
+ runtimeutils_manifest = mod.gen.client_manifest if mod else None
182
+ runtimeutils_exports_list = self._extract_client_exports(runtimeutils_manifest)
183
+
184
+ # Add React Router exports that are variable declarations (not functions)
185
+ # These need to be manually added since they're 'let' declarations, not 'def' functions
186
+ router_exports = [
187
+ "Router",
188
+ "Routes",
189
+ "Route",
190
+ "Link",
191
+ "Navigate",
192
+ "useNavigate",
193
+ "useLocation",
194
+ "useParams",
195
+ ]
196
+
197
+ # Combine manifest exports with router exports
198
+ all_exports = sorted(set(runtimeutils_exports_list + router_exports))
199
+
200
+ export_block = (
201
+ f"export {{ {', '.join(all_exports)} }};\n" if all_exports else ""
202
+ )
203
+
204
+ combined_runtime_utils_js = f"{runtimeutils_js}\n{export_block}"
205
+ (self.vite_package_json.parent / "src" / "client_runtime.js").write_text(
206
+ combined_runtime_utils_js, encoding="utf-8"
207
+ )
208
+
209
+ # Get manifest from JacProgram first to check for imports
210
+ # Collect exports/globals across root and recursive deps
211
+ module_js, mod = self._compile_to_js(module_path)
212
+ module_manifest = mod.gen.client_manifest if mod else None
213
+ collected_exports: set[str] = set(self._extract_client_exports(module_manifest))
214
+ client_globals_map = self._extract_client_globals(module_manifest, module)
215
+ collected_globals: dict[str, Any] = dict(client_globals_map)
216
+
217
+ # Recursively prepare dependencies and accumulate symbols
218
+ self._compile_dependencies_recursively(
219
+ module_path,
220
+ collected_exports=collected_exports,
221
+ collected_globals=collected_globals,
222
+ )
223
+
224
+ client_exports = sorted(collected_exports)
225
+ client_globals_map = collected_globals
226
+
227
+ entry_file = self.vite_package_json.parent / "src" / "main.js"
228
+
229
+ entry_content = """import React from "react";
230
+ import { createRoot } from "react-dom/client";
231
+ import { app as App } from "./app.js";
232
+
233
+ const root = createRoot(document.getElementById("root"));
234
+ root.render(<App />);
235
+ """
236
+ entry_file.write_text(entry_content, encoding="utf-8")
237
+
238
+ bundle_code, bundle_hash = self._bundle_with_vite(
239
+ module.__name__, client_exports
240
+ )
241
+
242
+ return ClientBundle(
243
+ module_name=module.__name__,
244
+ code=bundle_code,
245
+ client_functions=client_exports,
246
+ client_globals=list(client_globals_map.keys()),
247
+ hash=bundle_hash,
248
+ )
249
+
250
+ def _bundle_with_vite(
251
+ self, module_name: str, client_functions: list[str]
252
+ ) -> tuple[str, str]:
253
+ """Bundle JavaScript code using Vite for optimization.
254
+
255
+ Args:
256
+ module_name: Name of the module being bundled
257
+ client_functions: List of client function names
258
+
259
+ Returns:
260
+ Tuple of (bundle_code, bundle_hash)
261
+
262
+ Raises:
263
+ ClientBundleError: If Vite bundling fails
264
+ """
265
+ if not self.vite_package_json or not self.vite_package_json.exists():
266
+ raise ClientBundleError(
267
+ "Vite package.json not found. Set vite_package_json when using ViteClientBundleBuilder"
268
+ )
269
+
270
+ # Create temp directory for Vite build
271
+ project_dir = self.vite_package_json.parent
272
+ src_dir = project_dir / "src"
273
+ src_dir.mkdir(exist_ok=True)
274
+
275
+ output_dir = self.vite_output_dir or src_dir / "dist" / "assets"
276
+ output_dir.mkdir(parents=True, exist_ok=True)
277
+
278
+ try:
279
+ # Run Vite build from project directory
280
+ # need to install packages you told in package.json inside here
281
+ # first compile the code
282
+ command = ["npm", "run", "compile"]
283
+ subprocess.run(
284
+ command, cwd=project_dir, check=True, capture_output=True, text=True
285
+ )
286
+ # then build the code
287
+ command = ["npm", "run", "build"]
288
+ subprocess.run(
289
+ command, cwd=project_dir, check=True, capture_output=True, text=True
290
+ )
291
+ except subprocess.CalledProcessError as e:
292
+ raise ClientBundleError(f"Vite build failed: {e.stderr}") from e
293
+ except FileNotFoundError:
294
+ raise ClientBundleError(
295
+ "npx or vite command not found. Ensure Node.js and npm are installed."
296
+ )
297
+ # Find the generated bundle file
298
+ bundle_file = self._find_vite_bundle(output_dir)
299
+ if not bundle_file:
300
+ raise ClientBundleError("Vite build completed but no bundle file found")
301
+
302
+ # Read the bundled code
303
+ bundle_code = bundle_file.read_text(encoding="utf-8")
304
+ bundle_hash = hashlib.sha256(bundle_code.encode("utf-8")).hexdigest()
305
+
306
+ return bundle_code, bundle_hash
307
+
308
+ def _generate_vite_config(self, entry_file: Path, output_dir: Path) -> str:
309
+ """Generate Vite configuration for bundling."""
310
+ entry_name = entry_file.as_posix()
311
+ output_dir_name = output_dir.as_posix()
312
+ minify_setting = "true" if self.vite_minify else "false"
313
+
314
+ return f"""
315
+ import {{ defineConfig }} from 'vite';
316
+ import {{ resolve }} from 'path';
317
+
318
+ export default defineConfig({{
319
+ build: {{
320
+ outDir: '{output_dir_name}',
321
+ emptyOutDir: true,
322
+ rollupOptions: {{
323
+ input: {{
324
+ main: resolve(__dirname, '{entry_name}'),
325
+ }},
326
+ output: {{
327
+ entryFileNames: 'client.[hash].js',
328
+ format: 'iife',
329
+ name: 'JacClient',
330
+ }},
331
+ }},
332
+ minify: {minify_setting}, // Configurable minification
333
+ }},
334
+ resolve: {{
335
+ }}
336
+ }});
337
+ """
338
+
339
+ def _find_vite_bundle(self, output_dir: Path) -> Path | None:
340
+ """Find the generated Vite bundle file."""
341
+ for file in output_dir.glob("client.*.js"):
342
+ return file
343
+ return None
344
+
345
+ def cleanup_temp_dir(self) -> None:
346
+ """Clean up the src directory and its contents."""
347
+ if not self.vite_package_json or not self.vite_package_json.exists():
348
+ return
349
+
350
+ project_dir = self.vite_package_json.parent
351
+ temp_dir = project_dir / "src"
352
+
353
+ if temp_dir.exists():
354
+ with contextlib.suppress(OSError):
355
+ shutil.rmtree(temp_dir)
@@ -0,0 +1,2 @@
1
+ """Tests for jac-client package."""
2
+
@@ -0,0 +1,18 @@
1
+ """Sample Jac module containing client-side declarations."""
2
+
3
+ cl let API_LABEL: str = "Runtime Test";
4
+
5
+ cl obj ButtonProps {
6
+ has label: str = "Tap Me";
7
+ has color: str = "primary";
8
+ }
9
+
10
+ cl def app() {
11
+ let props = ButtonProps(label="Tap Me", color="primary");
12
+ return <div class="app">
13
+ <h1>{API_LABEL}</h1>
14
+ <button class={props.color} data-id="button">
15
+ {props.label}
16
+ </button>
17
+ </div>;
18
+ }