@alepha/react 0.14.2 → 0.14.3
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/dist/auth/index.js +2 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -4
- package/dist/core/index.js.map +1 -1
- package/dist/head/index.d.ts +17 -17
- package/dist/head/index.d.ts.map +1 -1
- package/dist/head/index.js +2 -1
- package/dist/head/index.js.map +1 -1
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -2
- package/dist/router/index.js.map +1 -1
- package/package.json +3 -3
- package/src/auth/__tests__/$auth.spec.ts +188 -0
- package/src/core/__tests__/Router.spec.tsx +169 -0
- package/src/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/core/hooks/useAction.ts +11 -0
- package/src/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/head/__tests__/expandSeo.spec.ts +203 -0
- package/src/head/__tests__/page-head.spec.ts +39 -0
- package/src/head/__tests__/seo-head.spec.ts +121 -0
- package/src/head/hooks/useHead.spec.tsx +288 -0
- package/src/head/index.ts +2 -1
- package/src/head/providers/BrowserHeadProvider.browser.spec.ts +271 -0
- package/src/head/providers/ServerHeadProvider.spec.ts +163 -0
- package/src/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/i18n/components/Localize.spec.tsx +357 -0
- package/src/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/router/primitives/$page.browser.spec.tsx +702 -0
- package/src/router/primitives/$page.spec.tsx +702 -0
- package/src/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/router/providers/ReactServerProvider.ts +4 -3
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.14.
|
|
5
|
+
"version": "0.14.3",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=22.0.0"
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"@testing-library/react": "^16.3.1",
|
|
26
26
|
"@types/react": "^19",
|
|
27
27
|
"@types/react-dom": "^19",
|
|
28
|
-
"alepha": "0.14.
|
|
28
|
+
"alepha": "0.14.3",
|
|
29
29
|
"jsdom": "^27.4.0",
|
|
30
30
|
"react": "^19.2.3",
|
|
31
31
|
"typescript": "^5.9.3",
|
|
32
32
|
"vitest": "^4.0.16"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"alepha": "0.14.
|
|
35
|
+
"alepha": "0.14.3",
|
|
36
36
|
"react": "^19"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
+
import { $realm } from "alepha/security";
|
|
5
|
+
import { HttpClient, ServerProvider } from "alepha/server";
|
|
6
|
+
import { $client } from "alepha/server/links";
|
|
7
|
+
import { AlephaServerSecurity } from "alepha/server/security";
|
|
8
|
+
import { describe, test } from "vitest";
|
|
9
|
+
import {
|
|
10
|
+
$auth,
|
|
11
|
+
alephaServerAuthRoutes,
|
|
12
|
+
type TokenResponse,
|
|
13
|
+
tokenResponseSchema,
|
|
14
|
+
tokensSchema
|
|
15
|
+
} from "alepha/server/auth";
|
|
16
|
+
import { ReactAuth, ReactAuthProvider } from "../index.ts";
|
|
17
|
+
|
|
18
|
+
describe("$auth", () => {
|
|
19
|
+
const user = {
|
|
20
|
+
id: randomUUID(),
|
|
21
|
+
name: "John Doe",
|
|
22
|
+
username: "john",
|
|
23
|
+
password: "***",
|
|
24
|
+
roles: ["admin"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
class App {
|
|
28
|
+
realm = $realm({
|
|
29
|
+
secret: "my-secret-key",
|
|
30
|
+
roles: [
|
|
31
|
+
{
|
|
32
|
+
name: "admin",
|
|
33
|
+
permissions: [{ name: "*" }],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
auth = $auth({
|
|
39
|
+
realm: this.realm,
|
|
40
|
+
credentials: {
|
|
41
|
+
account: () => user,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
api = $client<ReactAuthProvider>();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const userinfo = (alepha: Alepha, token?: string) =>
|
|
49
|
+
alepha
|
|
50
|
+
.inject(HttpClient)
|
|
51
|
+
.fetch(
|
|
52
|
+
`${alepha.inject(ServerProvider).hostname}${alephaServerAuthRoutes.userinfo}`,
|
|
53
|
+
{
|
|
54
|
+
method: "GET",
|
|
55
|
+
headers: {
|
|
56
|
+
authorization: `Bearer ${token}`,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
.then((it) => it.data);
|
|
61
|
+
|
|
62
|
+
const login = (alepha: Alepha) =>
|
|
63
|
+
alepha
|
|
64
|
+
.inject(HttpClient)
|
|
65
|
+
.fetch(
|
|
66
|
+
`${alepha.inject(ServerProvider).hostname}${alephaServerAuthRoutes.token}?provider=auth`,
|
|
67
|
+
{
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: JSON.stringify({
|
|
70
|
+
username: user.username,
|
|
71
|
+
password: user.password,
|
|
72
|
+
}),
|
|
73
|
+
schema: {
|
|
74
|
+
response: tokenResponseSchema,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const refresh = (alepha: Alepha, tokens: TokenResponse) =>
|
|
80
|
+
alepha
|
|
81
|
+
.inject(HttpClient)
|
|
82
|
+
.fetch(
|
|
83
|
+
`${alepha.inject(ServerProvider).hostname}${alephaServerAuthRoutes.refresh}?provider=auth`,
|
|
84
|
+
{
|
|
85
|
+
method: "POST",
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
refresh_token: tokens.refresh_token!,
|
|
88
|
+
access_token: tokens.access_token,
|
|
89
|
+
}),
|
|
90
|
+
schema: {
|
|
91
|
+
response: tokensSchema,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
test("should login with credentials", async ({ expect }) => {
|
|
97
|
+
const alepha = Alepha.create().with(App).with(AlephaServerSecurity);
|
|
98
|
+
const auth = alepha.inject(ReactAuth);
|
|
99
|
+
await alepha.start();
|
|
100
|
+
|
|
101
|
+
expect(auth.user).toBeUndefined();
|
|
102
|
+
await auth.login("auth", {
|
|
103
|
+
username: user.username,
|
|
104
|
+
password: user.password,
|
|
105
|
+
hostname: alepha.inject(ServerProvider).hostname,
|
|
106
|
+
});
|
|
107
|
+
expect(auth.user).toEqual({
|
|
108
|
+
id: user.id,
|
|
109
|
+
name: user.name,
|
|
110
|
+
roles: user.roles,
|
|
111
|
+
username: user.username,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("should get userinfo", async ({ expect }) => {
|
|
116
|
+
const alepha = Alepha.create().with(App).with(AlephaServerSecurity);
|
|
117
|
+
await alepha.start();
|
|
118
|
+
|
|
119
|
+
const { data: tokens } = await login(alepha);
|
|
120
|
+
|
|
121
|
+
expect(await userinfo(alepha, tokens.access_token)).toEqual({
|
|
122
|
+
user: {
|
|
123
|
+
id: user.id,
|
|
124
|
+
name: user.name,
|
|
125
|
+
roles: user.roles,
|
|
126
|
+
username: user.username,
|
|
127
|
+
sessionId: expect.any(String),
|
|
128
|
+
},
|
|
129
|
+
api: {
|
|
130
|
+
prefix: "/api",
|
|
131
|
+
links: [],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("should reject expired token", async ({ expect }) => {
|
|
137
|
+
const alepha = Alepha.create().with(App);
|
|
138
|
+
await alepha.start();
|
|
139
|
+
|
|
140
|
+
const { data: tokens } = await login(alepha);
|
|
141
|
+
|
|
142
|
+
await alepha.inject(DateTimeProvider).travel(1, "hour");
|
|
143
|
+
|
|
144
|
+
expect(await userinfo(alepha, tokens.access_token)).toEqual({
|
|
145
|
+
api: {
|
|
146
|
+
prefix: "/api",
|
|
147
|
+
links: [],
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("should refresh expired token", async ({ expect }) => {
|
|
153
|
+
const alepha = Alepha.create().with(App);
|
|
154
|
+
await alepha.start();
|
|
155
|
+
|
|
156
|
+
const { data: tokens } = await login(alepha);
|
|
157
|
+
|
|
158
|
+
await alepha.inject(DateTimeProvider).travel(1, "hour");
|
|
159
|
+
|
|
160
|
+
const { data: tokens2 } = await refresh(alepha, tokens);
|
|
161
|
+
|
|
162
|
+
expect(await userinfo(alepha, tokens2.access_token)).toEqual({
|
|
163
|
+
user: {
|
|
164
|
+
id: user.id,
|
|
165
|
+
name: user.name,
|
|
166
|
+
roles: user.roles,
|
|
167
|
+
username: user.username,
|
|
168
|
+
},
|
|
169
|
+
api: {
|
|
170
|
+
prefix: "/api",
|
|
171
|
+
links: [],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("should reject expired refresh token", async ({ expect }) => {
|
|
177
|
+
const alepha = Alepha.create().with(App);
|
|
178
|
+
await alepha.start();
|
|
179
|
+
|
|
180
|
+
const { data: tokens } = await login(alepha);
|
|
181
|
+
|
|
182
|
+
await alepha.inject(DateTimeProvider).travel(40, "days");
|
|
183
|
+
|
|
184
|
+
await expect(refresh(alepha, tokens)).rejects.toThrowError(
|
|
185
|
+
"Failed to refresh access token using the refresh token (realm)",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Alepha, t } from "alepha";
|
|
2
|
+
import { renderToString } from "react-dom/server";
|
|
3
|
+
import { test } from "vitest";
|
|
4
|
+
import { NestedView } from "../../router/index.ts";
|
|
5
|
+
import { ReactBrowserRouterProvider } from "../../router/providers/ReactBrowserRouterProvider.ts";
|
|
6
|
+
|
|
7
|
+
const setup = () => {
|
|
8
|
+
const alepha = Alepha.create();
|
|
9
|
+
const router = alepha.inject(ReactBrowserRouterProvider);
|
|
10
|
+
|
|
11
|
+
const render = async (path: string): Promise<string> => {
|
|
12
|
+
await router.transition(new URL(`http://localhost${path}`));
|
|
13
|
+
const state = alepha.store.get("alepha.react.router.state");
|
|
14
|
+
if (!state) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Router state not found. Ensure the router is properly configured.",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
const element = router.root(state);
|
|
20
|
+
return renderToString(element).replaceAll("<!-- -->", "");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
router,
|
|
25
|
+
render,
|
|
26
|
+
alepha,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
test("Router - Basic", async ({ expect }) => {
|
|
31
|
+
const { router, render, alepha } = setup();
|
|
32
|
+
|
|
33
|
+
router.add({
|
|
34
|
+
name: "Test",
|
|
35
|
+
path: "/",
|
|
36
|
+
component: () => "Hey",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.add({
|
|
40
|
+
name: "NotFound",
|
|
41
|
+
path: "/*",
|
|
42
|
+
component: () => "Not Found",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await alepha.start();
|
|
46
|
+
|
|
47
|
+
expect(await render("/")).toEqual("Hey");
|
|
48
|
+
expect(await render("/zz")).toEqual("Not Found");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("Router - NestedView", async ({ expect }) => {
|
|
52
|
+
const { router, render, alepha } = setup();
|
|
53
|
+
|
|
54
|
+
router.add({
|
|
55
|
+
name: "Test",
|
|
56
|
+
component: () => (
|
|
57
|
+
<>
|
|
58
|
+
((
|
|
59
|
+
<NestedView />
|
|
60
|
+
))
|
|
61
|
+
</>
|
|
62
|
+
),
|
|
63
|
+
children: [
|
|
64
|
+
{
|
|
65
|
+
name: "Home",
|
|
66
|
+
path: "/",
|
|
67
|
+
component: () => "Home",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "Hello",
|
|
71
|
+
path: "/hello/:name",
|
|
72
|
+
schema: {
|
|
73
|
+
params: t.object({
|
|
74
|
+
name: t.text(),
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
resolve: ({ params }) => params,
|
|
78
|
+
component: (props) => `Hello, ${props.name}!`,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await alepha.start();
|
|
84
|
+
|
|
85
|
+
expect(await render("/")).toEqual("((Home))");
|
|
86
|
+
expect(await render("/hello/jack")).toEqual("((Hello, jack!))");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Router - All routes", async ({ expect }) => {
|
|
90
|
+
const { router, render, alepha } = setup();
|
|
91
|
+
|
|
92
|
+
router.add({
|
|
93
|
+
children: [
|
|
94
|
+
{
|
|
95
|
+
path: "/*",
|
|
96
|
+
component: () => "404",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
component: () => "home",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: "about",
|
|
103
|
+
component: () => "about",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
path: "sub",
|
|
107
|
+
children: [
|
|
108
|
+
{
|
|
109
|
+
component: () => "a",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
path: "b",
|
|
113
|
+
component: () => "b",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
path: "users",
|
|
119
|
+
component: () => <NestedView>yo</NestedView>,
|
|
120
|
+
children: [
|
|
121
|
+
{
|
|
122
|
+
path: "new",
|
|
123
|
+
component: () => "users/new",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
path: ":id",
|
|
127
|
+
schema: { params: t.object({ id: t.text() }) },
|
|
128
|
+
resolve: ({ params }) => {
|
|
129
|
+
if (params.id === "boom") throw new Error("boom");
|
|
130
|
+
return params;
|
|
131
|
+
},
|
|
132
|
+
children: [
|
|
133
|
+
{
|
|
134
|
+
resolve: ({ params }) => params,
|
|
135
|
+
component: ({ id }) => `hey ${id}`,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
path: "profile",
|
|
139
|
+
resolve: ({ params }) => params,
|
|
140
|
+
component: ({ id }) => `profile of ${id}`,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
errorHandler: (error) => {
|
|
146
|
+
return `Error: ${error.message}`;
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await alepha.start();
|
|
153
|
+
|
|
154
|
+
expect(await render("/")).toEqual("home");
|
|
155
|
+
expect(await render("/about")).toEqual("about");
|
|
156
|
+
expect(await render("/noop")).toEqual("404");
|
|
157
|
+
expect(await render("/noop/noop")).toEqual("404");
|
|
158
|
+
expect(await render("/sub")).toEqual("a");
|
|
159
|
+
expect(await render("/sub/")).toEqual("a");
|
|
160
|
+
expect(await render("/sub/b")).toEqual("b");
|
|
161
|
+
expect(await render("/sub/noop")).toEqual("404");
|
|
162
|
+
expect(await render("/users")).toEqual("yo");
|
|
163
|
+
expect(await render("/users/")).toEqual("yo");
|
|
164
|
+
expect(await render("/users/a")).toEqual("hey a");
|
|
165
|
+
expect(await render("/users/boom")).toEqual("Error: boom");
|
|
166
|
+
expect(await render("/users/new")).toEqual("users/new");
|
|
167
|
+
expect(await render("/users/hey/ho")).toEqual("404");
|
|
168
|
+
expect(await render("/users/a/profile")).toEqual("profile of a");
|
|
169
|
+
});
|