@alepha/react 0.14.0 → 0.14.2

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