@alepha/react 0.14.4 → 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.d.ts +4 -4
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +22 -31
- 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.d.ts +30 -40
- package/dist/head/index.d.ts.map +1 -1
- package/dist/i18n/index.d.ts +33 -33
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/router/index.d.ts +458 -458
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +22 -31
- 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 +4 -4
- package/src/auth/__tests__/$auth.spec.ts +10 -11
- package/src/router/providers/ReactServerProvider.ts +3 -13
- package/src/router/providers/ReactServerTemplateProvider.ts +47 -29
|
@@ -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,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.
|
|
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": {
|
|
@@ -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
|
});
|
|
@@ -67,10 +67,6 @@ export class ReactServerProvider {
|
|
|
67
67
|
|
|
68
68
|
this.alepha.store.set("alepha.react.server.ssr", ssrEnabled);
|
|
69
69
|
|
|
70
|
-
if (ssrEnabled) {
|
|
71
|
-
this.log.info("SSR streaming enabled");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
70
|
// development mode
|
|
75
71
|
if (this.alepha.isViteDev()) {
|
|
76
72
|
await this.configureVite(ssrEnabled);
|
|
@@ -338,9 +334,9 @@ export class ReactServerProvider {
|
|
|
338
334
|
const result = await this.renderPage(route, state);
|
|
339
335
|
|
|
340
336
|
if (result.redirect) {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
return
|
|
337
|
+
// Return redirect URL - template provider will inject meta refresh
|
|
338
|
+
// since HTTP headers have already been sent
|
|
339
|
+
return { redirect: result.redirect };
|
|
344
340
|
}
|
|
345
341
|
|
|
346
342
|
return { state, reactStream: result.reactStream! };
|
|
@@ -389,10 +385,7 @@ export class ReactServerProvider {
|
|
|
389
385
|
state: ReactRouterState,
|
|
390
386
|
): Promise<{ redirect?: string; reactStream?: ReadableStream<Uint8Array> }> {
|
|
391
387
|
// Resolve page layers (loaders)
|
|
392
|
-
this.serverTimingProvider.beginTiming("createLayers");
|
|
393
388
|
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
394
|
-
this.serverTimingProvider.endTiming("createLayers");
|
|
395
|
-
|
|
396
389
|
if (redirect) {
|
|
397
390
|
this.log.debug("Resolver resulted in redirection", { redirect });
|
|
398
391
|
return { redirect };
|
|
@@ -409,7 +402,6 @@ export class ReactServerProvider {
|
|
|
409
402
|
}
|
|
410
403
|
|
|
411
404
|
// Render React to stream
|
|
412
|
-
this.serverTimingProvider.beginTiming("renderToStream");
|
|
413
405
|
|
|
414
406
|
const element = this.pageApi.root(state);
|
|
415
407
|
this.alepha.store.set("alepha.react.router.state", state);
|
|
@@ -426,8 +418,6 @@ export class ReactServerProvider {
|
|
|
426
418
|
},
|
|
427
419
|
});
|
|
428
420
|
|
|
429
|
-
this.serverTimingProvider.endTiming("renderToStream");
|
|
430
|
-
|
|
431
421
|
return { reactStream };
|
|
432
422
|
}
|
|
433
423
|
|
|
@@ -392,26 +392,31 @@ export class ReactServerTemplateProvider {
|
|
|
392
392
|
const { request, context, ...store } =
|
|
393
393
|
this.alepha.context.als?.getStore() ?? {};
|
|
394
394
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
395
|
+
const layers = state.layers.map((layer) => ({
|
|
396
|
+
name: layer.name,
|
|
397
|
+
props: layer.props,
|
|
398
|
+
config: layer.config,
|
|
399
|
+
error: layer.error
|
|
400
|
+
? {
|
|
401
|
+
...layer.error,
|
|
402
|
+
name: layer.error.name,
|
|
403
|
+
message: layer.error.message,
|
|
404
|
+
stack: !this.alepha.isProduction() ? layer.error.stack : undefined,
|
|
405
|
+
}
|
|
406
|
+
: undefined,
|
|
407
|
+
}));
|
|
408
|
+
|
|
409
|
+
const hydrationData: HydrationData = {
|
|
410
|
+
layers,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const [key, value] of Object.entries(store)) {
|
|
414
|
+
if (key.charAt(0) !== "_" && key !== "alepha.react.router.state" && key !== "registry") {
|
|
415
|
+
hydrationData[key] = value;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return hydrationData;
|
|
415
420
|
}
|
|
416
421
|
|
|
417
422
|
/**
|
|
@@ -613,17 +618,20 @@ export class ReactServerTemplateProvider {
|
|
|
613
618
|
*/
|
|
614
619
|
public createEarlyHtmlStream(
|
|
615
620
|
globalHead: SimpleHead,
|
|
616
|
-
asyncWork: () => Promise<
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
asyncWork: () => Promise<
|
|
622
|
+
| {
|
|
623
|
+
state: ReactRouterState;
|
|
624
|
+
reactStream: ReadableStream<Uint8Array>;
|
|
625
|
+
}
|
|
626
|
+
| { redirect: string }
|
|
627
|
+
| null
|
|
628
|
+
>,
|
|
620
629
|
options: {
|
|
621
630
|
hydration?: boolean;
|
|
622
631
|
onError?: (error: unknown) => void;
|
|
623
|
-
onRedirect?: (url: string) => void;
|
|
624
632
|
} = {},
|
|
625
633
|
): ReadableStream<Uint8Array> {
|
|
626
|
-
const { hydration = true, onError
|
|
634
|
+
const { hydration = true, onError } = options;
|
|
627
635
|
const slots = this.getSlots();
|
|
628
636
|
const encoder = this.encoder;
|
|
629
637
|
|
|
@@ -653,9 +661,19 @@ export class ReactServerTemplateProvider {
|
|
|
653
661
|
// === ASYNC WORK (createLayers, etc.) ===
|
|
654
662
|
const result = await asyncWork();
|
|
655
663
|
|
|
656
|
-
// Handle redirect -
|
|
657
|
-
if (!result) {
|
|
658
|
-
|
|
664
|
+
// Handle redirect - inject meta refresh since headers already sent
|
|
665
|
+
if (!result || "redirect" in result) {
|
|
666
|
+
if (result && "redirect" in result) {
|
|
667
|
+
this.log.debug(
|
|
668
|
+
"Loader redirect detected after streaming started, using meta refresh",
|
|
669
|
+
{ redirect: result.redirect },
|
|
670
|
+
);
|
|
671
|
+
controller.enqueue(
|
|
672
|
+
encoder.encode(
|
|
673
|
+
`<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`,
|
|
674
|
+
),
|
|
675
|
+
);
|
|
676
|
+
}
|
|
659
677
|
controller.enqueue(slots.headClose);
|
|
660
678
|
controller.enqueue(encoder.encode("<body></body></html>"));
|
|
661
679
|
controller.close();
|