@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@alepha/react",
3
3
  "description": "Build server-side rendered (SSR) or single-page React applications.",
4
- "version": "0.9.4",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.0.0"
@@ -17,30 +17,33 @@
17
17
  "src"
18
18
  ],
19
19
  "dependencies": {
20
- "@alepha/core": "0.9.4",
21
- "@alepha/datetime": "0.9.4",
22
- "@alepha/logger": "0.9.4",
23
- "@alepha/router": "0.9.4",
24
- "@alepha/server": "0.9.4",
25
- "@alepha/server-cache": "0.9.4",
26
- "@alepha/server-links": "0.9.4",
27
- "@alepha/server-static": "0.9.4",
28
- "react-dom": "^19.1.1"
20
+ "@alepha/core": "0.10.0",
21
+ "@alepha/datetime": "0.10.0",
22
+ "@alepha/logger": "0.10.0",
23
+ "@alepha/router": "0.10.0",
24
+ "@alepha/server": "0.10.0",
25
+ "@alepha/server-cache": "0.10.0",
26
+ "@alepha/server-links": "0.10.0",
27
+ "@alepha/server-static": "0.10.0"
29
28
  },
30
29
  "devDependencies": {
31
- "@types/react": "^19.1.10",
32
- "@types/react-dom": "^19.1.7",
30
+ "@biomejs/biome": "^2.2.4",
31
+ "@types/react": "^19.1.13",
32
+ "@types/react-dom": "^19.1.9",
33
33
  "react": "^19.1.1",
34
- "tsdown": "^0.14.1",
34
+ "react-dom": "^19.1.1",
35
+ "tsdown": "^0.15.3",
35
36
  "typescript": "^5.9.2",
36
37
  "vitest": "^3.2.4"
37
38
  },
38
39
  "peerDependencies": {
39
- "react": "^19"
40
+ "react": "*",
41
+ "react-dom": "*"
40
42
  },
41
43
  "scripts": {
42
44
  "check": "tsc",
43
45
  "test": "vitest run",
46
+ "lint": "biome check --write --unsafe",
44
47
  "build": "tsdown -c ../../tsdown.config.ts"
45
48
  },
46
49
  "homepage": "https://github.com/feunard/alepha",
@@ -1,18 +1,15 @@
1
- import type React from "react";
2
1
  import type { AnchorHTMLAttributes } from "react";
3
2
  import { useRouter } from "../hooks/useRouter.ts";
4
3
 
5
4
  export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
6
- to: string;
7
- children?: React.ReactNode;
5
+ href: string;
8
6
  }
9
7
 
