react-bun-ssr 0.3.2 → 0.4.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 +37 -5
- package/framework/cli/dev-runtime.ts +18 -1
- package/framework/runtime/action-stub.ts +26 -0
- package/framework/runtime/client-runtime.tsx +3 -3
- package/framework/runtime/helpers.ts +75 -1
- package/framework/runtime/index.ts +53 -23
- package/framework/runtime/module-loader.ts +197 -35
- package/framework/runtime/render.tsx +1 -1
- package/framework/runtime/response-context.ts +206 -0
- package/framework/runtime/route-api.ts +51 -18
- package/framework/runtime/route-scanner.ts +104 -12
- package/framework/runtime/server.ts +427 -91
- package/framework/runtime/tree.tsx +120 -4
- package/framework/runtime/types.ts +71 -10
- package/package.json +1 -1
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createContext,
|
|
3
|
+
useCallback,
|
|
3
4
|
useContext,
|
|
4
5
|
type ComponentType,
|
|
5
6
|
type Context,
|
|
6
7
|
type ReactElement,
|
|
7
8
|
type ReactNode,
|
|
8
9
|
} from "react";
|
|
9
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
ActionResponseEnvelope,
|
|
12
|
+
Params,
|
|
13
|
+
RenderPayload,
|
|
14
|
+
RouteErrorResponse,
|
|
15
|
+
RouteModuleBundle,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import { markRouteActionStub, type RouteActionStateHandler } from "./action-stub";
|
|
10
18
|
|
|
11
19
|
interface RuntimeState {
|
|
12
|
-
|
|
20
|
+
loaderData: unknown;
|
|
13
21
|
params: Params;
|
|
14
22
|
url: URL;
|
|
15
23
|
error?: unknown;
|
|
@@ -50,7 +58,115 @@ function useRuntimeState(): RuntimeState {
|
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
export function useLoaderData<T = unknown>(): T {
|
|
53
|
-
return useRuntimeState().
|
|
61
|
+
return useRuntimeState().loaderData as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isActionResponseEnvelope(value: unknown): value is ActionResponseEnvelope {
|
|
65
|
+
if (!value || typeof value !== "object") {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const candidate = value as {
|
|
70
|
+
type?: unknown;
|
|
71
|
+
status?: unknown;
|
|
72
|
+
};
|
|
73
|
+
return typeof candidate.type === "string" && typeof candidate.status === "number";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handleActionRedirect(location: string): Promise<void> {
|
|
77
|
+
if (typeof window === "undefined") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const redirectUrl = new URL(location, window.location.href);
|
|
82
|
+
if (redirectUrl.origin !== window.location.origin) {
|
|
83
|
+
window.location.assign(redirectUrl.toString());
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const runtime = await import("./client-runtime");
|
|
89
|
+
await runtime.navigateWithNavigationApiOrFallback(redirectUrl.toString(), {
|
|
90
|
+
replace: true,
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
window.location.assign(redirectUrl.toString());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function submitRouteAction<TState>(options: {
|
|
98
|
+
previousState: TState;
|
|
99
|
+
formData: FormData;
|
|
100
|
+
routeTarget: string;
|
|
101
|
+
}): Promise<TState> {
|
|
102
|
+
if (typeof window === "undefined") {
|
|
103
|
+
return options.previousState;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const endpoint = new URL("/__rbssr/action", window.location.origin);
|
|
107
|
+
endpoint.searchParams.set("to", options.routeTarget);
|
|
108
|
+
|
|
109
|
+
const response = await fetch(endpoint.toString(), {
|
|
110
|
+
method: "POST",
|
|
111
|
+
body: options.formData,
|
|
112
|
+
credentials: "same-origin",
|
|
113
|
+
headers: {
|
|
114
|
+
accept: "application/json",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
let payload: unknown;
|
|
119
|
+
try {
|
|
120
|
+
payload = await response.json();
|
|
121
|
+
} catch {
|
|
122
|
+
throw new Error("Action endpoint returned a non-JSON response.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!isActionResponseEnvelope(payload)) {
|
|
126
|
+
throw new Error("Action endpoint returned an invalid envelope.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (payload.type === "data") {
|
|
130
|
+
return payload.data as TState;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (payload.type === "redirect") {
|
|
134
|
+
await handleActionRedirect(payload.location);
|
|
135
|
+
return options.previousState;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (payload.type === "catch") {
|
|
139
|
+
throw payload.error;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(payload.message);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createRouteAction<TState = unknown>(): RouteActionStateHandler<TState> {
|
|
146
|
+
return markRouteActionStub(async (previousState: TState, formData: FormData) => {
|
|
147
|
+
const routeTarget = typeof window === "undefined"
|
|
148
|
+
? "/"
|
|
149
|
+
: window.location.pathname + window.location.search + window.location.hash;
|
|
150
|
+
|
|
151
|
+
return submitRouteAction({
|
|
152
|
+
previousState,
|
|
153
|
+
formData,
|
|
154
|
+
routeTarget,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function useRouteAction<TState = unknown>(): RouteActionStateHandler<TState> {
|
|
160
|
+
const requestUrl = useRequestUrl();
|
|
161
|
+
const routeTarget = requestUrl.pathname + requestUrl.search + requestUrl.hash;
|
|
162
|
+
|
|
163
|
+
return useCallback((previousState: TState, formData: FormData) => {
|
|
164
|
+
return submitRouteAction({
|
|
165
|
+
previousState,
|
|
166
|
+
formData,
|
|
167
|
+
routeTarget,
|
|
168
|
+
});
|
|
169
|
+
}, [routeTarget]);
|
|
54
170
|
}
|
|
55
171
|
|
|
56
172
|
export function useParams<T extends Params = Params>(): T {
|
|
@@ -83,7 +199,7 @@ export function createRouteTree(
|
|
|
83
199
|
} = {},
|
|
84
200
|
): ReactElement {
|
|
85
201
|
const runtimeState: RuntimeState = {
|
|
86
|
-
|
|
202
|
+
loaderData: payload.loaderData,
|
|
87
203
|
params: payload.params,
|
|
88
204
|
url: new URL(payload.url),
|
|
89
205
|
error: options.error ?? payload.error,
|
|
@@ -2,17 +2,41 @@ import type { ComponentType, ReactNode } from "react";
|
|
|
2
2
|
|
|
3
3
|
export type Params = Record<string, string>;
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface AppRouteLocals extends Record<string, unknown> {}
|
|
6
|
+
|
|
7
|
+
export interface ResponseCookieOptions {
|
|
8
|
+
path?: string;
|
|
9
|
+
domain?: string;
|
|
10
|
+
expires?: Date | string;
|
|
11
|
+
maxAge?: number;
|
|
12
|
+
secure?: boolean;
|
|
13
|
+
httpOnly?: boolean;
|
|
14
|
+
sameSite?: "Strict" | "Lax" | "None" | "strict" | "lax" | "none";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ResponseCookies {
|
|
18
|
+
get(name: string): string | undefined;
|
|
19
|
+
set(name: string, value: string, options?: ResponseCookieOptions): void;
|
|
20
|
+
delete(name: string, options?: Omit<ResponseCookieOptions, "expires" | "maxAge">): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ResponseContext {
|
|
24
|
+
headers: Pick<Headers, "set" | "append" | "delete" | "get" | "has">;
|
|
25
|
+
cookies: ResponseCookies;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RequestContext<Locals extends Record<string, unknown> = AppRouteLocals> {
|
|
6
29
|
request: Request;
|
|
7
30
|
url: URL;
|
|
8
31
|
params: Params;
|
|
9
32
|
cookies: Map<string, string>;
|
|
10
|
-
locals:
|
|
33
|
+
locals: Locals;
|
|
34
|
+
response: ResponseContext;
|
|
11
35
|
}
|
|
12
36
|
|
|
13
|
-
export interface LoaderContext extends RequestContext {}
|
|
37
|
+
export interface LoaderContext<Locals extends Record<string, unknown> = AppRouteLocals> extends RequestContext<Locals> {}
|
|
14
38
|
|
|
15
|
-
export interface ActionContext extends RequestContext {
|
|
39
|
+
export interface ActionContext<Locals extends Record<string, unknown> = AppRouteLocals> extends RequestContext<Locals> {
|
|
16
40
|
formData?: FormData;
|
|
17
41
|
json?: unknown;
|
|
18
42
|
}
|
|
@@ -37,8 +61,12 @@ export type LoaderResult =
|
|
|
37
61
|
| null;
|
|
38
62
|
export type ActionResult = LoaderResult | RedirectResult;
|
|
39
63
|
|
|
40
|
-
export type Loader
|
|
41
|
-
|
|
64
|
+
export type Loader<Locals extends Record<string, unknown> = AppRouteLocals> = (
|
|
65
|
+
ctx: LoaderContext<Locals>,
|
|
66
|
+
) => Promise<LoaderResult> | LoaderResult;
|
|
67
|
+
export type Action<Locals extends Record<string, unknown> = AppRouteLocals> = (
|
|
68
|
+
ctx: ActionContext<Locals>,
|
|
69
|
+
) => Promise<ActionResult> | ActionResult;
|
|
42
70
|
|
|
43
71
|
export interface RedirectResult {
|
|
44
72
|
type: "redirect";
|
|
@@ -46,8 +74,8 @@ export interface RedirectResult {
|
|
|
46
74
|
status?: 301 | 302 | 303 | 307 | 308;
|
|
47
75
|
}
|
|
48
76
|
|
|
49
|
-
export type Middleware = (
|
|
50
|
-
ctx: RequestContext
|
|
77
|
+
export type Middleware<Locals extends Record<string, unknown> = AppRouteLocals> = (
|
|
78
|
+
ctx: RequestContext<Locals>,
|
|
51
79
|
next: () => Promise<Response>,
|
|
52
80
|
) => Promise<Response> | Response;
|
|
53
81
|
|
|
@@ -109,7 +137,9 @@ export interface RouteModule {
|
|
|
109
137
|
|
|
110
138
|
export type LayoutModule = RouteModule;
|
|
111
139
|
|
|
112
|
-
export type ApiHandler
|
|
140
|
+
export type ApiHandler<Locals extends Record<string, unknown> = AppRouteLocals> = (
|
|
141
|
+
ctx: RequestContext<Locals>,
|
|
142
|
+
) => Promise<Response | unknown> | Response | unknown;
|
|
113
143
|
|
|
114
144
|
export interface ApiRouteModule {
|
|
115
145
|
GET?: ApiHandler;
|
|
@@ -170,6 +200,7 @@ export interface PageRouteDefinition {
|
|
|
170
200
|
type: "page";
|
|
171
201
|
id: string;
|
|
172
202
|
filePath: string;
|
|
203
|
+
serverFilePath?: string;
|
|
173
204
|
routePath: string;
|
|
174
205
|
segments: RouteSegment[];
|
|
175
206
|
score: number;
|
|
@@ -212,12 +243,42 @@ export interface BuildManifest {
|
|
|
212
243
|
|
|
213
244
|
export interface RenderPayload {
|
|
214
245
|
routeId: string;
|
|
215
|
-
|
|
246
|
+
loaderData: unknown;
|
|
216
247
|
params: Params;
|
|
217
248
|
url: string;
|
|
218
249
|
error?: unknown;
|
|
219
250
|
}
|
|
220
251
|
|
|
252
|
+
export interface ActionDataEnvelope {
|
|
253
|
+
type: "data";
|
|
254
|
+
status: number;
|
|
255
|
+
data: unknown;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface ActionRedirectEnvelope {
|
|
259
|
+
type: "redirect";
|
|
260
|
+
status: number;
|
|
261
|
+
location: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface ActionCatchEnvelope {
|
|
265
|
+
type: "catch";
|
|
266
|
+
status: number;
|
|
267
|
+
error: RouteErrorResponse;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface ActionErrorEnvelope {
|
|
271
|
+
type: "error";
|
|
272
|
+
status: number;
|
|
273
|
+
message: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export type ActionResponseEnvelope =
|
|
277
|
+
| ActionDataEnvelope
|
|
278
|
+
| ActionRedirectEnvelope
|
|
279
|
+
| ActionCatchEnvelope
|
|
280
|
+
| ActionErrorEnvelope;
|
|
281
|
+
|
|
221
282
|
export interface ClientRouteSnapshot {
|
|
222
283
|
id: string;
|
|
223
284
|
routePath: string;
|