@alepha/react 0.11.11 → 0.12.0

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 (128) hide show
  1. package/README.md +1 -183
  2. package/dist/auth/index.browser.js +1460 -0
  3. package/dist/auth/index.browser.js.map +1 -0
  4. package/dist/auth/index.cjs +3647 -0
  5. package/dist/auth/index.cjs.map +1 -0
  6. package/dist/auth/index.d.cts +564 -0
  7. package/dist/auth/index.d.cts.map +1 -0
  8. package/dist/auth/index.d.ts +564 -0
  9. package/dist/auth/index.d.ts.map +1 -0
  10. package/dist/auth/index.js +3615 -0
  11. package/dist/auth/index.js.map +1 -0
  12. package/dist/{index.browser.js → core/index.browser.js} +36 -35
  13. package/dist/core/index.browser.js.map +1 -0
  14. package/dist/{index.cjs → core/index.cjs} +141 -140
  15. package/dist/core/index.cjs.map +1 -0
  16. package/dist/{index.d.cts → core/index.d.cts} +68 -68
  17. package/dist/core/index.d.cts.map +1 -0
  18. package/dist/{index.d.ts → core/index.d.ts} +68 -68
  19. package/dist/core/index.d.ts.map +1 -0
  20. package/dist/{index.js → core/index.js} +39 -38
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/form/index.cjs +2054 -0
  23. package/dist/form/index.cjs.map +1 -0
  24. package/dist/form/index.d.cts +211 -0
  25. package/dist/form/index.d.cts.map +1 -0
  26. package/dist/form/index.d.ts +211 -0
  27. package/dist/form/index.d.ts.map +1 -0
  28. package/dist/form/index.js +2026 -0
  29. package/dist/form/index.js.map +1 -0
  30. package/dist/head/index.browser.js +1503 -0
  31. package/dist/head/index.browser.js.map +1 -0
  32. package/dist/head/index.cjs +1908 -0
  33. package/dist/head/index.cjs.map +1 -0
  34. package/dist/head/index.d.cts +595 -0
  35. package/dist/head/index.d.cts.map +1 -0
  36. package/dist/head/index.d.ts +601 -0
  37. package/dist/head/index.d.ts.map +1 -0
  38. package/dist/head/index.js +1880 -0
  39. package/dist/head/index.js.map +1 -0
  40. package/dist/i18n/index.cjs +1886 -0
  41. package/dist/i18n/index.cjs.map +1 -0
  42. package/dist/i18n/index.d.cts +168 -0
  43. package/dist/i18n/index.d.cts.map +1 -0
  44. package/dist/i18n/index.d.ts +168 -0
  45. package/dist/i18n/index.d.ts.map +1 -0
  46. package/dist/i18n/index.js +1857 -0
  47. package/dist/i18n/index.js.map +1 -0
  48. package/dist/websocket/index.cjs +1774 -0
  49. package/dist/websocket/index.cjs.map +1 -0
  50. package/dist/websocket/index.d.cts +118 -0
  51. package/dist/websocket/index.d.cts.map +1 -0
  52. package/dist/websocket/index.d.ts +118 -0
  53. package/dist/websocket/index.d.ts.map +1 -0
  54. package/dist/websocket/index.js +1750 -0
  55. package/dist/websocket/index.js.map +1 -0
  56. package/package.json +89 -67
  57. package/src/auth/descriptors/$auth.ts +436 -0
  58. package/src/auth/descriptors/$authApple.ts +8 -0
  59. package/src/auth/descriptors/$authGithub.ts +81 -0
  60. package/src/auth/descriptors/$authGoogle.ts +38 -0
  61. package/src/auth/errors/SessionExpiredError.ts +6 -0
  62. package/src/auth/hooks/useAuth.ts +31 -0
  63. package/src/auth/index.browser.ts +16 -0
  64. package/src/auth/index.shared.ts +3 -0
  65. package/src/auth/index.ts +47 -0
  66. package/src/auth/providers/ReactAuthProvider.ts +629 -0
  67. package/src/auth/schemas/tokenResponseSchema.ts +11 -0
  68. package/src/auth/schemas/tokensSchema.ts +21 -0
  69. package/src/auth/schemas/userinfoResponseSchema.ts +10 -0
  70. package/src/auth/services/ReactAuth.ts +124 -0
  71. package/src/{components → core/components}/ErrorViewer.tsx +3 -2
  72. package/src/{components → core/components}/NestedView.tsx +1 -1
  73. package/src/{contexts → core/contexts}/AlephaContext.ts +1 -1
  74. package/src/{descriptors → core/descriptors}/$page.ts +4 -4
  75. package/src/{hooks → core/hooks}/useAction.ts +1 -1
  76. package/src/{hooks → core/hooks}/useAlepha.ts +1 -1
  77. package/src/{hooks → core/hooks}/useClient.ts +1 -1
  78. package/src/{hooks → core/hooks}/useEvents.ts +1 -1
  79. package/src/{hooks → core/hooks}/useInject.ts +1 -1
  80. package/src/{hooks → core/hooks}/useQueryParams.ts +1 -1
  81. package/src/{hooks → core/hooks}/useRouterState.ts +1 -1
  82. package/src/{hooks → core/hooks}/useSchema.ts +3 -3
  83. package/src/{hooks → core/hooks}/useStore.ts +2 -2
  84. package/src/{index.browser.ts → core/index.browser.ts} +4 -4
  85. package/src/{index.ts → core/index.ts} +6 -6
  86. package/src/{providers → core/providers}/ReactBrowserProvider.ts +6 -6
  87. package/src/{providers → core/providers}/ReactBrowserRendererProvider.ts +2 -2
  88. package/src/{providers → core/providers}/ReactBrowserRouterProvider.ts +3 -3
  89. package/src/{providers → core/providers}/ReactPageProvider.ts +3 -3
  90. package/src/{providers → core/providers}/ReactServerProvider.ts +7 -7
  91. package/src/{services → core/services}/ReactPageServerService.ts +2 -2
  92. package/src/{services → core/services}/ReactPageService.ts +1 -1
  93. package/src/{services → core/services}/ReactRouter.ts +1 -1
  94. package/src/form/components/FormState.tsx +17 -0
  95. package/src/form/hooks/useForm.ts +47 -0
  96. package/src/form/hooks/useFormState.ts +130 -0
  97. package/src/form/index.ts +38 -0
  98. package/src/form/services/FormModel.ts +548 -0
  99. package/src/head/descriptors/$head.ts +25 -0
  100. package/src/head/hooks/useHead.ts +62 -0
  101. package/src/head/index.browser.ts +25 -0
  102. package/src/head/index.ts +47 -0
  103. package/src/head/interfaces/Head.ts +46 -0
  104. package/src/head/providers/BrowserHeadProvider.ts +105 -0
  105. package/src/head/providers/HeadProvider.ts +73 -0
  106. package/src/head/providers/ServerHeadProvider.ts +109 -0
  107. package/src/i18n/README.md +76 -0
  108. package/src/i18n/components/Localize.tsx +35 -0
  109. package/src/i18n/descriptors/$dictionary.ts +65 -0
  110. package/src/i18n/hooks/useI18n.ts +18 -0
  111. package/src/i18n/index.ts +34 -0
  112. package/src/i18n/providers/I18nProvider.ts +277 -0
  113. package/src/websocket/hooks/useRoom.tsx +223 -0
  114. package/src/websocket/index.ts +7 -0
  115. package/dist/index.browser.js.map +0 -1
  116. package/dist/index.cjs.map +0 -1
  117. package/dist/index.d.cts.map +0 -1
  118. package/dist/index.d.ts.map +0 -1
  119. package/dist/index.js.map +0 -1
  120. /package/src/{components → core/components}/ClientOnly.tsx +0 -0
  121. /package/src/{components → core/components}/ErrorBoundary.tsx +0 -0
  122. /package/src/{components → core/components}/Link.tsx +0 -0
  123. /package/src/{components → core/components}/NotFound.tsx +0 -0
  124. /package/src/{contexts → core/contexts}/RouterLayerContext.ts +0 -0
  125. /package/src/{errors → core/errors}/Redirection.ts +0 -0
  126. /package/src/{hooks → core/hooks}/useActive.ts +0 -0
  127. /package/src/{hooks → core/hooks}/useRouter.ts +0 -0
  128. /package/src/{index.shared.ts → core/index.shared.ts} +0 -0