10
8
  const Link = (props: LinkProps) => {
11
9
  const router = useRouter();
12
- const { to, ...anchorProps } = props;
13
10
 
14
11
  return (
15
- <a {...router.anchor(to)} {...anchorProps}>
12
+ <a {...props} {...router.anchor(props.href)}>
16
13
  {props.children}
17
14
  </a>
18
15
  );
@@ -1,13 +1,15 @@
1
- import type { ReactNode } from "react";
2
- import { useContext, useState } from "react";
1
+ import { memo, type ReactNode, use, useRef, useState } from "react";
3
2
  import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
3
+ import type { PageAnimation } from "../descriptors/$page.ts";
4
4
  import { Redirection } from "../errors/Redirection.ts";
5
- import { useAlepha } from "../hooks/useAlepha.ts";
6
5
  import { useRouterEvents } from "../hooks/useRouterEvents.ts";
6
+ import { useRouterState } from "../hooks/useRouterState.ts";
7
+ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
7
8
  import ErrorBoundary from "./ErrorBoundary.tsx";
8
9
 
9
10
  export interface NestedViewProps {
10
11
  children?: ReactNode;
12
+ errorBoundary?: false | ((error: Error) => ReactNode);
11
13
  }
12
14
 
13
15
  /**
@@ -17,7 +19,7 @@ export interface NestedViewProps {
17
19
  *
18
20
  * @example
19
21
  * ```tsx
20
- * import { NestedView } from "@alepha/react";
22
+ * import { NestedView } from "alepha/react";
21
23
  *
22
24
  * class App {
23
25
  * parent = $page({
@@ -32,30 +34,115 @@ export interface NestedViewProps {
32
34
  * ```
33
35
  */
34
36
  const NestedView = (props: NestedViewProps) => {
35
- const layer = useContext(RouterLayerContext);
36
- const index = layer?.index ?? 0;
37
- const alepha = useAlepha();
38
- const state = alepha.state("react.router.state");
39
- if (!state) {
40
- throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
41
- }
37
+ const index = use(RouterLayerContext)?.index ?? 0;
38
+ const state = useRouterState();
42
39
 
43
40
  const [view, setView] = useState<ReactNode | undefined>(
44
41
  state.layers[index]?.element,
45
42
  );
46
43
 
44
+ const [animation, setAnimation] = useState("");
45
+ const animationExitDuration = useRef<number>(0);
46
+ const animationExitNow = useRef<number>(0);
47
+
47
48
  useRouterEvents(
48
49
  {
49
- onEnd: ({ state }) => {
50
- if (!state.layers[index]?.cache) {
51
- setView(state.layers[index]?.element);
50
+ onBegin: async ({ previous, state }) => {
51
+ // --------- Animations Begin ---------
52
+ const layer = previous.layers[index];
53
+ if (`${state.url.pathname}/`.startsWith(`${layer?.path}/`)) {
54
+ return;
55
+ }
56
+
57
+ const animationExit = parseAnimation(
58
+ layer.route?.animation,
59
+ state,
60
+ "exit",
61
+ );
62
+
63
+ if (animationExit) {
64
+ const duration = animationExit.duration || 200;
65
+ animationExitNow.current = Date.now();
66
+ animationExitDuration.current = duration;
67
+ setAnimation(animationExit.animation);
68
+ } else {
69
+ animationExitNow.current = 0;
70
+ animationExitDuration.current = 0;
71
+ setAnimation("");
72
+ }
73
+ // --------- Animations End ---------
74
+ },
75
+ onEnd: async ({ state }) => {
76
+ const layer = state.layers[index];
77
+
78
+ // --------- Animations Begin ---------
79
+ if (animationExitNow.current) {
80
+ const duration = animationExitDuration.current;
81
+ const diff = Date.now() - animationExitNow.current;
82
+ if (diff < duration) {
83
+ await new Promise((resolve) =>
84
+ setTimeout(resolve, duration - diff),
85
+ );
86
+ }
87
+ }
88
+ // --------- Animations End ---------
89
+
90
+ if (!layer?.cache) {
91
+ setView(layer?.element);
92
+
93
+ // --------- Animations Begin ---------
94
+ const animationEnter = parseAnimation(
95
+ layer?.route?.animation,
96
+ state,
97
+ "enter",
98
+ );
99
+
100
+ if (animationEnter) {
101
+ setAnimation(animationEnter.animation);
102
+ } else {
103
+ setAnimation("");
104
+ }
105
+ // --------- Animations End ---------
52
106
  }
53
107
  },
54
108
  },
55
109
  [],
56
110
  );
57
111
 
58
- const element = view ?? props.children ?? null;
112
+ let element = view ?? props.children ?? null;
113
+
114
+ // --------- Animations Begin ---------
115
+ if (animation) {
116
+ element = (
117
+ <div
118
+ style={{
119
+ display: "flex",
120
+ flex: 1,
121
+ height: "100%",
122
+ width: "100%",
123
+ position: "relative",
124
+ overflow: "hidden",
125
+ }}
126
+ >
127
+ <div
128
+ style={{ height: "100%", width: "100%", display: "flex", animation }}
129
+ >
130
+ {element}
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+ // --------- Animations End ---------
136
+
137
+ if (props.errorBoundary === false) {
138
+ return <>{element}</>;
139
+ }
140
+
141
+ if (props.errorBoundary) {
142
+ return (
143
+ <ErrorBoundary fallback={props.errorBoundary}>{element}</ErrorBoundary>
144
+ );
145
+ }
59
146
 
60
147
  return (
61
148
  <ErrorBoundary
@@ -72,4 +159,60 @@ const NestedView = (props: NestedViewProps) => {
72
159
  );
73
160
  };
74
161
 
75
- export default NestedView;
162
+ export default memo(NestedView);
163
+
164
+ function parseAnimation(
165
+ animationLike: PageAnimation | undefined,
166
+ state: ReactRouterState,
167
+ type: "enter" | "exit" = "enter",
168
+ ):
169
+ | {
170
+ duration: number;
171
+ animation: string;
172
+ }
173
+ | undefined {
174
+ if (!animationLike) {
175
+ return undefined;
176
+ }
177
+
178
+ const DEFAULT_DURATION = 300;
179
+
180
+ const animation =
181
+ typeof animationLike === "function" ? animationLike(state) : animationLike;
182
+
183
+ if (typeof animation === "string") {
184
+ if (type === "exit") {
185
+ return;
186
+ }
187
+ return {
188
+ duration: DEFAULT_DURATION,
189
+ animation: `${DEFAULT_DURATION}ms ${animation}`,
190
+ };
191
+ }
192
+
193
+ if (typeof animation === "object") {
194
+ const anim = animation[type];
195
+ const duration =
196
+ typeof anim === "object"
197
+ ? (anim.duration ?? DEFAULT_DURATION)
198
+ : DEFAULT_DURATION;
199
+ const name = typeof anim === "object" ? anim.name : anim;
200
+
201
+ if (type === "exit") {
202
+ const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
203
+ return {
204
+ duration,
205
+ animation: `${duration}ms ${timing} ${name}`,
206
+ };
207
+ }
208
+
209
+ const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
210
+
211
+ return {
212
+ duration,
213
+ animation: `${duration}ms ${timing} ${name}`,
214
+ };
215
+ }
216
+
217
+ return undefined;
218
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ AlephaError,
2
3
  type Async,
3
4
  createDescriptor,
4
5
  Descriptor,
@@ -15,6 +16,91 @@ import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
15
16
 
16
17
  /**
17
18
  * Main descriptor for defining a React route in the application.
19
+ *
20
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
21
+ * It provides a declarative way to define pages with powerful features:
22
+ *
23
+ * **Routing & Navigation**
24
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
25
+ * - Nested routing with parent-child relationships
26
+ * - Type-safe URL parameter and query string validation
27
+ *
28
+ * **Data Loading**
29
+ * - Server-side data fetching with the `resolve` function
30
+ * - Automatic serialization and hydration for SSR
31
+ * - Access to request context, URL params, and parent data
32
+ *
33
+ * **Component Loading**
34
+ * - Direct component rendering or lazy loading for code splitting
35
+ * - Client-only rendering when browser APIs are needed
36
+ * - Automatic fallback handling during hydration
37
+ *
38
+ * **Performance Optimization**
39
+ * - Static generation for pre-rendered pages at build time
40
+ * - Server-side caching with configurable TTL and providers
41
+ * - Code splitting through lazy component loading
42
+ *
43
+ * **Error Handling**
44
+ * - Custom error handlers with support for redirects
45
+ * - Hierarchical error handling (child → parent)
46
+ * - HTTP status code handling (404, 401, etc.)
47
+ *
48
+ * **Page Animations**
49
+ * - CSS-based enter/exit animations
50
+ * - Dynamic animations based on page state
51
+ * - Custom timing and easing functions
52
+ *
53
+ * **Lifecycle Management**
54
+ * - Server response hooks for headers and status codes
55
+ * - Page leave handlers for cleanup (browser only)
56
+ * - Permission-based access control
57
+ *
58
+ * @example Simple page with data fetching
59
+ * ```typescript
60
+ * const userProfile = $page({
61
+ * path: "/users/:id",
62
+ * schema: {
63
+ * params: t.object({ id: t.int() }),
64
+ * query: t.object({ tab: t.optional(t.string()) })
65
+ * },
66
+ * resolve: async ({ params }) => {
67
+ * const user = await userApi.getUser(params.id);
68
+ * return { user };
69
+ * },
70
+ * lazy: () => import("./UserProfile.tsx")
71
+ * });
72
+ * ```
73
+ *
74
+ * @example Nested routing with error handling
75
+ * ```typescript
76
+ * const projectSection = $page({
77
+ * path: "/projects/:id",
78
+ * children: () => [projectBoard, projectSettings],
79
+ * resolve: async ({ params }) => {
80
+ * const project = await projectApi.get(params.id);
81
+ * return { project };
82
+ * },
83
+ * errorHandler: (error) => {
84
+ * if (HttpError.is(error, 404)) {
85
+ * return <ProjectNotFound />;
86
+ * }
87
+ * }
88
+ * });
89
+ * ```
90
+ *
91
+ * @example Static generation with caching
92
+ * ```typescript
93
+ * const blogPost = $page({
94
+ * path: "/blog/:slug",
95
+ * static: {
96
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
97
+ * },
98
+ * resolve: async ({ params }) => {
99
+ * const post = await loadPost(params.slug);
100
+ * return { post };
101
+ * }
102
+ * });
103
+ * ```
18
104
  */
19
105
  export const $page = <
20
106
  TConfig extends PageConfigSchema = PageConfigSchema,
@@ -179,6 +265,50 @@ export interface PageDescriptorOptions<
179
265
  * Called when user leaves the page. (browser only)
180
266
  */
181
267
  onLeave?: () => void;
268
+
269
+ /**
270
+ * @experimental
271
+ *
272
+ * Add a css animation when the page is loaded or unloaded.
273
+ * It uses CSS animations, so you need to define the keyframes in your CSS.
274
+ *
275
+ * @example Simple animation name
276
+ * ```ts
277
+ * animation: "fadeIn"
278
+ * ```
279
+ *
280
+ * CSS example:
281
+ * ```css
282
+ * @keyframes fadeIn {
283
+ * from { opacity: 0; }
284
+ * to { opacity: 1; }
285
+ * }
286
+ * ```
287
+ *
288
+ * @example Detailed animation
289
+ * ```ts
290
+ * animation: {
291
+ * enter: { name: "fadeIn", duration: 300 },
292
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
293
+ * }
294
+ * ```
295
+ *
296
+ * @example Only exit animation
297
+ * ```ts
298
+ * animation: {
299
+ * exit: "fadeOut"
300
+ * }
301
+ * ```
302
+ *
303
+ * @example With custom timing function
304
+ * ```ts
305
+ * animation: {
306
+ * enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
307
+ * exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
308
+ * }
309
+ * ```
310
+ */
311
+ animation?: PageAnimation;
182
312
  }
183
313
 
184
314
  export type ErrorHandler = (
@@ -211,7 +341,18 @@ export class PageDescriptor<
211
341
  public async render(
212
342
  options?: PageDescriptorRenderOptions,
213
343
  ): Promise<PageDescriptorRenderResult> {
214
- throw new Error("render method is not implemented in this environment");
344
+ throw new AlephaError(
345
+ "render() method is not implemented in this environment",
346
+ );
347
+ }
348
+
349
+ public async fetch(options?: PageDescriptorRenderOptions): Promise<{
350
+ html: string;
351
+ response: Response;
352
+ }> {
353
+ throw new AlephaError(
354
+ "fetch() method is not implemented in this environment",
355
+ );
215
356
  }
216
357
 
217
358
  public match(url: string): boolean {
@@ -241,6 +382,13 @@ export type TPropsParentDefault = {};
241
382
  export interface PageDescriptorRenderOptions {
242
383
  params?: Record<string, string>;
243
384
  query?: Record<string, string>;
385
+
386
+ /**
387
+ * If true, the HTML layout will be included in the response.
388
+ * If false, only the page content will be returned.
389
+ *
390
+ * @default true
391
+ */
244
392
  html?: boolean;
245
393
  hydration?: boolean;
246
394
  }
@@ -248,6 +396,7 @@ export interface PageDescriptorRenderOptions {
248
396
  export interface PageDescriptorRenderResult {
249
397
  html: string;
250
398
  state: ReactRouterState;
399
+ redirect?: string;
251
400
  }
252
401
 
253
402
  export interface PageRequestConfig<
@@ -268,3 +417,22 @@ export type PageResolve<
268
417
  > = PageRequestConfig<TConfig> &
269
418
  TPropsParent &
270
419
  Omit<ReactRouterState, "layers" | "onError">;
420
+
421
+ export type PageAnimation =
422
+ | PageAnimationObject
423
+ | ((state: ReactRouterState) => PageAnimationObject | undefined);
424
+
425
+ type PageAnimationObject =
426
+ | CssAnimationName
427
+ | {
428
+ enter?: CssAnimation | CssAnimationName;
429
+ exit?: CssAnimation | CssAnimationName;
430
+ };
431
+
432
+ type CssAnimationName = string;
433
+
434
+ type CssAnimation = {
435
+ name: string;
436
+ duration?: number;
437
+ timing?: string;
438
+ };
@@ -51,5 +51,4 @@ export interface UseActiveHook {
51
51
  isActive: boolean;
52
52
  anchorProps: AnchorProps;
53
53
  isPending: boolean;
54
- name?: string;
55
54
  }
@@ -11,7 +11,7 @@ import { AlephaContext } from "../contexts/AlephaContext.ts";
11
11
  *
12
12
  * - alepha.state() for state management
13
13
  * - alepha.inject() for dependency injection
14
- * - alepha.emit() for event handling
14
+ * - alepha.events.emit() for event handling
15
15
  * etc...
16
16
  */
17
17
  export const useAlepha = (): Alepha => {
@@ -9,14 +9,14 @@ import { useRouter } from "./useRouter.ts";
9
9
  export const useQueryParams = <T extends TObject>(
10
10
  schema: T,
11
11
  options: UseQueryParamsHookOptions = {},
12
- ): [Static<T>, (data: Static<T>) => void] => {
12
+ ): [Partial<Static<T>>, (data: Static<T>) => void] => {
13
13
  const alepha = useAlepha();
14
14
 
15
15
  const key = options.key ?? "q";
16
16
  const router = useRouter();
17
17
  const querystring = router.query[key];
18
18
 
19
- const [queryParams, setQueryParams] = useState(
19
+ const [queryParams = {}, setQueryParams] = useState<Static<T> | undefined>(
20
20
  decode(alepha, schema, router.query[key]),
21
21
  );
22
22
 
@@ -47,10 +47,14 @@ const encode = (alepha: Alepha, schema: TObject, data: any) => {
47
47
  return btoa(JSON.stringify(alepha.parse(schema, data)));
48
48
  };
49
49
 
50
- const decode = (alepha: Alepha, schema: TObject, data: any) => {
50
+ const decode = <T extends TObject>(
51
+ alepha: Alepha,
52
+ schema: T,
53
+ data: any,
54
+ ): Static<T> | undefined => {
51
55
  try {
52
56
  return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
53
- } catch (_error) {
54
- return {};
57
+ } catch {
58
+ return;
55
59
  }
56
60
  };
@@ -1,15 +1,23 @@
1
+ import type { Hooks } from "@alepha/core";
1
2
  import { useEffect } from "react";
2
- import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
3
3
  import { useAlepha } from "./useAlepha.ts";
4
4
 
5
+ type Hook<T extends keyof Hooks> =
6
+ | ((ev: Hooks[T]) => void)
7
+ | {
8
+ priority?: "first" | "last";
9
+ callback: (ev: Hooks[T]) => void;
10
+ };
11
+
5
12
  /**
6
13
  * Subscribe to various router events.
7
14
  */
8
15
  export const useRouterEvents = (
9
16
  opts: {
10
- onBegin?: (ev: { state: ReactRouterState }) => void;
11
- onEnd?: (ev: { state: ReactRouterState }) => void;
12
- onError?: (ev: { state: ReactRouterState; error: Error }) => void;
17
+ onBegin?: Hook<"react:transition:begin">;
18
+ onError?: Hook<"react:transition:error">;
19
+ onEnd?: Hook<"react:transition:end">;
20
+ onSuccess?: Hook<"react:transition:success">;
13
21
  } = {},
14
22
  deps: any[] = [],
15
23
  ) => {
@@ -20,33 +28,33 @@ export const useRouterEvents = (
20
28
  return;
21
29
  }
22
30
 
31
+ const cb = <T extends keyof Hooks>(callback: Hook<T>) => {
32
+ if (typeof callback === "function") {
33
+ return { callback };
34
+ }
35
+ return callback;
36
+ };
37
+
23
38
  const subs: Function[] = [];
24
39
  const onBegin = opts.onBegin;
25
40
  const onEnd = opts.onEnd;
26
41
  const onError = opts.onError;
42
+ const onSuccess = opts.onSuccess;
27
43
 
28
44
  if (onBegin) {
29
- subs.push(
30
- alepha.on("react:transition:begin", {
31
- callback: onBegin,
32
- }),
33
- );
45
+ subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
34
46
  }
35
47
 
36
48
  if (onEnd) {
37
- subs.push(
38
- alepha.on("react:transition:end", {
39
- callback: onEnd,
40
- }),
41
- );
49
+ subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
42
50
  }
43
51
 
44
52
  if (onError) {
45
- subs.push(
46
- alepha.on("react:transition:error", {
47
- callback: onError,
48
- }),
49
- );
53
+ subs.push(alepha.events.on("react:transition:error", cb(onError)));
54
+ }
55
+
56
+ if (onSuccess) {
57
+ subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
50
58
  }
51
59
 
52
60
  return () => {
@@ -12,19 +12,19 @@ export const useStore = <Key extends keyof State>(
12
12
  const alepha = useAlepha();
13
13
 
14
14
  useMemo(() => {
15
- if (defaultValue != null && alepha.state(key) == null) {
16
- alepha.state(key, defaultValue);
15
+ if (defaultValue != null && alepha.state.get(key) == null) {
16
+ alepha.state.set(key, defaultValue);
17
17
  }
18
18
  }, [defaultValue]);
19
19
 
20
- const [state, setState] = useState(alepha.state(key));
20
+ const [state, setState] = useState(alepha.state.get(key));
21
21
 
22
22
  useEffect(() => {
23
23
  if (!alepha.isBrowser()) {
24
24
  return;
25
25
  }
26
26
 
27
- return alepha.on("state:mutate", (ev) => {
27
+ return alepha.events.on("state:mutate", (ev) => {
28
28
  if (ev.key === key) {
29
29
  setState(ev.value);
30
30
  }
@@ -34,7 +34,7 @@ export const useStore = <Key extends keyof State>(
34
34
  return [
35
35
  state,
36
36
  (value: State[Key]) => {
37
- alepha.state(key, value);
37
+ alepha.state.set(key, value);
38
38
  },
39
39
  ] as const;
40
40
  };
@@ -3,6 +3,7 @@ import { AlephaServer } from "@alepha/server";
3
3
  import { AlephaServerLinks } from "@alepha/server-links";
4
4
  import { $page } from "./descriptors/$page.ts";
5
5
  import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
6
+ import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
6
7
  import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
7
8
  import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
8
9
  import { ReactRouter } from "./services/ReactRouter.ts";
@@ -24,6 +25,7 @@ export const AlephaReact = $module({
24
25
  ReactBrowserRouterProvider,
25
26
  ReactBrowserProvider,
26
27
  ReactRouter,
28
+ ReactBrowserRendererProvider,
27
29
  ],
28
30
  register: (alepha) =>
29
31
  alepha
@@ -32,5 +34,6 @@ export const AlephaReact = $module({
32
34
  .with(ReactPageProvider)
33
35
  .with(ReactBrowserProvider)
34
36
  .with(ReactBrowserRouterProvider)
37
+ .with(ReactBrowserRendererProvider)
35
38
  .with(ReactRouter),
36
39
  });