@alepha/react 0.9.3 → 0.9.5
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 +64 -6
- package/dist/index.browser.js +442 -328
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +644 -482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +402 -339
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +412 -349
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +641 -484
- package/dist/index.js.map +1 -1
- package/package.json +16 -11
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +164 -19
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +100 -5
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -35
- 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 +30 -22
- 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 +14 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +27 -31
- package/src/providers/ReactBrowserProvider.ts +151 -62
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +137 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +121 -104
- package/src/providers/ReactServerProvider.ts +90 -76
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +49 -62
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- package/src/providers/ReactBrowserRenderer.ts +0 -93
|
@@ -2,19 +2,19 @@ import {
|
|
|
2
2
|
$env,
|
|
3
3
|
$hook,
|
|
4
4
|
$inject,
|
|
5
|
-
$logger,
|
|
6
5
|
Alepha,
|
|
7
6
|
type Static,
|
|
7
|
+
type TSchema,
|
|
8
|
+
TypeGuard,
|
|
8
9
|
t,
|
|
9
10
|
} from "@alepha/core";
|
|
10
|
-
import
|
|
11
|
+
import { $logger } from "@alepha/logger";
|
|
11
12
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
12
13
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
13
14
|
import ErrorViewer from "../components/ErrorViewer.tsx";
|
|
14
15
|
import NestedView from "../components/NestedView.tsx";
|
|
15
16
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
16
17
|
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
17
|
-
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
18
18
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
19
19
|
import {
|
|
20
20
|
$page,
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
type PageDescriptorOptions,
|
|
24
24
|
} from "../descriptors/$page.ts";
|
|
25
25
|
import { Redirection } from "../errors/Redirection.ts";
|
|
26
|
-
import type { HrefLike } from "../hooks/RouterHookApi.ts";
|
|
27
26
|
|
|
28
27
|
const envSchema = t.object({
|
|
29
28
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -33,7 +32,7 @@ declare module "@alepha/core" {
|
|
|
33
32
|
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
34
33
|
}
|
|
35
34
|
|
|
36
|
-
export class
|
|
35
|
+
export class ReactPageProvider {
|
|
37
36
|
protected readonly log = $logger();
|
|
38
37
|
protected readonly env = $env(envSchema);
|
|
39
38
|
protected readonly alepha = $inject(Alepha);
|
|
@@ -86,29 +85,20 @@ export class PageDescriptorProvider {
|
|
|
86
85
|
|
|
87
86
|
public url(
|
|
88
87
|
name: string,
|
|
89
|
-
options: { params?: Record<string, string>;
|
|
88
|
+
options: { params?: Record<string, string>; host?: string } = {},
|
|
90
89
|
): URL {
|
|
91
90
|
return new URL(
|
|
92
91
|
this.pathname(name, options),
|
|
93
92
|
// use provided base or default to http://localhost
|
|
94
|
-
options.
|
|
93
|
+
options.host ?? `http://localhost`,
|
|
95
94
|
);
|
|
96
95
|
}
|
|
97
96
|
|
|
98
|
-
public root(state:
|
|
97
|
+
public root(state: ReactRouterState): ReactNode {
|
|
99
98
|
const root = createElement(
|
|
100
99
|
AlephaContext.Provider,
|
|
101
100
|
{ value: this.alepha },
|
|
102
|
-
createElement(
|
|
103
|
-
RouterContext.Provider,
|
|
104
|
-
{
|
|
105
|
-
value: {
|
|
106
|
-
state,
|
|
107
|
-
context,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
createElement(NestedView, {}, state.layers[0]?.element),
|
|
111
|
-
),
|
|
101
|
+
createElement(NestedView, {}, state.layers[0]?.element),
|
|
112
102
|
);
|
|
113
103
|
|
|
114
104
|
if (this.env.REACT_STRICT_MODE) {
|
|
@@ -118,15 +108,42 @@ export class PageDescriptorProvider {
|
|
|
118
108
|
return root;
|
|
119
109
|
}
|
|
120
110
|
|
|
111
|
+
protected convertStringObjectToObject = (
|
|
112
|
+
schema?: TSchema,
|
|
113
|
+
value?: any,
|
|
114
|
+
): any => {
|
|
115
|
+
if (TypeGuard.IsObject(schema) && typeof value === "object") {
|
|
116
|
+
for (const key in schema.properties) {
|
|
117
|
+
if (
|
|
118
|
+
TypeGuard.IsObject(schema.properties[key]) &&
|
|
119
|
+
typeof value[key] === "string"
|
|
120
|
+
) {
|
|
121
|
+
try {
|
|
122
|
+
value[key] = this.alepha.parse(
|
|
123
|
+
schema.properties[key],
|
|
124
|
+
decodeURIComponent(value[key]),
|
|
125
|
+
);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a new RouterState based on a given route and request.
|
|
137
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
138
|
+
* It also handles errors and redirects.
|
|
139
|
+
*/
|
|
121
140
|
public async createLayers(
|
|
122
141
|
route: PageRoute,
|
|
123
|
-
|
|
142
|
+
state: ReactRouterState,
|
|
143
|
+
previous: PreviousLayerData[] = [],
|
|
124
144
|
): Promise<CreateLayersResult> {
|
|
125
|
-
const { pathname, search } = request.url;
|
|
126
|
-
const layers: Layer[] = []; // result layers
|
|
127
145
|
let context: Record<string, any> = {}; // all props
|
|
128
146
|
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
129
|
-
request.onError = (error) => this.renderError(error); // error handler
|
|
130
147
|
|
|
131
148
|
let parent = route.parent;
|
|
132
149
|
while (parent) {
|
|
@@ -142,8 +159,9 @@ export class PageDescriptorProvider {
|
|
|
142
159
|
const config: Record<string, any> = {};
|
|
143
160
|
|
|
144
161
|
try {
|
|
162
|
+
this.convertStringObjectToObject(route.schema?.query, state.query);
|
|
145
163
|
config.query = route.schema?.query
|
|
146
|
-
? this.alepha.parse(route.schema.query,
|
|
164
|
+
? this.alepha.parse(route.schema.query, state.query)
|
|
147
165
|
: {};
|
|
148
166
|
} catch (e) {
|
|
149
167
|
it.error = e as Error;
|
|
@@ -152,7 +170,7 @@ export class PageDescriptorProvider {
|
|
|
152
170
|
|
|
153
171
|
try {
|
|
154
172
|
config.params = route.schema?.params
|
|
155
|
-
? this.alepha.parse(route.schema.params,
|
|
173
|
+
? this.alepha.parse(route.schema.params, state.params)
|
|
156
174
|
: {};
|
|
157
175
|
} catch (e) {
|
|
158
176
|
it.error = e as Error;
|
|
@@ -165,7 +183,6 @@ export class PageDescriptorProvider {
|
|
|
165
183
|
};
|
|
166
184
|
|
|
167
185
|
// check if previous layer is the same, reuse if possible
|
|
168
|
-
const previous = request.previous;
|
|
169
186
|
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
170
187
|
const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
|
|
171
188
|
|
|
@@ -203,7 +220,7 @@ export class PageDescriptorProvider {
|
|
|
203
220
|
try {
|
|
204
221
|
const props =
|
|
205
222
|
(await route.resolve?.({
|
|
206
|
-
...
|
|
223
|
+
...state, // request
|
|
207
224
|
...config, // params, query
|
|
208
225
|
...context, // previous props
|
|
209
226
|
} as any)) ?? {};
|
|
@@ -221,13 +238,12 @@ export class PageDescriptorProvider {
|
|
|
221
238
|
} catch (e) {
|
|
222
239
|
// check if we need to redirect
|
|
223
240
|
if (e instanceof Redirection) {
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
241
|
+
return {
|
|
242
|
+
redirect: e.redirect,
|
|
243
|
+
};
|
|
228
244
|
}
|
|
229
245
|
|
|
230
|
-
this.log.error(e);
|
|
246
|
+
this.log.error("Page resolver has failed", e);
|
|
231
247
|
|
|
232
248
|
it.error = e as Error;
|
|
233
249
|
break;
|
|
@@ -249,8 +265,8 @@ export class PageDescriptorProvider {
|
|
|
249
265
|
const path = acc.replace(/\/+/, "/");
|
|
250
266
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
251
267
|
if (localErrorHandler) {
|
|
252
|
-
const onErrorParent =
|
|
253
|
-
|
|
268
|
+
const onErrorParent = state.onError;
|
|
269
|
+
state.onError = (error, context) => {
|
|
254
270
|
const result = localErrorHandler(error, context);
|
|
255
271
|
// if nothing happen, call the parent
|
|
256
272
|
if (result === undefined) {
|
|
@@ -260,28 +276,51 @@ export class PageDescriptorProvider {
|
|
|
260
276
|
};
|
|
261
277
|
}
|
|
262
278
|
|
|
279
|
+
// normal use case
|
|
280
|
+
if (!it.error) {
|
|
281
|
+
try {
|
|
282
|
+
const element = await this.createElement(it.route, {
|
|
283
|
+
...props,
|
|
284
|
+
...context,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
state.layers.push({
|
|
288
|
+
name: it.route.name,
|
|
289
|
+
props,
|
|
290
|
+
part: it.route.path,
|
|
291
|
+
config: it.config,
|
|
292
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
293
|
+
index: i + 1,
|
|
294
|
+
path,
|
|
295
|
+
route: it.route,
|
|
296
|
+
cache: it.cache,
|
|
297
|
+
});
|
|
298
|
+
} catch (e) {
|
|
299
|
+
it.error = e as Error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
263
303
|
// handler has thrown an error, render an error view
|
|
264
304
|
if (it.error) {
|
|
265
305
|
try {
|
|
266
306
|
let element: ReactNode | Redirection | undefined =
|
|
267
|
-
await
|
|
307
|
+
await state.onError(it.error, state);
|
|
268
308
|
|
|
269
309
|
if (element === undefined) {
|
|
270
310
|
throw it.error;
|
|
271
311
|
}
|
|
272
312
|
|
|
273
313
|
if (element instanceof Redirection) {
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
314
|
+
return {
|
|
315
|
+
redirect: element.redirect,
|
|
316
|
+
};
|
|
278
317
|
}
|
|
279
318
|
|
|
280
319
|
if (element === null) {
|
|
281
320
|
element = this.renderError(it.error);
|
|
282
321
|
}
|
|
283
322
|
|
|
284
|
-
layers.push({
|
|
323
|
+
state.layers.push({
|
|
285
324
|
props,
|
|
286
325
|
error: it.error,
|
|
287
326
|
name: it.route.name,
|
|
@@ -295,47 +334,21 @@ export class PageDescriptorProvider {
|
|
|
295
334
|
break;
|
|
296
335
|
} catch (e) {
|
|
297
336
|
if (e instanceof Redirection) {
|
|
298
|
-
return
|
|
337
|
+
return {
|
|
338
|
+
redirect: e.redirect,
|
|
339
|
+
};
|
|
299
340
|
}
|
|
300
341
|
throw e;
|
|
301
342
|
}
|
|
302
343
|
}
|
|
303
|
-
|
|
304
|
-
// normal use case
|
|
305
|
-
|
|
306
|
-
const element = await this.createElement(it.route, {
|
|
307
|
-
...props,
|
|
308
|
-
...context,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
layers.push({
|
|
312
|
-
name: it.route.name,
|
|
313
|
-
props,
|
|
314
|
-
part: it.route.path,
|
|
315
|
-
config: it.config,
|
|
316
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
317
|
-
index: i + 1,
|
|
318
|
-
path,
|
|
319
|
-
route: it.route,
|
|
320
|
-
cache: it.cache,
|
|
321
|
-
});
|
|
322
344
|
}
|
|
323
345
|
|
|
324
|
-
return {
|
|
346
|
+
return { state };
|
|
325
347
|
}
|
|
326
348
|
|
|
327
|
-
protected createRedirectionLayer(
|
|
328
|
-
href: HrefLike,
|
|
329
|
-
context: {
|
|
330
|
-
pathname: string;
|
|
331
|
-
search: string;
|
|
332
|
-
},
|
|
333
|
-
) {
|
|
349
|
+
protected createRedirectionLayer(redirect: string): CreateLayersResult {
|
|
334
350
|
return {
|
|
335
|
-
|
|
336
|
-
redirect: typeof href === "string" ? href : this.href(href),
|
|
337
|
-
pathname: context.pathname,
|
|
338
|
-
search: context.search,
|
|
351
|
+
redirect,
|
|
339
352
|
};
|
|
340
353
|
}
|
|
341
354
|
|
|
@@ -352,6 +365,12 @@ export class PageDescriptorProvider {
|
|
|
352
365
|
page: PageRoute,
|
|
353
366
|
props: Record<string, any>,
|
|
354
367
|
): Promise<ReactNode> {
|
|
368
|
+
if (page.lazy && page.component) {
|
|
369
|
+
this.log.warn(
|
|
370
|
+
`Page ${page.name} has both lazy and component options, lazy will be used`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
355
374
|
if (page.lazy) {
|
|
356
375
|
const component = await page.lazy(); // load component
|
|
357
376
|
return createElement(component.default, props);
|
|
@@ -601,16 +620,36 @@ export interface AnchorProps {
|
|
|
601
620
|
onClick: (ev?: any) => any;
|
|
602
621
|
}
|
|
603
622
|
|
|
604
|
-
export interface
|
|
605
|
-
|
|
606
|
-
|
|
623
|
+
export interface ReactRouterState {
|
|
624
|
+
/**
|
|
625
|
+
* Stack of layers for the current page.
|
|
626
|
+
*/
|
|
607
627
|
layers: Array<Layer>;
|
|
608
|
-
}
|
|
609
628
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
629
|
+
/**
|
|
630
|
+
* URL of the current page.
|
|
631
|
+
*/
|
|
632
|
+
url: URL;
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Error handler for the current page.
|
|
636
|
+
*/
|
|
637
|
+
onError: ErrorHandler;
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Params extracted from the URL for the current page.
|
|
641
|
+
*/
|
|
642
|
+
params: Record<string, any>;
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Query parameters extracted from the URL for the current page.
|
|
646
|
+
*/
|
|
647
|
+
query: Record<string, string>;
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Optional meta information associated with the current page.
|
|
651
|
+
*/
|
|
652
|
+
meta: Record<string, any>;
|
|
614
653
|
}
|
|
615
654
|
|
|
616
655
|
export interface RouterStackItem {
|
|
@@ -621,33 +660,11 @@ export interface RouterStackItem {
|
|
|
621
660
|
cache?: boolean;
|
|
622
661
|
}
|
|
623
662
|
|
|
624
|
-
export interface
|
|
625
|
-
state: RouterState;
|
|
626
|
-
context: PageReactContext;
|
|
627
|
-
redirect?: string;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export interface PageRequest extends PageReactContext {
|
|
631
|
-
params: Record<string, any>;
|
|
632
|
-
query: Record<string, string>;
|
|
633
|
-
|
|
634
|
-
// previous layers (browser history or browser hydration, always null on server)
|
|
663
|
+
export interface TransitionOptions {
|
|
635
664
|
previous?: PreviousLayerData[];
|
|
636
665
|
}
|
|
637
666
|
|
|
638
|
-
export interface CreateLayersResult
|
|
667
|
+
export interface CreateLayersResult {
|
|
639
668
|
redirect?: string;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* It's like RouterState, but publicly available in React context.
|
|
644
|
-
* This is where we store all plugin data!
|
|
645
|
-
*/
|
|
646
|
-
export interface PageReactContext {
|
|
647
|
-
url: URL;
|
|
648
|
-
onError: ErrorHandler;
|
|
649
|
-
links?: ApiLinksResponse;
|
|
650
|
-
|
|
651
|
-
params: Record<string, any>;
|
|
652
|
-
query: Record<string, string>;
|
|
669
|
+
state?: ReactRouterState;
|
|
653
670
|
}
|