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