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,941 @@
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
+
6
+ cl {
7
+ # JSX factory function - uses React.createElement
8
+ def __jacJsx(tag: any, props: dict = {}, children: any = []) -> any {
9
+ # Handle fragments: when tag is None/null, use React.Fragment
10
+ if tag == None {
11
+ tag = React.Fragment;
12
+ }
13
+
14
+ childrenArray = [];
15
+ if children != None {
16
+ if Array.isArray(children) {
17
+ childrenArray = children;
18
+ } else {
19
+ childrenArray = [children];
20
+ }
21
+ }
22
+
23
+ # Filter out null/undefined children
24
+ reactChildren = [];
25
+ for child in childrenArray {
26
+ if child != None {
27
+ reactChildren.push(child);
28
+ }
29
+ }
30
+
31
+ if reactChildren.length > 0 {
32
+ args = [tag, props];
33
+ for child in reactChildren {
34
+ args.push(child);
35
+ }
36
+ return React.createElement.apply(React, args);
37
+ } else {
38
+ return React.createElement(tag, props);
39
+ }
40
+ }
41
+
42
+ def renderJsxTree(node: any, container: any) -> None {
43
+ try {
44
+ ReactDOM.createRoot(container).render(node);
45
+ } except Exception as err {
46
+ console.error("[Jac] Error in renderJsxTree:", err);
47
+ }
48
+ }
49
+
50
+ # ============================================================================
51
+ # Reactive State Management System
52
+ # ============================================================================
53
+
54
+ # Global reactive context for managing signals, effects, and re-renders
55
+ let __jacReactiveContext = {
56
+ "signals": [], # Global signal storage (enables closures)
57
+ "pendingRenders": [], # Batched re-renders queue
58
+ "flushScheduled": False, # Debounce flag for batching
59
+ "rootComponent": None, # Root function to re-render
60
+ "reactRoot": None, # Store the React 18 root instance
61
+ "currentComponent": None, # Current component ID being rendered
62
+ "currentEffect": None, # Current effect for dependency tracking
63
+ "router": None, # Global router instance
64
+ "mountedComponents": {} # Track which components have mounted (for onMount)
65
+ };
66
+
67
+ # Create a reactive signal (primitive value)
68
+ # Returns [getter, setter] tuple for reactive primitive value
69
+ def createSignal(initialValue: any) -> list {
70
+ signalId = __jacReactiveContext.signals.length;
71
+ signalData = {"value": initialValue, "subscribers": []};
72
+ __jacReactiveContext.signals.push(signalData);
73
+
74
+ def getter() -> any {
75
+ __jacTrackDependency(signalData.subscribers);
76
+ return signalData.value;
77
+ }
78
+
79
+ def setter(newValue: any) -> None {
80
+ if newValue != signalData.value {
81
+ signalData.value = newValue;
82
+ __jacNotifySubscribers(signalData.subscribers);
83
+ }
84
+ }
85
+
86
+ return [getter, setter];
87
+ }
88
+
89
+ # Create reactive state (object/dict)
90
+ # Returns [getter, setter] tuple for reactive object
91
+ def createState(initialState: dict) -> list {
92
+ signalId = __jacReactiveContext.signals.length;
93
+ signalData = {"value": initialState, "subscribers": []};
94
+ __jacReactiveContext.signals.push(signalData);
95
+
96
+ def getter() -> dict {
97
+ __jacTrackDependency(signalData.subscribers);
98
+ return signalData.value;
99
+ }
100
+
101
+ def setter(updates: dict) -> None {
102
+ # Shallow merge
103
+ newState = {};
104
+ stateValue = signalData.value;
105
+ for key in __objectKeys(stateValue) {
106
+ newState[key] = stateValue[key];
107
+ }
108
+ for key in __objectKeys(updates) {
109
+ newState[key] = updates[key];
110
+ }
111
+ signalData.value = newState;
112
+ __jacNotifySubscribers(signalData.subscribers);
113
+ }
114
+
115
+ return [getter, setter];
116
+ }
117
+
118
+ # Run effect when dependencies change
119
+ # Executes effectFn and re-runs when tracked signals change
120
+ def createEffect(effectFn: any) -> None {
121
+ __jacRunEffect(effectFn);
122
+ }
123
+
124
+ # Run effect once when component mounts (similar to useEffect with empty deps)
125
+ # Executes mountFn once when the component first renders
126
+ def onMount(mountFn: any) -> None {
127
+ currentComponent = __jacReactiveContext.currentComponent;
128
+ if not currentComponent {
129
+ # Not in a component context, run immediately
130
+ __jacRunEffect(mountFn);
131
+ return;
132
+ }
133
+
134
+ # Track mounted components
135
+ if not __jacReactiveContext.mountedComponents {
136
+ __jacReactiveContext.mountedComponents = {};
137
+ }
138
+
139
+ # Check if this component has already mounted
140
+ componentId = f"{currentComponent}";
141
+ if not __jacHasOwn(__jacReactiveContext.mountedComponents, componentId) {
142
+ # Mark as mounted and run the effect
143
+ __jacReactiveContext.mountedComponents[componentId] = True;
144
+
145
+ # Run in next tick to ensure component is fully rendered
146
+ try {
147
+ setTimeout(lambda -> None {
148
+ __jacRunEffect(mountFn);
149
+ }, 0);
150
+ } except Exception {
151
+ # Fallback if setTimeout not available
152
+ __jacRunEffect(mountFn);
153
+ }
154
+ }
155
+ }
156
+
157
+ # Internal: Track component dependencies
158
+ def __jacTrackDependency(subscribers: list) -> None {
159
+ currentEffect = __jacReactiveContext.currentEffect;
160
+ if currentEffect {
161
+ alreadySubscribed = False;
162
+ for sub in subscribers {
163
+ if sub == currentEffect {
164
+ alreadySubscribed = True;
165
+ }
166
+ }
167
+ if not alreadySubscribed {
168
+ subscribers.push(currentEffect);
169
+ }
170
+ }
171
+
172
+ currentComponent = __jacReactiveContext.currentComponent;
173
+ if currentComponent {
174
+ alreadySubscribed = False;
175
+ for sub in subscribers {
176
+ if sub == currentComponent {
177
+ alreadySubscribed = True;
178
+ }
179
+ }
180
+ if not alreadySubscribed {
181
+ subscribers.push(currentComponent);
182
+ }
183
+ }
184
+ }
185
+
186
+ # Internal: Notify subscribers of state change
187
+ def __jacNotifySubscribers(subscribers: list) -> None {
188
+ for subscriber in subscribers {
189
+ if __isFunction(subscriber) {
190
+ # It's an effect function - re-run it
191
+ __jacRunEffect(subscriber);
192
+ } else {
193
+ # It's a component ID - schedule re-render
194
+ __jacScheduleRerender(subscriber);
195
+ }
196
+ }
197
+ }
198
+
199
+ # Internal: Run an effect function with dependency tracking
200
+ def __jacRunEffect(effectFn: any) -> None {
201
+ previousEffect = __jacReactiveContext.currentEffect;
202
+ __jacReactiveContext.currentEffect = effectFn;
203
+
204
+ try {
205
+ effectFn();
206
+ } except Exception as err {
207
+ console.error("[Jac] Error in effect:", err);
208
+ }
209
+
210
+ __jacReactiveContext.currentEffect = previousEffect;
211
+ }
212
+
213
+ # Schedule a re-render (batched)
214
+ def __jacScheduleRerender(componentId: any) -> None {
215
+ pending = __jacReactiveContext.pendingRenders;
216
+
217
+ # Check if already scheduled
218
+ alreadyScheduled = False;
219
+ for item in pending {
220
+ if item == componentId {
221
+ alreadyScheduled = True;
222
+ }
223
+ }
224
+
225
+ if not alreadyScheduled {
226
+ pending.push(componentId);
227
+ __jacScheduleFlush();
228
+ }
229
+ }
230
+
231
+ # Schedule a flush of pending renders
232
+ def __jacScheduleFlush() -> None {
233
+ if not __jacReactiveContext.flushScheduled {
234
+ __jacReactiveContext.flushScheduled = True;
235
+
236
+ # Use requestAnimationFrame for batching, fallback to setTimeout
237
+ try {
238
+ requestAnimationFrame(__jacFlushRenders);
239
+ } except Exception {
240
+ setTimeout(__jacFlushRenders, 0);
241
+ }
242
+ }
243
+ }
244
+
245
+ # Flush all pending renders
246
+ def __jacFlushRenders() -> None {
247
+ pending = __jacReactiveContext.pendingRenders;
248
+ __jacReactiveContext.pendingRenders = [];
249
+ __jacReactiveContext.flushScheduled = False;
250
+
251
+ for componentId in pending {
252
+ __jacRerenderComponent(componentId);
253
+ }
254
+ }
255
+
256
+ # Re-render the root component
257
+ def __jacRerenderComponent(componentId: any) -> None {
258
+ # Use the stored React 18 root for re-rendering
259
+ reactRoot = __jacReactiveContext.reactRoot;
260
+ if not reactRoot {
261
+ console.error("[Jac] React root not initialized. Cannot re-render.");
262
+ return;
263
+ }
264
+
265
+ rootComponent = __jacReactiveContext.rootComponent;
266
+ if not rootComponent {
267
+ return;
268
+ }
269
+
270
+ try {
271
+ previousComponent = __jacReactiveContext.currentComponent;
272
+ __jacReactiveContext.currentComponent = componentId;
273
+
274
+ component = rootComponent();
275
+
276
+ # FIXED: Use the stored React 18 root's render method
277
+ reactRoot.render(component);
278
+
279
+ __jacReactiveContext.currentComponent = previousComponent;
280
+ } except Exception as err {
281
+ console.error("[Jac] Error re-rendering component:", err);
282
+ }
283
+ }
284
+
285
+ # ============================================================================
286
+ # Declarative Routing System
287
+ # ============================================================================
288
+
289
+ # Route configuration object
290
+ obj RouteConfig {
291
+ has path: str;
292
+ has component: any;
293
+ has guard: any = None;
294
+ }
295
+
296
+ # Create a router instance
297
+ def initRouter(routes: list, defaultRoute: str = "/") -> dict {
298
+ # Get initial path from hash or use default
299
+ initialPath = __jacGetHashPath();
300
+ if not initialPath {
301
+ initialPath = defaultRoute;
302
+ }
303
+
304
+ # Create reactive signal for current path
305
+ [currentPath, setCurrentPath] = createSignal(initialPath);
306
+
307
+ # Listen to hash changes
308
+ window.addEventListener("hashchange", lambda event: any -> None {
309
+ newPath = __jacGetHashPath();
310
+ if not newPath {
311
+ newPath = defaultRoute;
312
+ }
313
+ setCurrentPath(newPath);
314
+ });
315
+
316
+ # Listen to popstate (back/forward buttons)
317
+ window.addEventListener("popstate", lambda event: any -> None {
318
+ newPath = __jacGetHashPath();
319
+ if not newPath {
320
+ newPath = defaultRoute;
321
+ }
322
+ setCurrentPath(newPath);
323
+ });
324
+
325
+ # Render method - returns component for current route
326
+ def render() -> any {
327
+ path = currentPath(); # Track dependency!
328
+
329
+ # Find matching route
330
+ for route in routes {
331
+ if route.path == path {
332
+ # Check guard if present
333
+ if route.guard and not route.guard() {
334
+ return __jacJsx("div", {}, ["Access Denied"]);
335
+ }
336
+ return route.component();
337
+ }
338
+ }
339
+
340
+ # No match - show 404
341
+ return __jacJsx("div", {}, ["404 - Route not found: ", path]);
342
+ }
343
+
344
+ # Navigate method
345
+ def navigateTo(path: str) -> None {
346
+ window.location.hash = "#" + path;
347
+ setCurrentPath(path);
348
+ }
349
+
350
+ # Store router in global context
351
+ router = {
352
+ "path": currentPath,
353
+ "render": render,
354
+ "navigate": navigateTo
355
+ };
356
+ __jacReactiveContext.router = router;
357
+
358
+ return router;
359
+ }
360
+
361
+ # Route config factory
362
+ def Route(path: str, component: any, guard: any = None) -> dict {
363
+ return {"path": path, "component": component, "guard": guard};
364
+ }
365
+
366
+ # Link component for declarative navigation
367
+ def Link(props: dict) -> any {
368
+ href = props["href"] if "href" in props else "/";
369
+ children = props["children"] if "children" in props else [];
370
+
371
+ def handleClick(event: any) -> None {
372
+ console.log("Link clicked, navigating to:", href);
373
+ event.preventDefault();
374
+ navigate(href);
375
+ }
376
+
377
+ # Ensure children is properly handled - convert to array if it's not already
378
+ childrenArray = [];
379
+ if children != None {
380
+ if Array.isArray(children) {
381
+ childrenArray = children;
382
+ } else {
383
+ childrenArray = [children];
384
+ }
385
+ }
386
+
387
+ # Return JSX node manually to properly spread children
388
+ return __jacJsx("a", {"href": "#" + href, "onclick": handleClick}, childrenArray);
389
+ }
390
+
391
+ # Navigate programmatically
392
+ def navigate(path: str) -> None {
393
+ console.log("navigate() called with path:", path);
394
+ router = __jacReactiveContext.router;
395
+ if router {
396
+ console.log("Router found, calling router.navigate()");
397
+ router.navigate(path);
398
+ } else {
399
+ console.log("No router, setting hash directly");
400
+ window.location.hash = "#" + path;
401
+ }
402
+ }
403
+
404
+ # Hook to access router in components
405
+ def useRouter() -> dict {
406
+ return __jacReactiveContext.router;
407
+ }
408
+
409
+ # Internal: Get current hash path
410
+ def __jacGetHashPath() -> str {
411
+ hash = window.location.hash;
412
+ if hash {
413
+ return hash[1:]; # Remove '#'
414
+ }
415
+ return "";
416
+ }
417
+
418
+ # ============================================================================
419
+ # Walker spawn function
420
+ # ============================================================================
421
+
422
+ async def __jacSpawn(walker: str, fields: dict = {}, nd: str | None = None) -> any {
423
+ token = __getLocalStorage("jac_token");
424
+
425
+ response = await fetch(
426
+ f"/walker/{walker}",
427
+ {
428
+ "method": "POST",
429
+ "headers": {
430
+ "Content-Type": "application/json",
431
+ "Authorization": f"Bearer {token}" if token else ""
432
+ },
433
+ "body": JSON.stringify({"nd": nd if nd else "root", **fields})
434
+ }
435
+ );
436
+
437
+ if not response.ok {
438
+ error_text = await response.text();
439
+ raise Exception(f"Walker {walker} failed: {error_text}");
440
+ }
441
+
442
+ return JSON.parse(await response.text());
443
+ }
444
+
445
+ # Function call function - calls server-side functions from client
446
+ async def __jacCallFunction(function_name: str, args: dict = {}) -> any {
447
+ token = __getLocalStorage("jac_token");
448
+
449
+ response = await fetch(
450
+ f"/function/{function_name}",
451
+ {
452
+ "method": "POST",
453
+ "headers": {
454
+ "Content-Type": "application/json",
455
+ "Authorization": f"Bearer {token}" if token else ""
456
+ },
457
+ "body": JSON.stringify({"args": args})
458
+ }
459
+ );
460
+
461
+ if not response.ok {
462
+ error_text = await response.text();
463
+ raise Exception(f"Function {function_name} failed: {error_text}");
464
+ }
465
+
466
+ data = JSON.parse(await response.text());
467
+ return data["result"];
468
+ }
469
+
470
+ # Authentication helpers
471
+ async def jacSignup(username: str, password: str) -> dict {
472
+ response = await fetch(
473
+ "/user/create",
474
+ {
475
+ "method": "POST",
476
+ "headers": {"Content-Type": "application/json"},
477
+ "body": JSON.stringify({"username": username, "password": password})
478
+ }
479
+ );
480
+
481
+ if response.ok {
482
+ data = JSON.parse(await response.text());
483
+ token = data["token"];
484
+ if token {
485
+ __setLocalStorage("jac_token", token);
486
+ return {"success": True, "token": token, "username": username};
487
+ }
488
+ return {"success": False, "error": "No token received"};
489
+ } else {
490
+ error_text = await response.text();
491
+ try {
492
+ error_data = JSON.parse(error_text);
493
+ return {"success": False, "error": error_data["error"] if error_data["error"] != None else "Signup failed"};
494
+ } except Exception {
495
+ return {"success": False, "error": error_text};
496
+ }
497
+ }
498
+ }
499
+
500
+ async def jacLogin(username: str, password: str) -> bool {
501
+ response = await fetch(
502
+ "/user/login",
503
+ {
504
+ "method": "POST",
505
+ "headers": {"Content-Type": "application/json"},
506
+ "body": JSON.stringify({"username": username, "password": password})
507
+ }
508
+ );
509
+
510
+ if response.ok {
511
+ data = JSON.parse(await response.text());
512
+ token = data["token"];
513
+ if token {
514
+ __setLocalStorage("jac_token", token);
515
+ return True;
516
+ }
517
+ }
518
+ return False;
519
+ }
520
+
521
+ def jacLogout() -> None {
522
+ __removeLocalStorage("jac_token");
523
+ }
524
+
525
+ def jacIsLoggedIn() -> bool {
526
+ token = __getLocalStorage("jac_token");
527
+ return token != None and token != "";
528
+ }
529
+
530
+ # Browser API shims
531
+ def __getLocalStorage(key: str) -> str {
532
+ storage = globalThis.localStorage;
533
+ return storage.getItem(key) if storage else "";
534
+ }
535
+
536
+ def __setLocalStorage(key: str, value: str) -> None {
537
+ storage = globalThis.localStorage;
538
+ if storage {
539
+ storage.setItem(key, value);
540
+ }
541
+ }
542
+
543
+ def __removeLocalStorage(key: str) -> None {
544
+ storage = globalThis.localStorage;
545
+ if storage {
546
+ storage.removeItem(key);
547
+ }
548
+ }
549
+
550
+ def __isObject(value: any) -> bool {
551
+ if value == None {
552
+ return False;
553
+ }
554
+ return Object.prototype.toString.call(value) == "[object Object]";
555
+ }
556
+
557
+ def __isFunction(value: any) -> bool {
558
+ return Object.prototype.toString.call(value) == "[object Function]";
559
+ }
560
+
561
+ def __objectKeys(obj: any) -> list {
562
+ if obj == None {
563
+ return [];
564
+ }
565
+ return Object.keys(obj);
566
+ }
567
+
568
+ # Low-level helpers
569
+ def __jacHasOwn(obj: any, key: any) -> bool {
570
+ try {
571
+ return Object.prototype.hasOwnProperty.call(obj, key);
572
+ } except Exception {
573
+ return False;
574
+ }
575
+ }
576
+
577
+ # Internal polyfill for Python-style dict.get on plain JS objects
578
+ def __jacEnsureObjectGetPolyfill() -> None {
579
+ # No longer needed - we use standard JavaScript object access instead of .get() method
580
+ return;
581
+ }
582
+
583
+ # Common utility helpers
584
+ def __jacGetDocument(scope: any) -> any {
585
+ try {
586
+ return scope.document;
587
+ } except Exception {}
588
+ try {
589
+ return window.document;
590
+ } except Exception {}
591
+ return None;
592
+ }
593
+
594
+ def __jacParseJsonObject(text: str) -> any {
595
+ try {
596
+ parsed = JSON.parse(text);
597
+ if __isObject(parsed) {
598
+ return parsed;
599
+ }
600
+ console.error("[Jac] Hydration payload is not an object");
601
+ return None;
602
+ } except Exception as err {
603
+ console.error("[Jac] Failed to parse hydration payload", err);
604
+ return None;
605
+ }
606
+ }
607
+
608
+ def __jacBuildOrderedArgs(order: list, argsDict: dict) -> list {
609
+ result = [];
610
+ if not order {
611
+ return result;
612
+ }
613
+ values = argsDict if __isObject(argsDict) else {};
614
+ for name in order {
615
+ result.push(values[name]);
616
+ }
617
+ return result;
618
+ }
619
+
620
+ def __jacResolveRenderer(scope: any) -> any {
621
+ if scope.renderJsxTree {
622
+ return scope.renderJsxTree;
623
+ }
624
+ if __isFunction(renderJsxTree) {
625
+ return renderJsxTree;
626
+ }
627
+ return None;
628
+ }
629
+
630
+ def __jacResolveTarget(
631
+ moduleRecord: dict,
632
+ registry: dict,
633
+ name: str
634
+ ) -> any {
635
+ moduleFunctions = (
636
+ moduleRecord.moduleFunctions
637
+ if moduleRecord and moduleRecord.moduleFunctions
638
+ else {}
639
+ );
640
+ if __jacHasOwn(moduleFunctions, name) {
641
+ return moduleFunctions[name];
642
+ }
643
+ registryFunctions = (
644
+ registry.functions
645
+ if registry and registry.functions
646
+ else {}
647
+ );
648
+ if __jacHasOwn(registryFunctions, name) {
649
+ return registryFunctions[name];
650
+ }
651
+ return None;
652
+ }
653
+
654
+ def __jacSafeCallTarget(
655
+ target: any,
656
+ scope: any,
657
+ orderedArgs: list,
658
+ targetName: str
659
+ ) -> dict {
660
+ try {
661
+ result = target.apply(scope, orderedArgs);
662
+ return {"ok": True, "value": result};
663
+ } except Exception as err {
664
+ console.error("[Jac] Error executing client function " + targetName, err);
665
+ return {"ok": False, "value": None};
666
+ }
667
+ }
668
+
669
+ # Runtime support helpers for client module registration / hydration
670
+ def __jacGlobalScope() -> any {
671
+ try {
672
+ return globalThis;
673
+ } except Exception {}
674
+ try {
675
+ return window;
676
+ } except Exception {}
677
+ try {
678
+ return self;
679
+ } except Exception {}
680
+ return {};
681
+ }
682
+
683
+ def __jacEnsureRegistry() -> dict {
684
+ scope = __jacGlobalScope();
685
+ registry = scope.__jacClient;
686
+ if not registry {
687
+ registry = {
688
+ "functions": {},
689
+ "globals": {},
690
+ "modules": {},
691
+ "state": {"globals": {}},
692
+ "__hydration": {"registered": False},
693
+ "lastModule": None
694
+ };
695
+ scope.__jacClient = registry;
696
+ return registry;
697
+ }
698
+ if not registry.functions {
699
+ registry.functions = {};
700
+ }
701
+ if not registry.globals {
702
+ registry.globals = {};
703
+ }
704
+ if not registry.modules {
705
+ registry.modules = {};
706
+ }
707
+ if not registry.state {
708
+ registry.state = {"globals": {}};
709
+ } elif not registry.state.globals {
710
+ registry.state.globals = {};
711
+ }
712
+ if not registry.__hydration {
713
+ registry.__hydration = {"registered": False};
714
+ }
715
+ return registry;
716
+ }
717
+
718
+ def __jacApplyRender(renderer: any, container: any, node: any) -> None {
719
+ if not container {
720
+ return;
721
+ }
722
+ try {
723
+ # This function is now mostly legacy/fallback.
724
+ # The main hydration path in __jacHydrateFromDom handles React 18.
725
+ if renderer {
726
+ renderer(node, container);
727
+ } else {
728
+ console.warn("[Jac] No JSX renderer available.");
729
+ }
730
+ } except Exception as err {
731
+ console.error("[Jac] Failed to render JSX tree (fallback path)", err);
732
+ }
733
+ }
734
+
735
+ def __jacHydrateFromDom(defaultModuleName: str) -> None {
736
+ __jacEnsureObjectGetPolyfill();
737
+ scope = __jacGlobalScope();
738
+ documentRef = __jacGetDocument(scope);
739
+ if not documentRef {
740
+ return;
741
+ }
742
+
743
+ initEl = documentRef.getElementById("__jac_init__");
744
+ rootEl = documentRef.getElementById("__jac_root");
745
+ if not initEl or not rootEl {
746
+ return;
747
+ }
748
+
749
+ dataset = initEl.dataset if initEl.dataset else None;
750
+ if dataset and dataset.jacHydrated == "true" {
751
+ return;
752
+ }
753
+ if dataset {
754
+ dataset.jacHydrated = "true";
755
+ }
756
+
757
+ payloadText = initEl.textContent if initEl.textContent else "{}";
758
+ payload = __jacParseJsonObject(payloadText);
759
+ if not payload {
760
+ return;
761
+ }
762
+
763
+ targetName = payload["function"];
764
+ if not targetName {
765
+ return;
766
+ }
767
+
768
+ fallbackModule = defaultModuleName if defaultModuleName else "";
769
+ moduleCandidate = payload["module"];
770
+ moduleName = moduleCandidate if moduleCandidate else fallbackModule;
771
+
772
+ registry = __jacEnsureRegistry();
773
+ modulesStore = registry.modules if registry.modules else {};
774
+ moduleRecord = modulesStore[moduleName] if __jacHasOwn(modulesStore, moduleName) else None;
775
+ if not moduleRecord {
776
+ console.error("[Jac] Client module not registered: " + moduleName);
777
+ return;
778
+ }
779
+
780
+ argOrderRaw = payload["argOrder"] if payload["argOrder"] != None else [];
781
+ argOrder = argOrderRaw if Array.isArray(argOrderRaw) else [];
782
+ argsDictRaw = payload["args"] if payload["args"] != None else {};
783
+ argsDict = argsDictRaw if __isObject(argsDictRaw) else {};
784
+ orderedArgs = __jacBuildOrderedArgs(argOrder, argsDict);
785
+
786
+ payloadGlobalsRaw = payload["globals"] if payload["globals"] != None else {};
787
+ payloadGlobals = payloadGlobalsRaw if __isObject(payloadGlobalsRaw) else {};
788
+ registry.state.globals[moduleName] = payloadGlobals;
789
+ for gName in __objectKeys(payloadGlobals) {
790
+ gValue = payloadGlobals[gName];
791
+ scope[gName] = gValue;
792
+ registry.globals[gName] = gValue;
793
+ }
794
+
795
+ target = __jacResolveTarget(moduleRecord, registry, targetName);
796
+ if not target {
797
+ console.error("[Jac] Client function not found: " + targetName);
798
+ return;
799
+ }
800
+
801
+ # Set up reactive root component for automatic re-rendering
802
+ __jacReactiveContext.rootComponent = lambda -> any {
803
+ __jacReactiveContext.currentComponent = "__root__";
804
+ result = target.apply(scope, orderedArgs);
805
+ __jacReactiveContext.currentComponent = None;
806
+ return result;
807
+ };
808
+
809
+ renderer = __jacResolveRenderer(scope);
810
+ if not renderer {
811
+ console.warn("[Jac] renderJsxTree is not available in client bundle");
812
+ }
813
+
814
+ # FIX: Declare reactRoot in the function scope
815
+ let reactRoot = None;
816
+
817
+ # 1. Create and store the React 18 root instance
818
+ try {
819
+ reactRoot = ReactDOM.createRoot(rootEl);
820
+ __jacReactiveContext.reactRoot = reactRoot; # Store the root instance
821
+ } except Exception as err {
822
+ console.error("[Jac] Failed to create React root for hydration:", err);
823
+ return;
824
+ }
825
+
826
+ # 2. Initial render - call the root component
827
+ value = __jacReactiveContext.rootComponent();
828
+
829
+ # 3. Use the stored root for the initial render (hydration)
830
+ if value and __isObject(value) and __isFunction(value.then) {
831
+ value.then(
832
+ lambda node: any -> None {
833
+ reactRoot.render(node);
834
+ }
835
+ ).catch(
836
+ lambda err: any -> None {
837
+ console.error("[Jac] Error resolving client function promise", err);
838
+ }
839
+ );
840
+ } else {
841
+ reactRoot.render(value);
842
+ }
843
+ }
844
+
845
+ def __jacExecuteHydration() -> None {
846
+ registry = __jacEnsureRegistry();
847
+ defaultModule = registry.lastModule if registry.lastModule else "";
848
+ __jacHydrateFromDom(defaultModule);
849
+ }
850
+
851
+ def __jacEnsureHydration(moduleName: str) -> None {
852
+ __jacEnsureObjectGetPolyfill();
853
+ registry = __jacEnsureRegistry();
854
+ registry.lastModule = moduleName;
855
+
856
+ existingHydration = registry.__hydration if registry.__hydration else None;
857
+ hydration = existingHydration if existingHydration else {"registered": False};
858
+ registry.__hydration = hydration;
859
+
860
+ scope = __jacGlobalScope();
861
+ documentRef = __jacGetDocument(scope);
862
+ if not documentRef {
863
+ return;
864
+ }
865
+
866
+ alreadyRegistered = hydration.registered if hydration.registered else False;
867
+ if not alreadyRegistered {
868
+ hydration.registered = True;
869
+ documentRef.addEventListener(
870
+ "DOMContentLoaded",
871
+ lambda _event: any -> None {
872
+ __jacExecuteHydration();
873
+ },
874
+ {"once": True}
875
+ );
876
+ }
877
+ }
878
+
879
+ def __jacRegisterClientModule(
880
+ moduleName: str,
881
+ clientFunctions: list = [],
882
+ clientGlobals: dict = {}
883
+ ) -> None {
884
+ __jacEnsureObjectGetPolyfill();
885
+ scope = __jacGlobalScope();
886
+ registry = __jacEnsureRegistry();
887
+
888
+ moduleFunctions = {};
889
+ registeredFunctions = [];
890
+ if clientFunctions {
891
+ for funcName in clientFunctions {
892
+ funcRef = scope[funcName];
893
+ if not funcRef {
894
+ console.error("[Jac] Client function not found during registration: " + funcName);
895
+ continue;
896
+ }
897
+ moduleFunctions[funcName] = funcRef;
898
+ registry.functions[funcName] = funcRef;
899
+ scope[funcName] = funcRef;
900
+ registeredFunctions.push(funcName);
901
+ }
902
+ }
903
+
904
+ moduleGlobals = {};
905
+ globalNames = [];
906
+ globalsMap = clientGlobals if clientGlobals else {};
907
+ for gName in __objectKeys(globalsMap) {
908
+ globalNames.push(gName);
909
+ defaultValue = globalsMap[gName];
910
+ existing = scope[gName];
911
+ if existing == None {
912
+ scope[gName] = defaultValue;
913
+ moduleGlobals[gName] = defaultValue;
914
+ } else {
915
+ moduleGlobals[gName] = existing;
916
+ }
917
+ registry.globals[gName] = scope[gName];
918
+ }
919
+
920
+ modulesStore = registry.modules if registry.modules else {};
921
+ existingRecord = modulesStore[moduleName] if __jacHasOwn(modulesStore, moduleName) else None;
922
+ moduleRecord = existingRecord if existingRecord else {};
923
+ moduleRecord.moduleFunctions = moduleFunctions;
924
+ moduleRecord.moduleGlobals = moduleGlobals;
925
+ moduleRecord.functions = registeredFunctions;
926
+ moduleRecord.globals = globalNames;
927
+ moduleRecord.defaults = globalsMap;
928
+
929
+
930
+ # 1. Ensure the module is written to the registry FIRST.
931
+ registry.modules[moduleName] = moduleRecord;
932
+
933
+ stateGlobals = registry.state.globals;
934
+ if not __jacHasOwn(stateGlobals, moduleName) {
935
+ stateGlobals[moduleName] = {};
936
+ }
937
+
938
+ # 2. ONLY THEN, trigger the hydration/lookup process.
939
+ __jacEnsureHydration(moduleName);
940
+ }
941
+ }