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