jac-client 0.1.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 (33) hide show
  1. jac_client/docs/README.md +629 -0
  2. jac_client/docs/advanced-state.md +706 -0
  3. jac_client/docs/imports.md +650 -0
  4. jac_client/docs/lifecycle-hooks.md +554 -0
  5. jac_client/docs/routing.md +530 -0
  6. jac_client/examples/little-x/app.jac +615 -0
  7. jac_client/examples/little-x/package-lock.json +2840 -0
  8. jac_client/examples/little-x/package.json +23 -0
  9. jac_client/examples/little-x/submit-button.jac +8 -0
  10. jac_client/examples/todo-app/README.md +82 -0
  11. jac_client/examples/todo-app/app.jac +683 -0
  12. jac_client/examples/todo-app/package-lock.json +999 -0
  13. jac_client/examples/todo-app/package.json +22 -0
  14. jac_client/plugin/cli.py +328 -0
  15. jac_client/plugin/client.py +41 -0
  16. jac_client/plugin/client_runtime.jac +941 -0
  17. jac_client/plugin/vite_client_bundle.py +470 -0
  18. jac_client/tests/__init__.py +2 -0
  19. jac_client/tests/fixtures/button.jac +6 -0
  20. jac_client/tests/fixtures/client_app.jac +18 -0
  21. jac_client/tests/fixtures/client_app_with_antd.jac +21 -0
  22. jac_client/tests/fixtures/js_import.jac +30 -0
  23. jac_client/tests/fixtures/package-lock.json +329 -0
  24. jac_client/tests/fixtures/package.json +11 -0
  25. jac_client/tests/fixtures/relative_import.jac +13 -0
  26. jac_client/tests/fixtures/test_fragments_spread.jac +44 -0
  27. jac_client/tests/fixtures/utils.js +22 -0
  28. jac_client/tests/test_cl.py +360 -0
  29. jac_client/tests/test_create_jac_app.py +139 -0
  30. jac_client-0.1.0.dist-info/METADATA +126 -0
  31. jac_client-0.1.0.dist-info/RECORD +33 -0
  32. jac_client-0.1.0.dist-info/WHEEL +4 -0
  33. jac_client-0.1.0.dist-info/entry_points.txt +4 -0
