@alepha/react 0.14.3 → 0.15.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 +10 -0
- package/dist/auth/index.browser.js +29 -14
- package/dist/auth/index.browser.js.map +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +950 -194
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +118 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/form/index.d.ts +27 -28
- package/dist/form/index.d.ts.map +1 -1
- package/dist/head/index.browser.js +59 -19
- package/dist/head/index.browser.js.map +1 -1
- package/dist/head/index.d.ts +105 -576
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +91 -87
- package/dist/head/index.js.map +1 -1
- package/dist/i18n/index.d.ts +33 -33
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/router/index.browser.js +30 -15
- package/dist/router/index.browser.js.map +1 -1
- package/dist/router/index.d.ts +827 -403
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +951 -195
- package/dist/router/index.js.map +1 -1
- package/dist/websocket/index.d.ts +38 -39
- package/dist/websocket/index.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/auth/__tests__/$auth.spec.ts +10 -11
- package/src/core/__tests__/Router.spec.tsx +4 -4
- package/src/head/{__tests__/expandSeo.spec.ts → helpers/SeoExpander.spec.ts} +1 -1
- package/src/head/index.ts +10 -28
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +1 -76
- package/src/head/providers/BrowserHeadProvider.ts +25 -19
- package/src/head/providers/HeadProvider.ts +76 -10
- package/src/head/providers/ServerHeadProvider.ts +22 -138
- package/src/router/__tests__/page-head-browser.browser.spec.ts +91 -0
- package/src/router/__tests__/page-head.spec.ts +44 -0
- package/src/{head → router}/__tests__/seo-head.spec.ts +2 -2
- package/src/router/atoms/ssrManifestAtom.ts +60 -0
- package/src/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/router/errors/Redirection.ts +1 -1
- package/src/router/index.shared.ts +1 -0
- package/src/router/index.ts +16 -2
- package/src/router/primitives/$page.browser.spec.tsx +15 -15
- package/src/router/primitives/$page.spec.tsx +18 -18
- package/src/router/primitives/$page.ts +46 -10
- package/src/router/providers/ReactBrowserProvider.ts +14 -29
- package/src/router/providers/ReactBrowserRouterProvider.ts +5 -0
- package/src/router/providers/ReactPageProvider.ts +11 -4
- package/src/router/providers/ReactServerProvider.ts +321 -316
- package/src/router/providers/ReactServerTemplateProvider.ts +793 -0
- package/src/router/providers/SSRManifestProvider.ts +365 -0
- package/src/router/services/ReactPageServerService.ts +5 -3
- package/src/router/services/ReactRouter.ts +3 -3
- package/src/head/__tests__/page-head.spec.ts +0 -39
- package/src/head/providers/ServerHeadProvider.spec.ts +0 -163
|
@@ -61,7 +61,7 @@ describe("$page primitive tests", () => {
|
|
|
61
61
|
sort: t.optional(t.text()),
|
|
62
62
|
}),
|
|
63
63
|
},
|
|
64
|
-
|
|
64
|
+
loader: ({ params, query }) => ({ params, query }),
|
|
65
65
|
component: ({ params, query }) =>
|
|
66
66
|
`User ${params.id} - Tab: ${query.tab}`,
|
|
67
67
|
});
|
|
@@ -72,7 +72,7 @@ describe("$page primitive tests", () => {
|
|
|
72
72
|
|
|
73
73
|
expect(app.user.options.schema?.params).toBeDefined();
|
|
74
74
|
expect(app.user.options.schema?.query).toBeDefined();
|
|
75
|
-
expect(app.user.options.
|
|
75
|
+
expect(app.user.options.loader).toBeDefined();
|
|
76
76
|
expect(app.user.options.component).toBeDefined();
|
|
77
77
|
|
|
78
78
|
const rendered = await app.user.render({
|
|
@@ -89,7 +89,7 @@ describe("$page primitive tests", () => {
|
|
|
89
89
|
class App {
|
|
90
90
|
lazy = $page({
|
|
91
91
|
path: "/lazy",
|
|
92
|
-
|
|
92
|
+
loader: () => ({ message: "loaded" }),
|
|
93
93
|
lazy: async () => ({ default: LazyComponent }),
|
|
94
94
|
});
|
|
95
95
|
}
|
|
@@ -141,7 +141,7 @@ describe("$page primitive tests", () => {
|
|
|
141
141
|
id: t.text(),
|
|
142
142
|
}),
|
|
143
143
|
},
|
|
144
|
-
|
|
144
|
+
loader: async ({ params }) => {
|
|
145
145
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
146
146
|
return { data: `Data for ${params.id}`, timestamp: Date.now() };
|
|
147
147
|
},
|
|
@@ -152,8 +152,8 @@ describe("$page primitive tests", () => {
|
|
|
152
152
|
const app = alepha.inject(App);
|
|
153
153
|
await alepha.start();
|
|
154
154
|
|
|
155
|
-
expect(app.async.options.
|
|
156
|
-
expect(typeof app.async.options.
|
|
155
|
+
expect(app.async.options.loader).toBeDefined();
|
|
156
|
+
expect(typeof app.async.options.loader).toBe("function");
|
|
157
157
|
|
|
158
158
|
const mockContext = {
|
|
159
159
|
params: { id: "test" },
|
|
@@ -161,7 +161,7 @@ describe("$page primitive tests", () => {
|
|
|
161
161
|
pathname: "/async/test",
|
|
162
162
|
search: "",
|
|
163
163
|
};
|
|
164
|
-
const result = await app.async.options.
|
|
164
|
+
const result = await app.async.options.loader!(mockContext as any);
|
|
165
165
|
expect(result.data).toBe("Data for test");
|
|
166
166
|
expect(typeof result.timestamp).toBe("number");
|
|
167
167
|
|
|
@@ -173,14 +173,14 @@ describe("$page primitive tests", () => {
|
|
|
173
173
|
class App {
|
|
174
174
|
parent = $page({
|
|
175
175
|
path: "/parent",
|
|
176
|
-
|
|
176
|
+
loader: () => ({ parentData: "from parent" }),
|
|
177
177
|
children: [],
|
|
178
178
|
});
|
|
179
179
|
|
|
180
180
|
child = $page({
|
|
181
181
|
path: "/child",
|
|
182
182
|
parent: this.parent,
|
|
183
|
-
|
|
183
|
+
loader: ({ parentData }) => ({
|
|
184
184
|
childData: `child with ${parentData}`,
|
|
185
185
|
}),
|
|
186
186
|
component: ({ childData }) => childData,
|
|
@@ -261,7 +261,7 @@ describe("$page primitive tests", () => {
|
|
|
261
261
|
class App {
|
|
262
262
|
errorPage = $page({
|
|
263
263
|
path: "/error",
|
|
264
|
-
|
|
264
|
+
loader: () => {
|
|
265
265
|
throw new Error("Test error");
|
|
266
266
|
},
|
|
267
267
|
errorHandler: (error) => `Error: ${error.message}`,
|
|
@@ -289,7 +289,7 @@ describe("$page primitive tests", () => {
|
|
|
289
289
|
class App {
|
|
290
290
|
errorPage = $page({
|
|
291
291
|
path: "/error",
|
|
292
|
-
|
|
292
|
+
loader: () => {
|
|
293
293
|
throw new Error("unauthorized");
|
|
294
294
|
},
|
|
295
295
|
errorHandler: (error) => {
|
|
@@ -342,7 +342,7 @@ describe("$page primitive tests", () => {
|
|
|
342
342
|
static: {
|
|
343
343
|
entries: [{ params: { id: "1" } }, { params: { id: "2" } }],
|
|
344
344
|
},
|
|
345
|
-
|
|
345
|
+
loader: ({ params }) => ({ id: params.id }),
|
|
346
346
|
component: ({ id }) => `Static page ${id}`,
|
|
347
347
|
});
|
|
348
348
|
}
|
|
@@ -566,7 +566,7 @@ describe("$page primitive tests", () => {
|
|
|
566
566
|
limit: t.number({ default: 10 }),
|
|
567
567
|
}),
|
|
568
568
|
},
|
|
569
|
-
|
|
569
|
+
loader: ({ params, query }) => ({
|
|
570
570
|
user: { id: params.userId },
|
|
571
571
|
pagination: { page: query.page, limit: query.limit },
|
|
572
572
|
filters: query.filters,
|
|
@@ -582,7 +582,7 @@ describe("$page primitive tests", () => {
|
|
|
582
582
|
|
|
583
583
|
expect(app.complex.options.schema?.params).toBeDefined();
|
|
584
584
|
expect(app.complex.options.schema?.query).toBeDefined();
|
|
585
|
-
expect(app.complex.options.
|
|
585
|
+
expect(app.complex.options.loader).toBeDefined();
|
|
586
586
|
|
|
587
587
|
const rendered = await app.complex.render({
|
|
588
588
|
params: { userId: "123" },
|
|
@@ -608,7 +608,7 @@ describe("$page primitive tests", () => {
|
|
|
608
608
|
|
|
609
609
|
child = $page({
|
|
610
610
|
path: "/child",
|
|
611
|
-
|
|
611
|
+
loader: () => {
|
|
612
612
|
throw new Error("Child error");
|
|
613
613
|
},
|
|
614
614
|
errorHandler: (error) => {
|
|
@@ -648,13 +648,13 @@ describe("$page primitive tests", () => {
|
|
|
648
648
|
test("$page - resolve function receives parent props", async ({ expect }) => {
|
|
649
649
|
class App {
|
|
650
650
|
parent = $page({
|
|
651
|
-
|
|
651
|
+
loader: () => ({ parentValue: "from parent" }),
|
|
652
652
|
});
|
|
653
653
|
|
|
654
654
|
child = $page({
|
|
655
655
|
path: "/child",
|
|
656
656
|
parent: this.parent,
|
|
657
|
-
|
|
657
|
+
loader: ({ parentValue }) => ({
|
|
658
658
|
childData: `Child received: ${parentValue}`,
|
|
659
659
|
}),
|
|
660
660
|
component: ({ childData, parentValue }) =>
|
|
@@ -665,7 +665,7 @@ describe("$page primitive tests", () => {
|
|
|
665
665
|
const app = alepha.inject(App);
|
|
666
666
|
await alepha.start();
|
|
667
667
|
|
|
668
|
-
expect(app.child.options.
|
|
668
|
+
expect(app.child.options.loader).toBeDefined();
|
|
669
669
|
|
|
670
670
|
const rendered = await app.child.fetch();
|
|
671
671
|
expect(rendered.html).toBe("Child received: from parent and from parent");
|
|
@@ -14,6 +14,9 @@ import type { Redirection } from "../errors/Redirection.ts";
|
|
|
14
14
|
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
15
15
|
import { ReactPageService } from "../services/ReactPageService.ts";
|
|
16
16
|
import type { ClientOnlyProps } from "@alepha/react";
|
|
17
|
+
import type { Head } from "@alepha/react/head";
|
|
18
|
+
import { PAGE_PRELOAD_KEY } from "../constants/PAGE_PRELOAD_KEY.ts";
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Main primitive for defining a React route in the application.
|
|
@@ -27,7 +30,7 @@ import type { ClientOnlyProps } from "@alepha/react";
|
|
|
27
30
|
* - Type-safe URL parameter and query string validation
|
|
28
31
|
*
|
|
29
32
|
* **Data Loading**
|
|
30
|
-
* - Server-side data fetching with the `
|
|
33
|
+
* - Server-side data fetching with the `loader` function
|
|
31
34
|
* - Automatic serialization and hydration for SSR
|
|
32
35
|
* - Access to request context, URL params, and parent data
|
|
33
36
|
*
|
|
@@ -64,7 +67,7 @@ import type { ClientOnlyProps } from "@alepha/react";
|
|
|
64
67
|
* params: t.object({ id: t.integer() }),
|
|
65
68
|
* query: t.object({ tab: t.optional(t.text()) })
|
|
66
69
|
* },
|
|
67
|
-
*
|
|
70
|
+
* loader: async ({ params }) => {
|
|
68
71
|
* const user = await userApi.getUser(params.id);
|
|
69
72
|
* return { user };
|
|
70
73
|
* },
|
|
@@ -77,7 +80,7 @@ import type { ClientOnlyProps } from "@alepha/react";
|
|
|
77
80
|
* const projectSection = $page({
|
|
78
81
|
* path: "/projects/:id",
|
|
79
82
|
* children: () => [projectBoard, projectSettings],
|
|
80
|
-
*
|
|
83
|
+
* loader: async ({ params }) => {
|
|
81
84
|
* const project = await projectApi.get(params.id);
|
|
82
85
|
* return { project };
|
|
83
86
|
* },
|
|
@@ -96,7 +99,7 @@ import type { ClientOnlyProps } from "@alepha/react";
|
|
|
96
99
|
* static: {
|
|
97
100
|
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
98
101
|
* },
|
|
99
|
-
*
|
|
102
|
+
* loader: async ({ params }) => {
|
|
100
103
|
* const post = await loadPost(params.slug);
|
|
101
104
|
* return { post };
|
|
102
105
|
* }
|
|
@@ -155,12 +158,12 @@ export interface PagePrimitiveOptions<
|
|
|
155
158
|
*
|
|
156
159
|
* > In SSR, the returned data will be serialized and sent to the client, then reused during the client-side hydration.
|
|
157
160
|
*
|
|
158
|
-
*
|
|
161
|
+
* Loader can be stopped by throwing an error, which will be handled by the `errorHandler` function.
|
|
159
162
|
* It's common to throw a `NotFoundError` to display a 404 page.
|
|
160
163
|
*
|
|
161
164
|
* RedirectError can be thrown to redirect the user to another page.
|
|
162
165
|
*/
|
|
163
|
-
|
|
166
|
+
loader?: (context: PageLoader<TConfig, TPropsParent>) => Async<TProps>;
|
|
164
167
|
|
|
165
168
|
/**
|
|
166
169
|
* Default props to pass to the component when rendering the page.
|
|
@@ -205,7 +208,7 @@ export interface PagePrimitiveOptions<
|
|
|
205
208
|
can?: () => boolean;
|
|
206
209
|
|
|
207
210
|
/**
|
|
208
|
-
* Catch any error from the `
|
|
211
|
+
* Catch any error from the `loader` function or during `rendering`.
|
|
209
212
|
*
|
|
210
213
|
* Expected to return one of the following:
|
|
211
214
|
* - a ReactNode to render an error page
|
|
@@ -217,7 +220,7 @@ export interface PagePrimitiveOptions<
|
|
|
217
220
|
*
|
|
218
221
|
* @example Catch a 404 from API and render a custom not found component:
|
|
219
222
|
* ```ts
|
|
220
|
-
*
|
|
223
|
+
* loader: async ({ params, query }) => {
|
|
221
224
|
* api.fetch("/api/resource", { params, query });
|
|
222
225
|
* },
|
|
223
226
|
* errorHandler: (error, context) => {
|
|
@@ -229,7 +232,7 @@ export interface PagePrimitiveOptions<
|
|
|
229
232
|
*
|
|
230
233
|
* @example Catch an 401 error and redirect the user to the login page:
|
|
231
234
|
* ```ts
|
|
232
|
-
*
|
|
235
|
+
* loader: async ({ params, query }) => {
|
|
233
236
|
* // but the user is not authenticated
|
|
234
237
|
* api.fetch("/api/resource", { params, query });
|
|
235
238
|
* },
|
|
@@ -318,6 +321,39 @@ export interface PagePrimitiveOptions<
|
|
|
318
321
|
* ```
|
|
319
322
|
*/
|
|
320
323
|
animation?: PageAnimation;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Head configuration for the page (title, meta tags, etc.).
|
|
327
|
+
*
|
|
328
|
+
* Can be a static object or a function that receives resolved props.
|
|
329
|
+
*
|
|
330
|
+
* @example Static head
|
|
331
|
+
* ```ts
|
|
332
|
+
* head: {
|
|
333
|
+
* title: "My Page",
|
|
334
|
+
* description: "Page description",
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*
|
|
338
|
+
* @example Dynamic head based on props
|
|
339
|
+
* ```ts
|
|
340
|
+
* head: (props) => ({
|
|
341
|
+
* title: props.user.name,
|
|
342
|
+
* description: `Profile of ${props.user.name}`,
|
|
343
|
+
* })
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
head?: Head | ((props: TProps, previous?: Head) => Head);
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Source path for SSR module preloading.
|
|
350
|
+
*
|
|
351
|
+
* This is automatically injected by the viteAlephaPreload plugin.
|
|
352
|
+
* It maps to the source file path used in Vite's SSR manifest.
|
|
353
|
+
*
|
|
354
|
+
* @internal
|
|
355
|
+
*/
|
|
356
|
+
[PAGE_PRELOAD_KEY]?: string;
|
|
321
357
|
}
|
|
322
358
|
|
|
323
359
|
export type ErrorHandler = (
|
|
@@ -422,7 +458,7 @@ export interface PageRequestConfig<
|
|
|
422
458
|
: Record<string, string>;
|
|
423
459
|
}
|
|
424
460
|
|
|
425
|
-
export type
|
|
461
|
+
export type PageLoader<
|
|
426
462
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
427
463
|
TPropsParent extends object = TPropsParentDefault,
|
|
428
464
|
> = PageRequestConfig<TConfig> &
|
|
@@ -1,36 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
$atom,
|
|
3
|
-
$env,
|
|
4
|
-
$hook,
|
|
5
|
-
$inject,
|
|
6
|
-
$use,
|
|
7
|
-
Alepha,
|
|
8
|
-
type State,
|
|
9
|
-
type Static,
|
|
10
|
-
t,
|
|
11
|
-
} from "alepha";
|
|
1
|
+
import { $atom, $env, $hook, $inject, $use, Alepha, type State, type Static, t, } from "alepha";
|
|
12
2
|
import { DateTimeProvider } from "alepha/datetime";
|
|
13
3
|
import { $logger } from "alepha/logger";
|
|
14
4
|
import { LinkProvider } from "alepha/server/links";
|
|
5
|
+
import { BrowserHeadProvider } from "@alepha/react/head";
|
|
15
6
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
16
|
-
import type {
|
|
17
|
-
PreviousLayerData,
|
|
18
|
-
ReactRouterState,
|
|
19
|
-
TransitionOptions,
|
|
20
|
-
} from "./ReactPageProvider.ts";
|
|
7
|
+
import type { PreviousLayerData, ReactRouterState, } from "./ReactPageProvider.ts";
|
|
21
8
|
import type { RouterGoOptions } from "../services/ReactRouter.ts";
|
|
22
9
|
|
|
23
10
|
export type { RouterGoOptions } from "../services/ReactRouter.ts";
|
|
24
11
|
|
|
25
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
const envSchema = t.object({
|
|
28
|
-
REACT_ROOT_ID: t.text({ default: "root" }),
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
declare module "alepha" {
|
|
32
|
-
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
33
|
-
}
|
|
34
12
|
|
|
35
13
|
/**
|
|
36
14
|
* React browser renderer configuration atom
|
|
@@ -38,7 +16,7 @@ declare module "alepha" {
|
|
|
38
16
|
export const reactBrowserOptions = $atom({
|
|
39
17
|
name: "alepha.react.browser.options",
|
|
40
18
|
schema: t.object({
|
|
41
|
-
scrollRestoration: t.enum(["top", "manual"]),
|
|
19
|
+
scrollRestoration: t.enum(["top", "manual"]), // TODO: must be per page?
|
|
42
20
|
}),
|
|
43
21
|
default: {
|
|
44
22
|
scrollRestoration: "top" as const,
|
|
@@ -58,23 +36,27 @@ declare module "alepha" {
|
|
|
58
36
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
59
37
|
|
|
60
38
|
export class ReactBrowserProvider {
|
|
61
|
-
protected readonly env = $env(envSchema);
|
|
62
39
|
protected readonly log = $logger();
|
|
63
40
|
protected readonly client = $inject(LinkProvider);
|
|
64
41
|
protected readonly alepha = $inject(Alepha);
|
|
65
42
|
protected readonly router = $inject(ReactBrowserRouterProvider);
|
|
66
43
|
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
44
|
+
protected readonly browserHeadProvider = $inject(BrowserHeadProvider);
|
|
67
45
|
|
|
68
46
|
protected readonly options = $use(reactBrowserOptions);
|
|
69
47
|
|
|
48
|
+
public get rootId() {
|
|
49
|
+
return "root";
|
|
50
|
+
}
|
|
51
|
+
|
|
70
52
|
protected getRootElement() {
|
|
71
|
-
const root = this.document.getElementById(this.
|
|
53
|
+
const root = this.document.getElementById(this.rootId);
|
|
72
54
|
if (root) {
|
|
73
55
|
return root;
|
|
74
56
|
}
|
|
75
57
|
|
|
76
58
|
const div = this.document.createElement("div");
|
|
77
|
-
div.id = this.
|
|
59
|
+
div.id = this.rootId;
|
|
78
60
|
|
|
79
61
|
this.document.body.prepend(div);
|
|
80
62
|
|
|
@@ -281,6 +263,9 @@ export class ReactBrowserProvider {
|
|
|
281
263
|
state: this.state,
|
|
282
264
|
});
|
|
283
265
|
|
|
266
|
+
// Fill and render head from route configurations
|
|
267
|
+
this.browserHeadProvider.fillAndRenderHead(this.state);
|
|
268
|
+
|
|
284
269
|
window.addEventListener("popstate", () => {
|
|
285
270
|
// when you update silently queryParams or hash, skip rendering
|
|
286
271
|
// if you want to force a rendering, use #go()
|
|
@@ -2,6 +2,7 @@ import { $hook, $inject, Alepha } from "alepha";
|
|
|
2
2
|
import { $logger } from "alepha/logger";
|
|
3
3
|
import { type Route, RouterProvider } from "alepha/router";
|
|
4
4
|
import { createElement, type ReactNode } from "react";
|
|
5
|
+
import { BrowserHeadProvider } from "@alepha/react/head";
|
|
5
6
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
6
7
|
import {
|
|
7
8
|
isPageRoute,
|
|
@@ -23,6 +24,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
23
24
|
protected readonly log = $logger();
|
|
24
25
|
protected readonly alepha = $inject(Alepha);
|
|
25
26
|
protected readonly pageApi = $inject(ReactPageProvider);
|
|
27
|
+
protected readonly browserHeadProvider = $inject(BrowserHeadProvider);
|
|
26
28
|
|
|
27
29
|
public add(entry: PageRouteEntry) {
|
|
28
30
|
this.pageApi.add(entry);
|
|
@@ -147,6 +149,9 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
147
149
|
await this.alepha.events.emit("react:transition:end", {
|
|
148
150
|
state,
|
|
149
151
|
});
|
|
152
|
+
|
|
153
|
+
// Fill and render head from route configurations
|
|
154
|
+
this.browserHeadProvider.fillAndRenderHead(state);
|
|
150
155
|
}
|
|
151
156
|
|
|
152
157
|
public root(state: ReactRouterState): ReactNode {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
type PagePrimitive,
|
|
23
23
|
type PagePrimitiveOptions,
|
|
24
24
|
} from "../primitives/$page.ts";
|
|
25
|
+
import type { Head } from "@alepha/react/head";
|
|
25
26
|
|
|
26
27
|
const envSchema = t.object({
|
|
27
28
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -251,15 +252,15 @@ export class ReactPageProvider {
|
|
|
251
252
|
forceRefresh = true;
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
// no
|
|
255
|
-
if (!route.
|
|
255
|
+
// no loader, render a basic view by default
|
|
256
|
+
if (!route.loader) {
|
|
256
257
|
continue;
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
try {
|
|
260
261
|
const args = Object.create(state);
|
|
261
262
|
Object.assign(args, config, context);
|
|
262
|
-
const props = (await route.
|
|
263
|
+
const props = (await route.loader?.(args)) ?? {};
|
|
263
264
|
|
|
264
265
|
// save props
|
|
265
266
|
it.props = {
|
|
@@ -279,7 +280,7 @@ export class ReactPageProvider {
|
|
|
279
280
|
};
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
this.log.error("Page
|
|
283
|
+
this.log.error("Page loader has failed", e);
|
|
283
284
|
|
|
284
285
|
it.error = e as Error;
|
|
285
286
|
break;
|
|
@@ -696,6 +697,12 @@ export interface ReactRouterState {
|
|
|
696
697
|
*/
|
|
697
698
|
meta: Record<string, any>;
|
|
698
699
|
|
|
700
|
+
/**
|
|
701
|
+
* Head configuration for the current page (title, meta tags, etc.).
|
|
702
|
+
* Populated by HeadProvider during SSR.
|
|
703
|
+
*/
|
|
704
|
+
head: Head;
|
|
705
|
+
|
|
699
706
|
//
|
|
700
707
|
name?: string;
|
|
701
708
|
}
|