@alepha/react 0.9.4 → 0.10.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.
- package/README.md +101 -7
- package/dist/index.browser.js +290 -86
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +352 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +318 -180
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -110
- package/dist/index.js.map +1 -1
- package/package.json +17 -14
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +159 -16
- package/src/descriptors/$page.ts +169 -1
- package/src/hooks/useActive.ts +0 -1
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +27 -19
- package/src/hooks/useStore.ts +5 -5
- package/src/index.browser.ts +3 -0
- package/src/index.ts +6 -1
- package/src/providers/ReactBrowserProvider.ts +21 -16
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +11 -6
- package/src/providers/ReactPageProvider.ts +45 -1
- package/src/providers/ReactServerProvider.ts +105 -38
- package/src/services/ReactRouter.ts +6 -9
package/dist/index.browser.js
CHANGED
|
@@ -3,14 +3,99 @@ import { AlephaServer, HttpClient } from "@alepha/server";
|
|
|
3
3
|
import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
|
|
4
4
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
5
5
|
import { $logger } from "@alepha/logger";
|
|
6
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
7
6
|
import { RouterProvider } from "@alepha/router";
|
|
8
|
-
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
9
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
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
10
|
|
|
11
11
|
//#region src/descriptors/$page.ts
|
|
12
12
|
/**
|
|
13
13
|
* Main descriptor for defining a React route in the application.
|
|
14
|
+
*
|
|
15
|
+
* The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
16
|
+
* It provides a declarative way to define pages with powerful features:
|
|
17
|
+
*
|
|
18
|
+
* **Routing & Navigation**
|
|
19
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
20
|
+
* - Nested routing with parent-child relationships
|
|
21
|
+
* - Type-safe URL parameter and query string validation
|
|
22
|
+
*
|
|
23
|
+
* **Data Loading**
|
|
24
|
+
* - Server-side data fetching with the `resolve` function
|
|
25
|
+
* - Automatic serialization and hydration for SSR
|
|
26
|
+
* - Access to request context, URL params, and parent data
|
|
27
|
+
*
|
|
28
|
+
* **Component Loading**
|
|
29
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
30
|
+
* - Client-only rendering when browser APIs are needed
|
|
31
|
+
* - Automatic fallback handling during hydration
|
|
32
|
+
*
|
|
33
|
+
* **Performance Optimization**
|
|
34
|
+
* - Static generation for pre-rendered pages at build time
|
|
35
|
+
* - Server-side caching with configurable TTL and providers
|
|
36
|
+
* - Code splitting through lazy component loading
|
|
37
|
+
*
|
|
38
|
+
* **Error Handling**
|
|
39
|
+
* - Custom error handlers with support for redirects
|
|
40
|
+
* - Hierarchical error handling (child → parent)
|
|
41
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
42
|
+
*
|
|
43
|
+
* **Page Animations**
|
|
44
|
+
* - CSS-based enter/exit animations
|
|
45
|
+
* - Dynamic animations based on page state
|
|
46
|
+
* - Custom timing and easing functions
|
|
47
|
+
*
|
|
48
|
+
* **Lifecycle Management**
|
|
49
|
+
* - Server response hooks for headers and status codes
|
|
50
|
+
* - Page leave handlers for cleanup (browser only)
|
|
51
|
+
* - Permission-based access control
|
|
52
|
+
*
|
|
53
|
+
* @example Simple page with data fetching
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const userProfile = $page({
|
|
56
|
+
* path: "/users/:id",
|
|
57
|
+
* schema: {
|
|
58
|
+
* params: t.object({ id: t.int() }),
|
|
59
|
+
* query: t.object({ tab: t.optional(t.string()) })
|
|
60
|
+
* },
|
|
61
|
+
* resolve: async ({ params }) => {
|
|
62
|
+
* const user = await userApi.getUser(params.id);
|
|
63
|
+
* return { user };
|
|
64
|
+
* },
|
|
65
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example Nested routing with error handling
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const projectSection = $page({
|
|
72
|
+
* path: "/projects/:id",
|
|
73
|
+
* children: () => [projectBoard, projectSettings],
|
|
74
|
+
* resolve: async ({ params }) => {
|
|
75
|
+
* const project = await projectApi.get(params.id);
|
|
76
|
+
* return { project };
|
|
77
|
+
* },
|
|
78
|
+
* errorHandler: (error) => {
|
|
79
|
+
* if (HttpError.is(error, 404)) {
|
|
80
|
+
* return <ProjectNotFound />;
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example Static generation with caching
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const blogPost = $page({
|
|
89
|
+
* path: "/blog/:slug",
|
|
90
|
+
* static: {
|
|
91
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
92
|
+
* },
|
|
93
|
+
* resolve: async ({ params }) => {
|
|
94
|
+
* const post = await loadPost(params.slug);
|
|
95
|
+
* return { post };
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
14
99
|
*/
|
|
15
100
|
const $page = (options) => {
|
|
16
101
|
return createDescriptor(PageDescriptor, options);
|
|
@@ -30,7 +115,10 @@ var PageDescriptor = class extends Descriptor {
|
|
|
30
115
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
31
116
|
*/
|
|
32
117
|
async render(options) {
|
|
33
|
-
throw new
|
|
118
|
+
throw new AlephaError("render() method is not implemented in this environment");
|
|
119
|
+
}
|
|
120
|
+
async fetch(options) {
|
|
121
|
+
throw new AlephaError("fetch() method is not implemented in this environment");
|
|
34
122
|
}
|
|
35
123
|
match(url) {
|
|
36
124
|
return false;
|
|
@@ -84,7 +172,6 @@ const ClientOnly = (props) => {
|
|
|
84
172
|
if (props.disabled) return props.children;
|
|
85
173
|
return mounted ? props.children : props.fallback;
|
|
86
174
|
};
|
|
87
|
-
var ClientOnly_default = ClientOnly;
|
|
88
175
|
|
|
89
176
|
//#endregion
|
|
90
177
|
//#region src/components/ErrorViewer.tsx
|
|
@@ -192,7 +279,6 @@ const ErrorViewer = ({ error, alepha }) => {
|
|
|
192
279
|
})] })]
|
|
193
280
|
});
|
|
194
281
|
};
|
|
195
|
-
var ErrorViewer_default = ErrorViewer;
|
|
196
282
|
const ErrorViewerProduction = () => {
|
|
197
283
|
const styles = {
|
|
198
284
|
container: {
|
|
@@ -268,7 +354,7 @@ const AlephaContext = createContext(void 0);
|
|
|
268
354
|
*
|
|
269
355
|
* - alepha.state() for state management
|
|
270
356
|
* - alepha.inject() for dependency injection
|
|
271
|
-
* - alepha.emit() for event handling
|
|
357
|
+
* - alepha.events.emit() for event handling
|
|
272
358
|
* etc...
|
|
273
359
|
*/
|
|
274
360
|
const useAlepha = () => {
|
|
@@ -286,19 +372,55 @@ const useRouterEvents = (opts = {}, deps = []) => {
|
|
|
286
372
|
const alepha = useAlepha();
|
|
287
373
|
useEffect(() => {
|
|
288
374
|
if (!alepha.isBrowser()) return;
|
|
375
|
+
const cb = (callback) => {
|
|
376
|
+
if (typeof callback === "function") return { callback };
|
|
377
|
+
return callback;
|
|
378
|
+
};
|
|
289
379
|
const subs = [];
|
|
290
380
|
const onBegin = opts.onBegin;
|
|
291
381
|
const onEnd = opts.onEnd;
|
|
292
382
|
const onError = opts.onError;
|
|
293
|
-
|
|
294
|
-
if (
|
|
295
|
-
if (
|
|
383
|
+
const onSuccess = opts.onSuccess;
|
|
384
|
+
if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
|
|
385
|
+
if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
|
|
386
|
+
if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
|
|
387
|
+
if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
|
|
296
388
|
return () => {
|
|
297
389
|
for (const sub of subs) sub();
|
|
298
390
|
};
|
|
299
391
|
}, deps);
|
|
300
392
|
};
|
|
301
393
|
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/hooks/useStore.ts
|
|
396
|
+
/**
|
|
397
|
+
* Hook to access and mutate the Alepha state.
|
|
398
|
+
*/
|
|
399
|
+
const useStore = (key, defaultValue) => {
|
|
400
|
+
const alepha = useAlepha();
|
|
401
|
+
useMemo(() => {
|
|
402
|
+
if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
|
|
403
|
+
}, [defaultValue]);
|
|
404
|
+
const [state, setState] = useState(alepha.state.get(key));
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
if (!alepha.isBrowser()) return;
|
|
407
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
408
|
+
if (ev.key === key) setState(ev.value);
|
|
409
|
+
});
|
|
410
|
+
}, []);
|
|
411
|
+
return [state, (value) => {
|
|
412
|
+
alepha.state.set(key, value);
|
|
413
|
+
}];
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/hooks/useRouterState.ts
|
|
418
|
+
const useRouterState = () => {
|
|
419
|
+
const [state] = useStore("react.router.state");
|
|
420
|
+
if (!state) throw new AlephaError("Missing react router state");
|
|
421
|
+
return state;
|
|
422
|
+
};
|
|
423
|
+
|
|
302
424
|
//#endregion
|
|
303
425
|
//#region src/components/ErrorBoundary.tsx
|
|
304
426
|
/**
|
|
@@ -328,7 +450,6 @@ var ErrorBoundary = class extends React.Component {
|
|
|
328
450
|
return this.props.children;
|
|
329
451
|
}
|
|
330
452
|
};
|
|
331
|
-
var ErrorBoundary_default = ErrorBoundary;
|
|
332
453
|
|
|
333
454
|
//#endregion
|
|
334
455
|
//#region src/components/NestedView.tsx
|
|
@@ -339,7 +460,7 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
339
460
|
*
|
|
340
461
|
* @example
|
|
341
462
|
* ```tsx
|
|
342
|
-
* import { NestedView } from "
|
|
463
|
+
* import { NestedView } from "alepha/react";
|
|
343
464
|
*
|
|
344
465
|
* class App {
|
|
345
466
|
* parent = $page({
|
|
@@ -354,17 +475,69 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
354
475
|
* ```
|
|
355
476
|
*/
|
|
356
477
|
const NestedView = (props) => {
|
|
357
|
-
const
|
|
358
|
-
const
|
|
359
|
-
const alepha = useAlepha();
|
|
360
|
-
const state = alepha.state("react.router.state");
|
|
361
|
-
if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
478
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
479
|
+
const state = useRouterState();
|
|
362
480
|
const [view, setView] = useState(state.layers[index]?.element);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
481
|
+
const [animation, setAnimation] = useState("");
|
|
482
|
+
const animationExitDuration = useRef(0);
|
|
483
|
+
const animationExitNow = useRef(0);
|
|
484
|
+
useRouterEvents({
|
|
485
|
+
onBegin: async ({ previous, state: state$1 }) => {
|
|
486
|
+
const layer = previous.layers[index];
|
|
487
|
+
if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
|
|
488
|
+
const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
|
|
489
|
+
if (animationExit) {
|
|
490
|
+
const duration = animationExit.duration || 200;
|
|
491
|
+
animationExitNow.current = Date.now();
|
|
492
|
+
animationExitDuration.current = duration;
|
|
493
|
+
setAnimation(animationExit.animation);
|
|
494
|
+
} else {
|
|
495
|
+
animationExitNow.current = 0;
|
|
496
|
+
animationExitDuration.current = 0;
|
|
497
|
+
setAnimation("");
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
onEnd: async ({ state: state$1 }) => {
|
|
501
|
+
const layer = state$1.layers[index];
|
|
502
|
+
if (animationExitNow.current) {
|
|
503
|
+
const duration = animationExitDuration.current;
|
|
504
|
+
const diff = Date.now() - animationExitNow.current;
|
|
505
|
+
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
|
|
506
|
+
}
|
|
507
|
+
if (!layer?.cache) {
|
|
508
|
+
setView(layer?.element);
|
|
509
|
+
const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
|
|
510
|
+
if (animationEnter) setAnimation(animationEnter.animation);
|
|
511
|
+
else setAnimation("");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}, []);
|
|
515
|
+
let element = view ?? props.children ?? null;
|
|
516
|
+
if (animation) element = /* @__PURE__ */ jsx("div", {
|
|
517
|
+
style: {
|
|
518
|
+
display: "flex",
|
|
519
|
+
flex: 1,
|
|
520
|
+
height: "100%",
|
|
521
|
+
width: "100%",
|
|
522
|
+
position: "relative",
|
|
523
|
+
overflow: "hidden"
|
|
524
|
+
},
|
|
525
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
526
|
+
style: {
|
|
527
|
+
height: "100%",
|
|
528
|
+
width: "100%",
|
|
529
|
+
display: "flex",
|
|
530
|
+
animation
|
|
531
|
+
},
|
|
532
|
+
children: element
|
|
533
|
+
})
|
|
534
|
+
});
|
|
535
|
+
if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
|
|
536
|
+
if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
537
|
+
fallback: props.errorBoundary,
|
|
538
|
+
children: element
|
|
539
|
+
});
|
|
540
|
+
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
368
541
|
fallback: (error) => {
|
|
369
542
|
const result = state.onError(error, state);
|
|
370
543
|
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
@@ -373,7 +546,37 @@ const NestedView = (props) => {
|
|
|
373
546
|
children: element
|
|
374
547
|
});
|
|
375
548
|
};
|
|
376
|
-
var NestedView_default = NestedView;
|
|
549
|
+
var NestedView_default = memo(NestedView);
|
|
550
|
+
function parseAnimation(animationLike, state, type = "enter") {
|
|
551
|
+
if (!animationLike) return void 0;
|
|
552
|
+
const DEFAULT_DURATION = 300;
|
|
553
|
+
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
554
|
+
if (typeof animation === "string") {
|
|
555
|
+
if (type === "exit") return;
|
|
556
|
+
return {
|
|
557
|
+
duration: DEFAULT_DURATION,
|
|
558
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
if (typeof animation === "object") {
|
|
562
|
+
const anim = animation[type];
|
|
563
|
+
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
|
|
564
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
565
|
+
if (type === "exit") {
|
|
566
|
+
const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
567
|
+
return {
|
|
568
|
+
duration,
|
|
569
|
+
animation: `${duration}ms ${timing$1} ${name}`
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const timing = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
573
|
+
return {
|
|
574
|
+
duration,
|
|
575
|
+
animation: `${duration}ms ${timing} ${name}`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
return void 0;
|
|
579
|
+
}
|
|
377
580
|
|
|
378
581
|
//#endregion
|
|
379
582
|
//#region src/providers/ReactPageProvider.ts
|
|
@@ -414,6 +617,14 @@ var ReactPageProvider = class {
|
|
|
414
617
|
if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
|
|
415
618
|
return root;
|
|
416
619
|
}
|
|
620
|
+
convertStringObjectToObject = (schema, value) => {
|
|
621
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
622
|
+
for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
|
|
623
|
+
value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
|
|
624
|
+
} catch (e) {}
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
};
|
|
417
628
|
/**
|
|
418
629
|
* Create a new RouterState based on a given route and request.
|
|
419
630
|
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
@@ -433,6 +644,7 @@ var ReactPageProvider = class {
|
|
|
433
644
|
const route$1 = it.route;
|
|
434
645
|
const config = {};
|
|
435
646
|
try {
|
|
647
|
+
this.convertStringObjectToObject(route$1.schema?.query, state.query);
|
|
436
648
|
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
437
649
|
} catch (e) {
|
|
438
650
|
it.error = e;
|
|
@@ -559,6 +771,7 @@ var ReactPageProvider = class {
|
|
|
559
771
|
}
|
|
560
772
|
}
|
|
561
773
|
async createElement(page, props) {
|
|
774
|
+
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
|
|
562
775
|
if (page.lazy) {
|
|
563
776
|
const component = await page.lazy();
|
|
564
777
|
return createElement(component.default, props);
|
|
@@ -567,7 +780,7 @@ var ReactPageProvider = class {
|
|
|
567
780
|
return void 0;
|
|
568
781
|
}
|
|
569
782
|
renderError(error) {
|
|
570
|
-
return createElement(
|
|
783
|
+
return createElement(ErrorViewer, {
|
|
571
784
|
error,
|
|
572
785
|
alepha: this.alepha
|
|
573
786
|
});
|
|
@@ -593,7 +806,7 @@ var ReactPageProvider = class {
|
|
|
593
806
|
}
|
|
594
807
|
renderView(index, path, view, page) {
|
|
595
808
|
view ??= this.renderEmptyView();
|
|
596
|
-
const element = page.client ? createElement(
|
|
809
|
+
const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
597
810
|
return createElement(RouterLayerContext.Provider, { value: {
|
|
598
811
|
index,
|
|
599
812
|
path
|
|
@@ -692,17 +905,21 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
692
905
|
});
|
|
693
906
|
}
|
|
694
907
|
});
|
|
695
|
-
async transition(url, previous = []) {
|
|
908
|
+
async transition(url, previous = [], meta = {}) {
|
|
696
909
|
const { pathname, search } = url;
|
|
697
910
|
const entry = {
|
|
698
911
|
url,
|
|
699
912
|
query: {},
|
|
700
913
|
params: {},
|
|
701
914
|
layers: [],
|
|
702
|
-
onError: () => null
|
|
915
|
+
onError: () => null,
|
|
916
|
+
meta
|
|
703
917
|
};
|
|
704
918
|
const state = entry;
|
|
705
|
-
await this.alepha.emit("react:transition:begin", {
|
|
919
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
920
|
+
previous: this.alepha.state.get("react.router.state"),
|
|
921
|
+
state
|
|
922
|
+
});
|
|
706
923
|
try {
|
|
707
924
|
const { route, params } = this.match(pathname);
|
|
708
925
|
const query = {};
|
|
@@ -719,7 +936,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
719
936
|
index: 0,
|
|
720
937
|
path: "/"
|
|
721
938
|
});
|
|
722
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
939
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
723
940
|
} catch (e) {
|
|
724
941
|
this.log.error("Transition has failed", e);
|
|
725
942
|
state.layers = [{
|
|
@@ -728,7 +945,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
728
945
|
index: 0,
|
|
729
946
|
path: "/"
|
|
730
947
|
}];
|
|
731
|
-
await this.alepha.emit("react:transition:error", {
|
|
948
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
732
949
|
error: e,
|
|
733
950
|
state
|
|
734
951
|
});
|
|
@@ -737,8 +954,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
737
954
|
const layer = previous[i];
|
|
738
955
|
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
739
956
|
}
|
|
740
|
-
|
|
741
|
-
this.alepha.
|
|
957
|
+
this.alepha.state.set("react.router.state", state);
|
|
958
|
+
await this.alepha.events.emit("react:transition:end", { state });
|
|
742
959
|
}
|
|
743
960
|
root(state) {
|
|
744
961
|
return this.pageApi.root(state);
|
|
@@ -755,7 +972,6 @@ var ReactBrowserProvider = class {
|
|
|
755
972
|
alepha = $inject(Alepha);
|
|
756
973
|
router = $inject(ReactBrowserRouterProvider);
|
|
757
974
|
dateTimeProvider = $inject(DateTimeProvider);
|
|
758
|
-
root;
|
|
759
975
|
options = { scrollRestoration: "top" };
|
|
760
976
|
getRootElement() {
|
|
761
977
|
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
@@ -767,7 +983,7 @@ var ReactBrowserProvider = class {
|
|
|
767
983
|
}
|
|
768
984
|
transitioning;
|
|
769
985
|
get state() {
|
|
770
|
-
return this.alepha.state("react.router.state");
|
|
986
|
+
return this.alepha.state.get("react.router.state");
|
|
771
987
|
}
|
|
772
988
|
/**
|
|
773
989
|
* Accessor for Document DOM API.
|
|
@@ -831,7 +1047,8 @@ var ReactBrowserProvider = class {
|
|
|
831
1047
|
});
|
|
832
1048
|
await this.render({
|
|
833
1049
|
url,
|
|
834
|
-
previous: options.force ? [] : this.state.layers
|
|
1050
|
+
previous: options.force ? [] : this.state.layers,
|
|
1051
|
+
meta: options.meta
|
|
835
1052
|
});
|
|
836
1053
|
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
837
1054
|
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
@@ -848,7 +1065,7 @@ var ReactBrowserProvider = class {
|
|
|
848
1065
|
from: this.state?.url.pathname
|
|
849
1066
|
};
|
|
850
1067
|
this.log.debug("Transitioning...", { to: url });
|
|
851
|
-
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
|
|
1068
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
|
|
852
1069
|
if (redirect) {
|
|
853
1070
|
this.log.info("Redirecting to", { redirect });
|
|
854
1071
|
return await this.render({ url: redirect });
|
|
@@ -870,7 +1087,7 @@ var ReactBrowserProvider = class {
|
|
|
870
1087
|
onTransitionEnd = $hook({
|
|
871
1088
|
on: "react:transition:end",
|
|
872
1089
|
handler: () => {
|
|
873
|
-
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
|
|
1090
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
|
|
874
1091
|
this.log.trace("Restoring scroll position to top");
|
|
875
1092
|
window.scrollTo(0, 0);
|
|
876
1093
|
}
|
|
@@ -882,23 +1099,41 @@ var ReactBrowserProvider = class {
|
|
|
882
1099
|
const hydration = this.getHydrationState();
|
|
883
1100
|
const previous = hydration?.layers ?? [];
|
|
884
1101
|
if (hydration) {
|
|
885
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1102
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
|
|
886
1103
|
}
|
|
887
1104
|
await this.render({ previous });
|
|
888
1105
|
const element = this.router.root(this.state);
|
|
1106
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
1107
|
+
element,
|
|
1108
|
+
root: this.getRootElement(),
|
|
1109
|
+
hydration,
|
|
1110
|
+
state: this.state
|
|
1111
|
+
});
|
|
1112
|
+
window.addEventListener("popstate", () => {
|
|
1113
|
+
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1114
|
+
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
1115
|
+
this.render();
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
//#endregion
|
|
1122
|
+
//#region src/providers/ReactBrowserRendererProvider.ts
|
|
1123
|
+
var ReactBrowserRendererProvider = class {
|
|
1124
|
+
log = $logger();
|
|
1125
|
+
root;
|
|
1126
|
+
onBrowserRender = $hook({
|
|
1127
|
+
on: "react:browser:render",
|
|
1128
|
+
handler: async ({ hydration, root, element }) => {
|
|
889
1129
|
if (hydration?.layers) {
|
|
890
|
-
this.root = hydrateRoot(
|
|
1130
|
+
this.root = hydrateRoot(root, element);
|
|
891
1131
|
this.log.info("Hydrated root element");
|
|
892
1132
|
} else {
|
|
893
|
-
this.root ??= createRoot(
|
|
1133
|
+
this.root ??= createRoot(root);
|
|
894
1134
|
this.root.render(element);
|
|
895
1135
|
this.log.info("Created root element");
|
|
896
1136
|
}
|
|
897
|
-
window.addEventListener("popstate", () => {
|
|
898
|
-
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
899
|
-
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
900
|
-
this.render();
|
|
901
|
-
});
|
|
902
1137
|
}
|
|
903
1138
|
});
|
|
904
1139
|
};
|
|
@@ -909,7 +1144,7 @@ var ReactRouter = class {
|
|
|
909
1144
|
alepha = $inject(Alepha);
|
|
910
1145
|
pageApi = $inject(ReactPageProvider);
|
|
911
1146
|
get state() {
|
|
912
|
-
return this.alepha.state("react.router.state");
|
|
1147
|
+
return this.alepha.state.get("react.router.state");
|
|
913
1148
|
}
|
|
914
1149
|
get pages() {
|
|
915
1150
|
return this.pageApi.getPages();
|
|
@@ -1032,44 +1267,12 @@ const useRouter = () => {
|
|
|
1032
1267
|
//#region src/components/Link.tsx
|
|
1033
1268
|
const Link = (props) => {
|
|
1034
1269
|
const router = useRouter();
|
|
1035
|
-
const { to,...anchorProps } = props;
|
|
1036
1270
|
return /* @__PURE__ */ jsx("a", {
|
|
1037
|
-
...
|
|
1038
|
-
...
|
|
1271
|
+
...props,
|
|
1272
|
+
...router.anchor(props.href),
|
|
1039
1273
|
children: props.children
|
|
1040
1274
|
});
|
|
1041
1275
|
};
|
|
1042
|
-
var Link_default = Link;
|
|
1043
|
-
|
|
1044
|
-
//#endregion
|
|
1045
|
-
//#region src/hooks/useStore.ts
|
|
1046
|
-
/**
|
|
1047
|
-
* Hook to access and mutate the Alepha state.
|
|
1048
|
-
*/
|
|
1049
|
-
const useStore = (key, defaultValue) => {
|
|
1050
|
-
const alepha = useAlepha();
|
|
1051
|
-
useMemo(() => {
|
|
1052
|
-
if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
|
|
1053
|
-
}, [defaultValue]);
|
|
1054
|
-
const [state, setState] = useState(alepha.state(key));
|
|
1055
|
-
useEffect(() => {
|
|
1056
|
-
if (!alepha.isBrowser()) return;
|
|
1057
|
-
return alepha.on("state:mutate", (ev) => {
|
|
1058
|
-
if (ev.key === key) setState(ev.value);
|
|
1059
|
-
});
|
|
1060
|
-
}, []);
|
|
1061
|
-
return [state, (value) => {
|
|
1062
|
-
alepha.state(key, value);
|
|
1063
|
-
}];
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
//#endregion
|
|
1067
|
-
//#region src/hooks/useRouterState.ts
|
|
1068
|
-
const useRouterState = () => {
|
|
1069
|
-
const [state] = useStore("react.router.state");
|
|
1070
|
-
if (!state) throw new AlephaError("Missing react router state");
|
|
1071
|
-
return state;
|
|
1072
|
-
};
|
|
1073
1276
|
|
|
1074
1277
|
//#endregion
|
|
1075
1278
|
//#region src/hooks/useActive.ts
|
|
@@ -1127,7 +1330,7 @@ const useQueryParams = (schema, options = {}) => {
|
|
|
1127
1330
|
const key = options.key ?? "q";
|
|
1128
1331
|
const router = useRouter();
|
|
1129
1332
|
const querystring = router.query[key];
|
|
1130
|
-
const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1333
|
+
const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1131
1334
|
useEffect(() => {
|
|
1132
1335
|
setQueryParams(decode(alepha, schema, querystring));
|
|
1133
1336
|
}, [querystring]);
|
|
@@ -1147,8 +1350,8 @@ const encode = (alepha, schema, data) => {
|
|
|
1147
1350
|
const decode = (alepha, schema, data) => {
|
|
1148
1351
|
try {
|
|
1149
1352
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
1150
|
-
} catch
|
|
1151
|
-
return
|
|
1353
|
+
} catch {
|
|
1354
|
+
return;
|
|
1152
1355
|
}
|
|
1153
1356
|
};
|
|
1154
1357
|
|
|
@@ -1196,11 +1399,12 @@ const AlephaReact = $module({
|
|
|
1196
1399
|
ReactPageProvider,
|
|
1197
1400
|
ReactBrowserRouterProvider,
|
|
1198
1401
|
ReactBrowserProvider,
|
|
1199
|
-
ReactRouter
|
|
1402
|
+
ReactRouter,
|
|
1403
|
+
ReactBrowserRendererProvider
|
|
1200
1404
|
],
|
|
1201
|
-
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactRouter)
|
|
1405
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerLinks).with(ReactPageProvider).with(ReactBrowserProvider).with(ReactBrowserRouterProvider).with(ReactBrowserRendererProvider).with(ReactRouter)
|
|
1202
1406
|
});
|
|
1203
1407
|
|
|
1204
1408
|
//#endregion
|
|
1205
|
-
export { $page, AlephaContext, AlephaReact,
|
|
1409
|
+
export { $page, AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, ErrorViewer, Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactBrowserRouterProvider, ReactPageProvider, ReactRouter, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
|
|
1206
1410
|
//# sourceMappingURL=index.browser.js.map
|