@@ -0,0 +1,470 @@
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 json
8
+ import shutil
9
+ import subprocess
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+ from typing import Any, Sequence, TYPE_CHECKING
13
+
14
+ from jaclang.runtimelib.client_bundle import (
15
+ ClientBundle,
16
+ ClientBundleBuilder,
17
+ ClientBundleError,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from jaclang.compiler.codeinfo import ClientManifest
22
+
23
+
24
+ class ViteClientBundleBuilder(ClientBundleBuilder):
25
+ """Enhanced ClientBundleBuilder that uses Vite for optimized bundling."""
26
+
27
+ def __init__(
28
+ self,
29
+ runtime_path: Path | None = None,
30
+ vite_output_dir: Path | None = None,
31
+ vite_package_json: Path | None = None,
32
+ vite_minify: bool = False,
33
+ ) -> None:
34
+ """Initialize the Vite-enhanced bundle builder.
35
+
36
+ Args:
37
+ runtime_path: Path to client runtime file
38
+ vite_output_dir: Output directory for Vite builds (defaults to temp/dist)
39
+ vite_package_json: Path to package.json for Vite (required)
40
+ vite_minify: Whether to enable minification in Vite build
41
+ """
42
+ super().__init__(runtime_path)
43
+ self.vite_output_dir = vite_output_dir
44
+ self.vite_package_json = vite_package_json
45
+ self.vite_minify = vite_minify
46
+
47
+ def _process_imports(
48
+ self, manifest: ClientManifest | None, module_path: Path
49
+ ) -> list[Path | None]: # type: ignore[override]
50
+ """Process client imports for Vite bundling.
51
+
52
+ Only mark modules as bundled when we actually inline their code (.jac files we compile
53
+ and local .js files we embed). Bare package specifiers (e.g., "antd") are left as real
54
+ ES imports so Vite can resolve and bundle them.
55
+ """
56
+ # TODO: return pure js files separately
57
+ imported_js_modules: list[Path | None] = []
58
+
59
+ if manifest and manifest.imports:
60
+ for _, import_path in manifest.imports.items():
61
+ import_path_obj = Path(import_path)
62
+
63
+ if import_path_obj.suffix == ".js":
64
+ # Inline local JS files and mark as bundled
65
+ try:
66
+
67
+ imported_js_modules.append(import_path_obj)
68
+ except FileNotFoundError:
69
+ imported_js_modules.append(None)
70
+
71
+ elif import_path_obj.suffix == ".jac":
72
+ # Compile .jac imports and include transitive .jac imports
73
+ try:
74
+ imported_js_modules.append(import_path_obj)
75
+ except ClientBundleError:
76
+ imported_js_modules.append(None)
77
+
78
+ else:
79
+ # Non .jac/.js entries (likely bare specifiers) should be handled by Vite.
80
+ # Do not inline or mark as bundled so their import lines are preserved.
81
+ pass
82
+
83
+ return imported_js_modules
84
+
85
+ def _compile_dependencies_recursively(
86
+ self,
87
+ module_path: Path,
88
+ visited: set[Path] | None = None,
89
+ is_root: bool = False,
90
+ collected_exports: set[str] | None = None,
91
+ collected_globals: dict[str, Any] | None = None,
92
+ runtime_js: str | None = None,
93
+ ) -> None:
94
+ """Recursively compile/copy .jac/.js imports to temp, skipping bundling.
95
+
96
+ Only prepares dependency JS artifacts for Vite by writing compiled JS (.jac)
97
+ or copying local JS (.js) into the temp directory. Bare specifiers are left
98
+ untouched for Vite to resolve.
99
+ """
100
+ from jaclang.runtimelib.machine import JacMachine as Jac
101
+
102
+ if visited is None:
103
+ visited = set()
104
+ if collected_exports is None:
105
+ collected_exports = set()
106
+ if collected_globals is None:
107
+ collected_globals = {}
108
+
109
+ module_path = module_path.resolve()
110
+ if module_path in visited:
111
+ return
112
+ visited.add(module_path)
113
+ manifest = None
114
+ if not is_root:
115
+ # Compile current module to JS and append registration
116
+ module_js, mod = self._compile_to_js(module_path)
117
+ manifest = mod.gen.client_manifest if mod else None
118
+
119
+ # Extract exports from manifest
120
+ exports_list = self._extract_client_exports(manifest)
121
+ collected_exports.update(exports_list)
122
+
123
+ # Build globals map using manifest.globals_values only for non-root
124
+ non_root_globals: dict[str, Any] = {}
125
+ if manifest:
126
+ for name in manifest.globals:
127
+ non_root_globals[name] = manifest.globals_values.get(name)
128
+ collected_globals.update(non_root_globals)
129
+
130
+ exposure_js = self._generate_global_exposure_code(exports_list)
131
+ registration_js = self._generate_registration_js(
132
+ module_path.stem,
133
+ exports_list,
134
+ non_root_globals,
135
+ )
136
+ export_block = (
137
+ f"export {{ {', '.join(exports_list)} }};\n" if exports_list else ""
138
+ )
139
+
140
+ combined_js = f"{module_js}\n{exposure_js}\n{registration_js}\n{runtime_js}\n{export_block}"
141
+ if self.vite_package_json is not None:
142
+ (
143
+ self.vite_package_json.parent / "temp" / f"{module_path.stem}.js"
144
+ ).write_text(combined_js, encoding="utf-8")
145
+ else:
146
+ mod = Jac.program.mod.hub.get(str(module_path))
147
+ manifest = mod.gen.client_manifest if mod else None
148
+
149
+ if not manifest or not manifest.imports:
150
+ return
151
+
152
+ for _name, import_path in manifest.imports.items():
153
+ path_obj = Path(import_path).resolve()
154
+ # Avoid re-processing
155
+ if path_obj in visited:
156
+ continue
157
+ if path_obj.suffix == ".jac":
158
+ # Recurse into transitive deps
159
+ self._compile_dependencies_recursively(
160
+ path_obj,
161
+ visited,
162
+ is_root=False,
163
+ collected_exports=collected_exports,
164
+ collected_globals=collected_globals,
165
+ runtime_js=runtime_js,
166
+ )
167
+ elif path_obj.suffix == ".js":
168
+ try:
169
+ js_code = path_obj.read_text(encoding="utf-8")
170
+ if self.vite_package_json is not None:
171
+ (
172
+ self.vite_package_json.parent / "temp" / path_obj.name
173
+ ).write_text(js_code, encoding="utf-8")
174
+ except FileNotFoundError:
175
+ pass
176
+ else:
177
+ # Bare specifiers or other assets handled by Vite
178
+ continue
179
+
180
+ def _compile_bundle(
181
+ self,
182
+ module: ModuleType,
183
+ module_path: Path,
184
+ ) -> ClientBundle:
185
+ """Override to use Vite bundling instead of simple concatenation."""
186
+ # Get manifest from JacProgram first to check for imports
187
+ from jaclang.runtimelib.machine import JacMachine as Jac
188
+
189
+ mod = Jac.program.mod.hub.get(str(module_path))
190
+ manifest = mod.gen.client_manifest if mod else None
191
+
192
+ module_js, _ = self._compile_to_js(module_path)
193
+
194
+ # Compile runtime to JS and add to temp for Vite to consume
195
+ runtime_js, mod = self._compile_to_js(self.runtime_path)
196
+
197
+ # Collect exports/globals across root and recursive deps
198
+ collected_exports: set[str] = set(self._extract_client_exports(manifest))
199
+ client_globals_map = self._extract_client_globals(manifest, module)
200
+ collected_globals: dict[str, Any] = dict(client_globals_map)
201
+
202
+ # Recursively prepare dependencies and accumulate symbols
203
+ self._compile_dependencies_recursively(
204
+ module_path,
205
+ is_root=True,
206
+ collected_exports=collected_exports,
207
+ collected_globals=collected_globals,
208
+ runtime_js=runtime_js,
209
+ )
210
+
211
+ client_exports = sorted(collected_exports)
212
+ client_globals_map = collected_globals
213
+
214
+ bundle_pieces = []
215
+
216
+ # Add main module (without registration_js - we'll handle that in Jac init script)
217
+ bundle_pieces.extend(
218
+ [
219
+ "// Runtime module:",
220
+ runtime_js,
221
+ f"// Client module: {module.__name__}",
222
+ module_js,
223
+ "",
224
+ ]
225
+ )
226
+
227
+ # Add global exposure code first (before Jac initialization)
228
+ global_exposure_code = self._generate_global_exposure_code(client_exports)
229
+ bundle_pieces.append(global_exposure_code)
230
+
231
+ # Add Jac runtime initialization script (includes globals)
232
+ jac_init_script = self._generate_jac_init_script(
233
+ module_path.stem, client_exports, client_globals_map
234
+ )
235
+ bundle_pieces.append(jac_init_script)
236
+
237
+ # Do not add export block for root since output is iife
238
+
239
+ # Use Vite bundling instead of simple concatenation
240
+ bundle_code, bundle_hash = self._bundle_with_vite(
241
+ bundle_pieces, module.__name__, client_exports
242
+ )
243
+
244
+ return ClientBundle(
245
+ module_name=module.__name__,
246
+ code=bundle_code,
247
+ client_functions=client_exports,
248
+ client_globals=list(client_globals_map.keys()),
249
+ hash=bundle_hash,
250
+ )
251
+
252
+ def _bundle_with_vite(
253
+ self, bundle_pieces: list[str], module_name: str, client_functions: list[str]
254
+ ) -> tuple[str, str]:
255
+ """Bundle JavaScript code using Vite for optimization.
256
+
257
+ Args:
258
+ bundle_pieces: List of JavaScript code pieces to bundle
259
+ module_name: Name of the module being bundled
260
+ client_functions: List of client function names
261
+
262
+ Returns:
263
+ Tuple of (bundle_code, bundle_hash)
264
+
265
+ Raises:
266
+ ClientBundleError: If Vite bundling fails
267
+ """
268
+ if not self.vite_package_json or not self.vite_package_json.exists():
269
+ raise ClientBundleError(
270
+ "Vite package.json not found. Set vite_package_json when using ViteClientBundleBuilder"
271
+ )
272
+
273
+ # Create temp directory for Vite build
274
+ project_dir = self.vite_package_json.parent
275
+ temp_dir = project_dir / "temp"
276
+ temp_dir.mkdir(exist_ok=True)
277
+
278
+ # Create entry file with stitched content
279
+ entry_file = temp_dir / "app.js"
280
+
281
+ entry_content = "\n".join(piece for piece in bundle_pieces if piece is not None)
282
+ entry_file.write_text(entry_content, encoding="utf-8")
283
+
284
+ # Create Vite config in the project directory (where node_modules exists)
285
+ vite_config = project_dir / "temp_vite.config.js"
286
+ output_dir = self.vite_output_dir or temp_dir / "dist"
287
+ output_dir.mkdir(exist_ok=True)
288
+
289
+ config_content = self._generate_vite_config(entry_file, output_dir)
290
+ vite_config.write_text(config_content, encoding="utf-8")
291
+
292
+ try:
293
+ # Run Vite build from project directory
294
+ # need to install packages you told in package.json inside here
295
+ command = ["npx", "vite", "build", "--config", str(vite_config)]
296
+ subprocess.run(
297
+ command, cwd=project_dir, check=True, capture_output=True, text=True
298
+ )
299
+ except subprocess.CalledProcessError as e:
300
+ raise ClientBundleError(f"Vite build failed: {e.stderr}") from e
301
+ except FileNotFoundError:
302
+ raise ClientBundleError(
303
+ "npx or vite command not found. Ensure Node.js and npm are installed."
304
+ )
305
+ finally:
306
+ # Clean up temp config file
307
+ if vite_config.exists():
308
+ vite_config.unlink()
309
+
310
+ # Find the generated bundle file
311
+ bundle_file = self._find_vite_bundle(output_dir)
312
+ if not bundle_file:
313
+ raise ClientBundleError("Vite build completed but no bundle file found")
314
+
315
+ # Read the bundled code
316
+ bundle_code = bundle_file.read_text(encoding="utf-8")
317
+ bundle_hash = hashlib.sha256(bundle_code.encode("utf-8")).hexdigest()
318
+
319
+ return bundle_code, bundle_hash
320
+
321
+ def _generate_vite_config(self, entry_file: Path, output_dir: Path) -> str:
322
+ """Generate Vite configuration for bundling."""
323
+ entry_name = entry_file.as_posix()
324
+ output_dir_name = output_dir.as_posix()
325
+ minify_setting = "true" if self.vite_minify else "false"
326
+
327
+ return f"""
328
+ import {{ defineConfig }} from 'vite';
329
+ import {{ resolve }} from 'path';
330
+
331
+ export default defineConfig({{
332
+ build: {{
333
+ outDir: '{output_dir_name}',
334
+ emptyOutDir: true,
335
+ rollupOptions: {{
336
+ input: {{
337
+ main: resolve(__dirname, '{entry_name}'),
338
+ }},
339
+ output: {{
340
+ entryFileNames: 'client.[hash].js',
341
+ format: 'iife',
342
+ name: 'JacClient',
343
+ }},
344
+ }},
345
+ minify: {minify_setting}, // Configurable minification
346
+ }},
347
+ resolve: {{
348
+ }}
349
+ }});
350
+ """
351
+
352
+ def _find_vite_bundle(self, output_dir: Path) -> Path | None:
353
+ """Find the generated Vite bundle file."""
354
+ for file in output_dir.glob("client.*.js"):
355
+ return file
356
+ return None
357
+
358
+ def _generate_jac_init_script(
359
+ self,
360
+ module_name: str,
361
+ client_functions: list[str],
362
+ client_globals: dict[str, Any],
363
+ ) -> str:
364
+ """Generate Jac runtime initialization script."""
365
+ if not client_functions:
366
+ return ""
367
+
368
+ # Generate function map dynamically
369
+ map_entries = []
370
+ for func_name in client_functions:
371
+ map_entries.append(f' "{func_name}": {func_name}')
372
+ function_map_str = "{\n" + ",\n".join(map_entries) + "\n}"
373
+
374
+ # Generate globals map
375
+ globals_entries = []
376
+ for name, value in client_globals.items():
377
+ identifier = json.dumps(name)
378
+ try:
379
+ value_literal = json.dumps(value)
380
+ except TypeError:
381
+ value_literal = "null"
382
+ globals_entries.append(f"{identifier}: {value_literal}")
383
+ globals_literal = (
384
+ "{ " + ", ".join(globals_entries) + " }" if globals_entries else "{}"
385
+ )
386
+
387
+ # Find the main app function (usually the last function or one ending with '_app')
388
+ main_app_func = (
389
+ "jac_app" # this need to be always same and defined by our run time
390
+ )
391
+ # for func_name in reversed(client_functions):
392
+ # if func_name.endswith('_app') or func_name == 'App':
393
+ # main_app_func = func_name
394
+ # break
395
+
396
+ return f"""
397
+ // --- JAC CLIENT INITIALIZATION SCRIPT ---
398
+ // Expose functions globally for Jac runtime registration
399
+ const clientFunctions = {client_functions};
400
+ const functionMap = {function_map_str};
401
+ for (const funcName of clientFunctions) {{
402
+ globalThis[funcName] = functionMap[funcName];
403
+ }}
404
+ __jacRegisterClientModule("{module_name}", clientFunctions, {globals_literal});
405
+ globalThis.start_app = {main_app_func};
406
+ // Call the start function immediately if we're not hydrating from the server
407
+ if (!document.getElementById('__jac_init__')) {{
408
+ globalThis.start_app();
409
+ }}
410
+ // --- END JAC CLIENT INITIALIZATION SCRIPT ---
411
+ """
412
+
413
+ def _generate_global_exposure_code(self, client_functions: list[str]) -> str:
414
+ """Generate code to expose functions globally for Vite IIFE."""
415
+ if not client_functions:
416
+ return ""
417
+
418
+ # Generate function map dynamically
419
+ map_entries = []
420
+ for func_name in client_functions:
421
+ map_entries.append(f' "{func_name}": {func_name}')
422
+ function_map_str = "{\n" + ",\n".join(map_entries) + "\n}"
423
+
424
+ return f"""
425
+ // --- GLOBAL EXPOSURE FOR VITE IIFE ---
426
+ // Expose functions globally so they're available on globalThis
427
+ const globalClientFunctions = {client_functions};
428
+ const globalFunctionMap = {function_map_str};
429
+ for (const funcName of globalClientFunctions) {{
430
+ globalThis[funcName] = globalFunctionMap[funcName];
431
+ }}
432
+ // --- END GLOBAL EXPOSURE ---
433
+ """
434
+
435
+ def cleanup_temp_dir(self) -> None:
436
+ """Clean up the temp directory and its contents."""
437
+ if not self.vite_package_json or not self.vite_package_json.exists():
438
+ return
439
+
440
+ project_dir = self.vite_package_json.parent
441
+ temp_dir = project_dir / "temp"
442
+
443
+ if temp_dir.exists():
444
+ with contextlib.suppress(OSError):
445
+ shutil.rmtree(temp_dir)
446
+
447
+ @staticmethod
448
+ def _generate_registration_js(
449
+ module_name: str,
450
+ client_functions: Sequence[str],
451
+ client_globals: dict[str, Any],
452
+ ) -> str:
453
+ """Generate registration code that exposes client symbols globally."""
454
+ globals_entries: list[str] = []
455
+ for name, value in client_globals.items():
456
+ identifier = json.dumps(name)
457
+ try:
458
+ value_literal = json.dumps(value)
459
+ except TypeError:
460
+ value_literal = "null"
461
+ globals_entries.append(f"{identifier}: {value_literal}")
462
+
463
+ globals_literal = (
464
+ "{ " + ", ".join(globals_entries) + " }" if globals_entries else "{}"
465
+ )
466
+ functions_literal = json.dumps(list(client_functions))
467
+ module_literal = json.dumps(module_name)
468
+
469
+ # Use the registration function from client_runtime.jac
470
+ return f"__jacRegisterClientModule({module_literal}, {functions_literal}, {globals_literal});"
@@ -0,0 +1,2 @@
1
+ """Tests for jac-client package."""
2
+
@@ -0,0 +1,6 @@
1
+ cl import from antd {
2
+ Button
3
+ }
4
+ cl def CustomButton() -> any {
5
+ return <Button>Click Me</Button>;
6
+ }
@@ -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 client_page() {
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
+ }
@@ -0,0 +1,21 @@
1
+ """Sample Jac module using Ant Design components."""
2
+
3
+ cl import from antd {
4
+ Button
5
+ }
6
+ cl let APP_NAME: str = "Ant Design Test";
7
+
8
+ cl def ButtonTest() {
9
+ return <div>
10
+ <h1>{APP_NAME}</h1>
11
+ <p>Testing Ant Design integration</p>
12
+ <Button>Click Me</Button>
13
+ </div>;
14
+ }
15
+
16
+ cl def CardTest() {
17
+ return <div class="card-wrapper">
18
+ <h2>Card Component</h2>
19
+ </div>;
20
+ }
21
+
@@ -0,0 +1,30 @@
1
+ """Test module that imports JavaScript functions."""
2
+
3
+ cl import from .utils {
4
+ formatMessage,
5
+ calculateSum,
6
+ JS_CONSTANT,
7
+ MessageFormatter
8
+ }
9
+
10
+ cl let JS_IMPORT_LABEL: str = "JavaScript Import Test";
11
+
12
+ cl def JsImportTest() -> any {
13
+ let greeting = formatMessage("Jac");
14
+ let sum = calculateSum(5, 3);
15
+ let formatter = MessageFormatter("JS");
16
+ let formatted = formatter.format("Hello from JS class");
17
+
18
+ return <div class="js-import-test">
19
+ <h1>{JS_IMPORT_LABEL}</h1>
20
+ <p>Greeting: {greeting}</p>
21
+ <p>Sum (5 + 3): {sum}</p>
22
+ <p>Constant: {JS_CONSTANT}</p>
23
+ <p>Formatted: {formatted}</p>
24
+ </div>;
25
+ }
26
+
27
+ cl def Main() -> any {
28
+ return <JsImportTest />;
29
+ }
30
+