@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
|
@@ -4,14 +4,15 @@ import {
|
|
|
4
4
|
$env,
|
|
5
5
|
$hook,
|
|
6
6
|
$inject,
|
|
7
|
-
$logger,
|
|
8
7
|
Alepha,
|
|
8
|
+
AlephaError,
|
|
9
9
|
type Static,
|
|
10
10
|
t,
|
|
11
11
|
} from "@alepha/core";
|
|
12
|
+
import { $logger } from "@alepha/logger";
|
|
12
13
|
import {
|
|
13
|
-
apiLinksResponseSchema,
|
|
14
14
|
type ServerHandler,
|
|
15
|
+
ServerProvider,
|
|
15
16
|
ServerRouterProvider,
|
|
16
17
|
ServerTimingProvider,
|
|
17
18
|
} from "@alepha/server";
|
|
@@ -21,16 +22,15 @@ import { renderToString } from "react-dom/server";
|
|
|
21
22
|
import {
|
|
22
23
|
$page,
|
|
23
24
|
type PageDescriptorRenderOptions,
|
|
25
|
+
type PageDescriptorRenderResult,
|
|
24
26
|
} from "../descriptors/$page.ts";
|
|
25
27
|
import { Redirection } from "../errors/Redirection.ts";
|
|
28
|
+
import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
|
|
26
29
|
import {
|
|
27
|
-
PageDescriptorProvider,
|
|
28
|
-
type PageReactContext,
|
|
29
|
-
type PageRequest,
|
|
30
30
|
type PageRoute,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
ReactPageProvider,
|
|
32
|
+
type ReactRouterState,
|
|
33
|
+
} from "./ReactPageProvider.ts";
|
|
34
34
|
|
|
35
35
|
const envSchema = t.object({
|
|
36
36
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
@@ -54,7 +54,8 @@ declare module "@alepha/core" {
|
|
|
54
54
|
export class ReactServerProvider {
|
|
55
55
|
protected readonly log = $logger();
|
|
56
56
|
protected readonly alepha = $inject(Alepha);
|
|
57
|
-
protected readonly
|
|
57
|
+
protected readonly pageApi = $inject(ReactPageProvider);
|
|
58
|
+
protected readonly serverProvider = $inject(ServerProvider);
|
|
58
59
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
59
60
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
60
61
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
@@ -76,6 +77,19 @@ export class ReactServerProvider {
|
|
|
76
77
|
|
|
77
78
|
for (const page of pages) {
|
|
78
79
|
page.render = this.createRenderFunction(page.name);
|
|
80
|
+
page.fetch = async (options) => {
|
|
81
|
+
const response = await fetch(
|
|
82
|
+
`${this.serverProvider.hostname}/${page.pathname(options)}`,
|
|
83
|
+
);
|
|
84
|
+
const html = await response.text();
|
|
85
|
+
if (options?.html) return { html, response };
|
|
86
|
+
// take only text inside the root div
|
|
87
|
+
const match = html.match(this.ROOT_DIV_REGEX);
|
|
88
|
+
if (match) {
|
|
89
|
+
return { html: match[3], response };
|
|
90
|
+
}
|
|
91
|
+
throw new AlephaError("Invalid HTML response");
|
|
92
|
+
};
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
// development mode
|
|
@@ -136,7 +150,7 @@ export class ReactServerProvider {
|
|
|
136
150
|
}
|
|
137
151
|
|
|
138
152
|
protected async registerPages(templateLoader: TemplateLoader) {
|
|
139
|
-
for (const page of this.
|
|
153
|
+
for (const page of this.pageApi.getPages()) {
|
|
140
154
|
if (page.children?.length) {
|
|
141
155
|
continue;
|
|
142
156
|
}
|
|
@@ -196,48 +210,60 @@ export class ReactServerProvider {
|
|
|
196
210
|
* For testing purposes, creates a render function that can be used.
|
|
197
211
|
*/
|
|
198
212
|
protected createRenderFunction(name: string, withIndex = false) {
|
|
199
|
-
return async (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
213
|
+
return async (
|
|
214
|
+
options: PageDescriptorRenderOptions = {},
|
|
215
|
+
): Promise<PageDescriptorRenderResult> => {
|
|
216
|
+
const page = this.pageApi.page(name);
|
|
217
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
218
|
+
|
|
219
|
+
const entry: Partial<ReactRouterState> = {
|
|
203
220
|
url,
|
|
204
221
|
params: options.params ?? {},
|
|
205
222
|
query: options.query ?? {},
|
|
206
|
-
head: {},
|
|
207
223
|
onError: () => null,
|
|
224
|
+
layers: [],
|
|
225
|
+
meta: {},
|
|
208
226
|
};
|
|
209
227
|
|
|
228
|
+
const state = entry as ReactRouterState;
|
|
229
|
+
|
|
230
|
+
this.log.trace("Rendering", {
|
|
231
|
+
url,
|
|
232
|
+
});
|
|
233
|
+
|
|
210
234
|
await this.alepha.emit("react:server:render:begin", {
|
|
211
|
-
|
|
235
|
+
state,
|
|
212
236
|
});
|
|
213
237
|
|
|
214
|
-
const
|
|
238
|
+
const { redirect } = await this.pageApi.createLayers(
|
|
215
239
|
page,
|
|
216
|
-
|
|
240
|
+
state as ReactRouterState,
|
|
217
241
|
);
|
|
218
242
|
|
|
243
|
+
if (redirect) {
|
|
244
|
+
return { state, html: "", redirect };
|
|
245
|
+
}
|
|
246
|
+
|
|
219
247
|
if (!withIndex && !options.html) {
|
|
248
|
+
this.alepha.state("react.router.state", state);
|
|
249
|
+
|
|
220
250
|
return {
|
|
221
|
-
|
|
222
|
-
html: renderToString(
|
|
223
|
-
this.pageDescriptorProvider.root(state, context),
|
|
224
|
-
),
|
|
251
|
+
state,
|
|
252
|
+
html: renderToString(this.pageApi.root(state)),
|
|
225
253
|
};
|
|
226
254
|
}
|
|
227
255
|
|
|
228
256
|
const html = this.renderToHtml(
|
|
229
257
|
this.template ?? "",
|
|
230
258
|
state,
|
|
231
|
-
context,
|
|
232
259
|
options.hydration,
|
|
233
260
|
);
|
|
234
261
|
|
|
235
262
|
if (html instanceof Redirection) {
|
|
236
|
-
|
|
263
|
+
return { state, html: "", redirect };
|
|
237
264
|
}
|
|
238
265
|
|
|
239
266
|
const result = {
|
|
240
|
-
context,
|
|
241
267
|
state,
|
|
242
268
|
html,
|
|
243
269
|
};
|
|
@@ -249,7 +275,7 @@ export class ReactServerProvider {
|
|
|
249
275
|
}
|
|
250
276
|
|
|
251
277
|
protected createHandler(
|
|
252
|
-
|
|
278
|
+
route: PageRoute,
|
|
253
279
|
templateLoader: TemplateLoader,
|
|
254
280
|
): ServerHandler {
|
|
255
281
|
return async (serverRequest) => {
|
|
@@ -260,36 +286,32 @@ export class ReactServerProvider {
|
|
|
260
286
|
}
|
|
261
287
|
|
|
262
288
|
this.log.trace("Rendering page", {
|
|
263
|
-
name:
|
|
289
|
+
name: route.name,
|
|
264
290
|
});
|
|
265
291
|
|
|
266
|
-
const
|
|
292
|
+
const entry: Partial<ReactRouterState> = {
|
|
267
293
|
url,
|
|
268
294
|
params,
|
|
269
295
|
query,
|
|
270
|
-
// plugins
|
|
271
|
-
head: {},
|
|
272
296
|
onError: () => null,
|
|
297
|
+
layers: [],
|
|
273
298
|
};
|
|
274
299
|
|
|
275
|
-
|
|
276
|
-
const srv = this.alepha.inject(ServerLinksProvider);
|
|
277
|
-
const schema = apiLinksResponseSchema as any;
|
|
300
|
+
const state = entry as ReactRouterState;
|
|
278
301
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
302
|
+
if (this.alepha.has(ServerLinksProvider)) {
|
|
303
|
+
this.alepha.state(
|
|
304
|
+
"api",
|
|
305
|
+
await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
306
|
+
user: (serverRequest as any).user, // TODO: fix type
|
|
283
307
|
authorization: serverRequest.headers.authorization,
|
|
284
308
|
}),
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
this.alepha.context.set("links", context.links);
|
|
309
|
+
);
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
let target: PageRoute | undefined =
|
|
312
|
+
let target: PageRoute | undefined = route; // TODO: move to PageDescriptorProvider
|
|
291
313
|
while (target) {
|
|
292
|
-
if (
|
|
314
|
+
if (route.can && !route.can()) {
|
|
293
315
|
// if the page is not accessible, return 403
|
|
294
316
|
reply.status = 403;
|
|
295
317
|
reply.headers["content-type"] = "text/plain";
|
|
@@ -309,27 +331,19 @@ export class ReactServerProvider {
|
|
|
309
331
|
// return;
|
|
310
332
|
// }
|
|
311
333
|
|
|
312
|
-
await this.alepha.emit("react:transition:begin", {
|
|
313
|
-
request: serverRequest,
|
|
314
|
-
context,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
334
|
await this.alepha.emit("react:server:render:begin", {
|
|
318
335
|
request: serverRequest,
|
|
319
|
-
|
|
336
|
+
state,
|
|
320
337
|
});
|
|
321
338
|
|
|
322
339
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
323
340
|
|
|
324
|
-
const
|
|
325
|
-
page,
|
|
326
|
-
context,
|
|
327
|
-
);
|
|
341
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
328
342
|
|
|
329
343
|
this.serverTimingProvider.endTiming("createLayers");
|
|
330
344
|
|
|
331
|
-
if (
|
|
332
|
-
return reply.redirect(
|
|
345
|
+
if (redirect) {
|
|
346
|
+
return reply.redirect(redirect);
|
|
333
347
|
}
|
|
334
348
|
|
|
335
349
|
reply.headers["content-type"] = "text/html";
|
|
@@ -341,34 +355,28 @@ export class ReactServerProvider {
|
|
|
341
355
|
reply.headers.pragma = "no-cache";
|
|
342
356
|
reply.headers.expires = "0";
|
|
343
357
|
|
|
344
|
-
|
|
345
|
-
if (page.cache && serverRequest.user) {
|
|
346
|
-
delete context.links;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const html = this.renderToHtml(template, state, context);
|
|
358
|
+
const html = this.renderToHtml(template, state);
|
|
350
359
|
if (html instanceof Redirection) {
|
|
351
360
|
reply.redirect(
|
|
352
|
-
typeof html.
|
|
353
|
-
? html.
|
|
354
|
-
: this.
|
|
361
|
+
typeof html.redirect === "string"
|
|
362
|
+
? html.redirect
|
|
363
|
+
: this.pageApi.href(html.redirect),
|
|
355
364
|
);
|
|
356
365
|
return;
|
|
357
366
|
}
|
|
358
367
|
|
|
359
368
|
const event = {
|
|
360
369
|
request: serverRequest,
|
|
361
|
-
context,
|
|
362
370
|
state,
|
|
363
371
|
html,
|
|
364
372
|
};
|
|
365
373
|
|
|
366
374
|
await this.alepha.emit("react:server:render:end", event);
|
|
367
375
|
|
|
368
|
-
|
|
376
|
+
route.onServerResponse?.(serverRequest);
|
|
369
377
|
|
|
370
378
|
this.log.trace("Page rendered", {
|
|
371
|
-
name:
|
|
379
|
+
name: route.name,
|
|
372
380
|
});
|
|
373
381
|
|
|
374
382
|
return event.html;
|
|
@@ -377,28 +385,32 @@ export class ReactServerProvider {
|
|
|
377
385
|
|
|
378
386
|
public renderToHtml(
|
|
379
387
|
template: string,
|
|
380
|
-
state:
|
|
381
|
-
context: PageReactContext,
|
|
388
|
+
state: ReactRouterState,
|
|
382
389
|
hydration = true,
|
|
383
390
|
): string | Redirection {
|
|
384
|
-
const element = this.
|
|
391
|
+
const element = this.pageApi.root(state);
|
|
385
392
|
|
|
386
|
-
|
|
393
|
+
// attach react router state to the http request context
|
|
394
|
+
this.alepha.state("react.router.state", state);
|
|
387
395
|
|
|
396
|
+
this.serverTimingProvider.beginTiming("renderToString");
|
|
388
397
|
let app = "";
|
|
389
398
|
try {
|
|
390
399
|
app = renderToString(element);
|
|
391
400
|
} catch (error) {
|
|
392
|
-
this.log.error(
|
|
393
|
-
|
|
401
|
+
this.log.error(
|
|
402
|
+
"renderToString has failed, fallback to error handler",
|
|
403
|
+
error,
|
|
404
|
+
);
|
|
405
|
+
const element = state.onError(error as Error, state);
|
|
394
406
|
if (element instanceof Redirection) {
|
|
395
407
|
// if the error is a redirection, return the redirection URL
|
|
396
408
|
return element;
|
|
397
409
|
}
|
|
398
410
|
|
|
399
411
|
app = renderToString(element);
|
|
412
|
+
this.log.debug("Error handled successfully with fallback");
|
|
400
413
|
}
|
|
401
|
-
|
|
402
414
|
this.serverTimingProvider.endTiming("renderToString");
|
|
403
415
|
|
|
404
416
|
const response = {
|
|
@@ -406,11 +418,13 @@ export class ReactServerProvider {
|
|
|
406
418
|
};
|
|
407
419
|
|
|
408
420
|
if (hydration) {
|
|
409
|
-
const { request, context, ...
|
|
410
|
-
this.alepha.context.als?.getStore() ?? {};
|
|
421
|
+
const { request, context, ...store } =
|
|
422
|
+
this.alepha.context.als?.getStore() ?? {}; /// TODO: als must be protected, find a way to iterate on alepha.state
|
|
411
423
|
|
|
412
424
|
const hydrationData: ReactHydrationState = {
|
|
413
|
-
...
|
|
425
|
+
...store,
|
|
426
|
+
// map react.router.state to the hydration state
|
|
427
|
+
"react.router.state": undefined,
|
|
414
428
|
layers: state.layers.map((it) => ({
|
|
415
429
|
...it,
|
|
416
430
|
error: it.error
|
|
@@ -1,38 +1,45 @@
|
|
|
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
|
+
}
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
public get pages() {
|
|
22
|
+
return this.pageApi.getPages();
|
|
23
|
+
}
|
|
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>,
|
|
28
35
|
config: {
|
|
29
|
-
params?: Record<string,
|
|
30
|
-
query?: Record<string,
|
|
36
|
+
params?: Record<string, any>;
|
|
37
|
+
query?: Record<string, any>;
|
|
31
38
|
} = {},
|
|
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>,
|
|
@@ -134,19 +116,17 @@ export class RouterHookApi<T extends object> {
|
|
|
134
116
|
await this.browser?.go(path as string, options);
|
|
135
117
|
}
|
|
136
118
|
|
|
137
|
-
public anchor(
|
|
138
|
-
path: string,
|
|
139
|
-
options?: { params?: Record<string, any> },
|
|
140
|
-
): AnchorProps;
|
|
119
|
+
public anchor(path: string, options?: RouterGoOptions): AnchorProps;
|
|
141
120
|
public anchor(
|
|
142
121
|
path: keyof VirtualRouter<T>,
|
|
143
|
-
options?:
|
|
122
|
+
options?: RouterGoOptions,
|
|
144
123
|
): AnchorProps;
|
|
145
124
|
public anchor(
|
|
146
125
|
path: string | keyof VirtualRouter<T>,
|
|
147
|
-
options:
|
|
126
|
+
options: RouterGoOptions = {},
|
|
148
127
|
): AnchorProps {
|
|
149
128
|
let href = path as string;
|
|
129
|
+
|
|
150
130
|
for (const page of this.pages) {
|
|
151
131
|
if (page.name === path) {
|
|
152
132
|
href = this.path(path as keyof VirtualRouter<T>, options);
|
|
@@ -155,7 +135,7 @@ export class RouterHookApi<T extends object> {
|
|
|
155
135
|
}
|
|
156
136
|
|
|
157
137
|
return {
|
|
158
|
-
href,
|
|
138
|
+
href: this.base(href),
|
|
159
139
|
onClick: (ev: any) => {
|
|
160
140
|
ev.stopPropagation();
|
|
161
141
|
ev.preventDefault();
|
|
@@ -165,6 +145,15 @@ export class RouterHookApi<T extends object> {
|
|
|
165
145
|
};
|
|
166
146
|
}
|
|
167
147
|
|
|
148
|
+
public base(path: string): string {
|
|
149
|
+
const base = import.meta.env?.BASE_URL;
|
|
150
|
+
if (!base || base === "/") {
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return base + path;
|
|
155
|
+
}
|
|
156
|
+
|
|
168
157
|
/**
|
|
169
158
|
* Set query params.
|
|
170
159
|
*
|
|
@@ -194,8 +183,6 @@ export class RouterHookApi<T extends object> {
|
|
|
194
183
|
}
|
|
195
184
|
}
|
|
196
185
|
|
|
197
|
-
export type HrefLike = string | { options: { path?: string; name?: string } };
|
|
198
|
-
|
|
199
186
|
export type VirtualRouter<T> = {
|
|
200
187
|
[K in keyof T as T[K] extends PageDescriptor ? K : never]: T[K];
|
|
201
188
|
};
|
|
@@ -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
|
-
}
|