@alepha/react 0.8.0 → 0.9.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 +32 -1
- package/dist/index.browser.js +88 -45
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +277 -247
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -72
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +71 -73
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +282 -249
- package/dist/index.js.map +1 -1
- package/package.json +9 -8
- package/src/components/Link.tsx +3 -5
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +39 -58
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useInject.ts +1 -1
- package/src/hooks/useRouter.ts +3 -2
- package/src/index.browser.ts +13 -8
- package/src/index.shared.ts +1 -0
- package/src/index.ts +16 -12
- package/src/providers/PageDescriptorProvider.ts +49 -36
- package/src/providers/ReactBrowserProvider.ts +37 -4
- package/src/providers/ReactBrowserRenderer.ts +22 -2
- package/src/providers/ReactServerProvider.ts +13 -15
package/src/descriptors/$page.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
__descriptor,
|
|
3
2
|
type Async,
|
|
3
|
+
createDescriptor,
|
|
4
|
+
Descriptor,
|
|
4
5
|
KIND,
|
|
5
6
|
NotImplementedError,
|
|
6
|
-
OPTIONS,
|
|
7
7
|
type Static,
|
|
8
8
|
type TSchema,
|
|
9
9
|
} from "@alepha/core";
|
|
@@ -13,16 +13,23 @@ import type { FC, ReactNode } from "react";
|
|
|
13
13
|
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
14
14
|
import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Main descriptor for defining a React route in the application.
|
|
18
|
+
*/
|
|
19
|
+
export const $page = <
|
|
20
|
+
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
21
|
+
TProps extends object = TPropsDefault,
|
|
22
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
23
|
+
>(
|
|
24
|
+
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
25
|
+
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
26
|
+
return createDescriptor(
|
|
27
|
+
PageDescriptor<TConfig, TProps, TPropsParent>,
|
|
28
|
+
options,
|
|
29
|
+
);
|
|
30
|
+
};
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
26
33
|
|
|
27
34
|
export interface PageDescriptorOptions<
|
|
28
35
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
@@ -96,11 +103,9 @@ export interface PageDescriptorOptions<
|
|
|
96
103
|
*
|
|
97
104
|
* If you still want to render at this pathname, add a child page with an empty path.
|
|
98
105
|
*/
|
|
99
|
-
children?:
|
|
100
|
-
| Array<{ [OPTIONS]: PageDescriptorOptions }>
|
|
101
|
-
| (() => Array<{ [OPTIONS]: PageDescriptorOptions }>);
|
|
106
|
+
children?: Array<PageDescriptor> | (() => Array<PageDescriptor>);
|
|
102
107
|
|
|
103
|
-
parent?:
|
|
108
|
+
parent?: PageDescriptor<PageConfigSchema, TPropsParent>;
|
|
104
109
|
|
|
105
110
|
can?: () => boolean;
|
|
106
111
|
|
|
@@ -128,63 +133,39 @@ export interface PageDescriptorOptions<
|
|
|
128
133
|
cache?: ServerRouteCache;
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
export
|
|
136
|
+
export class PageDescriptor<
|
|
132
137
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
133
138
|
TProps extends object = TPropsDefault,
|
|
134
139
|
TPropsParent extends object = TPropsParentDefault,
|
|
135
|
-
> {
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
> extends Descriptor<PageDescriptorOptions<TConfig, TProps, TPropsParent>> {
|
|
141
|
+
public get name(): string {
|
|
142
|
+
return this.options.name ?? this.config.propertyKey;
|
|
143
|
+
}
|
|
138
144
|
|
|
139
145
|
/**
|
|
140
146
|
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
141
147
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
142
148
|
*/
|
|
143
|
-
render
|
|
149
|
+
public async render(
|
|
144
150
|
options?: PageDescriptorRenderOptions,
|
|
145
|
-
)
|
|
151
|
+
): Promise<PageDescriptorRenderResult> {
|
|
152
|
+
throw new NotImplementedError("");
|
|
153
|
+
}
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
|
|
149
|
-
* Main descriptor for defining a React route in the application.
|
|
150
|
-
*/
|
|
151
|
-
export const $page = <
|
|
152
|
-
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
153
|
-
TProps extends object = TPropsDefault,
|
|
154
|
-
TPropsParent extends object = TPropsParentDefault,
|
|
155
|
-
>(
|
|
156
|
-
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
157
|
-
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
158
|
-
__descriptor(KEY);
|
|
159
|
-
|
|
160
|
-
// if (options.children) {
|
|
161
|
-
// for (const child of options.children) {
|
|
162
|
-
// child[OPTIONS].parent = {
|
|
163
|
-
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
164
|
-
// };
|
|
165
|
-
// }
|
|
166
|
-
// }
|
|
167
|
-
|
|
168
|
-
// if (options.parent) {
|
|
169
|
-
// options.parent[OPTIONS].children ??= [];
|
|
170
|
-
// options.parent[OPTIONS].children.push({
|
|
171
|
-
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
172
|
-
// });
|
|
173
|
-
// }
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
[KIND]: KEY,
|
|
177
|
-
[OPTIONS]: options,
|
|
178
|
-
render: () => {
|
|
179
|
-
throw new NotImplementedError(KEY);
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
$page[KIND] = KEY;
|
|
156
|
+
$page[KIND] = PageDescriptor;
|
|
185
157
|
|
|
186
158
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
187
159
|
|
|
160
|
+
export interface PageConfigSchema {
|
|
161
|
+
query?: TSchema;
|
|
162
|
+
params?: TSchema;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type TPropsDefault = any;
|
|
166
|
+
|
|
167
|
+
export type TPropsParentDefault = {};
|
|
168
|
+
|
|
188
169
|
export interface PageDescriptorRenderOptions {
|
|
189
170
|
params?: Record<string, string>;
|
|
190
171
|
query?: Record<string, string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
2
2
|
import type {
|
|
3
3
|
AnchorProps,
|
|
4
|
+
PageReactContext,
|
|
4
5
|
PageRoute,
|
|
5
6
|
RouterState,
|
|
6
7
|
} from "../providers/PageDescriptorProvider.ts";
|
|
@@ -12,6 +13,7 @@ import type {
|
|
|
12
13
|
export class RouterHookApi {
|
|
13
14
|
constructor(
|
|
14
15
|
private readonly pages: PageRoute[],
|
|
16
|
+
private readonly context: PageReactContext,
|
|
15
17
|
private readonly state: RouterState,
|
|
16
18
|
private readonly layer: {
|
|
17
19
|
path: string;
|
|
@@ -19,6 +21,21 @@ export class RouterHookApi {
|
|
|
19
21
|
private readonly browser?: ReactBrowserProvider,
|
|
20
22
|
) {}
|
|
21
23
|
|
|
24
|
+
public getURL(): URL {
|
|
25
|
+
if (!this.browser) {
|
|
26
|
+
return this.context.url;
|
|
27
|
+
}
|
|
28
|
+
return new URL(this.location.href);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public get location(): Location {
|
|
32
|
+
if (!this.browser) {
|
|
33
|
+
throw new Error("Browser is required");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.browser.location;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
public get current(): RouterState {
|
|
23
40
|
return this.state;
|
|
24
41
|
}
|
package/src/hooks/useInject.ts
CHANGED
package/src/hooks/useRouter.ts
CHANGED
|
@@ -13,17 +13,18 @@ export const useRouter = (): RouterHookApi => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const pages = useMemo(() => {
|
|
16
|
-
return ctx.alepha.
|
|
16
|
+
return ctx.alepha.inject(PageDescriptorProvider).getPages();
|
|
17
17
|
}, []);
|
|
18
18
|
|
|
19
19
|
return useMemo(
|
|
20
20
|
() =>
|
|
21
21
|
new RouterHookApi(
|
|
22
22
|
pages,
|
|
23
|
+
ctx.context,
|
|
23
24
|
ctx.state,
|
|
24
25
|
layer,
|
|
25
26
|
ctx.alepha.isBrowser()
|
|
26
|
-
? ctx.alepha.
|
|
27
|
+
? ctx.alepha.inject(ReactBrowserProvider)
|
|
27
28
|
: undefined,
|
|
28
29
|
),
|
|
29
30
|
[layer],
|
package/src/index.browser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $module } from "@alepha/core";
|
|
2
2
|
import { AlephaServer } from "@alepha/server";
|
|
3
3
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
4
4
|
import { $page } from "./descriptors/$page.ts";
|
|
@@ -16,16 +16,21 @@ export * from "./providers/ReactBrowserProvider.ts";
|
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
18
18
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export const AlephaReact = $module({
|
|
20
|
+
name: "alepha.react",
|
|
21
|
+
descriptors: [$page],
|
|
22
|
+
services: [
|
|
23
|
+
PageDescriptorProvider,
|
|
24
|
+
ReactBrowserRenderer,
|
|
25
|
+
BrowserRouterProvider,
|
|
26
|
+
ReactBrowserProvider,
|
|
27
|
+
],
|
|
28
|
+
register: (alepha) =>
|
|
22
29
|
alepha
|
|
23
30
|
.with(AlephaServer)
|
|
24
31
|
.with(AlephaServerLinks)
|
|
25
32
|
.with(PageDescriptorProvider)
|
|
26
33
|
.with(ReactBrowserProvider)
|
|
27
34
|
.with(BrowserRouterProvider)
|
|
28
|
-
.with(ReactBrowserRenderer)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
__bind($page, AlephaReact);
|
|
35
|
+
.with(ReactBrowserRenderer),
|
|
36
|
+
});
|
package/src/index.shared.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
|
3
3
|
export * from "./components/ErrorViewer.tsx";
|
|
4
4
|
export { default as Link } from "./components/Link.tsx";
|
|
5
5
|
export { default as NestedView } from "./components/NestedView.tsx";
|
|
6
|
+
export { default as NotFound } from "./components/NotFound.tsx";
|
|
6
7
|
export * from "./contexts/RouterContext.ts";
|
|
7
8
|
export * from "./contexts/RouterLayerContext.ts";
|
|
8
9
|
export * from "./descriptors/$page.ts";
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $module } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, type ServerRequest } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
type PageRequest,
|
|
10
10
|
type RouterState,
|
|
11
11
|
} from "./providers/PageDescriptorProvider.ts";
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
ReactBrowserProvider,
|
|
14
|
+
type ReactHydrationState,
|
|
15
|
+
} from "./providers/ReactBrowserProvider.ts";
|
|
13
16
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
14
17
|
|
|
15
18
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -66,23 +69,24 @@ declare module "@alepha/core" {
|
|
|
66
69
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
|
-
*
|
|
72
|
+
* Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
70
73
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
74
|
+
* The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
75
|
+
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
76
|
+
* type safety and schema validation for route parameters and data.
|
|
73
77
|
*
|
|
74
78
|
* @see {@link $page}
|
|
75
79
|
* @module alepha.react
|
|
76
80
|
*/
|
|
77
|
-
export
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
export const AlephaReact = $module({
|
|
82
|
+
name: "alepha.react",
|
|
83
|
+
descriptors: [$page],
|
|
84
|
+
services: [ReactServerProvider, PageDescriptorProvider, ReactBrowserProvider],
|
|
85
|
+
register: (alepha) =>
|
|
80
86
|
alepha
|
|
81
87
|
.with(AlephaServer)
|
|
82
88
|
.with(AlephaServerCache)
|
|
83
89
|
.with(AlephaServerLinks)
|
|
84
90
|
.with(ReactServerProvider)
|
|
85
|
-
.with(PageDescriptorProvider)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
__bind($page, AlephaReact);
|
|
91
|
+
.with(PageDescriptorProvider),
|
|
92
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
$logger,
|
|
6
|
+
Alepha,
|
|
7
|
+
type Static,
|
|
8
|
+
t,
|
|
9
|
+
} from "@alepha/core";
|
|
3
10
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
4
11
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
5
12
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
@@ -8,7 +15,11 @@ import NestedView from "../components/NestedView.tsx";
|
|
|
8
15
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
9
16
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
10
17
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
11
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
$page,
|
|
20
|
+
type PageDescriptor,
|
|
21
|
+
type PageDescriptorOptions,
|
|
22
|
+
} from "../descriptors/$page.ts";
|
|
12
23
|
import { RedirectionError } from "../errors/RedirectionError.ts";
|
|
13
24
|
|
|
14
25
|
const envSchema = t.object({
|
|
@@ -21,7 +32,7 @@ declare module "@alepha/core" {
|
|
|
21
32
|
|
|
22
33
|
export class PageDescriptorProvider {
|
|
23
34
|
protected readonly log = $logger();
|
|
24
|
-
protected readonly env = $
|
|
35
|
+
protected readonly env = $env(envSchema);
|
|
25
36
|
protected readonly alepha = $inject(Alepha);
|
|
26
37
|
protected readonly pages: PageRoute[] = [];
|
|
27
38
|
|
|
@@ -109,7 +120,7 @@ export class PageDescriptorProvider {
|
|
|
109
120
|
try {
|
|
110
121
|
config.query = route.schema?.query
|
|
111
122
|
? this.alepha.parse(route.schema.query, request.query)
|
|
112
|
-
:
|
|
123
|
+
: {};
|
|
113
124
|
} catch (e) {
|
|
114
125
|
it.error = e as Error;
|
|
115
126
|
break;
|
|
@@ -118,7 +129,7 @@ export class PageDescriptorProvider {
|
|
|
118
129
|
try {
|
|
119
130
|
config.params = route.schema?.params
|
|
120
131
|
? this.alepha.parse(route.schema.params, request.params)
|
|
121
|
-
:
|
|
132
|
+
: {};
|
|
122
133
|
} catch (e) {
|
|
123
134
|
it.error = e as Error;
|
|
124
135
|
break;
|
|
@@ -129,11 +140,6 @@ export class PageDescriptorProvider {
|
|
|
129
140
|
...config,
|
|
130
141
|
};
|
|
131
142
|
|
|
132
|
-
// no resolve, render a basic view by default
|
|
133
|
-
if (!route.resolve) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
143
|
// check if previous layer is the same, reuse if possible
|
|
138
144
|
const previous = request.previous;
|
|
139
145
|
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
@@ -153,16 +159,23 @@ export class PageDescriptorProvider {
|
|
|
153
159
|
// part is the same, reuse previous layer
|
|
154
160
|
it.props = previous[i].props;
|
|
155
161
|
it.error = previous[i].error;
|
|
162
|
+
it.cache = true;
|
|
156
163
|
context = {
|
|
157
164
|
...context,
|
|
158
165
|
...it.props,
|
|
159
166
|
};
|
|
160
167
|
continue;
|
|
161
168
|
}
|
|
169
|
+
|
|
162
170
|
// part is different, force refresh of next layers
|
|
163
171
|
forceRefresh = true;
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
// no resolve, render a basic view by default
|
|
175
|
+
if (!route.resolve) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
166
179
|
try {
|
|
167
180
|
const props =
|
|
168
181
|
(await route.resolve?.({
|
|
@@ -233,7 +246,7 @@ export class PageDescriptorProvider {
|
|
|
233
246
|
element: this.renderView(i + 1, path, element, it.route),
|
|
234
247
|
index: i + 1,
|
|
235
248
|
path,
|
|
236
|
-
route,
|
|
249
|
+
route: it.route,
|
|
237
250
|
});
|
|
238
251
|
break;
|
|
239
252
|
}
|
|
@@ -253,7 +266,8 @@ export class PageDescriptorProvider {
|
|
|
253
266
|
element: this.renderView(i + 1, path, element, it.route),
|
|
254
267
|
index: i + 1,
|
|
255
268
|
path,
|
|
256
|
-
route,
|
|
269
|
+
route: it.route,
|
|
270
|
+
cache: it.cache,
|
|
257
271
|
});
|
|
258
272
|
}
|
|
259
273
|
|
|
@@ -353,14 +367,14 @@ export class PageDescriptorProvider {
|
|
|
353
367
|
on: "configure",
|
|
354
368
|
handler: () => {
|
|
355
369
|
let hasNotFoundHandler = false;
|
|
356
|
-
const pages = this.alepha.
|
|
370
|
+
const pages = this.alepha.descriptors($page);
|
|
357
371
|
|
|
358
|
-
const hasParent = (it:
|
|
372
|
+
const hasParent = (it: PageDescriptor) => {
|
|
359
373
|
for (const page of pages) {
|
|
360
|
-
const children = page.
|
|
361
|
-
? Array.isArray(page.
|
|
362
|
-
? page.
|
|
363
|
-
: page.
|
|
374
|
+
const children = page.options.children
|
|
375
|
+
? Array.isArray(page.options.children)
|
|
376
|
+
? page.options.children
|
|
377
|
+
: page.options.children()
|
|
364
378
|
: [];
|
|
365
379
|
if (children.includes(it)) {
|
|
366
380
|
return true;
|
|
@@ -368,21 +382,17 @@ export class PageDescriptorProvider {
|
|
|
368
382
|
}
|
|
369
383
|
};
|
|
370
384
|
|
|
371
|
-
for (const
|
|
372
|
-
|
|
373
|
-
|
|
385
|
+
for (const page of pages) {
|
|
386
|
+
if (page.options.path === "/*") {
|
|
387
|
+
hasNotFoundHandler = true;
|
|
388
|
+
}
|
|
374
389
|
|
|
375
|
-
for (const { value } of pages) {
|
|
376
390
|
// skip children, we only want root pages
|
|
377
|
-
if (hasParent(
|
|
391
|
+
if (hasParent(page)) {
|
|
378
392
|
continue;
|
|
379
393
|
}
|
|
380
394
|
|
|
381
|
-
|
|
382
|
-
hasNotFoundHandler = true;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
this.add(this.map(pages, value));
|
|
395
|
+
this.add(this.map(pages, page));
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
if (!hasNotFoundHandler && pages.length > 0) {
|
|
@@ -401,17 +411,18 @@ export class PageDescriptorProvider {
|
|
|
401
411
|
});
|
|
402
412
|
|
|
403
413
|
protected map(
|
|
404
|
-
pages: Array<
|
|
405
|
-
target:
|
|
414
|
+
pages: Array<PageDescriptor>,
|
|
415
|
+
target: PageDescriptor,
|
|
406
416
|
): PageRouteEntry {
|
|
407
|
-
const children = target
|
|
408
|
-
? Array.isArray(target
|
|
409
|
-
? target
|
|
410
|
-
: target
|
|
417
|
+
const children = target.options.children
|
|
418
|
+
? Array.isArray(target.options.children)
|
|
419
|
+
? target.options.children
|
|
420
|
+
: target.options.children()
|
|
411
421
|
: [];
|
|
412
422
|
|
|
413
423
|
return {
|
|
414
|
-
...target
|
|
424
|
+
...target.options,
|
|
425
|
+
name: target.name,
|
|
415
426
|
parent: undefined,
|
|
416
427
|
children: children.map((it) => this.map(pages, it)),
|
|
417
428
|
} as PageRoute;
|
|
@@ -499,6 +510,7 @@ export interface Layer {
|
|
|
499
510
|
index: number;
|
|
500
511
|
path: string;
|
|
501
512
|
route?: PageRoute;
|
|
513
|
+
cache?: boolean;
|
|
502
514
|
}
|
|
503
515
|
|
|
504
516
|
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
@@ -525,6 +537,7 @@ export interface RouterStackItem {
|
|
|
525
537
|
config?: Record<string, any>;
|
|
526
538
|
props?: Record<string, any>;
|
|
527
539
|
error?: Error;
|
|
540
|
+
cache?: boolean;
|
|
528
541
|
}
|
|
529
542
|
|
|
530
543
|
export interface RouterRenderResult {
|
|
@@ -35,8 +35,35 @@ export class ReactBrowserProvider {
|
|
|
35
35
|
return window.history;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
public get location() {
|
|
39
|
+
return window.location;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
public get url(): string {
|
|
39
|
-
|
|
43
|
+
let url = this.location.pathname + this.location.search;
|
|
44
|
+
|
|
45
|
+
if (import.meta?.env?.BASE_URL) {
|
|
46
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
47
|
+
if (!url.startsWith("/")) {
|
|
48
|
+
url = `/${url}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public pushState(url: string, replace?: boolean) {
|
|
56
|
+
let path = url;
|
|
57
|
+
|
|
58
|
+
if (import.meta?.env?.BASE_URL) {
|
|
59
|
+
path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (replace) {
|
|
63
|
+
this.history.replaceState({}, "", path);
|
|
64
|
+
} else {
|
|
65
|
+
this.history.pushState({}, "", path);
|
|
66
|
+
}
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
public async invalidate(props?: Record<string, any>) {
|
|
@@ -72,16 +99,16 @@ export class ReactBrowserProvider {
|
|
|
72
99
|
// when redirecting in browser
|
|
73
100
|
if (result.context.url.pathname !== url) {
|
|
74
101
|
// TODO: check if losing search params is acceptable?
|
|
75
|
-
this.
|
|
102
|
+
this.pushState(result.context.url.pathname);
|
|
76
103
|
return;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
if (options.replace) {
|
|
80
|
-
this.
|
|
107
|
+
this.pushState(url);
|
|
81
108
|
return;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
this.
|
|
111
|
+
this.pushState(url);
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
protected async render(
|
|
@@ -145,6 +172,12 @@ export class ReactBrowserProvider {
|
|
|
145
172
|
});
|
|
146
173
|
|
|
147
174
|
window.addEventListener("popstate", () => {
|
|
175
|
+
// when you update silently queryparams or hash, skip rendering
|
|
176
|
+
// if you want to force a rendering, use #go()
|
|
177
|
+
if (this.state.pathname === location.pathname) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
this.render();
|
|
149
182
|
});
|
|
150
183
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $hook, $inject, $logger, type Static, t } from "@alepha/core";
|
|
1
|
+
import { $env, $hook, $inject, $logger, type Static, t } from "@alepha/core";
|
|
2
2
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
3
3
|
import type { Root } from "react-dom/client";
|
|
4
4
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
@@ -17,15 +17,23 @@ declare module "@alepha/core" {
|
|
|
17
17
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface ReactBrowserRendererOptions {
|
|
21
|
+
scrollRestoration?: "top" | "manual";
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
// TODO: move to ReactBrowserProvider when it will be removed from server-side imports
|
|
21
25
|
export class ReactBrowserRenderer {
|
|
22
26
|
protected readonly browserProvider = $inject(ReactBrowserProvider);
|
|
23
27
|
protected readonly browserRouterProvider = $inject(BrowserRouterProvider);
|
|
24
|
-
protected readonly env = $
|
|
28
|
+
protected readonly env = $env(envSchema);
|
|
25
29
|
protected readonly log = $logger();
|
|
26
30
|
|
|
27
31
|
protected root!: Root;
|
|
28
32
|
|
|
33
|
+
public options: ReactBrowserRendererOptions = {
|
|
34
|
+
scrollRestoration: "top",
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
protected getRootElement() {
|
|
30
38
|
const root = this.browserProvider.document.getElementById(
|
|
31
39
|
this.env.REACT_ROOT_ID,
|
|
@@ -57,6 +65,18 @@ export class ReactBrowserRenderer {
|
|
|
57
65
|
}
|
|
58
66
|
},
|
|
59
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
|
+
});
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
// ---------------------------------------------------------------------------------------------------------------------
|