@alepha/react 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +46 -0
  2. package/dist/index.browser.js +315 -320
  3. package/dist/index.browser.js.map +1 -1
  4. package/dist/index.cjs +496 -457
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.d.cts +276 -258
  7. package/dist/index.d.cts.map +1 -1
  8. package/dist/index.d.ts +274 -256
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +494 -460
  11. package/dist/index.js.map +1 -1
  12. package/package.json +13 -10
  13. package/src/components/NestedView.tsx +15 -13
  14. package/src/components/NotFound.tsx +1 -1
  15. package/src/descriptors/$page.ts +16 -4
  16. package/src/errors/Redirection.ts +8 -5
  17. package/src/hooks/useActive.ts +25 -34
  18. package/src/hooks/useAlepha.ts +16 -2
  19. package/src/hooks/useClient.ts +7 -4
  20. package/src/hooks/useInject.ts +4 -1
  21. package/src/hooks/useQueryParams.ts +9 -6
  22. package/src/hooks/useRouter.ts +18 -31
  23. package/src/hooks/useRouterEvents.ts +7 -7
  24. package/src/hooks/useRouterState.ts +8 -20
  25. package/src/hooks/useSchema.ts +10 -15
  26. package/src/hooks/useStore.ts +0 -7
  27. package/src/index.browser.ts +11 -11
  28. package/src/index.shared.ts +2 -3
  29. package/src/index.ts +21 -30
  30. package/src/providers/ReactBrowserProvider.ts +149 -65
  31. package/src/providers/ReactBrowserRouterProvider.ts +132 -0
  32. package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
  33. package/src/providers/ReactServerProvider.ts +69 -74
  34. package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
  35. package/src/contexts/RouterContext.ts +0 -14
  36. package/src/providers/BrowserRouterProvider.ts +0 -155
  37. package/src/providers/ReactBrowserRenderer.ts +0 -93
@@ -1,27 +1,34 @@
1
+ import { $inject, Alepha } from "@alepha/core";
1
2
  import type { PageDescriptor } from "../descriptors/$page.ts";
