@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.
- package/README.md +46 -0
- package/dist/index.browser.js +315 -320
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +496 -457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +276 -258
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +274 -256
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +494 -460
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/components/NestedView.tsx +15 -13
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +16 -4
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -34
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -4
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -31
- package/src/hooks/useRouterEvents.ts +7 -7
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +0 -7
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +149 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
- package/src/providers/ReactServerProvider.ts +69 -74
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- 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
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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.
|
|
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.
|
|
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():
|
|
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
|
-
}
|