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.
@@ -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 { Params, RenderPayload, RouteErrorResponse, RouteModuleBundle } from "./types";
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
- data: unknown;
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().data as T;
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
- data: payload.data,
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 RequestContext {
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: Record<string, unknown>;
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 = (ctx: LoaderContext) => Promise<LoaderResult> | LoaderResult;
41
- export type Action = (ctx: ActionContext) => Promise<ActionResult> | ActionResult;
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 = (ctx: RequestContext) => Promise<Response | unknown> | Response | unknown;
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
- data: unknown;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-bun-ssr",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,