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.
- jac_client/docs/README.md +629 -0
- jac_client/docs/advanced-state.md +706 -0
- jac_client/docs/imports.md +650 -0
- jac_client/docs/lifecycle-hooks.md +554 -0
- jac_client/docs/routing.md +530 -0
- jac_client/examples/little-x/app.jac +615 -0
- jac_client/examples/little-x/package-lock.json +2840 -0
- jac_client/examples/little-x/package.json +23 -0
- jac_client/examples/little-x/submit-button.jac +8 -0
- jac_client/examples/todo-app/README.md +82 -0
- jac_client/examples/todo-app/app.jac +683 -0
- jac_client/examples/todo-app/package-lock.json +999 -0
- jac_client/examples/todo-app/package.json +22 -0
- jac_client/plugin/cli.py +328 -0
- jac_client/plugin/client.py +41 -0
- jac_client/plugin/client_runtime.jac +941 -0
- jac_client/plugin/vite_client_bundle.py +470 -0
- jac_client/tests/__init__.py +2 -0
- jac_client/tests/fixtures/button.jac +6 -0
- jac_client/tests/fixtures/client_app.jac +18 -0
- jac_client/tests/fixtures/client_app_with_antd.jac +21 -0
- jac_client/tests/fixtures/js_import.jac +30 -0
- jac_client/tests/fixtures/package-lock.json +329 -0
- jac_client/tests/fixtures/package.json +11 -0
- jac_client/tests/fixtures/relative_import.jac +13 -0
- jac_client/tests/fixtures/test_fragments_spread.jac +44 -0
- jac_client/tests/fixtures/utils.js +22 -0
- jac_client/tests/test_cl.py +360 -0
- jac_client/tests/test_create_jac_app.py +139 -0
- jac_client-0.1.0.dist-info/METADATA +126 -0
- jac_client-0.1.0.dist-info/RECORD +33 -0
- jac_client-0.1.0.dist-info/WHEEL +4 -0
- 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
|
+
}
|