@@ -0,0 +1,1460 @@
1
+ import { $atom, $env, $hook, $inject, $module, $use, Alepha, AlephaError, Atom, Descriptor, KIND, createDescriptor, t } from "alepha";
2
+ import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
3
+ import { AlephaServer, HttpClient } from "alepha/server";
4
+ import { AlephaServerLinks, LinkProvider, apiLinksResponseSchema } from "alepha/server/links";
5
+ import { $logger } from "alepha/logger";
6
+ import { RouterProvider } from "alepha/router";
7
+ import React, { StrictMode, createContext, createElement, memo, use, useContext, useEffect, useMemo, useRef, useState } from "react";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { createRoot, hydrateRoot } from "react-dom/client";
10
+ import { userAccountInfoSchema } from "alepha/security";
11
+
12
+ //#region src/core/services/ReactPageService.ts
13
+ var ReactPageService = class {
14
+ fetch(pathname, options = {}) {
15
+ throw new AlephaError("Fetch is not available for this environment.");
16
+ }
17
+ render(name, options = {}) {
18
+ throw new AlephaError("Render is not available for this environment.");
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ //#region src/core/descriptors/$page.ts
24
+ /**
25
+ * Main descriptor for defining a React route in the application.
26
+ *
27
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
28
+ * It provides a declarative way to define pages with powerful features:
29
+ *
30
+ * **Routing & Navigation**
31
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
32
+ * - Nested routing with parent-child relationships
33
+ * - Type-safe URL parameter and query string validation
34
+ *
35
+ * **Data Loading**
36
+ * - Server-side data fetching with the `resolve` function
37
+ * - Automatic serialization and hydration for SSR
38
+ * - Access to request context, URL params, and parent data
39
+ *
40
+ * **Component Loading**
41
+ * - Direct component rendering or lazy loading for code splitting
42
+ * - Client-only rendering when browser APIs are needed
43
+ * - Automatic fallback handling during hydration
44
+ *
45
+ * **Performance Optimization**
46
+ * - Static generation for pre-rendered pages at build time
47
+ * - Server-side caching with configurable TTL and providers
48
+ * - Code splitting through lazy component loading
49
+ *
50
+ * **Error Handling**
51
+ * - Custom error handlers with support for redirects
52
+ * - Hierarchical error handling (child → parent)
53
+ * - HTTP status code handling (404, 401, etc.)
54
+ *
55
+ * **Page Animations**
56
+ * - CSS-based enter/exit animations
57
+ * - Dynamic animations based on page state
58
+ * - Custom timing and easing functions
59
+ *
60
+ * **Lifecycle Management**
61
+ * - Server response hooks for headers and status codes
62
+ * - Page leave handlers for cleanup (browser only)
63
+ * - Permission-based access control
64
+ *
65
+ * @example Simple page with data fetching
66
+ * ```typescript
67
+ * const userProfile = $page({
68
+ * path: "/users/:id",
69
+ * schema: {
70
+ * params: t.object({ id: t.int() }),
71
+ * query: t.object({ tab: t.optional(t.text()) })
72
+ * },
73
+ * resolve: async ({ params }) => {
74
+ * const user = await userApi.getUser(params.id);
75
+ * return { user };
76
+ * },
77
+ * lazy: () => import("./UserProfile.tsx")
78
+ * });
79
+ * ```
80
+ *
81
+ * @example Nested routing with error handling
82
+ * ```typescript
83
+ * const projectSection = $page({
84
+ * path: "/projects/:id",
85
+ * children: () => [projectBoard, projectSettings],
86
+ * resolve: async ({ params }) => {
87
+ * const project = await projectApi.get(params.id);
88
+ * return { project };
89
+ * },
90
+ * errorHandler: (error) => {
91
+ * if (HttpError.is(error, 404)) {
92
+ * return <ProjectNotFound />;
93
+ * }
94
+ * }
95
+ * });
96
+ * ```
97
+ *
98
+ * @example Static generation with caching
99
+ * ```typescript
100
+ * const blogPost = $page({
101
+ * path: "/blog/:slug",
102
+ * static: {
103
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
104
+ * },
105
+ * resolve: async ({ params }) => {
106
+ * const post = await loadPost(params.slug);
107
+ * return { post };
108
+ * }
109
+ * });
110
+ * ```
111
+ */
112
+ const $page = (options) => {
113
+ return createDescriptor(PageDescriptor, options);
114
+ };
115
+ var PageDescriptor = class extends Descriptor {
116
+ reactPageService = $inject(ReactPageService);
117
+ onInit() {
118
+ if (this.options.static) this.options.cache ??= { store: {
119
+ provider: "memory",
120
+ ttl: [1, "week"]
121
+ } };
122
+ }
123
+ get name() {
124
+ return this.options.name ?? this.config.propertyKey;
125
+ }
126
+ /**
127
+ * For testing or build purposes.
128
+ *
129
+ * This will render the page (HTML layout included or not) and return the HTML + context.
130
+ * Only valid for server-side rendering, it will throw an error if called on the client-side.
131
+ */
132
+ async render(options) {
133
+ return this.reactPageService.render(this.name, options);
134
+ }
135
+ async fetch(options) {
136
+ return this.reactPageService.fetch(this.options.path || "", options);
137
+ }
138
+ match(url) {
139
+ return false;
140
+ }
141
+ pathname(config) {
142
+ return this.options.path || "";
143
+ }
144
+ };
145
+ $page[KIND] = PageDescriptor;
146
+
147
+ //#endregion
148
+ //#region src/core/components/NotFound.tsx
149
+ function NotFoundPage(props) {
150
+ return /* @__PURE__ */ jsx("div", {
151
+ style: {
152
+ height: "100vh",
153
+ display: "flex",
154
+ flexDirection: "column",
155
+ justifyContent: "center",
156
+ alignItems: "center",
157
+ textAlign: "center",
158
+ fontFamily: "sans-serif",
159
+ padding: "1rem",
160
+ ...props.style
161
+ },
162
+ children: /* @__PURE__ */ jsx("h1", {
163
+ style: {
164
+ fontSize: "1rem",
165
+ marginBottom: "0.5rem"
166
+ },
167
+ children: "404 - This page does not exist"
168
+ })
169
+ });
170
+ }
171
+
172
+ //#endregion
173
+ //#region src/core/components/ClientOnly.tsx
174
+ /**
175
+ * A small utility component that renders its children only on the client side.
176
+ *
177
+ * Optionally, you can provide a fallback React node that will be rendered.
178
+ *
179
+ * You should use this component when
180
+ * - you have code that relies on browser-specific APIs
181
+ * - you want to avoid server-side rendering for a specific part of your application
182
+ * - you want to prevent pre-rendering of a component
183
+ */
184
+ const ClientOnly = (props) => {
185
+ const [mounted, setMounted] = useState(false);
186
+ useEffect(() => setMounted(true), []);
187
+ if (props.disabled) return props.children;
188
+ return mounted ? props.children : props.fallback;
189
+ };
190
+ var ClientOnly_default = ClientOnly;
191
+
192
+ //#endregion
193
+ //#region src/core/components/ErrorViewer.tsx
194
+ const ErrorViewer = ({ error, alepha }) => {
195
+ const [expanded, setExpanded] = useState(false);
196
+ if (alepha.isProduction()) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
197
+ const stackLines = error.stack?.split("\n") ?? [];
198
+ const previewLines = stackLines.slice(0, 5);
199
+ const hiddenLineCount = stackLines.length - previewLines.length;
200
+ const copyToClipboard = (text) => {
201
+ navigator.clipboard.writeText(text).catch((err) => {
202
+ console.error("Clipboard error:", err);
203
+ });
204
+ };
205
+ const styles = {
206
+ container: {
207
+ padding: "24px",
208
+ backgroundColor: "#FEF2F2",
209
+ color: "#7F1D1D",
210
+ border: "1px solid #FECACA",
211
+ borderRadius: "16px",
212
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
213
+ fontFamily: "monospace",
214
+ maxWidth: "768px",
215
+ margin: "40px auto"
216
+ },
217
+ heading: {
218
+ fontSize: "20px",
219
+ fontWeight: "bold",
220
+ marginBottom: "10px"
221
+ },
222
+ name: {
223
+ fontSize: "16px",
224
+ fontWeight: 600
225
+ },
226
+ message: {
227
+ fontSize: "14px",
228
+ marginBottom: "16px"
229
+ },
230
+ sectionHeader: {
231
+ display: "flex",
232
+ justifyContent: "space-between",
233
+ alignItems: "center",
234
+ fontSize: "12px",
235
+ marginBottom: "4px",
236
+ color: "#991B1B"
237
+ },
238
+ copyButton: {
239
+ fontSize: "12px",
240
+ color: "#DC2626",
241
+ background: "none",
242
+ border: "none",
243
+ cursor: "pointer",
244
+ textDecoration: "underline"
245
+ },
246
+ stackContainer: {
247
+ backgroundColor: "#FEE2E2",
248
+ padding: "12px",
249
+ borderRadius: "8px",
250
+ fontSize: "13px",
251
+ lineHeight: "1.4",
252
+ overflowX: "auto",
253
+ whiteSpace: "pre-wrap"
254
+ },
255
+ expandLine: {
256
+ color: "#F87171",
257
+ cursor: "pointer",
258
+ marginTop: "8px"
259
+ }
260
+ };
261
+ return /* @__PURE__ */ jsxs("div", {
262
+ style: styles.container,
263
+ children: [/* @__PURE__ */ jsxs("div", { children: [
264
+ /* @__PURE__ */ jsx("div", {
265
+ style: styles.heading,
266
+ children: "🔥 Error"
267
+ }),
268
+ /* @__PURE__ */ jsx("div", {
269
+ style: styles.name,
270
+ children: error.name
271
+ }),
272
+ /* @__PURE__ */ jsx("div", {
273
+ style: styles.message,
274
+ children: error.message
275
+ })
276
+ ] }), stackLines.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
277
+ style: styles.sectionHeader,
278
+ children: [/* @__PURE__ */ jsx("span", { children: "Stack trace" }), /* @__PURE__ */ jsx("button", {
279
+ type: "button",
280
+ onClick: () => copyToClipboard(error.stack),
281
+ style: styles.copyButton,
282
+ children: "Copy all"
283
+ })]
284
+ }), /* @__PURE__ */ jsxs("pre", {
285
+ style: styles.stackContainer,
286
+ children: [(expanded ? stackLines : previewLines).map((line, i) => /* @__PURE__ */ jsx("div", { children: line }, i)), !expanded && hiddenLineCount > 0 && /* @__PURE__ */ jsxs("div", {
287
+ style: styles.expandLine,
288
+ onClick: () => setExpanded(true),
289
+ children: [
290
+ "+ ",
291
+ hiddenLineCount,
292
+ " more lines..."
293
+ ]
294
+ })]
295
+ })] })]
296
+ });
297
+ };
298
+ var ErrorViewer_default = ErrorViewer;
299
+ const ErrorViewerProduction = () => {
300
+ const styles = {
301
+ container: {
302
+ padding: "24px",
303
+ backgroundColor: "#FEF2F2",
304
+ color: "#7F1D1D",
305
+ border: "1px solid #FECACA",
306
+ borderRadius: "16px",
307
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
308
+ fontFamily: "monospace",
309
+ maxWidth: "768px",
310
+ margin: "40px auto",
311
+ textAlign: "center"
312
+ },
313
+ heading: {
314
+ fontSize: "20px",
315
+ fontWeight: "bold",
316
+ marginBottom: "8px"
317
+ },
318
+ name: {
319
+ fontSize: "16px",
320
+ fontWeight: 600,
321
+ marginBottom: "4px"
322
+ },
323
+ message: {
324
+ fontSize: "14px",
325
+ opacity: .85
326
+ }
327
+ };
328
+ return /* @__PURE__ */ jsxs("div", {
329
+ style: styles.container,
330
+ children: [/* @__PURE__ */ jsx("div", {
331
+ style: styles.heading,
332
+ children: "🚨 An error occurred"
333
+ }), /* @__PURE__ */ jsx("div", {
334
+ style: styles.message,
335
+ children: "Something went wrong. Please try again later."
336
+ })]
337
+ });
338
+ };
339
+
340
+ //#endregion
341
+ //#region src/core/contexts/RouterLayerContext.ts
342
+ const RouterLayerContext = createContext(void 0);
343
+
344
+ //#endregion
345
+ //#region src/core/errors/Redirection.ts
346
+ /**
347
+ * Used for Redirection during the page loading.
348
+ *
349
+ * Depends on the context, it can be thrown or just returned.
350
+ */
351
+ var Redirection = class extends Error {
352
+ redirect;
353
+ constructor(redirect) {
354
+ super("Redirection");
355
+ this.redirect = redirect;
356
+ }
357
+ };
358
+
359
+ //#endregion
360
+ //#region src/core/contexts/AlephaContext.ts
361
+ const AlephaContext = createContext(void 0);
362
+
363
+ //#endregion
364
+ //#region src/core/hooks/useAlepha.ts
365
+ /**
366
+ * Main Alepha hook.
367
+ *
368
+ * It provides access to the Alepha instance within a React component.
369
+ *
370
+ * With Alepha, you can access the core functionalities of the framework:
371
+ *
372
+ * - alepha.state() for state management
373
+ * - alepha.inject() for dependency injection
374
+ * - alepha.events.emit() for event handling
375
+ * etc...
376
+ */
377
+ const useAlepha = () => {
378
+ const alepha = useContext(AlephaContext);
379
+ if (!alepha) throw new AlephaError("Hook 'useAlepha()' must be used within an AlephaContext.Provider");
380
+ return alepha;
381
+ };
382
+
383
+ //#endregion
384
+ //#region src/core/hooks/useEvents.ts
385
+ /**
386
+ * Allow subscribing to multiple Alepha events. See {@link Hooks} for available events.
387
+ *
388
+ * useEvents is fully typed to ensure correct event callback signatures.
389
+ *
390
+ * @example
391
+ * ```tsx
392
+ * useEvents(
393
+ * {
394
+ * "react:transition:begin": (ev) => {
395
+ * console.log("Transition began to:", ev.to);
396
+ * },
397
+ * "react:transition:error": {
398
+ * priority: "first",
399
+ * callback: (ev) => {
400
+ * console.error("Transition error:", ev.error);
401
+ * },
402
+ * },
403
+ * },
404
+ * [],
405
+ * );
406
+ * ```
407
+ */
408
+ const useEvents = (opts, deps) => {
409
+ const alepha = useAlepha();
410
+ useEffect(() => {
411
+ if (!alepha.isBrowser()) return;
412
+ const subs = [];
413
+ for (const [name, hook] of Object.entries(opts)) subs.push(alepha.events.on(name, hook));
414
+ return () => {
415
+ for (const clear of subs) clear();
416
+ };
417
+ }, deps);
418
+ };
419
+
420
+ //#endregion
421
+ //#region src/core/hooks/useStore.ts
422
+ function useStore(target, defaultValue) {
423
+ const alepha = useAlepha();
424
+ useMemo(() => {
425
+ if (defaultValue != null && alepha.state.get(target) == null) alepha.state.set(target, defaultValue);
426
+ }, [defaultValue]);
427
+ const [state, setState] = useState(alepha.state.get(target));
428
+ useEffect(() => {
429
+ if (!alepha.isBrowser()) return;
430
+ const key = target instanceof Atom ? target.key : target;
431
+ return alepha.events.on("state:mutate", (ev) => {
432
+ if (ev.key === key) setState(ev.value);
433
+ });
434
+ }, []);
435
+ return [state, (value) => {
436
+ alepha.state.set(target, value);
437
+ }];
438
+ }
439
+
440
+ //#endregion
441
+ //#region src/core/hooks/useRouterState.ts
442
+ const useRouterState = () => {
443
+ const [state] = useStore("alepha.react.router.state");
444
+ if (!state) throw new AlephaError("Missing react router state");
445
+ return state;
446
+ };
447
+
448
+ //#endregion
449
+ //#region src/core/components/ErrorBoundary.tsx
450
+ /**
451
+ * A reusable error boundary for catching rendering errors
452
+ * in any part of the React component tree.
453
+ */
454
+ var ErrorBoundary = class extends React.Component {
455
+ constructor(props) {
456
+ super(props);
457
+ this.state = {};
458
+ }
459
+ /**
460
+ * Update state so the next render shows the fallback UI.
461
+ */
462
+ static getDerivedStateFromError(error) {
463
+ return { error };
464
+ }
465
+ /**
466
+ * Lifecycle method called when an error is caught.
467
+ * You can log the error or perform side effects here.
468
+ */
469
+ componentDidCatch(error, info) {
470
+ if (this.props.onError) this.props.onError(error, info);
471
+ }
472
+ render() {
473
+ if (this.state.error) return this.props.fallback(this.state.error);
474
+ return this.props.children;
475
+ }
476
+ };
477
+ var ErrorBoundary_default = ErrorBoundary;
478
+
479
+ //#endregion
480
+ //#region src/core/components/NestedView.tsx
481
+ /**
482
+ * A component that renders the current view of the nested router layer.
483
+ *
484
+ * To be simple, it renders the `element` of the current child page of a parent page.
485
+ *
486
+ * @example
487
+ * ```tsx
488
+ * import { NestedView } from "@alepha/react";
489
+ *
490
+ * class App {
491
+ * parent = $page({
492
+ * component: () => <NestedView />,
493
+ * });
494
+ *
495
+ * child = $page({
496
+ * parent: this.root,
497
+ * component: () => <div>Child Page</div>,
498
+ * });
499
+ * }
500
+ * ```
501
+ */
502
+ const NestedView = (props) => {
503
+ const index = use(RouterLayerContext)?.index ?? 0;
504
+ const state = useRouterState();
505
+ const [view, setView] = useState(state.layers[index]?.element);
506
+ const [animation, setAnimation] = useState("");
507
+ const animationExitDuration = useRef(0);
508
+ const animationExitNow = useRef(0);
509
+ useEvents({
510
+ "react:transition:begin": async ({ previous, state: state$1 }) => {
511
+ const layer = previous.layers[index];
512
+ if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
513
+ const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
514
+ if (animationExit) {
515
+ const duration = animationExit.duration || 200;
516
+ animationExitNow.current = Date.now();
517
+ animationExitDuration.current = duration;
518
+ setAnimation(animationExit.animation);
519
+ } else {
520
+ animationExitNow.current = 0;
521
+ animationExitDuration.current = 0;
522
+ setAnimation("");
523
+ }
524
+ },
525
+ "react:transition:end": async ({ state: state$1 }) => {
526
+ const layer = state$1.layers[index];
527
+ if (animationExitNow.current) {
528
+ const duration = animationExitDuration.current;
529
+ const diff = Date.now() - animationExitNow.current;
530
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
531
+ }
532
+ if (!layer?.cache) {
533
+ setView(layer?.element);
534
+ const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
535
+ if (animationEnter) setAnimation(animationEnter.animation);
536
+ else setAnimation("");
537
+ }
538
+ }
539
+ }, []);
540
+ let element = view ?? props.children ?? null;
541
+ if (animation) element = /* @__PURE__ */ jsx("div", {
542
+ style: {
543
+ display: "flex",
544
+ flex: 1,
545
+ height: "100%",
546
+ width: "100%",
547
+ position: "relative",
548
+ overflow: "hidden"
549
+ },
550
+ children: /* @__PURE__ */ jsx("div", {
551
+ style: {
552
+ height: "100%",
553
+ width: "100%",
554
+ display: "flex",
555
+ animation
556
+ },
557
+ children: element
558
+ })
559
+ });
560
+ if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
561
+ if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary_default, {
562
+ fallback: props.errorBoundary,
563
+ children: element
564
+ });
565
+ return /* @__PURE__ */ jsx(ErrorBoundary_default, {
566
+ fallback: (error) => {
567
+ const result = state.onError(error, state);
568
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
569
+ return result;
570
+ },
571
+ children: element
572
+ });
573
+ };
574
+ var NestedView_default = memo(NestedView);
575
+ function parseAnimation(animationLike, state, type = "enter") {
576
+ if (!animationLike) return;
577
+ const DEFAULT_DURATION = 300;
578
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
579
+ if (typeof animation === "string") {
580
+ if (type === "exit") return;
581
+ return {
582
+ duration: DEFAULT_DURATION,
583
+ animation: `${DEFAULT_DURATION}ms ${animation}`
584
+ };
585
+ }
586
+ if (typeof animation === "object") {
587
+ const anim = animation[type];
588
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
589
+ const name = typeof anim === "object" ? anim.name : anim;
590
+ if (type === "exit") return {
591
+ duration,
592
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
593
+ };
594
+ return {
595
+ duration,
596
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
597
+ };
598
+ }
599
+ }
600
+
601
+ //#endregion
602
+ //#region src/core/providers/ReactPageProvider.ts
603
+ const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
604
+ var ReactPageProvider = class {
605
+ log = $logger();
606
+ env = $env(envSchema$1);
607
+ alepha = $inject(Alepha);
608
+ pages = [];
609
+ getPages() {
610
+ return this.pages;
611
+ }
612
+ getConcretePages() {
613
+ const pages = [];
614
+ for (const page of this.pages) {
615
+ if (page.children && page.children.length > 0) continue;
616
+ const fullPath = this.pathname(page.name);
617
+ if (fullPath.includes(":") || fullPath.includes("*")) {
618
+ if (typeof page.static === "object") {
619
+ const entries = page.static.entries;
620
+ if (entries && entries.length > 0) for (const entry of entries) {
621
+ const params = entry.params;
622
+ const path = this.compile(page.path ?? "", params);
623
+ if (!path.includes(":") && !path.includes("*")) pages.push({
624
+ ...page,
625
+ name: params[Object.keys(params)[0]],
626
+ path,
627
+ ...entry
628
+ });
629
+ }
630
+ }
631
+ continue;
632
+ }
633
+ pages.push(page);
634
+ }
635
+ return pages;
636
+ }
637
+ page(name) {
638
+ for (const page of this.pages) if (page.name === name) return page;
639
+ throw new AlephaError(`Page '${name}' not found`);
640
+ }
641
+ pathname(name, options = {}) {
642
+ const page = this.page(name);
643
+ if (!page) throw new Error(`Page ${name} not found`);
644
+ let url = page.path ?? "";
645
+ let parent = page.parent;
646
+ while (parent) {
647
+ url = `${parent.path ?? ""}/${url}`;
648
+ parent = parent.parent;
649
+ }
650
+ url = this.compile(url, options.params ?? {});
651
+ if (options.query) {
652
+ const query = new URLSearchParams(options.query);
653
+ if (query.toString()) url += `?${query.toString()}`;
654
+ }
655
+ return url.replace(/\/\/+/g, "/") || "/";
656
+ }
657
+ url(name, options = {}) {
658
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
659
+ }
660
+ root(state) {
661
+ const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
662
+ if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
663
+ return root;
664
+ }
665
+ convertStringObjectToObject = (schema, value) => {
666
+ if (t.schema.isObject(schema) && typeof value === "object") {
667
+ for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
668
+ value[key] = this.alepha.codec.decode(schema.properties[key], decodeURIComponent(value[key]));
669
+ } catch (e) {}
670
+ }
671
+ return value;
672
+ };
673
+ /**
674
+ * Create a new RouterState based on a given route and request.
675
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
676
+ * It also handles errors and redirects.
677
+ */
678
+ async createLayers(route, state, previous = []) {
679
+ let context = {};
680
+ const stack = [{ route }];
681
+ let parent = route.parent;
682
+ while (parent) {
683
+ stack.unshift({ route: parent });
684
+ parent = parent.parent;
685
+ }
686
+ let forceRefresh = false;
687
+ for (let i = 0; i < stack.length; i++) {
688
+ const it = stack[i];
689
+ const route$1 = it.route;
690
+ const config = {};
691
+ try {
692
+ this.convertStringObjectToObject(route$1.schema?.query, state.query);
693
+ config.query = route$1.schema?.query ? this.alepha.codec.decode(route$1.schema.query, state.query) : {};
694
+ } catch (e) {
695
+ it.error = e;
696
+ break;
697
+ }
698
+ try {
699
+ config.params = route$1.schema?.params ? this.alepha.codec.decode(route$1.schema.params, state.params) : {};
700
+ } catch (e) {
701
+ it.error = e;
702
+ break;
703
+ }
704
+ it.config = { ...config };
705
+ if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
706
+ const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
707
+ if (JSON.stringify({
708
+ part: url(previous[i].part),
709
+ params: previous[i].config?.params ?? {}
710
+ }) === JSON.stringify({
711
+ part: url(route$1.path),
712
+ params: config.params ?? {}
713
+ })) {
714
+ it.props = previous[i].props;
715
+ it.error = previous[i].error;
716
+ it.cache = true;
717
+ context = {
718
+ ...context,
719
+ ...it.props
720
+ };
721
+ continue;
722
+ }
723
+ forceRefresh = true;
724
+ }
725
+ if (!route$1.resolve) continue;
726
+ try {
727
+ const args = Object.create(state);
728
+ Object.assign(args, config, context);
729
+ const props = await route$1.resolve?.(args) ?? {};
730
+ it.props = { ...props };
731
+ context = {
732
+ ...context,
733
+ ...props
734
+ };
735
+ } catch (e) {
736
+ if (e instanceof Redirection) return { redirect: e.redirect };
737
+ this.log.error("Page resolver has failed", e);
738
+ it.error = e;
739
+ break;
740
+ }
741
+ }
742
+ let acc = "";
743
+ for (let i = 0; i < stack.length; i++) {
744
+ const it = stack[i];
745
+ const props = it.props ?? {};
746
+ const params = { ...it.config?.params };
747
+ for (const key of Object.keys(params)) params[key] = String(params[key]);
748
+ acc += "/";
749
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
750
+ const path = acc.replace(/\/+/, "/");
751
+ const localErrorHandler = this.getErrorHandler(it.route);
752
+ if (localErrorHandler) {
753
+ const onErrorParent = state.onError;
754
+ state.onError = (error, context$1) => {
755
+ const result = localErrorHandler(error, context$1);
756
+ if (result === void 0) return onErrorParent(error, context$1);
757
+ return result;
758
+ };
759
+ }
760
+ if (!it.error) try {
761
+ const element = await this.createElement(it.route, {
762
+ ...props,
763
+ ...context
764
+ });
765
+ state.layers.push({
766
+ name: it.route.name,
767
+ props,
768
+ part: it.route.path,
769
+ config: it.config,
770
+ element: this.renderView(i + 1, path, element, it.route),
771
+ index: i + 1,
772
+ path,
773
+ route: it.route,
774
+ cache: it.cache
775
+ });
776
+ } catch (e) {
777
+ it.error = e;
778
+ }
779
+ if (it.error) try {
780
+ let element = await state.onError(it.error, state);
781
+ if (element === void 0) throw it.error;
782
+ if (element instanceof Redirection) return { redirect: element.redirect };
783
+ if (element === null) element = this.renderError(it.error);
784
+ state.layers.push({
785
+ props,
786
+ error: it.error,
787
+ name: it.route.name,
788
+ part: it.route.path,
789
+ config: it.config,
790
+ element: this.renderView(i + 1, path, element, it.route),
791
+ index: i + 1,
792
+ path,
793
+ route: it.route
794
+ });
795
+ break;
796
+ } catch (e) {
797
+ if (e instanceof Redirection) return { redirect: e.redirect };
798
+ throw e;
799
+ }
800
+ }
801
+ return { state };
802
+ }
803
+ createRedirectionLayer(redirect) {
804
+ return { redirect };
805
+ }
806
+ getErrorHandler(route) {
807
+ if (route.errorHandler) return route.errorHandler;
808
+ let parent = route.parent;
809
+ while (parent) {
810
+ if (parent.errorHandler) return parent.errorHandler;
811
+ parent = parent.parent;
812
+ }
813
+ }
814
+ async createElement(page, props) {
815
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
816
+ if (page.lazy) return createElement((await page.lazy()).default, props);
817
+ if (page.component) return createElement(page.component, props);
818
+ }
819
+ renderError(error) {
820
+ return createElement(ErrorViewer_default, {
821
+ error,
822
+ alepha: this.alepha
823
+ });
824
+ }
825
+ renderEmptyView() {
826
+ return createElement(NestedView_default, {});
827
+ }
828
+ href(page, params = {}) {
829
+ const found = this.pages.find((it) => it.name === page.options.name);
830
+ if (!found) throw new Error(`Page ${page.options.name} not found`);
831
+ let url = found.path ?? "";
832
+ let parent = found.parent;
833
+ while (parent) {
834
+ url = `${parent.path ?? ""}/${url}`;
835
+ parent = parent.parent;
836
+ }
837
+ url = this.compile(url, params);
838
+ return url.replace(/\/\/+/g, "/") || "/";
839
+ }
840
+ compile(path, params = {}) {
841
+ for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
842
+ return path;
843
+ }
844
+ renderView(index, path, view, page) {
845
+ view ??= this.renderEmptyView();
846
+ const element = page.client ? createElement(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
847
+ return createElement(RouterLayerContext.Provider, { value: {
848
+ index,
849
+ path
850
+ } }, element);
851
+ }
852
+ configure = $hook({
853
+ on: "configure",
854
+ handler: () => {
855
+ let hasNotFoundHandler = false;
856
+ const pages = this.alepha.descriptors($page);
857
+ const hasParent = (it) => {
858
+ if (it.options.parent) return true;
859
+ for (const page of pages) if ((page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : []).includes(it)) return true;
860
+ };
861
+ for (const page of pages) {
862
+ if (page.options.path === "/*") hasNotFoundHandler = true;
863
+ if (hasParent(page)) continue;
864
+ this.add(this.map(pages, page));
865
+ }
866
+ if (!hasNotFoundHandler && pages.length > 0) this.add({
867
+ path: "/*",
868
+ name: "notFound",
869
+ cache: true,
870
+ component: NotFoundPage,
871
+ onServerResponse: ({ reply }) => {
872
+ reply.status = 404;
873
+ }
874
+ });
875
+ }
876
+ });
877
+ map(pages, target) {
878
+ const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
879
+ const getChildrenFromParent = (it) => {
880
+ const children$1 = [];
881
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
882
+ return children$1;
883
+ };
884
+ children.push(...getChildrenFromParent(target));
885
+ return {
886
+ ...target.options,
887
+ name: target.name,
888
+ parent: void 0,
889
+ children: children.map((it) => this.map(pages, it))
890
+ };
891
+ }
892
+ add(entry) {
893
+ if (this.alepha.isReady()) throw new AlephaError("Router is already initialized");
894
+ entry.name ??= this.nextId();
895
+ const page = entry;
896
+ page.match = this.createMatch(page);
897
+ this.pages.push(page);
898
+ if (page.children) for (const child of page.children) {
899
+ child.parent = page;
900
+ this.add(child);
901
+ }
902
+ }
903
+ createMatch(page) {
904
+ let url = page.path ?? "/";
905
+ let target = page.parent;
906
+ while (target) {
907
+ url = `${target.path ?? ""}/${url}`;
908
+ target = target.parent;
909
+ }
910
+ let path = url.replace(/\/\/+/g, "/");
911
+ if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
912
+ return path;
913
+ }
914
+ _next = 0;
915
+ nextId() {
916
+ this._next += 1;
917
+ return `P${this._next}`;
918
+ }
919
+ };
920
+ const isPageRoute = (it) => {
921
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
922
+ };
923
+
924
+ //#endregion
925
+ //#region src/core/providers/ReactBrowserRouterProvider.ts
926
+ var ReactBrowserRouterProvider = class extends RouterProvider {
927
+ log = $logger();
928
+ alepha = $inject(Alepha);
929
+ pageApi = $inject(ReactPageProvider);
930
+ add(entry) {
931
+ this.pageApi.add(entry);
932
+ }
933
+ configure = $hook({
934
+ on: "configure",
935
+ handler: async () => {
936
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
937
+ path: page.match,
938
+ page
939
+ });
940
+ }
941
+ });
942
+ async transition(url, previous = [], meta = {}) {
943
+ const { pathname, search } = url;
944
+ const state = {
945
+ url,
946
+ query: {},
947
+ params: {},
948
+ layers: [],
949
+ onError: () => null,
950
+ meta
951
+ };
952
+ await this.alepha.events.emit("react:action:begin", { type: "transition" });
953
+ await this.alepha.events.emit("react:transition:begin", {
954
+ previous: this.alepha.state.get("alepha.react.router.state"),
955
+ state
956
+ });
957
+ try {
958
+ const { route, params } = this.match(pathname);
959
+ const query = {};
960
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
961
+ state.query = query;
962
+ state.params = params ?? {};
963
+ if (isPageRoute(route)) {
964
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
965
+ if (redirect) return redirect;
966
+ }
967
+ if (state.layers.length === 0) state.layers.push({
968
+ name: "not-found",
969
+ element: createElement(NotFoundPage),
970
+ index: 0,
971
+ path: "/"
972
+ });
973
+ await this.alepha.events.emit("react:action:success", { type: "transition" });
974
+ await this.alepha.events.emit("react:transition:success", { state });
975
+ } catch (e) {
976
+ this.log.error("Transition has failed", e);
977
+ state.layers = [{
978
+ name: "error",
979
+ element: this.pageApi.renderError(e),
980
+ index: 0,
981
+ path: "/"
982
+ }];
983
+ await this.alepha.events.emit("react:action:error", {
984
+ type: "transition",
985
+ error: e
986
+ });
987
+ await this.alepha.events.emit("react:transition:error", {
988
+ error: e,
989
+ state
990
+ });
991
+ }
992
+ if (previous) for (let i = 0; i < previous.length; i++) {
993
+ const layer = previous[i];
994
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
995
+ }
996
+ this.alepha.state.set("alepha.react.router.state", state);
997
+ await this.alepha.events.emit("react:action:end", { type: "transition" });
998
+ await this.alepha.events.emit("react:transition:end", { state });
999
+ }
1000
+ root(state) {
1001
+ return this.pageApi.root(state);
1002
+ }
1003
+ };
1004
+
1005
+ //#endregion
1006
+ //#region src/core/providers/ReactBrowserProvider.ts
1007
+ const envSchema = t.object({ REACT_ROOT_ID: t.text({ default: "root" }) });
1008
+ /**
1009
+ * React browser renderer configuration atom
1010
+ */
1011
+ const reactBrowserOptions = $atom({
1012
+ name: "alepha.react.browser.options",
1013
+ schema: t.object({ scrollRestoration: t.enum(["top", "manual"]) }),
1014
+ default: { scrollRestoration: "top" }
1015
+ });
1016
+ var ReactBrowserProvider = class {
1017
+ env = $env(envSchema);
1018
+ log = $logger();
1019
+ client = $inject(LinkProvider);
1020
+ alepha = $inject(Alepha);
1021
+ router = $inject(ReactBrowserRouterProvider);
1022
+ dateTimeProvider = $inject(DateTimeProvider);
1023
+ options = $use(reactBrowserOptions);
1024
+ getRootElement() {
1025
+ const root = this.document.getElementById(this.env.REACT_ROOT_ID);
1026
+ if (root) return root;
1027
+ const div = this.document.createElement("div");
1028
+ div.id = this.env.REACT_ROOT_ID;
1029
+ this.document.body.prepend(div);
1030
+ return div;
1031
+ }
1032
+ transitioning;
1033
+ get state() {
1034
+ return this.alepha.state.get("alepha.react.router.state");
1035
+ }
1036
+ /**
1037
+ * Accessor for Document DOM API.
1038
+ */
1039
+ get document() {
1040
+ return window.document;
1041
+ }
1042
+ /**
1043
+ * Accessor for History DOM API.
1044
+ */
1045
+ get history() {
1046
+ return window.history;
1047
+ }
1048
+ /**
1049
+ * Accessor for Location DOM API.
1050
+ */
1051
+ get location() {
1052
+ return window.location;
1053
+ }
1054
+ get base() {
1055
+ const base = import.meta.env?.BASE_URL;
1056
+ if (!base || base === "/") return "";
1057
+ return base;
1058
+ }
1059
+ get url() {
1060
+ const url = this.location.pathname + this.location.search;
1061
+ if (this.base) return url.replace(this.base, "");
1062
+ return url;
1063
+ }
1064
+ pushState(path, replace) {
1065
+ const url = this.base + path;
1066
+ if (replace) this.history.replaceState({}, "", url);
1067
+ else this.history.pushState({}, "", url);
1068
+ }
1069
+ async invalidate(props) {
1070
+ const previous = [];
1071
+ this.log.trace("Invalidating layers");
1072
+ if (props) {
1073
+ const [key] = Object.keys(props);
1074
+ const value = props[key];
1075
+ for (const layer of this.state.layers) {
1076
+ if (layer.props?.[key]) {
1077
+ previous.push({
1078
+ ...layer,
1079
+ props: {
1080
+ ...layer.props,
1081
+ [key]: value
1082
+ }
1083
+ });
1084
+ break;
1085
+ }
1086
+ previous.push(layer);
1087
+ }
1088
+ }
1089
+ await this.render({ previous });
1090
+ }
1091
+ async go(url, options = {}) {
1092
+ this.log.trace(`Going to ${url}`, {
1093
+ url,
1094
+ options
1095
+ });
1096
+ await this.render({
1097
+ url,
1098
+ previous: options.force ? [] : this.state.layers,
1099
+ meta: options.meta
1100
+ });
1101
+ if (this.state.url.pathname + this.state.url.search !== url) {
1102
+ this.pushState(this.state.url.pathname + this.state.url.search);
1103
+ return;
1104
+ }
1105
+ this.pushState(url, options.replace);
1106
+ }
1107
+ async render(options = {}) {
1108
+ const previous = options.previous ?? this.state.layers;
1109
+ const url = options.url ?? this.url;
1110
+ const start = this.dateTimeProvider.now();
1111
+ this.transitioning = {
1112
+ to: url,
1113
+ from: this.state?.url.pathname
1114
+ };
1115
+ this.log.debug("Transitioning...", { to: url });
1116
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
1117
+ if (redirect) {
1118
+ this.log.info("Redirecting to", { redirect });
1119
+ if (redirect.startsWith("http")) window.location.href = redirect;
1120
+ else return await this.render({ url: redirect });
1121
+ }
1122
+ const ms = this.dateTimeProvider.now().diff(start);
1123
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
1124
+ this.transitioning = void 0;
1125
+ }
1126
+ /**
1127
+ * Get embedded layers from the server.
1128
+ */
1129
+ getHydrationState() {
1130
+ try {
1131
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
1132
+ } catch (error) {
1133
+ console.error(error);
1134
+ }
1135
+ }
1136
+ onTransitionEnd = $hook({
1137
+ on: "react:transition:end",
1138
+ handler: () => {
1139
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
1140
+ this.log.trace("Restoring scroll position to top");
1141
+ window.scrollTo(0, 0);
1142
+ }
1143
+ }
1144
+ });
1145
+ ready = $hook({
1146
+ on: "ready",
1147
+ handler: async () => {
1148
+ const hydration = this.getHydrationState();
1149
+ const previous = hydration?.layers ?? [];
1150
+ if (hydration) {
1151
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
1152
+ }
1153
+ await this.render({ previous });
1154
+ const element = this.router.root(this.state);
1155
+ await this.alepha.events.emit("react:browser:render", {
1156
+ element,
1157
+ root: this.getRootElement(),
1158
+ hydration,
1159
+ state: this.state
1160
+ });
1161
+ window.addEventListener("popstate", () => {
1162
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
1163
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
1164
+ this.render();
1165
+ });
1166
+ }
1167
+ });
1168
+ };
1169
+
1170
+ //#endregion
1171
+ //#region src/core/providers/ReactBrowserRendererProvider.ts
1172
+ var ReactBrowserRendererProvider = class {
1173
+ log = $logger();
1174
+ root;
1175
+ onBrowserRender = $hook({
1176
+ on: "react:browser:render",
1177
+ handler: async ({ hydration, root, element }) => {
1178
+ if (hydration?.layers) {
1179
+ this.root = hydrateRoot(root, element);
1180
+ this.log.info("Hydrated root element");
1181
+ } else {
1182
+ this.root ??= createRoot(root);
1183
+ this.root.render(element);
1184
+ this.log.info("Created root element");
1185
+ }
1186
+ }
1187
+ });
1188
+ };
1189
+
1190
+ //#endregion
1191
+ //#region src/core/services/ReactRouter.ts
1192
+ var ReactRouter = class {
1193
+ alepha = $inject(Alepha);
1194
+ pageApi = $inject(ReactPageProvider);
1195
+ get state() {
1196
+ return this.alepha.state.get("alepha.react.router.state");
1197
+ }
1198
+ get pages() {
1199
+ return this.pageApi.getPages();
1200
+ }
1201
+ get concretePages() {
1202
+ return this.pageApi.getConcretePages();
1203
+ }
1204
+ get browser() {
1205
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
1206
+ }
1207
+ isActive(href, options = {}) {
1208
+ const current = this.state.url.pathname;
1209
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
1210
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
1211
+ return isActive;
1212
+ }
1213
+ path(name, config = {}) {
1214
+ return this.pageApi.pathname(name, {
1215
+ params: {
1216
+ ...this.state.params,
1217
+ ...config.params
1218
+ },
1219
+ query: config.query
1220
+ });
1221
+ }
1222
+ /**
1223
+ * Reload the current page.
1224
+ * This is equivalent to calling `go()` with the current pathname and search.
1225
+ */
1226
+ async reload() {
1227
+ if (!this.browser) return;
1228
+ await this.go(this.location.pathname + this.location.search, {
1229
+ replace: true,
1230
+ force: true
1231
+ });
1232
+ }
1233
+ getURL() {
1234
+ if (!this.browser) return this.state.url;
1235
+ return new URL(this.location.href);
1236
+ }
1237
+ get location() {
1238
+ if (!this.browser) throw new Error("Browser is required");
1239
+ return this.browser.location;
1240
+ }
1241
+ get current() {
1242
+ return this.state;
1243
+ }
1244
+ get pathname() {
1245
+ return this.state.url.pathname;
1246
+ }
1247
+ get query() {
1248
+ const query = {};
1249
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
1250
+ return query;
1251
+ }
1252
+ async back() {
1253
+ this.browser?.history.back();
1254
+ }
1255
+ async forward() {
1256
+ this.browser?.history.forward();
1257
+ }
1258
+ async invalidate(props) {
1259
+ await this.browser?.invalidate(props);
1260
+ }
1261
+ async go(path, options) {
1262
+ for (const page of this.pages) if (page.name === path) {
1263
+ await this.browser?.go(this.path(path, options), options);
1264
+ return;
1265
+ }
1266
+ await this.browser?.go(path, options);
1267
+ }
1268
+ anchor(path, options = {}) {
1269
+ let href = path;
1270
+ for (const page of this.pages) if (page.name === path) {
1271
+ href = this.path(path, options);
1272
+ break;
1273
+ }
1274
+ return {
1275
+ href: this.base(href),
1276
+ onClick: (ev) => {
1277
+ ev.stopPropagation();
1278
+ ev.preventDefault();
1279
+ this.go(href, options).catch(console.error);
1280
+ }
1281
+ };
1282
+ }
1283
+ base(path) {
1284
+ const base = import.meta.env?.BASE_URL;
1285
+ if (!base || base === "/") return path;
1286
+ return base + path;
1287
+ }
1288
+ /**
1289
+ * Set query params.
1290
+ *
1291
+ * @param record
1292
+ * @param options
1293
+ */
1294
+ setQueryParams(record, options = {}) {
1295
+ const func = typeof record === "function" ? record : () => record;
1296
+ const search = new URLSearchParams(func(this.query)).toString();
1297
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
1298
+ if (options.push) window.history.pushState({}, "", state);
1299
+ else window.history.replaceState({}, "", state);
1300
+ }
1301
+ };
1302
+
1303
+ //#endregion
1304
+ //#region src/core/index.browser.ts
1305
+ const AlephaReact = $module({
1306
+ name: "alepha.react",
1307
+ descriptors: [$page],
1308
+ services: [
1309
+ ReactPageProvider,
1310
+ ReactBrowserRouterProvider,
1311
+ ReactBrowserProvider,
1312
+ ReactRouter,
1313
+ ReactBrowserRendererProvider,
1314
+ ReactPageService
1315
+ ],
1316
+ register: (alepha) => alepha.with(AlephaDateTime).with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(ReactRouter)
1317
+ });
1318
+
1319
+ //#endregion
1320
+ //#region src/auth/schemas/tokensSchema.ts
1321
+ const tokensSchema = t.object({
1322
+ provider: t.text(),
1323
+ access_token: t.text({ size: "rich" }),
1324
+ issued_at: t.number(),
1325
+ expires_in: t.optional(t.number()),
1326
+ refresh_token: t.optional(t.text({ size: "rich" })),
1327
+ refresh_token_expires_in: t.optional(t.number()),
1328
+ refresh_expires_in: t.optional(t.number({ description: "Alias of `refresh_token_expires_in` for compatibility with some providers." })),
1329
+ id_token: t.optional(t.text({ size: "rich" })),
1330
+ scope: t.optional(t.text())
1331
+ });
1332
+
1333
+ //#endregion
1334
+ //#region src/auth/schemas/tokenResponseSchema.ts
1335
+ const tokenResponseSchema = t.extend(tokensSchema, {
1336
+ user: userAccountInfoSchema,
1337
+ api: apiLinksResponseSchema
1338
+ });
1339
+
1340
+ //#endregion
1341
+ //#region src/auth/schemas/userinfoResponseSchema.ts
1342
+ const userinfoResponseSchema = t.object({
1343
+ user: t.optional(userAccountInfoSchema),
1344
+ api: apiLinksResponseSchema
1345
+ });
1346
+
1347
+ //#endregion
1348
+ //#region src/auth/services/ReactAuth.ts
1349
+ /**
1350
+ * Browser, SSR friendly, service to handle authentication.
1351
+ */
1352
+ var ReactAuth = class ReactAuth {
1353
+ log = $logger();
1354
+ alepha = $inject(Alepha);
1355
+ httpClient = $inject(HttpClient);
1356
+ static path = {
1357
+ login: "/oauth/login",
1358
+ callback: "/oauth/callback",
1359
+ logout: "/oauth/logout",
1360
+ token: "/_auth/token",
1361
+ refresh: "/_auth/refresh",
1362
+ userinfo: "/_auth/userinfo"
1363
+ };
1364
+ onBeginTransition = $hook({
1365
+ on: "react:transition:begin",
1366
+ handler: async (event) => {
1367
+ if (this.alepha.isBrowser()) Object.defineProperty(event.state, "user", { get: () => this.user });
1368
+ }
1369
+ });
1370
+ onFetchRequest = $hook({
1371
+ on: "client:onRequest",
1372
+ handler: async ({ request }) => {
1373
+ if (this.alepha.isBrowser() && this.user) request.credentials ??= "include";
1374
+ }
1375
+ });
1376
+ /**
1377
+ * Get the current authenticated user.
1378
+ *
1379
+ * Alias for `alepha.state.get("user")`
1380
+ */
1381
+ get user() {
1382
+ return this.alepha.state.get("alepha.server.request.user");
1383
+ }
1384
+ async ping() {
1385
+ const { data } = await this.httpClient.fetch(ReactAuth.path.userinfo, { schema: { response: userinfoResponseSchema } });
1386
+ this.alepha.state.set("alepha.server.request.apiLinks", data.api);
1387
+ this.alepha.state.set("alepha.server.request.user", data.user);
1388
+ return data.user;
1389
+ }
1390
+ async login(provider, options) {
1391
+ if (options.username || options.password) {
1392
+ const { data } = await this.httpClient.fetch(`${options.hostname || ""}${ReactAuth.path.token}?provider=${provider}`, {
1393
+ method: "POST",
1394
+ body: JSON.stringify({
1395
+ username: options.username,
1396
+ password: options.password,
1397
+ ...options
1398
+ }),
1399
+ schema: { response: tokenResponseSchema }
1400
+ });
1401
+ this.alepha.state.set("alepha.server.request.apiLinks", data.api);
1402
+ this.alepha.state.set("alepha.server.request.user", data.user);
1403
+ return data;
1404
+ }
1405
+ if (this.alepha.isBrowser()) {
1406
+ const browser = this.alepha.inject(ReactBrowserProvider);
1407
+ const redirect = options.redirect || (browser.transitioning ? window.location.origin + browser.transitioning.to : window.location.href);
1408
+ const href = `${window.location.origin}${ReactAuth.path.login}?provider=${provider}&redirect_uri=${encodeURIComponent(redirect)}`;
1409
+ if (browser.transitioning) throw new Redirection(href);
1410
+ else {
1411
+ window.location.href = href;
1412
+ return {};
1413
+ }
1414
+ }
1415
+ throw new Redirection(`${ReactAuth.path.login}?provider=${provider}&redirect_uri=${options.redirect || "/"}`);
1416
+ }
1417
+ logout() {
1418
+ window.location.href = `${ReactAuth.path.logout}?post_logout_redirect_uri=${encodeURIComponent(window.location.origin)}`;
1419
+ }
1420
+ };
1421
+
1422
+ //#endregion
1423
+ //#region src/auth/errors/SessionExpiredError.ts
1424
+ var SessionExpiredError = class extends AlephaError {
1425
+ name = "SessionExpiredError";
1426
+ status = 401;
1427
+ };
1428
+
1429
+ //#endregion
1430
+ //#region src/auth/hooks/useAuth.ts
1431
+ const useAuth = () => {
1432
+ const alepha = useAlepha();
1433
+ const [user] = useStore("alepha.server.request.user");
1434
+ return {
1435
+ user,
1436
+ logout: () => {
1437
+ alepha.inject(ReactAuth).logout();
1438
+ },
1439
+ login: async (provider, options = {}) => {
1440
+ await alepha.inject(ReactAuth).login(provider, options);
1441
+ },
1442
+ can: (name) => {
1443
+ return alepha.inject(LinkProvider).can(name);
1444
+ }
1445
+ };
1446
+ };
1447
+
1448
+ //#endregion
1449
+ //#region src/auth/index.browser.ts
1450
+ const AlephaReactAuth = $module({
1451
+ name: "alepha.react.auth",
1452
+ descriptors: [],
1453
+ register: (alepha) => {
1454
+ alepha.with(ReactAuth);
1455
+ }
1456
+ });
1457
+
1458
+ //#endregion
1459
+ export { AlephaReactAuth, ReactAuth, SessionExpiredError, useAuth };
1460
+ //# sourceMappingURL=index.browser.js.map