@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
|
@@ -2,54 +2,53 @@ import { ChannelPrimitive, TWSObject } from "alepha/websocket";
|
|
|
2
2
|
import { Static } from "alepha";
|
|
3
3
|
|
|
4
4
|
//#region ../../src/websocket/hooks/useRoom.d.ts
|
|
5
|
-
|
|
6
5
|
/**
|
|
7
6
|
* UseRoom options
|
|
8
7
|
*/
|
|
9
8
|
interface UseRoomOptions<TClient extends TWSObject, TServer extends TWSObject> {
|
|
10
9
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
* Room ID to connect to
|
|
11
|
+
*/
|
|
13
12
|
roomId: string;
|
|
14
13
|
/**
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
* Channel primitive defining the schemas
|
|
15
|
+
*/
|
|
17
16
|
channel: ChannelPrimitive<TClient, TServer>;
|
|
18
17
|
/**
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
* Handler for incoming messages from the server
|
|
19
|
+
*/
|
|
21
20
|
handler: (message: Static<TClient>) => void;
|
|
22
21
|
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
* Optional WebSocket URL override
|
|
23
|
+
* Defaults to auto-detected URL based on window.location
|
|
24
|
+
*/
|
|
26
25
|
url?: string;
|
|
27
26
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
* Enable automatic reconnection on disconnect
|
|
28
|
+
* @default true
|
|
29
|
+
*/
|
|
31
30
|
autoReconnect?: boolean;
|
|
32
31
|
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
* Reconnection interval in milliseconds
|
|
33
|
+
* @default 3000
|
|
34
|
+
*/
|
|
36
35
|
reconnectInterval?: number;
|
|
37
36
|
/**
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
* Maximum reconnection attempts (-1 for infinite)
|
|
38
|
+
* @default 10
|
|
39
|
+
*/
|
|
41
40
|
maxReconnectAttempts?: number;
|
|
42
41
|
/**
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
* Called when connection is established
|
|
43
|
+
*/
|
|
45
44
|
onConnect?: () => void;
|
|
46
45
|
/**
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
* Called when connection is closed
|
|
47
|
+
*/
|
|
49
48
|
onDisconnect?: () => void;
|
|
50
49
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
* Called on connection error
|
|
51
|
+
*/
|
|
53
52
|
onError?: (error: Error) => void;
|
|
54
53
|
}
|
|
55
54
|
/**
|
|
@@ -57,32 +56,32 @@ interface UseRoomOptions<TClient extends TWSObject, TServer extends TWSObject> {
|
|
|
57
56
|
*/
|
|
58
57
|
interface UseRoomReturn<TServer extends TWSObject> {
|
|
59
58
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
* Send a message to the server
|
|
60
|
+
*/
|
|
62
61
|
send: (message: Static<TServer>) => Promise<void>;
|
|
63
62
|
/**
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
* Whether the connection is established
|
|
64
|
+
*/
|
|
66
65
|
isConnected: boolean;
|
|
67
66
|
/**
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
* Whether the connection is in progress
|
|
68
|
+
*/
|
|
70
69
|
isConnecting: boolean;
|
|
71
70
|
/**
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
* Whether there was an error
|
|
72
|
+
*/
|
|
74
73
|
isError: boolean;
|
|
75
74
|
/**
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
* The error object if any
|
|
76
|
+
*/
|
|
78
77
|
error?: Error;
|
|
79
78
|
/**
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
* Manually reconnect
|
|
80
|
+
*/
|
|
82
81
|
reconnect: () => void;
|
|
83
82
|
/**
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
* Manually disconnect
|
|
84
|
+
*/
|
|
86
85
|
disconnect: () => void;
|
|
87
86
|
}
|
|
88
87
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/websocket/hooks/useRoom.tsx"],"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/websocket/hooks/useRoom.tsx"],"mappings":";;;;;AASA;;UAAiB,cAAA,iBACC,SAAA,kBACA,SAAA;EAAA;;;EAAA,MAAA;EAAA;;;EAAA,OAAA,EAUP,gBAAA,CAAiB,OAAA,EAAS,OAAA;EAAA;;;EAAA,OAAA,GAAA,OAAA,EAKhB,MAAA,CAAO,OAAA;EAAA;;;;EAAA,GAAA;EAAA;;;;EAAA,aAAA;EAAA;;;;EAAA,iBAAA;EAAA;;;;EAAA,oBAAA;EAAA;;;EAAA,SAAA;EAAA;;;EAAA,YAAA;EAAA;;;EAAA,OAAA,IAAA,KAAA,EAuCR,KAAA;AAAA;AAAA;;AAMpB;AANoB,UAMH,aAAA,iBAA8B,SAAA;EAAA;;;EAAA,IAAA,GAAA,OAAA,EAI7B,MAAA,CAAO,OAAA,MAAa,OAAA;EAAA;;;EAAA,WAAA;EAAA;;;EAAA,YAAA;EAAA;;;EAAA,OAAA;EAAA;;;EAAA,KAAA,GAoB5B,KAAA;EAAA;;AAwCV;EAxCU,SAAA;EAAA;;AAwCV;EAxCU,UAAA;AAAA;AAAA;;AAwCV;;;;;;;;;;;;;;;;;;;;;;;;;AAxCU,cAwCG,OAAA,mBAA2B,SAAA,kBAA2B,SAAA,EAAA,OAAA,EACxD,cAAA,CAAe,OAAA,EAAS,OAAA,GAAA,IAAA,gBAEhC,aAAA,CAAc,OAAA"}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@alepha/react",
|
|
3
3
|
"description": "React components and hooks for building Alepha applications.",
|
|
4
4
|
"author": "Nicolas Foures",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.15.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.0.0"
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"@testing-library/react": "^16.3.1",
|
|
26
26
|
"@types/react": "^19",
|
|
27
27
|
"@types/react-dom": "^19",
|
|
28
|
-
"alepha": "0.
|
|
28
|
+
"alepha": "0.15.0",
|
|
29
29
|
"jsdom": "^27.4.0",
|
|
30
30
|
"react": "^19.2.3",
|
|
31
31
|
"typescript": "^5.9.3",
|
|
32
|
-
"vitest": "^4.0.
|
|
32
|
+
"vitest": "^4.0.17"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"alepha": "0.
|
|
35
|
+
"alepha": "0.15.0",
|
|
36
36
|
"react": "^19"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"lint": "alepha lint",
|
|
40
|
-
"
|
|
40
|
+
"typecheck": "alepha typecheck",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"build": "node scripts/build.ts"
|
|
43
43
|
},
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Alepha } from "alepha";
|
|
3
3
|
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
-
import { $
|
|
5
|
-
import { HttpClient, ServerProvider } from "alepha/server";
|
|
4
|
+
import { $issuer, AlephaSecurity } from "alepha/security";
|
|
5
|
+
import { AlephaServer, HttpClient, ServerProvider } from "alepha/server";
|
|
6
6
|
import { $client } from "alepha/server/links";
|
|
7
|
-
import { AlephaServerSecurity } from "alepha/server/security";
|
|
8
7
|
import { describe, test } from "vitest";
|
|
9
8
|
import {
|
|
10
9
|
$auth,
|
|
@@ -25,7 +24,7 @@ describe("$auth", () => {
|
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
class App {
|
|
28
|
-
|
|
27
|
+
issuer = $issuer({
|
|
29
28
|
secret: "my-secret-key",
|
|
30
29
|
roles: [
|
|
31
30
|
{
|
|
@@ -36,7 +35,7 @@ describe("$auth", () => {
|
|
|
36
35
|
});
|
|
37
36
|
|
|
38
37
|
auth = $auth({
|
|
39
|
-
|
|
38
|
+
issuer: this.issuer,
|
|
40
39
|
credentials: {
|
|
41
40
|
account: () => user,
|
|
42
41
|
},
|
|
@@ -94,7 +93,7 @@ describe("$auth", () => {
|
|
|
94
93
|
);
|
|
95
94
|
|
|
96
95
|
test("should login with credentials", async ({ expect }) => {
|
|
97
|
-
const alepha = Alepha.create().with(
|
|
96
|
+
const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
|
|
98
97
|
const auth = alepha.inject(ReactAuth);
|
|
99
98
|
await alepha.start();
|
|
100
99
|
|
|
@@ -113,7 +112,7 @@ describe("$auth", () => {
|
|
|
113
112
|
});
|
|
114
113
|
|
|
115
114
|
test("should get userinfo", async ({ expect }) => {
|
|
116
|
-
const alepha = Alepha.create().with(
|
|
115
|
+
const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
|
|
117
116
|
await alepha.start();
|
|
118
117
|
|
|
119
118
|
const { data: tokens } = await login(alepha);
|
|
@@ -134,7 +133,7 @@ describe("$auth", () => {
|
|
|
134
133
|
});
|
|
135
134
|
|
|
136
135
|
test("should reject expired token", async ({ expect }) => {
|
|
137
|
-
const alepha = Alepha.create().with(App);
|
|
136
|
+
const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
|
|
138
137
|
await alepha.start();
|
|
139
138
|
|
|
140
139
|
const { data: tokens } = await login(alepha);
|
|
@@ -150,7 +149,7 @@ describe("$auth", () => {
|
|
|
150
149
|
});
|
|
151
150
|
|
|
152
151
|
test("should refresh expired token", async ({ expect }) => {
|
|
153
|
-
const alepha = Alepha.create().with(App);
|
|
152
|
+
const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
|
|
154
153
|
await alepha.start();
|
|
155
154
|
|
|
156
155
|
const { data: tokens } = await login(alepha);
|
|
@@ -174,7 +173,7 @@ describe("$auth", () => {
|
|
|
174
173
|
});
|
|
175
174
|
|
|
176
175
|
test("should reject expired refresh token", async ({ expect }) => {
|
|
177
|
-
const alepha = Alepha.create().with(App);
|
|
176
|
+
const alepha = Alepha.create().with(AlephaServer).with(AlephaSecurity).with(App);
|
|
178
177
|
await alepha.start();
|
|
179
178
|
|
|
180
179
|
const { data: tokens } = await login(alepha);
|
|
@@ -182,7 +181,7 @@ describe("$auth", () => {
|
|
|
182
181
|
await alepha.inject(DateTimeProvider).travel(40, "days");
|
|
183
182
|
|
|
184
183
|
await expect(refresh(alepha, tokens)).rejects.toThrowError(
|
|
185
|
-
"Failed to refresh access token using the refresh token (
|
|
184
|
+
"Failed to refresh access token using the refresh token (issuer)",
|
|
186
185
|
);
|
|
187
186
|
});
|
|
188
187
|
});
|
|
@@ -74,7 +74,7 @@ test("Router - NestedView", async ({ expect }) => {
|
|
|
74
74
|
name: t.text(),
|
|
75
75
|
}),
|
|
76
76
|
},
|
|
77
|
-
|
|
77
|
+
loader: ({ params }) => params,
|
|
78
78
|
component: (props) => `Hello, ${props.name}!`,
|
|
79
79
|
},
|
|
80
80
|
],
|
|
@@ -125,18 +125,18 @@ test("Router - All routes", async ({ expect }) => {
|
|
|
125
125
|
{
|
|
126
126
|
path: ":id",
|
|
127
127
|
schema: { params: t.object({ id: t.text() }) },
|
|
128
|
-
|
|
128
|
+
loader: ({ params }) => {
|
|
129
129
|
if (params.id === "boom") throw new Error("boom");
|
|
130
130
|
return params;
|
|
131
131
|
},
|
|
132
132
|
children: [
|
|
133
133
|
{
|
|
134
|
-
|
|
134
|
+
loader: ({ params }) => params,
|
|
135
135
|
component: ({ id }) => `hey ${id}`,
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
path: "profile",
|
|
139
|
-
|
|
139
|
+
loader: ({ params }) => params,
|
|
140
140
|
component: ({ id }) => `profile of ${id}`,
|
|
141
141
|
},
|
|
142
142
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Alepha } from "alepha";
|
|
2
2
|
import { describe, it } from "vitest";
|
|
3
|
-
import { SeoExpander } from "
|
|
3
|
+
import { SeoExpander } from "./SeoExpander.ts";
|
|
4
4
|
|
|
5
5
|
describe("SeoExpander", () => {
|
|
6
6
|
it("should expand basic SEO configuration", ({ expect }) => {
|
package/src/head/index.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import { AlephaReact } from "@alepha/react";
|
|
2
|
-
import type {
|
|
3
|
-
PageConfigSchema,
|
|
4
|
-
TPropsDefault,
|
|
5
|
-
TPropsParentDefault,
|
|
6
|
-
} from "@alepha/react/router";
|
|
7
2
|
import { $module } from "alepha";
|
|
8
3
|
import { $head } from "./primitives/$head.ts";
|
|
9
|
-
import
|
|
10
|
-
import { ServerHeadProvider } from "./providers/ServerHeadProvider.ts";
|
|
4
|
+
import { BrowserHeadProvider } from "./providers/BrowserHeadProvider.ts";
|
|
11
5
|
import { HeadProvider } from "./providers/HeadProvider.ts";
|
|
12
6
|
import { SeoExpander } from "./helpers/SeoExpander.ts";
|
|
7
|
+
import { ServerHeadProvider } from "./providers/ServerHeadProvider.ts";
|
|
13
8
|
|
|
14
9
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
15
10
|
|
|
@@ -18,26 +13,7 @@ export * from "./hooks/useHead.ts";
|
|
|
18
13
|
export * from "./interfaces/Head.ts";
|
|
19
14
|
export * from "./helpers/SeoExpander.ts";
|
|
20
15
|
export * from "./providers/ServerHeadProvider.ts";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
// Augment PagePrimitiveOptions in router module
|
|
25
|
-
declare module "@alepha/react/router" {
|
|
26
|
-
interface PagePrimitiveOptions<
|
|
27
|
-
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
28
|
-
TProps extends object = TPropsDefault,
|
|
29
|
-
TPropsParent extends object = TPropsParentDefault,
|
|
30
|
-
> {
|
|
31
|
-
head?: Head | ((props: TProps, previous?: Head) => Head);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Augment ReactRouterState in router module
|
|
36
|
-
declare module "@alepha/react/router" {
|
|
37
|
-
interface ReactRouterState {
|
|
38
|
-
head: Head;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
16
|
+
export * from "./providers/BrowserHeadProvider.ts";
|
|
41
17
|
|
|
42
18
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
43
19
|
|
|
@@ -55,5 +31,11 @@ declare module "@alepha/react/router" {
|
|
|
55
31
|
export const AlephaReactHead = $module({
|
|
56
32
|
name: "alepha.react.head",
|
|
57
33
|
primitives: [$head],
|
|
58
|
-
services: [
|
|
34
|
+
services: [
|
|
35
|
+
AlephaReact,
|
|
36
|
+
BrowserHeadProvider,
|
|
37
|
+
HeadProvider,
|
|
38
|
+
SeoExpander,
|
|
39
|
+
ServerHeadProvider,
|
|
40
|
+
],
|
|
59
41
|
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { $page } from "@alepha/react/router";
|
|
2
1
|
import { Alepha } from "alepha";
|
|
3
|
-
import {
|
|
4
|
-
import { AlephaReactHead } from "../index.browser.ts";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
5
3
|
import type { Head } from "../interfaces/Head.ts";
|
|
6
4
|
import { BrowserHeadProvider } from "./BrowserHeadProvider.ts";
|
|
7
5
|
|
|
@@ -195,77 +193,4 @@ describe("BrowserHeadProvider", () => {
|
|
|
195
193
|
expect(authorMeta?.getAttribute("content")).toBe("Test Author");
|
|
196
194
|
});
|
|
197
195
|
});
|
|
198
|
-
|
|
199
|
-
describe("$page integration", () => {
|
|
200
|
-
class TestApp {
|
|
201
|
-
simplePage = $page({
|
|
202
|
-
path: "/",
|
|
203
|
-
head: {
|
|
204
|
-
title: "Simple Page",
|
|
205
|
-
bodyAttributes: { class: "simple-page" },
|
|
206
|
-
},
|
|
207
|
-
component: () => "Simple content",
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
complexPage = $page({
|
|
211
|
-
path: "/complex",
|
|
212
|
-
head: {
|
|
213
|
-
title: "Complex Page",
|
|
214
|
-
htmlAttributes: {
|
|
215
|
-
lang: "en",
|
|
216
|
-
"data-theme": "dark",
|
|
217
|
-
},
|
|
218
|
-
bodyAttributes: {
|
|
219
|
-
class: "complex-page",
|
|
220
|
-
style: "background: black;",
|
|
221
|
-
},
|
|
222
|
-
meta: [
|
|
223
|
-
{ name: "description", content: "Complex test page" },
|
|
224
|
-
{
|
|
225
|
-
name: "viewport",
|
|
226
|
-
content: "width=device-width, initial-scale=1",
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
},
|
|
230
|
-
component: () => "Complex content",
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
afterEach(() => {
|
|
235
|
-
document.body.querySelector("#root")?.remove();
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("should render simple page head configuration", async () => {
|
|
239
|
-
const alepha = Alepha.create().with(AlephaReactHead).with(TestApp);
|
|
240
|
-
await alepha.start();
|
|
241
|
-
|
|
242
|
-
expect(document.title).toBe("Simple Page");
|
|
243
|
-
expect(document.body.getAttribute("class")).toBe("simple-page");
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("should get current head state and match page configuration", async () => {
|
|
247
|
-
const alepha = Alepha.create().with(AlephaReactHead);
|
|
248
|
-
const app = alepha.inject(TestApp);
|
|
249
|
-
await alepha.start();
|
|
250
|
-
|
|
251
|
-
// Apply complex page head
|
|
252
|
-
const headConfig = app.complexPage.options.head as Head;
|
|
253
|
-
provider.renderHead(document, headConfig);
|
|
254
|
-
|
|
255
|
-
// Get current head state
|
|
256
|
-
const currentHead = provider.getHead(document);
|
|
257
|
-
|
|
258
|
-
expect(currentHead.title).toBe(headConfig.title);
|
|
259
|
-
expect(currentHead.htmlAttributes?.lang).toBe(
|
|
260
|
-
headConfig.htmlAttributes?.lang,
|
|
261
|
-
);
|
|
262
|
-
expect(currentHead.bodyAttributes?.class).toBe(
|
|
263
|
-
headConfig.bodyAttributes?.class,
|
|
264
|
-
);
|
|
265
|
-
expect(currentHead.meta).toContainEqual({
|
|
266
|
-
name: "description",
|
|
267
|
-
content: "Complex test page",
|
|
268
|
-
});
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
196
|
});
|
|
@@ -1,33 +1,39 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $inject, Alepha } from "alepha";
|
|
2
2
|
import type { Head, HeadMeta } from "../interfaces/Head.ts";
|
|
3
3
|
import { HeadProvider } from "./HeadProvider.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Browser-side head provider that manages document head elements.
|
|
7
|
+
*
|
|
8
|
+
* Used by ReactBrowserProvider and ReactBrowserRouterProvider to update
|
|
9
|
+
* document title, meta tags, and other head elements during client-side
|
|
10
|
+
* navigation.
|
|
11
|
+
*/
|
|
5
12
|
export class BrowserHeadProvider {
|
|
13
|
+
protected readonly alepha = $inject(Alepha);
|
|
6
14
|
protected readonly headProvider = $inject(HeadProvider);
|
|
7
15
|
|
|
8
16
|
protected get document(): Document {
|
|
9
17
|
return window.document;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Fill head state from route configurations and render to document.
|
|
22
|
+
* Combines fillHead from HeadProvider with renderHead to the DOM.
|
|
23
|
+
*
|
|
24
|
+
* Only runs in browser environment - no-op on server.
|
|
25
|
+
*/
|
|
26
|
+
public fillAndRenderHead(state: { head: Head; layers: Array<any> }): void {
|
|
27
|
+
// Skip on server-side
|
|
28
|
+
if (!this.alepha.isBrowser()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.renderHead(this.document, state.head);
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
});
|
|
32
|
+
this.headProvider.fillHead(state as any);
|
|
33
|
+
if (state.head) {
|
|
34
|
+
this.renderHead(this.document, state.head);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
31
37
|
|
|
32
38
|
public getHead(document: Document): Head {
|
|
33
39
|
return {
|
|
@@ -1,14 +1,54 @@
|
|
|
1
|
-
import type { PageRoute, ReactRouterState } from "@alepha/react/router";
|
|
2
1
|
import { $inject } from "alepha";
|
|
2
|
+
import { $logger } from "alepha/logger";
|
|
3
3
|
import { SeoExpander } from "../helpers/SeoExpander.ts";
|
|
4
4
|
import type { Head } from "../interfaces/Head.ts";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Provides methods to fill and merge head information into the application state.
|
|
8
|
+
*
|
|
9
|
+
* Used both on server and client side to manage document head.
|
|
10
|
+
*
|
|
11
|
+
* @see {@link SeoExpander}
|
|
12
|
+
* @see {@link ServerHeadProvider}
|
|
13
|
+
* @see {@link BrowserHeadProvider}
|
|
14
|
+
*/
|
|
6
15
|
export class HeadProvider {
|
|
16
|
+
protected readonly log = $logger();
|
|
7
17
|
protected readonly seoExpander = $inject(SeoExpander);
|
|
8
18
|
|
|
9
19
|
public global?: Array<Head | (() => Head)> = [];
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Track if we've warned about page-level htmlAttributes to avoid spam.
|
|
23
|
+
*/
|
|
24
|
+
protected warnedAboutHtmlAttributes = false;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve global head configuration (from $head primitives only).
|
|
28
|
+
*
|
|
29
|
+
* This is used to get htmlAttributes early, before page loaders run.
|
|
30
|
+
* Only htmlAttributes from global $head are allowed; page-level htmlAttributes
|
|
31
|
+
* are ignored for early streaming optimization.
|
|
32
|
+
*
|
|
33
|
+
* @returns Merged global head with htmlAttributes
|
|
34
|
+
*/
|
|
35
|
+
public resolveGlobalHead(): Head {
|
|
36
|
+
const head: Head = {};
|
|
37
|
+
|
|
38
|
+
for (const h of this.global ?? []) {
|
|
39
|
+
const resolved = typeof h === "function" ? h() : h;
|
|
40
|
+
if (resolved.htmlAttributes) {
|
|
41
|
+
head.htmlAttributes = {
|
|
42
|
+
...head.htmlAttributes,
|
|
43
|
+
...resolved.htmlAttributes,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return head;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public fillHead(state: HeadState) {
|
|
12
52
|
state.head = {
|
|
13
53
|
...state.head,
|
|
14
54
|
};
|
|
@@ -25,7 +65,7 @@ export class HeadProvider {
|
|
|
25
65
|
}
|
|
26
66
|
}
|
|
27
67
|
|
|
28
|
-
protected mergeHead(state:
|
|
68
|
+
protected mergeHead(state: HeadState, head: Head): void {
|
|
29
69
|
// Expand SEO fields into meta tags
|
|
30
70
|
const { meta, link } = this.seoExpander.expand(head);
|
|
31
71
|
state.head = {
|
|
@@ -38,8 +78,8 @@ export class HeadProvider {
|
|
|
38
78
|
}
|
|
39
79
|
|
|
40
80
|
protected fillHeadByPage(
|
|
41
|
-
page:
|
|
42
|
-
state:
|
|
81
|
+
page: HeadRoute,
|
|
82
|
+
state: HeadState,
|
|
43
83
|
props: Record<string, any>,
|
|
44
84
|
): void {
|
|
45
85
|
if (!page.head) {
|
|
@@ -70,11 +110,14 @@ export class HeadProvider {
|
|
|
70
110
|
state.head.titleSeparator = head.titleSeparator;
|
|
71
111
|
}
|
|
72
112
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
// htmlAttributes from pages are ignored for early streaming optimization.
|
|
114
|
+
// Only global $head can set htmlAttributes.
|
|
115
|
+
if (head.htmlAttributes && !this.warnedAboutHtmlAttributes) {
|
|
116
|
+
this.warnedAboutHtmlAttributes = true;
|
|
117
|
+
this.log.warn(
|
|
118
|
+
"Page-level htmlAttributes are ignored. Use global $head() for htmlAttributes instead, " +
|
|
119
|
+
"as they are sent before page loaders run for early streaming optimization.",
|
|
120
|
+
);
|
|
78
121
|
}
|
|
79
122
|
|
|
80
123
|
if (head.bodyAttributes) {
|
|
@@ -97,3 +140,26 @@ export class HeadProvider {
|
|
|
97
140
|
}
|
|
98
141
|
}
|
|
99
142
|
}
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Minimal route interface for head processing.
|
|
148
|
+
* Avoids circular dependency with @alepha/react/router.
|
|
149
|
+
*/
|
|
150
|
+
interface HeadRoute {
|
|
151
|
+
head?: Head | ((props: Record<string, any>, previous?: Head) => Head);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Minimal state interface for head processing.
|
|
156
|
+
* Avoids circular dependency with @alepha/react/router.
|
|
157
|
+
*/
|
|
158
|
+
interface HeadState {
|
|
159
|
+
head: Head;
|
|
160
|
+
layers: Array<{
|
|
161
|
+
route?: HeadRoute;
|
|
162
|
+
props?: Record<string, any>;
|
|
163
|
+
error?: Error;
|
|
164
|
+
}>;
|
|
165
|
+
}
|