2
- import type {
3
- AnchorProps,
4
- PageDescriptorProvider,
5
- PageReactContext,
6
- PageRoute,
7
- RouterState,
8
- } from "../providers/PageDescriptorProvider.ts";
9
- import type {
3
+ import {
10
4
  ReactBrowserProvider,
11
- RouterGoOptions,
5
+ type RouterGoOptions,
12
6
  } from "../providers/ReactBrowserProvider.ts";
7
+ import {
8
+ type AnchorProps,
9
+ ReactPageProvider,
10
+ type ReactRouterState,
11
+ } from "../providers/ReactPageProvider.ts";
12
+
13
+ export class ReactRouter<T extends object> {
14
+ protected readonly alepha = $inject(Alepha);
15
+ protected readonly pageApi = $inject(ReactPageProvider);
16
+
17
+ public get state(): ReactRouterState {
18
+ return this.alepha.state("react.router.state")!;
19
+ }
20
+
21
+ public get pages() {
22
+ return this.pageApi.getPages();
23
+ }
13
24
 
14
- export class RouterHookApi<T extends object> {
15
- constructor(
16
- private readonly pages: PageRoute[],
17
- private readonly context: PageReactContext,
18
- private readonly state: RouterState,
19
- private readonly layer: {
20
- path: string;
21
- },
22
- private readonly pageApi: PageDescriptorProvider,
23
- private readonly browser?: ReactBrowserProvider,
24
- ) {}
25
+ public get browser(): ReactBrowserProvider | undefined {
26
+ if (this.alepha.isBrowser()) {
27
+ return this.alepha.inject(ReactBrowserProvider);
28
+ }
29
+ // server-side
30
+ return undefined;
31
+ }
25
32
 
26
33
  public path(
27
34
  name: keyof VirtualRouter<T>,
@@ -32,7 +39,7 @@ export class RouterHookApi<T extends object> {
32
39
  ): string {
33
40
  return this.pageApi.pathname(name as string, {
34
41
  params: {
35
- ...this.context.params,
42
+ ...this.state.params,
36
43
  ...config.params,
37
44
  },
38
45
  query: config.query,
@@ -41,8 +48,9 @@ export class RouterHookApi<T extends object> {
41
48
 
42
49
  public getURL(): URL {
43
50
  if (!this.browser) {
44
- return this.context.url;
51
+ return this.state.url;
45
52
  }
53
+
46
54
  return new URL(this.location.href);
47
55
  }
48
56
 
@@ -54,19 +62,19 @@ export class RouterHookApi<T extends object> {
54
62
  return this.browser.location;
55
63
  }
56
64
 
57
- public get current(): RouterState {
65
+ public get current(): ReactRouterState {
58
66
  return this.state;
59
67
  }
60
68
 
61
69
  public get pathname(): string {
62
- return this.state.pathname;
70
+ return this.state.url.pathname;
63
71
  }
64
72
 
65
73
  public get query(): Record<string, string> {
66
74
  const query: Record<string, string> = {};
67
75
 
68
76
  for (const [key, value] of new URLSearchParams(
69
- this.state.search,
77
+ this.state.url.search,
70
78
  ).entries()) {
71
79
  query[key] = String(value);
72
80
  }
@@ -86,32 +94,6 @@ export class RouterHookApi<T extends object> {
86
94
  await this.browser?.invalidate(props);
87
95
  }
88
96
 
89
- /**
90
- * Create a valid href for the given pathname.
91
- *
92
- * @param pathname
93
- * @param layer
94
- */
95
- public createHref(
96
- pathname: HrefLike,
97
- layer: { path: string } = this.layer,
98
- options: { params?: Record<string, any> } = {},
99
- ) {
100
- if (typeof pathname === "object") {
101
- pathname = pathname.options.path ?? "";
102
- }
103
-
104
- if (options.params) {
105
- for (const [key, value] of Object.entries(options.params)) {
106
- pathname = pathname.replace(`:${key}`, String(value));
107
- }
108
- }
109
-
110
- return pathname.startsWith("/")
111
- ? pathname
112
- : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
113
- }
114
-
115
97
  public async go(path: string, options?: RouterGoOptions): Promise<void>;
116
98
  public async go(
117
99
  path: keyof VirtualRouter<T>,
@@ -147,6 +129,7 @@ export class RouterHookApi<T extends object> {
147
129
  options: { params?: Record<string, any> } = {},
148
130
  ): AnchorProps {
149
131
  let href = path as string;
132
+
150
133
  for (const page of this.pages) {
151
134
  if (page.name === path) {
152
135
  href = this.path(path as keyof VirtualRouter<T>, options);
@@ -155,7 +138,7 @@ export class RouterHookApi<T extends object> {
155
138
  }
156
139
 
157
140
  return {
158
- href,
141
+ href: this.base(href),
159
142
  onClick: (ev: any) => {
160
143
  ev.stopPropagation();
161
144
  ev.preventDefault();
@@ -165,6 +148,15 @@ export class RouterHookApi<T extends object> {
165
148
  };
166
149
  }
167
150
 
151
+ public base(path: string): string {
152
+ const base = import.meta.env?.BASE_URL;
153
+ if (!base || base === "/") {
154
+ return path;
155
+ }
156
+
157
+ return base + path;
158
+ }
159
+
168
160
  /**
169
161
  * Set query params.
170
162
  *
@@ -194,8 +186,6 @@ export class RouterHookApi<T extends object> {
194
186
  }
195
187
  }
196
188
 
197
- export type HrefLike = string | { options: { path?: string; name?: string } };
198
-
199
189
  export type VirtualRouter<T> = {
200
190
  [K in keyof T as T[K] extends PageDescriptor ? K : never]: T[K];
201
191
  };
@@ -1,14 +0,0 @@
1
- import { createContext } from "react";
2
- import type {
3
- PageReactContext,
4
- RouterState,
5
- } from "../providers/PageDescriptorProvider.ts";
6
-
7
- export interface RouterContextValue {
8
- state: RouterState;
9
- context: PageReactContext;
10
- }
11
-
12
- export const RouterContext = createContext<RouterContextValue | undefined>(
13
- undefined,
14
- );
@@ -1,155 +0,0 @@
1
- import { $hook, $inject, $logger, Alepha } from "@alepha/core";
2
- import { type Route, RouterProvider } from "@alepha/router";
3
- import { createElement, type ReactNode } from "react";
4
- import NotFoundPage from "../components/NotFound.tsx";
5
- import {
6
- isPageRoute,
7
- PageDescriptorProvider,
8
- type PageReactContext,
9
- type PageRequest,
10
- type PageRoute,
11
- type PageRouteEntry,
12
- type RouterRenderResult,
13
- type RouterState,
14
- type TransitionOptions,
15
- } from "./PageDescriptorProvider.ts";
16
-
17
- export interface BrowserRoute extends Route {
18
- page: PageRoute;
19
- }
20
-
21
- export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
22
- protected readonly log = $logger();
23
- protected readonly alepha = $inject(Alepha);
24
- protected readonly pageDescriptorProvider = $inject(PageDescriptorProvider);
25
-
26
- public add(entry: PageRouteEntry) {
27
- this.pageDescriptorProvider.add(entry);
28
- }
29
-
30
- protected readonly configure = $hook({
31
- on: "configure",
32
- handler: async () => {
33
- for (const page of this.pageDescriptorProvider.getPages()) {
34
- // mount only if a view is provided
35
- if (page.component || page.lazy) {
36
- this.push({
37
- path: page.match,
38
- page,
39
- });
40
- }
41
- }
42
- },
43
- });
44
-
45
- public async transition(
46
- url: URL,
47
- options: TransitionOptions = {},
48
- ): Promise<RouterRenderResult> {
49
- const { pathname, search } = url;
50
- const state: RouterState = {
51
- pathname,
52
- search,
53
- layers: [],
54
- };
55
-
56
- const context = {
57
- url,
58
- query: {},
59
- params: {},
60
- onError: () => null,
61
- ...(options.context ?? {}),
62
- } as PageRequest;
63
-
64
- await this.alepha.emit("react:transition:begin", { state, context });
65
-
66
- try {
67
- const previous = options.previous;
68
- const { route, params } = this.match(pathname);
69
-
70
- const query: Record<string, string> = {};
71
- if (search) {
72
- for (const [key, value] of new URLSearchParams(search).entries()) {
73
- query[key] = String(value);
74
- }
75
- }
76
-
77
- context.query = query;
78
- context.params = params ?? {};
79
- context.previous = previous;
80
-
81
- if (isPageRoute(route)) {
82
- const result = await this.pageDescriptorProvider.createLayers(
83
- route.page,
84
- context,
85
- );
86
-
87
- if (result.redirect) {
88
- return {
89
- redirect: result.redirect,
90
- state,
91
- context,
92
- };
93
- }
94
-
95
- state.layers = result.layers;
96
- }
97
-
98
- if (state.layers.length === 0) {
99
- state.layers.push({
100
- name: "not-found",
101
- element: createElement(NotFoundPage),
102
- index: 0,
103
- path: "/",
104
- });
105
- }
106
-
107
- await this.alepha.emit("react:transition:success", { state, context });
108
- } catch (e) {
109
- this.log.error(e);
110
- state.layers = [
111
- {
112
- name: "error",
113
- element: this.pageDescriptorProvider.renderError(e as Error),
114
- index: 0,
115
- path: "/",
116
- },
117
- ];
118
-
119
- await this.alepha.emit("react:transition:error", {
120
- error: e as Error,
121
- state,
122
- context,
123
- });
124
- }
125
-
126
- if (options.state) {
127
- options.state.layers = state.layers;
128
- options.state.pathname = state.pathname;
129
- options.state.search = state.search;
130
- }
131
-
132
- if (options.previous) {
133
- for (let i = 0; i < options.previous.length; i++) {
134
- const layer = options.previous[i];
135
- if (state.layers[i]?.name !== layer.name) {
136
- this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
137
- }
138
- }
139
- }
140
-
141
- await this.alepha.emit("react:transition:end", {
142
- state: options.state,
143
- context,
144
- });
145
-
146
- return {
147
- context,
148
- state,
149
- };
150
- }
151
-
152
- public root(state: RouterState, context: PageReactContext): ReactNode {
153
- return this.pageDescriptorProvider.root(state, context);
154
- }
155
- }
@@ -1,93 +0,0 @@
1
- import { $env, $hook, $inject, $logger, type Static, t } from "@alepha/core";
2
- import type { ApiLinksResponse } from "@alepha/server";
3
- import type { Root } from "react-dom/client";
4
- import { createRoot, hydrateRoot } from "react-dom/client";
5
- import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
6
- import type {
7
- PreviousLayerData,
8
- TransitionOptions,
9
- } from "./PageDescriptorProvider.ts";
10
- import { ReactBrowserProvider } from "./ReactBrowserProvider.ts";
11
-
12
- const envSchema = t.object({
13
- REACT_ROOT_ID: t.string({ default: "root" }),
14
- });
15
-
16
- declare module "@alepha/core" {
17
- interface Env extends Partial<Static<typeof envSchema>> {}
18
- }
19
-
20
- export interface ReactBrowserRendererOptions {
21
- scrollRestoration?: "top" | "manual";
22
- }
23
-
24
- // TODO: move to ReactBrowserProvider when it will be removed from server-side imports
25
- export class ReactBrowserRenderer {
26
- protected readonly browserProvider = $inject(ReactBrowserProvider);
27
- protected readonly browserRouterProvider = $inject(BrowserRouterProvider);
28
- protected readonly env = $env(envSchema);
29
- protected readonly log = $logger();
30
-
31
- protected root!: Root;
32
-
33
- public options: ReactBrowserRendererOptions = {
34
- scrollRestoration: "top",
35
- };
36
-
37
- protected getRootElement() {
38
- const root = this.browserProvider.document.getElementById(
39
- this.env.REACT_ROOT_ID,
40
- );
41
- if (root) {
42
- return root;
43
- }
44
-
45
- const div = this.browserProvider.document.createElement("div");
46
- div.id = this.env.REACT_ROOT_ID;
47
-
48
- this.browserProvider.document.body.prepend(div);
49
-
50
- return div;
51
- }
52
-
53
- public readonly ready = $hook({
54
- on: "react:browser:render",
55
- handler: async ({ state, context, hydration }) => {
56
- const element = this.browserRouterProvider.root(state, context);
57
-
58
- if (hydration?.layers) {
59
- this.root = hydrateRoot(this.getRootElement(), element);
60
- this.log.info("Hydrated root element");
61
- } else {
62
- this.root ??= createRoot(this.getRootElement());
63
- this.root.render(element);
64
- this.log.info("Created root element");
65
- }
66
- },
67
- });
68
-
69
- protected readonly onTransitionEnd = $hook({
70
- on: "react:transition:end",
71
- handler: () => {
72
- if (
73
- this.options.scrollRestoration === "top" &&
74
- typeof window !== "undefined"
75
- ) {
76
- window.scrollTo(0, 0);
77
- }
78
- },
79
- });
80
- }
81
-
82
- // ---------------------------------------------------------------------------------------------------------------------
83
-
84
- export interface RouterGoOptions {
85
- replace?: boolean;
86
- match?: TransitionOptions;
87
- params?: Record<string, string>;
88
- }
89
-
90
- export interface ReactHydrationState {
91
- layers?: Array<PreviousLayerData>;
92
- links?: ApiLinksResponse;
93
- }