@alepha/react 0.11.9 → 0.11.10

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.
package/dist/index.cjs ADDED
@@ -0,0 +1,2082 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ let __alepha_core = require("@alepha/core");
25
+ let __alepha_datetime = require("@alepha/datetime");
26
+ let __alepha_server = require("@alepha/server");
27
+ let __alepha_server_cache = require("@alepha/server-cache");
28
+ let __alepha_server_links = require("@alepha/server-links");
29
+ let __alepha_logger = require("@alepha/logger");
30
+ let react = require("react");
31
+ react = __toESM(react);
32
+ let react_jsx_runtime = require("react/jsx-runtime");
33
+ let node_fs = require("node:fs");
34
+ let node_path = require("node:path");
35
+ let __alepha_server_static = require("@alepha/server-static");
36
+ let react_dom_server = require("react-dom/server");
37
+ let __alepha_router = require("@alepha/router");
38
+
39
+ //#region src/services/ReactPageService.ts
40
+ var ReactPageService = class {
41
+ fetch(pathname, options = {}) {
42
+ throw new __alepha_core.AlephaError("Fetch is not available for this environment.");
43
+ }
44
+ render(name, options = {}) {
45
+ throw new __alepha_core.AlephaError("Render is not available for this environment.");
46
+ }
47
+ };
48
+
49
+ //#endregion
50
+ //#region src/descriptors/$page.ts
51
+ /**
52
+ * Main descriptor for defining a React route in the application.
53
+ *
54
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
55
+ * It provides a declarative way to define pages with powerful features:
56
+ *
57
+ * **Routing & Navigation**
58
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
59
+ * - Nested routing with parent-child relationships
60
+ * - Type-safe URL parameter and query string validation
61
+ *
62
+ * **Data Loading**
63
+ * - Server-side data fetching with the `resolve` function
64
+ * - Automatic serialization and hydration for SSR
65
+ * - Access to request context, URL params, and parent data
66
+ *
67
+ * **Component Loading**
68
+ * - Direct component rendering or lazy loading for code splitting
69
+ * - Client-only rendering when browser APIs are needed
70
+ * - Automatic fallback handling during hydration
71
+ *
72
+ * **Performance Optimization**
73
+ * - Static generation for pre-rendered pages at build time
74
+ * - Server-side caching with configurable TTL and providers
75
+ * - Code splitting through lazy component loading
76
+ *
77
+ * **Error Handling**
78
+ * - Custom error handlers with support for redirects
79
+ * - Hierarchical error handling (child → parent)
80
+ * - HTTP status code handling (404, 401, etc.)
81
+ *
82
+ * **Page Animations**
83
+ * - CSS-based enter/exit animations
84
+ * - Dynamic animations based on page state
85
+ * - Custom timing and easing functions
86
+ *
87
+ * **Lifecycle Management**
88
+ * - Server response hooks for headers and status codes
89
+ * - Page leave handlers for cleanup (browser only)
90
+ * - Permission-based access control
91
+ *
92
+ * @example Simple page with data fetching
93
+ * ```typescript
94
+ * const userProfile = $page({
95
+ * path: "/users/:id",
96
+ * schema: {
97
+ * params: t.object({ id: t.int() }),
98
+ * query: t.object({ tab: t.optional(t.text()) })
99
+ * },
100
+ * resolve: async ({ params }) => {
101
+ * const user = await userApi.getUser(params.id);
102
+ * return { user };
103
+ * },
104
+ * lazy: () => import("./UserProfile.tsx")
105
+ * });
106
+ * ```
107
+ *
108
+ * @example Nested routing with error handling
109
+ * ```typescript
110
+ * const projectSection = $page({
111
+ * path: "/projects/:id",
112
+ * children: () => [projectBoard, projectSettings],
113
+ * resolve: async ({ params }) => {
114
+ * const project = await projectApi.get(params.id);
115
+ * return { project };
116
+ * },
117
+ * errorHandler: (error) => {
118
+ * if (HttpError.is(error, 404)) {
119
+ * return <ProjectNotFound />;
120
+ * }
121
+ * }
122
+ * });
123
+ * ```
124
+ *
125
+ * @example Static generation with caching
126
+ * ```typescript
127
+ * const blogPost = $page({
128
+ * path: "/blog/:slug",
129
+ * static: {
130
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
131
+ * },
132
+ * resolve: async ({ params }) => {
133
+ * const post = await loadPost(params.slug);
134
+ * return { post };
135
+ * }
136
+ * });
137
+ * ```
138
+ */
139
+ const $page = (options) => {
140
+ return (0, __alepha_core.createDescriptor)(PageDescriptor, options);
141
+ };
142
+ var PageDescriptor = class extends __alepha_core.Descriptor {
143
+ reactPageService = (0, __alepha_core.$inject)(ReactPageService);
144
+ onInit() {
145
+ if (this.options.static) this.options.cache ??= { store: {
146
+ provider: "memory",
147
+ ttl: [1, "week"]
148
+ } };
149
+ }
150
+ get name() {
151
+ return this.options.name ?? this.config.propertyKey;
152
+ }
153
+ /**
154
+ * For testing or build purposes.
155
+ *
156
+ * This will render the page (HTML layout included or not) and return the HTML + context.
157
+ * Only valid for server-side rendering, it will throw an error if called on the client-side.
158
+ */
159
+ async render(options) {
160
+ return this.reactPageService.render(this.name, options);
161
+ }
162
+ async fetch(options) {
163
+ return this.reactPageService.fetch(this.options.path || "", options);
164
+ }
165
+ match(url) {
166
+ return false;
167
+ }
168
+ pathname(config) {
169
+ return this.options.path || "";
170
+ }
171
+ };
172
+ $page[__alepha_core.KIND] = PageDescriptor;
173
+
174
+ //#endregion
175
+ //#region src/components/ClientOnly.tsx
176
+ /**
177
+ * A small utility component that renders its children only on the client side.
178
+ *
179
+ * Optionally, you can provide a fallback React node that will be rendered.
180
+ *
181
+ * You should use this component when
182
+ * - you have code that relies on browser-specific APIs
183
+ * - you want to avoid server-side rendering for a specific part of your application
184
+ * - you want to prevent pre-rendering of a component
185
+ */
186
+ const ClientOnly = (props) => {
187
+ const [mounted, setMounted] = (0, react.useState)(false);
188
+ (0, react.useEffect)(() => setMounted(true), []);
189
+ if (props.disabled) return props.children;
190
+ return mounted ? props.children : props.fallback;
191
+ };
192
+ var ClientOnly_default = ClientOnly;
193
+
194
+ //#endregion
195
+ //#region src/components/ErrorViewer.tsx
196
+ const ErrorViewer = ({ error, alepha }) => {
197
+ const [expanded, setExpanded] = (0, react.useState)(false);
198
+ if (alepha.isProduction()) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorViewerProduction, {});
199
+ const stackLines = error.stack?.split("\n") ?? [];
200
+ const previewLines = stackLines.slice(0, 5);
201
+ const hiddenLineCount = stackLines.length - previewLines.length;
202
+ const copyToClipboard = (text) => {
203
+ navigator.clipboard.writeText(text).catch((err) => {
204
+ console.error("Clipboard error:", err);
205
+ });
206
+ };
207
+ const styles = {
208
+ container: {
209
+ padding: "24px",
210
+ backgroundColor: "#FEF2F2",
211
+ color: "#7F1D1D",
212
+ border: "1px solid #FECACA",
213
+ borderRadius: "16px",
214
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
215
+ fontFamily: "monospace",
216
+ maxWidth: "768px",
217
+ margin: "40px auto"
218
+ },
219
+ heading: {
220
+ fontSize: "20px",
221
+ fontWeight: "bold",
222
+ marginBottom: "10px"
223
+ },
224
+ name: {
225
+ fontSize: "16px",
226
+ fontWeight: 600
227
+ },
228
+ message: {
229
+ fontSize: "14px",
230
+ marginBottom: "16px"
231
+ },
232
+ sectionHeader: {
233
+ display: "flex",
234
+ justifyContent: "space-between",
235
+ alignItems: "center",
236
+ fontSize: "12px",
237
+ marginBottom: "4px",
238
+ color: "#991B1B"
239
+ },
240
+ copyButton: {
241
+ fontSize: "12px",
242
+ color: "#DC2626",
243
+ background: "none",
244
+ border: "none",
245
+ cursor: "pointer",
246
+ textDecoration: "underline"
247
+ },
248
+ stackContainer: {
249
+ backgroundColor: "#FEE2E2",
250
+ padding: "12px",
251
+ borderRadius: "8px",
252
+ fontSize: "13px",
253
+ lineHeight: "1.4",
254
+ overflowX: "auto",
255
+ whiteSpace: "pre-wrap"
256
+ },
257
+ expandLine: {
258
+ color: "#F87171",
259
+ cursor: "pointer",
260
+ marginTop: "8px"
261
+ }
262
+ };
263
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
264
+ style: styles.container,
265
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
266
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
267
+ style: styles.heading,
268
+ children: "🔥 Error"
269
+ }),
270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
271
+ style: styles.name,
272
+ children: error.name
273
+ }),
274
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
275
+ style: styles.message,
276
+ children: error.message
277
+ })
278
+ ] }), stackLines.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
279
+ style: styles.sectionHeader,
280
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Stack trace" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
281
+ onClick: () => copyToClipboard(error.stack),
282
+ style: styles.copyButton,
283
+ children: "Copy all"
284
+ })]
285
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pre", {
286
+ style: styles.stackContainer,
287
+ children: [(expanded ? stackLines : previewLines).map((line, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: line }, i)), !expanded && hiddenLineCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
288
+ style: styles.expandLine,
289
+ onClick: () => setExpanded(true),
290
+ children: [
291
+ "+ ",
292
+ hiddenLineCount,
293
+ " more lines..."
294
+ ]
295
+ })]
296
+ })] })]
297
+ });
298
+ };
299
+ var ErrorViewer_default = ErrorViewer;
300
+ const ErrorViewerProduction = () => {
301
+ const styles = {
302
+ container: {
303
+ padding: "24px",
304
+ backgroundColor: "#FEF2F2",
305
+ color: "#7F1D1D",
306
+ border: "1px solid #FECACA",
307
+ borderRadius: "16px",
308
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
309
+ fontFamily: "monospace",
310
+ maxWidth: "768px",
311
+ margin: "40px auto",
312
+ textAlign: "center"
313
+ },
314
+ heading: {
315
+ fontSize: "20px",
316
+ fontWeight: "bold",
317
+ marginBottom: "8px"
318
+ },
319
+ name: {
320
+ fontSize: "16px",
321
+ fontWeight: 600,
322
+ marginBottom: "4px"
323
+ },
324
+ message: {
325
+ fontSize: "14px",
326
+ opacity: .85
327
+ }
328
+ };
329
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
330
+ style: styles.container,
331
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
332
+ style: styles.heading,
333
+ children: "🚨 An error occurred"
334
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
335
+ style: styles.message,
336
+ children: "Something went wrong. Please try again later."
337
+ })]
338
+ });
339
+ };
340
+
341
+ //#endregion
342
+ //#region src/contexts/RouterLayerContext.ts
343
+ const RouterLayerContext = (0, react.createContext)(void 0);
344
+
345
+ //#endregion
346
+ //#region src/errors/Redirection.ts
347
+ /**
348
+ * Used for Redirection during the page loading.
349
+ *
350
+ * Depends on the context, it can be thrown or just returned.
351
+ */
352
+ var Redirection = class extends Error {
353
+ redirect;
354
+ constructor(redirect) {
355
+ super("Redirection");
356
+ this.redirect = redirect;
357
+ }
358
+ };
359
+
360
+ //#endregion
361
+ //#region src/contexts/AlephaContext.ts
362
+ const AlephaContext = (0, react.createContext)(void 0);
363
+
364
+ //#endregion
365
+ //#region src/hooks/useAlepha.ts
366
+ /**
367
+ * Main Alepha hook.
368
+ *
369
+ * It provides access to the Alepha instance within a React component.
370
+ *
371
+ * With Alepha, you can access the core functionalities of the framework:
372
+ *
373
+ * - alepha.state() for state management
374
+ * - alepha.inject() for dependency injection
375
+ * - alepha.events.emit() for event handling
376
+ * etc...
377
+ */
378
+ const useAlepha = () => {
379
+ const alepha = (0, react.useContext)(AlephaContext);
380
+ if (!alepha) throw new __alepha_core.AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
381
+ return alepha;
382
+ };
383
+
384
+ //#endregion
385
+ //#region src/hooks/useEvents.ts
386
+ /**
387
+ * Allow subscribing to multiple Alepha events. See {@link Hooks} for available events.
388
+ *
389
+ * useEvents is fully typed to ensure correct event callback signatures.
390
+ *
391
+ * @example
392
+ * ```tsx
393
+ * useEvents(
394
+ * {
395
+ * "react:transition:begin": (ev) => {
396
+ * console.log("Transition began to:", ev.to);
397
+ * },
398
+ * "react:transition:error": {
399
+ * priority: "first",
400
+ * callback: (ev) => {
401
+ * console.error("Transition error:", ev.error);
402
+ * },
403
+ * },
404
+ * },
405
+ * [],
406
+ * );
407
+ * ```
408
+ */
409
+ const useEvents = (opts, deps) => {
410
+ const alepha = useAlepha();
411
+ (0, react.useEffect)(() => {
412
+ if (!alepha.isBrowser()) return;
413
+ const subs = [];
414
+ for (const [name, hook] of Object.entries(opts)) subs.push(alepha.events.on(name, hook));
415
+ return () => {
416
+ for (const clear of subs) clear();
417
+ };
418
+ }, deps);
419
+ };
420
+
421
+ //#endregion
422
+ //#region src/hooks/useStore.ts
423
+ function useStore(target, defaultValue) {
424
+ const alepha = useAlepha();
425
+ (0, react.useMemo)(() => {
426
+ if (defaultValue != null && alepha.state.get(target) == null) alepha.state.set(target, defaultValue);
427
+ }, [defaultValue]);
428
+ const [state, setState] = (0, react.useState)(alepha.state.get(target));
429
+ (0, react.useEffect)(() => {
430
+ if (!alepha.isBrowser()) return;
431
+ const key = target instanceof __alepha_core.Atom ? target.key : target;
432
+ return alepha.events.on("state:mutate", (ev) => {
433
+ if (ev.key === key) setState(ev.value);
434
+ });
435
+ }, []);
436
+ return [state, (value) => {
437
+ alepha.state.set(target, value);
438
+ }];
439
+ }
440
+
441
+ //#endregion
442
+ //#region src/hooks/useRouterState.ts
443
+ const useRouterState = () => {
444
+ const [state] = useStore("alepha.react.router.state");
445
+ if (!state) throw new __alepha_core.AlephaError("Missing react router state");
446
+ return state;
447
+ };
448
+
449
+ //#endregion
450
+ //#region src/components/ErrorBoundary.tsx
451
+ /**
452
+ * A reusable error boundary for catching rendering errors
453
+ * in any part of the React component tree.
454
+ */
455
+ var ErrorBoundary = class extends react.default.Component {
456
+ constructor(props) {
457
+ super(props);
458
+ this.state = {};
459
+ }
460
+ /**
461
+ * Update state so the next render shows the fallback UI.
462
+ */
463
+ static getDerivedStateFromError(error) {
464
+ return { error };
465
+ }
466
+ /**
467
+ * Lifecycle method called when an error is caught.
468
+ * You can log the error or perform side effects here.
469
+ */
470
+ componentDidCatch(error, info) {
471
+ if (this.props.onError) this.props.onError(error, info);
472
+ }
473
+ render() {
474
+ if (this.state.error) return this.props.fallback(this.state.error);
475
+ return this.props.children;
476
+ }
477
+ };
478
+ var ErrorBoundary_default = ErrorBoundary;
479
+
480
+ //#endregion
481
+ //#region src/components/NestedView.tsx
482
+ /**
483
+ * A component that renders the current view of the nested router layer.
484
+ *
485
+ * To be simple, it renders the `element` of the current child page of a parent page.
486
+ *
487
+ * @example
488
+ * ```tsx
489
+ * import { NestedView } from "alepha/react";
490
+ *
491
+ * class App {
492
+ * parent = $page({
493
+ * component: () => <NestedView />,
494
+ * });
495
+ *
496
+ * child = $page({
497
+ * parent: this.root,
498
+ * component: () => <div>Child Page</div>,
499
+ * });
500
+ * }
501
+ * ```
502
+ */
503
+ const NestedView = (props) => {
504
+ const index = (0, react.use)(RouterLayerContext)?.index ?? 0;
505
+ const state = useRouterState();
506
+ const [view, setView] = (0, react.useState)(state.layers[index]?.element);
507
+ const [animation, setAnimation] = (0, react.useState)("");
508
+ const animationExitDuration = (0, react.useRef)(0);
509
+ const animationExitNow = (0, react.useRef)(0);
510
+ useEvents({
511
+ "react:transition:begin": async ({ previous, state: state$1 }) => {
512
+ const layer = previous.layers[index];
513
+ if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
514
+ const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
515
+ if (animationExit) {
516
+ const duration = animationExit.duration || 200;
517
+ animationExitNow.current = Date.now();
518
+ animationExitDuration.current = duration;
519
+ setAnimation(animationExit.animation);
520
+ } else {
521
+ animationExitNow.current = 0;
522
+ animationExitDuration.current = 0;
523
+ setAnimation("");
524
+ }
525
+ },
526
+ "react:transition:end": async ({ state: state$1 }) => {
527
+ const layer = state$1.layers[index];
528
+ if (animationExitNow.current) {
529
+ const duration = animationExitDuration.current;
530
+ const diff = Date.now() - animationExitNow.current;
531
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
532
+ }
533
+ if (!layer?.cache) {
534
+ setView(layer?.element);
535
+ const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
536
+ if (animationEnter) setAnimation(animationEnter.animation);
537
+ else setAnimation("");
538
+ }
539
+ }
540
+ }, []);
541
+ let element = view ?? props.children ?? null;
542
+ if (animation) element = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
543
+ style: {
544
+ display: "flex",
545
+ flex: 1,
546
+ height: "100%",
547
+ width: "100%",
548
+ position: "relative",
549
+ overflow: "hidden"
550
+ },
551
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
552
+ style: {
553
+ height: "100%",
554
+ width: "100%",
555
+ display: "flex",
556
+ animation
557
+ },
558
+ children: element
559
+ })
560
+ });
561
+ if (props.errorBoundary === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: element });
562
+ if (props.errorBoundary) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
563
+ fallback: props.errorBoundary,
564
+ children: element
565
+ });
566
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
567
+ fallback: (error) => {
568
+ const result = state.onError(error, state);
569
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
570
+ return result;
571
+ },
572
+ children: element
573
+ });
574
+ };
575
+ var NestedView_default = (0, react.memo)(NestedView);
576
+ function parseAnimation(animationLike, state, type = "enter") {
577
+ if (!animationLike) return;
578
+ const DEFAULT_DURATION = 300;
579
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
580
+ if (typeof animation === "string") {
581
+ if (type === "exit") return;
582
+ return {
583
+ duration: DEFAULT_DURATION,
584
+ animation: `${DEFAULT_DURATION}ms ${animation}`
585
+ };
586
+ }
587
+ if (typeof animation === "object") {
588
+ const anim = animation[type];
589
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
590
+ const name = typeof anim === "object" ? anim.name : anim;
591
+ if (type === "exit") return {
592
+ duration,
593
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
594
+ };
595
+ return {
596
+ duration,
597
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
598
+ };
599
+ }
600
+ }
601
+
602
+ //#endregion
603
+ //#region src/components/NotFound.tsx
604
+ function NotFoundPage(props) {
605
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
606
+ style: {
607
+ height: "100vh",
608
+ display: "flex",
609
+ flexDirection: "column",
610
+ justifyContent: "center",
611
+ alignItems: "center",
612
+ textAlign: "center",
613
+ fontFamily: "sans-serif",
614
+ padding: "1rem",
615
+ ...props.style
616
+ },
617
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
618
+ style: {
619
+ fontSize: "1rem",
620
+ marginBottom: "0.5rem"
621
+ },
622
+ children: "404 - This page does not exist"
623
+ })
624
+ });
625
+ }
626
+
627
+ //#endregion
628
+ //#region src/providers/ReactPageProvider.ts
629
+ const envSchema$2 = __alepha_core.t.object({ REACT_STRICT_MODE: __alepha_core.t.boolean({ default: true }) });
630
+ var ReactPageProvider = class {
631
+ log = (0, __alepha_logger.$logger)();
632
+ env = (0, __alepha_core.$env)(envSchema$2);
633
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
634
+ pages = [];
635
+ getPages() {
636
+ return this.pages;
637
+ }
638
+ getConcretePages() {
639
+ const pages = [];
640
+ for (const page of this.pages) {
641
+ if (page.children && page.children.length > 0) continue;
642
+ const fullPath = this.pathname(page.name);
643
+ if (fullPath.includes(":") || fullPath.includes("*")) {
644
+ if (typeof page.static === "object") {
645
+ const entries = page.static.entries;
646
+ if (entries && entries.length > 0) for (const entry of entries) {
647
+ const params = entry.params;
648
+ const path = this.compile(page.path ?? "", params);
649
+ if (!path.includes(":") && !path.includes("*")) pages.push({
650
+ ...page,
651
+ name: params[Object.keys(params)[0]],
652
+ path,
653
+ ...entry
654
+ });
655
+ }
656
+ }
657
+ continue;
658
+ }
659
+ pages.push(page);
660
+ }
661
+ return pages;
662
+ }
663
+ page(name) {
664
+ for (const page of this.pages) if (page.name === name) return page;
665
+ throw new __alepha_core.AlephaError(`Page '${name}' not found`);
666
+ }
667
+ pathname(name, options = {}) {
668
+ const page = this.page(name);
669
+ if (!page) throw new Error(`Page ${name} not found`);
670
+ let url = page.path ?? "";
671
+ let parent = page.parent;
672
+ while (parent) {
673
+ url = `${parent.path ?? ""}/${url}`;
674
+ parent = parent.parent;
675
+ }
676
+ url = this.compile(url, options.params ?? {});
677
+ if (options.query) {
678
+ const query = new URLSearchParams(options.query);
679
+ if (query.toString()) url += `?${query.toString()}`;
680
+ }
681
+ return url.replace(/\/\/+/g, "/") || "/";
682
+ }
683
+ url(name, options = {}) {
684
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
685
+ }
686
+ root(state) {
687
+ const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(NestedView_default, {}, state.layers[0]?.element));
688
+ if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
689
+ return root;
690
+ }
691
+ convertStringObjectToObject = (schema, value) => {
692
+ if (__alepha_core.t.schema.isObject(schema) && typeof value === "object") {
693
+ for (const key in schema.properties) if (__alepha_core.t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
694
+ value[key] = this.alepha.codec.decode(schema.properties[key], decodeURIComponent(value[key]));
695
+ } catch (e) {}
696
+ }
697
+ return value;
698
+ };
699
+ /**
700
+ * Create a new RouterState based on a given route and request.
701
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
702
+ * It also handles errors and redirects.
703
+ */
704
+ async createLayers(route, state, previous = []) {
705
+ let context = {};
706
+ const stack = [{ route }];
707
+ let parent = route.parent;
708
+ while (parent) {
709
+ stack.unshift({ route: parent });
710
+ parent = parent.parent;
711
+ }
712
+ let forceRefresh = false;
713
+ for (let i = 0; i < stack.length; i++) {
714
+ const it = stack[i];
715
+ const route$1 = it.route;
716
+ const config = {};
717
+ try {
718
+ this.convertStringObjectToObject(route$1.schema?.query, state.query);
719
+ config.query = route$1.schema?.query ? this.alepha.codec.decode(route$1.schema.query, state.query) : {};
720
+ } catch (e) {
721
+ it.error = e;
722
+ break;
723
+ }
724
+ try {
725
+ config.params = route$1.schema?.params ? this.alepha.codec.decode(route$1.schema.params, state.params) : {};
726
+ } catch (e) {
727
+ it.error = e;
728
+ break;
729
+ }
730
+ it.config = { ...config };
731
+ if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
732
+ const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
733
+ if (JSON.stringify({
734
+ part: url(previous[i].part),
735
+ params: previous[i].config?.params ?? {}
736
+ }) === JSON.stringify({
737
+ part: url(route$1.path),
738
+ params: config.params ?? {}
739
+ })) {
740
+ it.props = previous[i].props;
741
+ it.error = previous[i].error;
742
+ it.cache = true;
743
+ context = {
744
+ ...context,
745
+ ...it.props
746
+ };
747
+ continue;
748
+ }
749
+ forceRefresh = true;
750
+ }
751
+ if (!route$1.resolve) continue;
752
+ try {
753
+ const args = Object.create(state);
754
+ Object.assign(args, config, context);
755
+ const props = await route$1.resolve?.(args) ?? {};
756
+ it.props = { ...props };
757
+ context = {
758
+ ...context,
759
+ ...props
760
+ };
761
+ } catch (e) {
762
+ if (e instanceof Redirection) return { redirect: e.redirect };
763
+ this.log.error("Page resolver has failed", e);
764
+ it.error = e;
765
+ break;
766
+ }
767
+ }
768
+ let acc = "";
769
+ for (let i = 0; i < stack.length; i++) {
770
+ const it = stack[i];
771
+ const props = it.props ?? {};
772
+ const params = { ...it.config?.params };
773
+ for (const key of Object.keys(params)) params[key] = String(params[key]);
774
+ acc += "/";
775
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
776
+ const path = acc.replace(/\/+/, "/");
777
+ const localErrorHandler = this.getErrorHandler(it.route);
778
+ if (localErrorHandler) {
779
+ const onErrorParent = state.onError;
780
+ state.onError = (error, context$1) => {
781
+ const result = localErrorHandler(error, context$1);
782
+ if (result === void 0) return onErrorParent(error, context$1);
783
+ return result;
784
+ };
785
+ }
786
+ if (!it.error) try {
787
+ const element = await this.createElement(it.route, {
788
+ ...props,
789
+ ...context
790
+ });
791
+ state.layers.push({
792
+ name: it.route.name,
793
+ props,
794
+ part: it.route.path,
795
+ config: it.config,
796
+ element: this.renderView(i + 1, path, element, it.route),
797
+ index: i + 1,
798
+ path,
799
+ route: it.route,
800
+ cache: it.cache
801
+ });
802
+ } catch (e) {
803
+ it.error = e;
804
+ }
805
+ if (it.error) try {
806
+ let element = await state.onError(it.error, state);
807
+ if (element === void 0) throw it.error;
808
+ if (element instanceof Redirection) return { redirect: element.redirect };
809
+ if (element === null) element = this.renderError(it.error);
810
+ state.layers.push({
811
+ props,
812
+ error: it.error,
813
+ name: it.route.name,
814
+ part: it.route.path,
815
+ config: it.config,
816
+ element: this.renderView(i + 1, path, element, it.route),
817
+ index: i + 1,
818
+ path,
819
+ route: it.route
820
+ });
821
+ break;
822
+ } catch (e) {
823
+ if (e instanceof Redirection) return { redirect: e.redirect };
824
+ throw e;
825
+ }
826
+ }
827
+ return { state };
828
+ }
829
+ createRedirectionLayer(redirect) {
830
+ return { redirect };
831
+ }
832
+ getErrorHandler(route) {
833
+ if (route.errorHandler) return route.errorHandler;
834
+ let parent = route.parent;
835
+ while (parent) {
836
+ if (parent.errorHandler) return parent.errorHandler;
837
+ parent = parent.parent;
838
+ }
839
+ }
840
+ async createElement(page, props) {
841
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
842
+ if (page.lazy) return (0, react.createElement)((await page.lazy()).default, props);
843
+ if (page.component) return (0, react.createElement)(page.component, props);
844
+ }
845
+ renderError(error) {
846
+ return (0, react.createElement)(ErrorViewer_default, {
847
+ error,
848
+ alepha: this.alepha
849
+ });
850
+ }
851
+ renderEmptyView() {
852
+ return (0, react.createElement)(NestedView_default, {});
853
+ }
854
+ href(page, params = {}) {
855
+ const found = this.pages.find((it) => it.name === page.options.name);
856
+ if (!found) throw new Error(`Page ${page.options.name} not found`);
857
+ let url = found.path ?? "";
858
+ let parent = found.parent;
859
+ while (parent) {
860
+ url = `${parent.path ?? ""}/${url}`;
861
+ parent = parent.parent;
862
+ }
863
+ url = this.compile(url, params);
864
+ return url.replace(/\/\/+/g, "/") || "/";
865
+ }
866
+ compile(path, params = {}) {
867
+ for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
868
+ return path;
869
+ }
870
+ renderView(index, path, view, page) {
871
+ view ??= this.renderEmptyView();
872
+ const element = page.client ? (0, react.createElement)(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
873
+ return (0, react.createElement)(RouterLayerContext.Provider, { value: {
874
+ index,
875
+ path
876
+ } }, element);
877
+ }
878
+ configure = (0, __alepha_core.$hook)({
879
+ on: "configure",
880
+ handler: () => {
881
+ let hasNotFoundHandler = false;
882
+ const pages = this.alepha.descriptors($page);
883
+ const hasParent = (it) => {
884
+ if (it.options.parent) return true;
885
+ for (const page of pages) if ((page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : []).includes(it)) return true;
886
+ };
887
+ for (const page of pages) {
888
+ if (page.options.path === "/*") hasNotFoundHandler = true;
889
+ if (hasParent(page)) continue;
890
+ this.add(this.map(pages, page));
891
+ }
892
+ if (!hasNotFoundHandler && pages.length > 0) this.add({
893
+ path: "/*",
894
+ name: "notFound",
895
+ cache: true,
896
+ component: NotFoundPage,
897
+ onServerResponse: ({ reply }) => {
898
+ reply.status = 404;
899
+ }
900
+ });
901
+ }
902
+ });
903
+ map(pages, target) {
904
+ const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
905
+ const getChildrenFromParent = (it) => {
906
+ const children$1 = [];
907
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
908
+ return children$1;
909
+ };
910
+ children.push(...getChildrenFromParent(target));
911
+ return {
912
+ ...target.options,
913
+ name: target.name,
914
+ parent: void 0,
915
+ children: children.map((it) => this.map(pages, it))
916
+ };
917
+ }
918
+ add(entry) {
919
+ if (this.alepha.isReady()) throw new __alepha_core.AlephaError("Router is already initialized");
920
+ entry.name ??= this.nextId();
921
+ const page = entry;
922
+ page.match = this.createMatch(page);
923
+ this.pages.push(page);
924
+ if (page.children) for (const child of page.children) {
925
+ child.parent = page;
926
+ this.add(child);
927
+ }
928
+ }
929
+ createMatch(page) {
930
+ let url = page.path ?? "/";
931
+ let target = page.parent;
932
+ while (target) {
933
+ url = `${target.path ?? ""}/${url}`;
934
+ target = target.parent;
935
+ }
936
+ let path = url.replace(/\/\/+/g, "/");
937
+ if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
938
+ return path;
939
+ }
940
+ _next = 0;
941
+ nextId() {
942
+ this._next += 1;
943
+ return `P${this._next}`;
944
+ }
945
+ };
946
+ const isPageRoute = (it) => {
947
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
948
+ };
949
+
950
+ //#endregion
951
+ //#region src/providers/ReactServerProvider.ts
952
+ const envSchema$1 = __alepha_core.t.object({
953
+ REACT_SSR_ENABLED: __alepha_core.t.optional(__alepha_core.t.boolean()),
954
+ REACT_ROOT_ID: __alepha_core.t.text({ default: "root" }),
955
+ REACT_SERVER_TEMPLATE: __alepha_core.t.optional(__alepha_core.t.text({ size: "rich" }))
956
+ });
957
+ /**
958
+ * React server provider configuration atom
959
+ */
960
+ const reactServerOptions = (0, __alepha_core.$atom)({
961
+ name: "alepha.react.server.options",
962
+ schema: __alepha_core.t.object({
963
+ publicDir: __alepha_core.t.string(),
964
+ staticServer: __alepha_core.t.object({
965
+ disabled: __alepha_core.t.boolean(),
966
+ path: __alepha_core.t.string({ description: "URL path where static files will be served." })
967
+ })
968
+ }),
969
+ default: {
970
+ publicDir: "public",
971
+ staticServer: {
972
+ disabled: false,
973
+ path: "/"
974
+ }
975
+ }
976
+ });
977
+ var ReactServerProvider = class {
978
+ log = (0, __alepha_logger.$logger)();
979
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
980
+ env = (0, __alepha_core.$env)(envSchema$1);
981
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
982
+ serverProvider = (0, __alepha_core.$inject)(__alepha_server.ServerProvider);
983
+ serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
984
+ serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
985
+ serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
986
+ ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
987
+ preprocessedTemplate = null;
988
+ options = (0, __alepha_core.$use)(reactServerOptions);
989
+ /**
990
+ * Configure the React server provider.
991
+ */
992
+ onConfigure = (0, __alepha_core.$hook)({
993
+ on: "configure",
994
+ handler: async () => {
995
+ const ssrEnabled = this.alepha.descriptors($page).length > 0 && this.env.REACT_SSR_ENABLED !== false;
996
+ this.alepha.state.set("alepha.react.server.ssr", ssrEnabled);
997
+ if (this.alepha.isViteDev()) {
998
+ await this.configureVite(ssrEnabled);
999
+ return;
1000
+ }
1001
+ let root = "";
1002
+ if (!this.alepha.isServerless()) {
1003
+ root = this.getPublicDirectory();
1004
+ if (!root) this.log.warn("Missing static files, static file server will be disabled");
1005
+ else {
1006
+ this.log.debug(`Using static files from: ${root}`);
1007
+ await this.configureStaticServer(root);
1008
+ }
1009
+ }
1010
+ if (ssrEnabled) {
1011
+ await this.registerPages(async () => this.template);
1012
+ this.log.info("SSR OK");
1013
+ return;
1014
+ }
1015
+ this.log.info("SSR is disabled, use History API fallback");
1016
+ this.serverRouterProvider.createRoute({
1017
+ path: "*",
1018
+ handler: async ({ url, reply }) => {
1019
+ if (url.pathname.includes(".")) {
1020
+ reply.headers["content-type"] = "text/plain";
1021
+ reply.body = "Not Found";
1022
+ reply.status = 404;
1023
+ return;
1024
+ }
1025
+ reply.headers["content-type"] = "text/html";
1026
+ return this.template;
1027
+ }
1028
+ });
1029
+ }
1030
+ });
1031
+ get template() {
1032
+ return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
1033
+ }
1034
+ async registerPages(templateLoader) {
1035
+ const template = await templateLoader();
1036
+ if (template) this.preprocessedTemplate = this.preprocessTemplate(template);
1037
+ for (const page of this.pageApi.getPages()) {
1038
+ if (page.children?.length) continue;
1039
+ this.log.debug(`+ ${page.match} -> ${page.name}`);
1040
+ this.serverRouterProvider.createRoute({
1041
+ ...page,
1042
+ schema: void 0,
1043
+ method: "GET",
1044
+ path: page.match,
1045
+ handler: this.createHandler(page, templateLoader)
1046
+ });
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Get the public directory path where static files are located.
1051
+ */
1052
+ getPublicDirectory() {
1053
+ const maybe = [(0, node_path.join)(process.cwd(), `dist/${this.options.publicDir}`), (0, node_path.join)(process.cwd(), this.options.publicDir)];
1054
+ for (const it of maybe) if ((0, node_fs.existsSync)(it)) return it;
1055
+ return "";
1056
+ }
1057
+ /**
1058
+ * Configure the static file server to serve files from the given root directory.
1059
+ */
1060
+ async configureStaticServer(root) {
1061
+ await this.serverStaticProvider.createStaticServer({
1062
+ root,
1063
+ cacheControl: {
1064
+ maxAge: 3600,
1065
+ immutable: true
1066
+ },
1067
+ ...this.options.staticServer
1068
+ });
1069
+ }
1070
+ /**
1071
+ * Configure Vite for SSR.
1072
+ */
1073
+ async configureVite(ssrEnabled) {
1074
+ if (!ssrEnabled) return;
1075
+ this.log.info("SSR (dev) OK");
1076
+ const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
1077
+ await this.registerPages(() => fetch(`${url}/index.html`).then((it) => it.text()).catch(() => void 0));
1078
+ }
1079
+ /**
1080
+ * For testing purposes, creates a render function that can be used.
1081
+ */
1082
+ async render(name, options = {}) {
1083
+ const page = this.pageApi.page(name);
1084
+ const url = new URL(this.pageApi.url(name, options));
1085
+ const state = {
1086
+ url,
1087
+ params: options.params ?? {},
1088
+ query: options.query ?? {},
1089
+ onError: () => null,
1090
+ layers: [],
1091
+ meta: {}
1092
+ };
1093
+ this.log.trace("Rendering", { url });
1094
+ await this.alepha.events.emit("react:server:render:begin", { state });
1095
+ const { redirect } = await this.pageApi.createLayers(page, state);
1096
+ if (redirect) return {
1097
+ state,
1098
+ html: "",
1099
+ redirect
1100
+ };
1101
+ if (!options.html) {
1102
+ this.alepha.state.set("alepha.react.router.state", state);
1103
+ return {
1104
+ state,
1105
+ html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
1106
+ };
1107
+ }
1108
+ const template = this.template ?? "";
1109
+ const html = this.renderToHtml(template, state, options.hydration);
1110
+ if (html instanceof Redirection) return {
1111
+ state,
1112
+ html: "",
1113
+ redirect
1114
+ };
1115
+ const result = {
1116
+ state,
1117
+ html
1118
+ };
1119
+ await this.alepha.events.emit("react:server:render:end", result);
1120
+ return result;
1121
+ }
1122
+ createHandler(route, templateLoader) {
1123
+ return async (serverRequest) => {
1124
+ const { url, reply, query, params } = serverRequest;
1125
+ const template = await templateLoader();
1126
+ if (!template) throw new __alepha_core.AlephaError("Missing template for SSR rendering");
1127
+ this.log.trace("Rendering page", { name: route.name });
1128
+ const state = {
1129
+ url,
1130
+ params,
1131
+ query,
1132
+ onError: () => null,
1133
+ layers: []
1134
+ };
1135
+ if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state.set("alepha.server.request.apiLinks", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
1136
+ user: serverRequest.user,
1137
+ authorization: serverRequest.headers.authorization
1138
+ }));
1139
+ let target = route;
1140
+ while (target) {
1141
+ if (route.can && !route.can()) {
1142
+ reply.status = 403;
1143
+ reply.headers["content-type"] = "text/plain";
1144
+ return "Forbidden";
1145
+ }
1146
+ target = target.parent;
1147
+ }
1148
+ await this.alepha.events.emit("react:server:render:begin", {
1149
+ request: serverRequest,
1150
+ state
1151
+ });
1152
+ this.serverTimingProvider.beginTiming("createLayers");
1153
+ const { redirect } = await this.pageApi.createLayers(route, state);
1154
+ this.serverTimingProvider.endTiming("createLayers");
1155
+ if (redirect) return reply.redirect(redirect);
1156
+ reply.headers["content-type"] = "text/html";
1157
+ reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
1158
+ reply.headers.pragma = "no-cache";
1159
+ reply.headers.expires = "0";
1160
+ const html = this.renderToHtml(template, state);
1161
+ if (html instanceof Redirection) {
1162
+ reply.redirect(typeof html.redirect === "string" ? html.redirect : this.pageApi.href(html.redirect));
1163
+ return;
1164
+ }
1165
+ const event = {
1166
+ request: serverRequest,
1167
+ state,
1168
+ html
1169
+ };
1170
+ await this.alepha.events.emit("react:server:render:end", event);
1171
+ route.onServerResponse?.(serverRequest);
1172
+ this.log.trace("Page rendered", { name: route.name });
1173
+ return event.html;
1174
+ };
1175
+ }
1176
+ renderToHtml(template, state, hydration = true) {
1177
+ const element = this.pageApi.root(state);
1178
+ this.alepha.state.set("alepha.react.router.state", state);
1179
+ this.serverTimingProvider.beginTiming("renderToString");
1180
+ let app = "";
1181
+ try {
1182
+ app = (0, react_dom_server.renderToString)(element);
1183
+ } catch (error) {
1184
+ this.log.error("renderToString has failed, fallback to error handler", error);
1185
+ const element$1 = state.onError(error, state);
1186
+ if (element$1 instanceof Redirection) return element$1;
1187
+ app = (0, react_dom_server.renderToString)(element$1);
1188
+ this.log.debug("Error handled successfully with fallback");
1189
+ }
1190
+ this.serverTimingProvider.endTiming("renderToString");
1191
+ const response = { html: template };
1192
+ if (hydration) {
1193
+ const { request, context, ...store } = this.alepha.context.als?.getStore() ?? {};
1194
+ const hydrationData = {
1195
+ ...store,
1196
+ "alepha.react.router.state": void 0,
1197
+ layers: state.layers.map((it) => ({
1198
+ ...it,
1199
+ error: it.error ? {
1200
+ ...it.error,
1201
+ name: it.error.name,
1202
+ message: it.error.message,
1203
+ stack: !this.alepha.isProduction() ? it.error.stack : void 0
1204
+ } : void 0,
1205
+ index: void 0,
1206
+ path: void 0,
1207
+ element: void 0,
1208
+ route: void 0
1209
+ }))
1210
+ };
1211
+ const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}<\/script>`;
1212
+ this.fillTemplate(response, app, script);
1213
+ }
1214
+ return response.html;
1215
+ }
1216
+ preprocessTemplate(template) {
1217
+ const bodyCloseIndex = template.match(/<\/body>/i)?.index ?? template.length;
1218
+ const beforeScript = template.substring(0, bodyCloseIndex);
1219
+ const afterScript = template.substring(bodyCloseIndex);
1220
+ const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
1221
+ if (rootDivMatch) {
1222
+ const beforeDiv = beforeScript.substring(0, rootDivMatch.index);
1223
+ const afterDivStart = rootDivMatch.index + rootDivMatch[0].length;
1224
+ const afterDiv = beforeScript.substring(afterDivStart);
1225
+ return {
1226
+ beforeApp: `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`,
1227
+ afterApp: `</div>${afterDiv}`,
1228
+ beforeScript: "",
1229
+ afterScript
1230
+ };
1231
+ }
1232
+ const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
1233
+ if (bodyMatch) {
1234
+ const beforeBody = beforeScript.substring(0, bodyMatch.index + bodyMatch[0].length);
1235
+ const afterBody = beforeScript.substring(bodyMatch.index + bodyMatch[0].length);
1236
+ return {
1237
+ beforeApp: `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`,
1238
+ afterApp: `</div>${afterBody}`,
1239
+ beforeScript: "",
1240
+ afterScript
1241
+ };
1242
+ }
1243
+ return {
1244
+ beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
1245
+ afterApp: `</div>`,
1246
+ beforeScript,
1247
+ afterScript
1248
+ };
1249
+ }
1250
+ fillTemplate(response, app, script) {
1251
+ if (!this.preprocessedTemplate) this.preprocessedTemplate = this.preprocessTemplate(response.html);
1252
+ response.html = this.preprocessedTemplate.beforeApp + app + this.preprocessedTemplate.afterApp + script + this.preprocessedTemplate.afterScript;
1253
+ }
1254
+ };
1255
+
1256
+ //#endregion
1257
+ //#region src/services/ReactPageServerService.ts
1258
+ var ReactPageServerService = class extends ReactPageService {
1259
+ reactServerProvider = (0, __alepha_core.$inject)(ReactServerProvider);
1260
+ serverProvider = (0, __alepha_core.$inject)(__alepha_server.ServerProvider);
1261
+ async render(name, options = {}) {
1262
+ return this.reactServerProvider.render(name, options);
1263
+ }
1264
+ async fetch(pathname, options = {}) {
1265
+ const response = await fetch(`${this.serverProvider.hostname}/${pathname}`);
1266
+ const html = await response.text();
1267
+ if (options?.html) return {
1268
+ html,
1269
+ response
1270
+ };
1271
+ const match = html.match(this.reactServerProvider.ROOT_DIV_REGEX);
1272
+ if (match) return {
1273
+ html: match[3],
1274
+ response
1275
+ };
1276
+ throw new __alepha_core.AlephaError("Invalid HTML response");
1277
+ }
1278
+ };
1279
+
1280
+ //#endregion
1281
+ //#region src/providers/ReactBrowserRouterProvider.ts
1282
+ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1283
+ log = (0, __alepha_logger.$logger)();
1284
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1285
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1286
+ add(entry) {
1287
+ this.pageApi.add(entry);
1288
+ }
1289
+ configure = (0, __alepha_core.$hook)({
1290
+ on: "configure",
1291
+ handler: async () => {
1292
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
1293
+ path: page.match,
1294
+ page
1295
+ });
1296
+ }
1297
+ });
1298
+ async transition(url, previous = [], meta = {}) {
1299
+ const { pathname, search } = url;
1300
+ const state = {
1301
+ url,
1302
+ query: {},
1303
+ params: {},
1304
+ layers: [],
1305
+ onError: () => null,
1306
+ meta
1307
+ };
1308
+ await this.alepha.events.emit("react:action:begin", { type: "transition" });
1309
+ await this.alepha.events.emit("react:transition:begin", {
1310
+ previous: this.alepha.state.get("alepha.react.router.state"),
1311
+ state
1312
+ });
1313
+ try {
1314
+ const { route, params } = this.match(pathname);
1315
+ const query = {};
1316
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
1317
+ state.query = query;
1318
+ state.params = params ?? {};
1319
+ if (isPageRoute(route)) {
1320
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
1321
+ if (redirect) return redirect;
1322
+ }
1323
+ if (state.layers.length === 0) state.layers.push({
1324
+ name: "not-found",
1325
+ element: (0, react.createElement)(NotFoundPage),
1326
+ index: 0,
1327
+ path: "/"
1328
+ });
1329
+ await this.alepha.events.emit("react:action:success", { type: "transition" });
1330
+ await this.alepha.events.emit("react:transition:success", { state });
1331
+ } catch (e) {
1332
+ this.log.error("Transition has failed", e);
1333
+ state.layers = [{
1334
+ name: "error",
1335
+ element: this.pageApi.renderError(e),
1336
+ index: 0,
1337
+ path: "/"
1338
+ }];
1339
+ await this.alepha.events.emit("react:action:error", {
1340
+ type: "transition",
1341
+ error: e
1342
+ });
1343
+ await this.alepha.events.emit("react:transition:error", {
1344
+ error: e,
1345
+ state
1346
+ });
1347
+ }
1348
+ if (previous) for (let i = 0; i < previous.length; i++) {
1349
+ const layer = previous[i];
1350
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1351
+ }
1352
+ this.alepha.state.set("alepha.react.router.state", state);
1353
+ await this.alepha.events.emit("react:action:end", { type: "transition" });
1354
+ await this.alepha.events.emit("react:transition:end", { state });
1355
+ }
1356
+ root(state) {
1357
+ return this.pageApi.root(state);
1358
+ }
1359
+ };
1360
+
1361
+ //#endregion
1362
+ //#region src/providers/ReactBrowserProvider.ts
1363
+ const envSchema = __alepha_core.t.object({ REACT_ROOT_ID: __alepha_core.t.text({ default: "root" }) });
1364
+ /**
1365
+ * React browser renderer configuration atom
1366
+ */
1367
+ const reactBrowserOptions = (0, __alepha_core.$atom)({
1368
+ name: "alepha.react.browser.options",
1369
+ schema: __alepha_core.t.object({ scrollRestoration: __alepha_core.t.enum(["top", "manual"]) }),
1370
+ default: { scrollRestoration: "top" }
1371
+ });
1372
+ var ReactBrowserProvider = class {
1373
+ env = (0, __alepha_core.$env)(envSchema);
1374
+ log = (0, __alepha_logger.$logger)();
1375
+ client = (0, __alepha_core.$inject)(__alepha_server_links.LinkProvider);
1376
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1377
+ router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
1378
+ dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
1379
+ options = (0, __alepha_core.$use)(reactBrowserOptions);
1380
+ getRootElement() {
1381
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
1382
+ if (root) return root;
1383
+ const div = this.document.createElement("div");
1384
+ div.id = this.env.REACT_ROOT_ID;
1385
+ this.document.body.prepend(div);
1386
+ return div;
1387
+ }
1388
+ transitioning;
1389
+ get state() {
1390
+ return this.alepha.state.get("alepha.react.router.state");
1391
+ }
1392
+ /**
1393
+ * Accessor for Document DOM API.
1394
+ */
1395
+ get document() {
1396
+ return window.document;
1397
+ }
1398
+ /**
1399
+ * Accessor for History DOM API.
1400
+ */
1401
+ get history() {
1402
+ return window.history;
1403
+ }
1404
+ /**
1405
+ * Accessor for Location DOM API.
1406
+ */
1407
+ get location() {
1408
+ return window.location;
1409
+ }
1410
+ get base() {
1411
+ const base = {}.env?.BASE_URL;
1412
+ if (!base || base === "/") return "";
1413
+ return base;
1414
+ }
1415
+ get url() {
1416
+ const url = this.location.pathname + this.location.search;
1417
+ if (this.base) return url.replace(this.base, "");
1418
+ return url;
1419
+ }
1420
+ pushState(path, replace) {
1421
+ const url = this.base + path;
1422
+ if (replace) this.history.replaceState({}, "", url);
1423
+ else this.history.pushState({}, "", url);
1424
+ }
1425
+ async invalidate(props) {
1426
+ const previous = [];
1427
+ this.log.trace("Invalidating layers");
1428
+ if (props) {
1429
+ const [key] = Object.keys(props);
1430
+ const value = props[key];
1431
+ for (const layer of this.state.layers) {
1432
+ if (layer.props?.[key]) {
1433
+ previous.push({
1434
+ ...layer,
1435
+ props: {
1436
+ ...layer.props,
1437
+ [key]: value
1438
+ }
1439
+ });
1440
+ break;
1441
+ }
1442
+ previous.push(layer);
1443
+ }
1444
+ }
1445
+ await this.render({ previous });
1446
+ }
1447
+ async go(url, options = {}) {
1448
+ this.log.trace(`Going to ${url}`, {
1449
+ url,
1450
+ options
1451
+ });
1452
+ await this.render({
1453
+ url,
1454
+ previous: options.force ? [] : this.state.layers,
1455
+ meta: options.meta
1456
+ });
1457
+ if (this.state.url.pathname + this.state.url.search !== url) {
1458
+ this.pushState(this.state.url.pathname + this.state.url.search);
1459
+ return;
1460
+ }
1461
+ this.pushState(url, options.replace);
1462
+ }
1463
+ async render(options = {}) {
1464
+ const previous = options.previous ?? this.state.layers;
1465
+ const url = options.url ?? this.url;
1466
+ const start = this.dateTimeProvider.now();
1467
+ this.transitioning = {
1468
+ to: url,
1469
+ from: this.state?.url.pathname
1470
+ };
1471
+ this.log.debug("Transitioning...", { to: url });
1472
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
1473
+ if (redirect) {
1474
+ this.log.info("Redirecting to", { redirect });
1475
+ if (redirect.startsWith("http")) window.location.href = redirect;
1476
+ else return await this.render({ url: redirect });
1477
+ }
1478
+ const ms = this.dateTimeProvider.now().diff(start);
1479
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1480
+ this.transitioning = void 0;
1481
+ }
1482
+ /**
1483
+ * Get embedded layers from the server.
1484
+ */
1485
+ getHydrationState() {
1486
+ try {
1487
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1488
+ } catch (error) {
1489
+ console.error(error);
1490
+ }
1491
+ }
1492
+ onTransitionEnd = (0, __alepha_core.$hook)({
1493
+ on: "react:transition:end",
1494
+ handler: () => {
1495
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
1496
+ this.log.trace("Restoring scroll position to top");
1497
+ window.scrollTo(0, 0);
1498
+ }
1499
+ }
1500
+ });
1501
+ ready = (0, __alepha_core.$hook)({
1502
+ on: "ready",
1503
+ handler: async () => {
1504
+ const hydration = this.getHydrationState();
1505
+ const previous = hydration?.layers ?? [];
1506
+ if (hydration) {
1507
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
1508
+ }
1509
+ await this.render({ previous });
1510
+ const element = this.router.root(this.state);
1511
+ await this.alepha.events.emit("react:browser:render", {
1512
+ element,
1513
+ root: this.getRootElement(),
1514
+ hydration,
1515
+ state: this.state
1516
+ });
1517
+ window.addEventListener("popstate", () => {
1518
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1519
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1520
+ this.render();
1521
+ });
1522
+ }
1523
+ });
1524
+ };
1525
+
1526
+ //#endregion
1527
+ //#region src/services/ReactRouter.ts
1528
+ var ReactRouter = class {
1529
+ alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1530
+ pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1531
+ get state() {
1532
+ return this.alepha.state.get("alepha.react.router.state");
1533
+ }
1534
+ get pages() {
1535
+ return this.pageApi.getPages();
1536
+ }
1537
+ get concretePages() {
1538
+ return this.pageApi.getConcretePages();
1539
+ }
1540
+ get browser() {
1541
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1542
+ }
1543
+ isActive(href, options = {}) {
1544
+ const current = this.state.url.pathname;
1545
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
1546
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
1547
+ return isActive;
1548
+ }
1549
+ path(name, config = {}) {
1550
+ return this.pageApi.pathname(name, {
1551
+ params: {
1552
+ ...this.state.params,
1553
+ ...config.params
1554
+ },
1555
+ query: config.query
1556
+ });
1557
+ }
1558
+ /**
1559
+ * Reload the current page.
1560
+ * This is equivalent to calling `go()` with the current pathname and search.
1561
+ */
1562
+ async reload() {
1563
+ if (!this.browser) return;
1564
+ await this.go(this.location.pathname + this.location.search, {
1565
+ replace: true,
1566
+ force: true
1567
+ });
1568
+ }
1569
+ getURL() {
1570
+ if (!this.browser) return this.state.url;
1571
+ return new URL(this.location.href);
1572
+ }
1573
+ get location() {
1574
+ if (!this.browser) throw new Error("Browser is required");
1575
+ return this.browser.location;
1576
+ }
1577
+ get current() {
1578
+ return this.state;
1579
+ }
1580
+ get pathname() {
1581
+ return this.state.url.pathname;
1582
+ }
1583
+ get query() {
1584
+ const query = {};
1585
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1586
+ return query;
1587
+ }
1588
+ async back() {
1589
+ this.browser?.history.back();
1590
+ }
1591
+ async forward() {
1592
+ this.browser?.history.forward();
1593
+ }
1594
+ async invalidate(props) {
1595
+ await this.browser?.invalidate(props);
1596
+ }
1597
+ async go(path, options) {
1598
+ for (const page of this.pages) if (page.name === path) {
1599
+ await this.browser?.go(this.path(path, options), options);
1600
+ return;
1601
+ }
1602
+ await this.browser?.go(path, options);
1603
+ }
1604
+ anchor(path, options = {}) {
1605
+ let href = path;
1606
+ for (const page of this.pages) if (page.name === path) {
1607
+ href = this.path(path, options);
1608
+ break;
1609
+ }
1610
+ return {
1611
+ href: this.base(href),
1612
+ onClick: (ev) => {
1613
+ ev.stopPropagation();
1614
+ ev.preventDefault();
1615
+ this.go(href, options).catch(console.error);
1616
+ }
1617
+ };
1618
+ }
1619
+ base(path) {
1620
+ const base = {}.env?.BASE_URL;
1621
+ if (!base || base === "/") return path;
1622
+ return base + path;
1623
+ }
1624
+ /**
1625
+ * Set query params.
1626
+ *
1627
+ * @param record
1628
+ * @param options
1629
+ */
1630
+ setQueryParams(record, options = {}) {
1631
+ const func = typeof record === "function" ? record : () => record;
1632
+ const search = new URLSearchParams(func(this.query)).toString();
1633
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
1634
+ if (options.push) window.history.pushState({}, "", state);
1635
+ else window.history.replaceState({}, "", state);
1636
+ }
1637
+ };
1638
+
1639
+ //#endregion
1640
+ //#region src/hooks/useInject.ts
1641
+ /**
1642
+ * Hook to inject a service instance.
1643
+ * It's a wrapper of `useAlepha().inject(service)` with a memoization.
1644
+ */
1645
+ const useInject = (service) => {
1646
+ const alepha = useAlepha();
1647
+ return (0, react.useMemo)(() => alepha.inject(service), []);
1648
+ };
1649
+
1650
+ //#endregion
1651
+ //#region src/hooks/useRouter.ts
1652
+ /**
1653
+ * Use this hook to access the React Router instance.
1654
+ *
1655
+ * You can add a type parameter to specify the type of your application.
1656
+ * This will allow you to use the router in a typesafe way.
1657
+ *
1658
+ * @example
1659
+ * class App {
1660
+ * home = $page();
1661
+ * }
1662
+ *
1663
+ * const router = useRouter<App>();
1664
+ * router.go("home"); // typesafe
1665
+ */
1666
+ const useRouter = () => {
1667
+ return useInject(ReactRouter);
1668
+ };
1669
+
1670
+ //#endregion
1671
+ //#region src/components/Link.tsx
1672
+ const Link = (props) => {
1673
+ const router = useRouter();
1674
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1675
+ ...props,
1676
+ ...router.anchor(props.href),
1677
+ children: props.children
1678
+ });
1679
+ };
1680
+ var Link_default = Link;
1681
+
1682
+ //#endregion
1683
+ //#region src/hooks/useAction.ts
1684
+ /**
1685
+ * Hook for handling async actions with automatic error handling and event emission.
1686
+ *
1687
+ * By default, prevents concurrent executions - if an action is running and you call it again,
1688
+ * the second call will be ignored. Use `debounce` option to delay execution instead.
1689
+ *
1690
+ * Emits lifecycle events:
1691
+ * - `react:action:begin` - When action starts
1692
+ * - `react:action:success` - When action completes successfully
1693
+ * - `react:action:error` - When action throws an error
1694
+ * - `react:action:end` - Always emitted at the end
1695
+ *
1696
+ * @example Basic usage
1697
+ * ```tsx
1698
+ * const action = useAction({
1699
+ * handler: async (data) => {
1700
+ * await api.save(data);
1701
+ * }
1702
+ * }, []);
1703
+ *
1704
+ * <button onClick={() => action.run(data)} disabled={action.loading}>
1705
+ * Save
1706
+ * </button>
1707
+ * ```
1708
+ *
1709
+ * @example With debounce (search input)
1710
+ * ```tsx
1711
+ * const search = useAction({
1712
+ * handler: async (query: string) => {
1713
+ * await api.search(query);
1714
+ * },
1715
+ * debounce: 300 // Wait 300ms after last call
1716
+ * }, []);
1717
+ *
1718
+ * <input onChange={(e) => search.run(e.target.value)} />
1719
+ * ```
1720
+ *
1721
+ * @example Run on component mount
1722
+ * ```tsx
1723
+ * const fetchData = useAction({
1724
+ * handler: async () => {
1725
+ * const data = await api.getData();
1726
+ * return data;
1727
+ * },
1728
+ * runOnInit: true // Runs once when component mounts
1729
+ * }, []);
1730
+ * ```
1731
+ *
1732
+ * @example Run periodically (polling)
1733
+ * ```tsx
1734
+ * const pollStatus = useAction({
1735
+ * handler: async () => {
1736
+ * const status = await api.getStatus();
1737
+ * return status;
1738
+ * },
1739
+ * runEvery: 5000 // Run every 5 seconds
1740
+ * }, []);
1741
+ *
1742
+ * // Or with duration tuple
1743
+ * const pollStatus = useAction({
1744
+ * handler: async () => {
1745
+ * const status = await api.getStatus();
1746
+ * return status;
1747
+ * },
1748
+ * runEvery: [30, 'seconds'] // Run every 30 seconds
1749
+ * }, []);
1750
+ * ```
1751
+ *
1752
+ * @example With AbortController
1753
+ * ```tsx
1754
+ * const fetch = useAction({
1755
+ * handler: async (url, { signal }) => {
1756
+ * const response = await fetch(url, { signal });
1757
+ * return response.json();
1758
+ * }
1759
+ * }, []);
1760
+ * // Automatically cancelled on unmount or when new request starts
1761
+ * ```
1762
+ *
1763
+ * @example With error handling
1764
+ * ```tsx
1765
+ * const deleteAction = useAction({
1766
+ * handler: async (id: string) => {
1767
+ * await api.delete(id);
1768
+ * },
1769
+ * onError: (error) => {
1770
+ * if (error.code === 'NOT_FOUND') {
1771
+ * // Custom error handling
1772
+ * }
1773
+ * }
1774
+ * }, []);
1775
+ *
1776
+ * {deleteAction.error && <div>Error: {deleteAction.error.message}</div>}
1777
+ * ```
1778
+ *
1779
+ * @example Global error handling
1780
+ * ```tsx
1781
+ * // In your root app setup
1782
+ * alepha.events.on("react:action:error", ({ error }) => {
1783
+ * toast.danger(error.message);
1784
+ * Sentry.captureException(error);
1785
+ * });
1786
+ * ```
1787
+ */
1788
+ function useAction(options, deps) {
1789
+ const alepha = useAlepha();
1790
+ const dateTimeProvider = useInject(__alepha_datetime.DateTimeProvider);
1791
+ const [loading, setLoading] = (0, react.useState)(false);
1792
+ const [error, setError] = (0, react.useState)();
1793
+ const isExecutingRef = (0, react.useRef)(false);
1794
+ const debounceTimerRef = (0, react.useRef)(void 0);
1795
+ const abortControllerRef = (0, react.useRef)(void 0);
1796
+ const isMountedRef = (0, react.useRef)(true);
1797
+ const intervalRef = (0, react.useRef)(void 0);
1798
+ (0, react.useEffect)(() => {
1799
+ return () => {
1800
+ isMountedRef.current = false;
1801
+ if (debounceTimerRef.current) {
1802
+ dateTimeProvider.clearTimeout(debounceTimerRef.current);
1803
+ debounceTimerRef.current = void 0;
1804
+ }
1805
+ if (intervalRef.current) {
1806
+ dateTimeProvider.clearInterval(intervalRef.current);
1807
+ intervalRef.current = void 0;
1808
+ }
1809
+ if (abortControllerRef.current) {
1810
+ abortControllerRef.current.abort();
1811
+ abortControllerRef.current = void 0;
1812
+ }
1813
+ };
1814
+ }, []);
1815
+ const executeAction = (0, react.useCallback)(async (...args) => {
1816
+ if (isExecutingRef.current) return;
1817
+ if (abortControllerRef.current) abortControllerRef.current.abort();
1818
+ const abortController = new AbortController();
1819
+ abortControllerRef.current = abortController;
1820
+ isExecutingRef.current = true;
1821
+ setLoading(true);
1822
+ setError(void 0);
1823
+ await alepha.events.emit("react:action:begin", {
1824
+ type: "custom",
1825
+ id: options.id
1826
+ });
1827
+ try {
1828
+ const result = await options.handler(...args, { signal: abortController.signal });
1829
+ if (!isMountedRef.current || abortController.signal.aborted) return;
1830
+ await alepha.events.emit("react:action:success", {
1831
+ type: "custom",
1832
+ id: options.id
1833
+ });
1834
+ if (options.onSuccess) await options.onSuccess(result);
1835
+ return result;
1836
+ } catch (err) {
1837
+ if (err instanceof Error && err.name === "AbortError") return;
1838
+ if (!isMountedRef.current) return;
1839
+ const error$1 = err;
1840
+ setError(error$1);
1841
+ await alepha.events.emit("react:action:error", {
1842
+ type: "custom",
1843
+ id: options.id,
1844
+ error: error$1
1845
+ });
1846
+ if (options.onError) await options.onError(error$1);
1847
+ else throw error$1;
1848
+ } finally {
1849
+ isExecutingRef.current = false;
1850
+ setLoading(false);
1851
+ await alepha.events.emit("react:action:end", {
1852
+ type: "custom",
1853
+ id: options.id
1854
+ });
1855
+ if (abortControllerRef.current === abortController) abortControllerRef.current = void 0;
1856
+ }
1857
+ }, [
1858
+ ...deps,
1859
+ options.id,
1860
+ options.onError,
1861
+ options.onSuccess
1862
+ ]);
1863
+ const handler = (0, react.useCallback)(async (...args) => {
1864
+ if (options.debounce) {
1865
+ if (debounceTimerRef.current) dateTimeProvider.clearTimeout(debounceTimerRef.current);
1866
+ return new Promise((resolve) => {
1867
+ debounceTimerRef.current = dateTimeProvider.createTimeout(async () => {
1868
+ resolve(await executeAction(...args));
1869
+ }, options.debounce ?? 0);
1870
+ });
1871
+ }
1872
+ return executeAction(...args);
1873
+ }, [executeAction, options.debounce]);
1874
+ const cancel = (0, react.useCallback)(() => {
1875
+ if (debounceTimerRef.current) {
1876
+ dateTimeProvider.clearTimeout(debounceTimerRef.current);
1877
+ debounceTimerRef.current = void 0;
1878
+ }
1879
+ if (abortControllerRef.current) {
1880
+ abortControllerRef.current.abort();
1881
+ abortControllerRef.current = void 0;
1882
+ }
1883
+ if (isMountedRef.current) {
1884
+ isExecutingRef.current = false;
1885
+ setLoading(false);
1886
+ }
1887
+ }, []);
1888
+ (0, react.useEffect)(() => {
1889
+ if (options.runOnInit) handler(...[]);
1890
+ }, deps);
1891
+ (0, react.useEffect)(() => {
1892
+ if (!options.runEvery) return;
1893
+ intervalRef.current = dateTimeProvider.createInterval(() => handler(...[]), options.runEvery, true);
1894
+ return () => {
1895
+ if (intervalRef.current) {
1896
+ dateTimeProvider.clearInterval(intervalRef.current);
1897
+ intervalRef.current = void 0;
1898
+ }
1899
+ };
1900
+ }, [handler, options.runEvery]);
1901
+ return {
1902
+ run: handler,
1903
+ loading,
1904
+ error,
1905
+ cancel
1906
+ };
1907
+ }
1908
+
1909
+ //#endregion
1910
+ //#region src/hooks/useActive.ts
1911
+ const useActive = (args) => {
1912
+ const router = useRouter();
1913
+ const [isPending, setPending] = (0, react.useState)(false);
1914
+ useRouterState().url.pathname;
1915
+ const options = typeof args === "string" ? { href: args } : {
1916
+ ...args,
1917
+ href: args.href
1918
+ };
1919
+ const href = options.href;
1920
+ const isActive = router.isActive(href, options);
1921
+ return {
1922
+ isPending,
1923
+ isActive,
1924
+ anchorProps: {
1925
+ href: router.base(href),
1926
+ onClick: async (ev) => {
1927
+ ev?.stopPropagation();
1928
+ ev?.preventDefault();
1929
+ if (isActive) return;
1930
+ if (isPending) return;
1931
+ setPending(true);
1932
+ try {
1933
+ await router.go(href);
1934
+ } finally {
1935
+ setPending(false);
1936
+ }
1937
+ }
1938
+ }
1939
+ };
1940
+ };
1941
+
1942
+ //#endregion
1943
+ //#region src/hooks/useClient.ts
1944
+ /**
1945
+ * Hook to get a virtual client for the specified scope.
1946
+ *
1947
+ * It's the React-hook version of `$client()`, from `AlephaServerLinks` module.
1948
+ */
1949
+ const useClient = (scope) => {
1950
+ return useInject(__alepha_server_links.LinkProvider).client(scope);
1951
+ };
1952
+
1953
+ //#endregion
1954
+ //#region src/hooks/useQueryParams.ts
1955
+ /**
1956
+ * Not well tested. Use with caution.
1957
+ */
1958
+ const useQueryParams = (schema, options = {}) => {
1959
+ const alepha = useAlepha();
1960
+ const key = options.key ?? "q";
1961
+ const router = useRouter();
1962
+ const querystring = router.query[key];
1963
+ const [queryParams = {}, setQueryParams] = (0, react.useState)(decode(alepha, schema, router.query[key]));
1964
+ (0, react.useEffect)(() => {
1965
+ setQueryParams(decode(alepha, schema, querystring));
1966
+ }, [querystring]);
1967
+ return [queryParams, (queryParams$1) => {
1968
+ setQueryParams(queryParams$1);
1969
+ router.setQueryParams((data) => {
1970
+ return {
1971
+ ...data,
1972
+ [key]: encode(alepha, schema, queryParams$1)
1973
+ };
1974
+ });
1975
+ }];
1976
+ };
1977
+ const encode = (alepha, schema, data) => {
1978
+ return btoa(JSON.stringify(alepha.codec.decode(schema, data)));
1979
+ };
1980
+ const decode = (alepha, schema, data) => {
1981
+ try {
1982
+ return alepha.codec.decode(schema, JSON.parse(atob(decodeURIComponent(data))));
1983
+ } catch {
1984
+ return;
1985
+ }
1986
+ };
1987
+
1988
+ //#endregion
1989
+ //#region src/hooks/useSchema.ts
1990
+ const useSchema = (action) => {
1991
+ const name = action.name;
1992
+ const alepha = useAlepha();
1993
+ const httpClient = useInject(__alepha_server.HttpClient);
1994
+ const [schema, setSchema] = (0, react.useState)(ssrSchemaLoading(alepha, name));
1995
+ (0, react.useEffect)(() => {
1996
+ if (!schema.loading) return;
1997
+ httpClient.fetch(`${__alepha_server_links.LinkProvider.path.apiLinks}/${name}/schema`, { localCache: true }).then((it) => setSchema(it.data));
1998
+ }, [name]);
1999
+ return schema;
2000
+ };
2001
+ /**
2002
+ * Get an action schema during server-side rendering (SSR) or client-side rendering (CSR).
2003
+ */
2004
+ const ssrSchemaLoading = (alepha, name) => {
2005
+ if (!alepha.isBrowser()) {
2006
+ const linkProvider = alepha.inject(__alepha_server_links.LinkProvider);
2007
+ const can = linkProvider.getServerLinks().find((link) => link.name === name);
2008
+ if (can) {
2009
+ const schema$1 = linkProvider.links.find((it) => it.name === name)?.schema;
2010
+ if (schema$1) {
2011
+ can.schema = schema$1;
2012
+ return schema$1;
2013
+ }
2014
+ }
2015
+ return { loading: true };
2016
+ }
2017
+ const schema = alepha.inject(__alepha_server_links.LinkProvider).links.find((it) => it.name === name)?.schema;
2018
+ if (schema) return schema;
2019
+ return { loading: true };
2020
+ };
2021
+
2022
+ //#endregion
2023
+ //#region src/index.ts
2024
+ /**
2025
+ * Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
2026
+ *
2027
+ * The React module enables building modern React applications using the `$page` descriptor on class properties.
2028
+ * It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
2029
+ * type safety and schema validation for route parameters and data.
2030
+ *
2031
+ * @see {@link $page}
2032
+ * @module alepha.react
2033
+ */
2034
+ const AlephaReact = (0, __alepha_core.$module)({
2035
+ name: "alepha.react",
2036
+ descriptors: [$page],
2037
+ services: [
2038
+ ReactServerProvider,
2039
+ ReactPageProvider,
2040
+ ReactRouter,
2041
+ ReactPageService,
2042
+ ReactPageServerService
2043
+ ],
2044
+ register: (alepha) => alepha.with(__alepha_datetime.AlephaDateTime).with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with({
2045
+ provide: ReactPageService,
2046
+ use: ReactPageServerService
2047
+ }).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
2048
+ });
2049
+
2050
+ //#endregion
2051
+ exports.$page = $page;
2052
+ exports.AlephaContext = AlephaContext;
2053
+ exports.AlephaReact = AlephaReact;
2054
+ exports.ClientOnly = ClientOnly_default;
2055
+ exports.ErrorBoundary = ErrorBoundary_default;
2056
+ exports.ErrorViewer = ErrorViewer_default;
2057
+ exports.Link = Link_default;
2058
+ exports.NestedView = NestedView_default;
2059
+ exports.NotFound = NotFoundPage;
2060
+ exports.PageDescriptor = PageDescriptor;
2061
+ exports.ReactBrowserProvider = ReactBrowserProvider;
2062
+ exports.ReactPageProvider = ReactPageProvider;
2063
+ exports.ReactRouter = ReactRouter;
2064
+ exports.ReactServerProvider = ReactServerProvider;
2065
+ exports.Redirection = Redirection;
2066
+ exports.RouterLayerContext = RouterLayerContext;
2067
+ exports.isPageRoute = isPageRoute;
2068
+ exports.reactBrowserOptions = reactBrowserOptions;
2069
+ exports.reactServerOptions = reactServerOptions;
2070
+ exports.ssrSchemaLoading = ssrSchemaLoading;
2071
+ exports.useAction = useAction;
2072
+ exports.useActive = useActive;
2073
+ exports.useAlepha = useAlepha;
2074
+ exports.useClient = useClient;
2075
+ exports.useEvents = useEvents;
2076
+ exports.useInject = useInject;
2077
+ exports.useQueryParams = useQueryParams;
2078
+ exports.useRouter = useRouter;
2079
+ exports.useRouterState = useRouterState;
2080
+ exports.useSchema = useSchema;
2081
+ exports.useStore = useStore;
2082
+ //# sourceMappingURL=index.cjs.map