react-bun-ssr 0.3.2 → 0.4.1

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 CHANGED
@@ -38,11 +38,6 @@ Prerequisites:
38
38
  - Bun `>= 1.3.10`
39
39
  - `rbssr` available on PATH in the workflow you use to start a new app
40
40
 
41
- Try in browser:
42
-
43
- - StackBlitz (primary): https://stackblitz.com/github/react-formation/react-bun-ssr/tree/main/examples/sandbox-starter
44
- - CodeSandbox (fallback): https://codesandbox.io/s/github/react-formation/react-bun-ssr/tree/main/examples/sandbox-starter
45
-
46
41
  Minimal setup:
47
42
 
48
43
  ```bash
@@ -114,11 +109,50 @@ Read more:
114
109
 
115
110
  For a page request, the framework resolves the matching route, runs global and nested middleware, executes the matched loader or action, and then renders an HTML response or returns a direct `Response` when the route short-circuits. API routes use the same route tree and middleware model, but return handler responses instead of page HTML.
116
111
 
112
+ Under the hood, page HTML, API, internal action, and internal transition requests now share the same runtime request boundary, which keeps middleware, redirects, and response finalization behavior aligned across request kinds.
113
+
117
114
  Read more:
118
115
 
119
116
  - https://react-bun-ssr.dev/docs/routing/middleware
120
117
  - https://react-bun-ssr.dev/docs/data/loaders
121
118
 
119
+ ### Actions with React `useActionState`
120
+
121
+ Page mutations use React 19 form actions (`useActionState`) with an explicit route stub:
122
+
123
+ ```tsx
124
+ // app/routes/login.tsx
125
+ import { useActionState } from "react";
126
+ import { createRouteAction } from "react-bun-ssr/route";
127
+
128
+ type LoginState = { error?: string };
129
+ export const action = createRouteAction<LoginState>();
130
+
131
+ export default function LoginPage() {
132
+ const [state, formAction, pending] = useActionState(action, {});
133
+ return (
134
+ <form action={formAction}>
135
+ {state.error ? <p>{state.error}</p> : null}
136
+ <button disabled={pending}>Sign in</button>
137
+ </form>
138
+ );
139
+ }
140
+ ```
141
+
142
+ ```tsx
143
+ // app/routes/login.server.tsx
144
+ import { redirect } from "react-bun-ssr";
145
+ import type { Action } from "react-bun-ssr/route";
146
+
147
+ export const action: Action = async (ctx) => {
148
+ const email = String(ctx.formData?.get("email") ?? "").trim();
149
+ if (!email) return { error: "Email is required" };
150
+ return redirect("/dashboard");
151
+ };
152
+ ```
153
+
154
+ `createRouteAction` is the preferred pattern. `useRouteAction` remains available for backward compatibility.
155
+
122
156
  ### Rendering model
123
157
 
124
158
  SSR is the default model. HTML responses stream, deferred loader data is supported, and soft client transitions are handled through `Link` and `useRouter`. The docs site in this repository uses the same routing, rendering, markdown, and transition model that framework users get.
@@ -26,13 +26,20 @@ function isConfigFileName(fileName: string): boolean {
26
26
  }
27
27
 
28
28
  function isTopLevelAppRuntimeFile(relativePath: string): boolean {
29
- return /^root\.(tsx|jsx|ts|js)$/.test(relativePath) || /^middleware\.(tsx|jsx|ts|js)$/.test(relativePath);
29
+ return /^root(?:\.server)?\.(tsx|jsx|ts|js)$/.test(relativePath)
30
+ || /^middleware(?:\.server)?\.(tsx|jsx|ts|js)$/.test(relativePath);
30
31
  }
31
32
 
32
33
  function isMarkdownRouteFile(relativePath: string): boolean {
33
34
  return /^routes\/.+\.md$/.test(relativePath);
34
35
  }
35
36
 
37
+ function isServerOnlyRuntimeFile(relativePath: string): boolean {
38
+ return /^root\.server\.(tsx|jsx|ts|js)$/.test(relativePath)
39
+ || /^middleware\.server\.(tsx|jsx|ts|js)$/.test(relativePath)
40
+ || /^routes\/.+\.server\.(tsx|jsx|ts|js)$/.test(relativePath);
41
+ }
42
+
36
43
  function isStructuralAppPath(relativePath: string): boolean {
37
44
  return relativePath === "routes"
38
45
  || relativePath.startsWith("routes/")
@@ -265,11 +272,21 @@ export async function runHotDevChild(options: {
265
272
  return;
266
273
  }
267
274
 
275
+ if (eventType === "rename" && isServerOnlyRuntimeFile(relativePath)) {
276
+ publishReload("server-runtime");
277
+ return;
278
+ }
279
+
268
280
  if (eventType === "rename" && isStructuralAppPath(relativePath)) {
269
281
  scheduleStructuralSync();
270
282
  return;
271
283
  }
272
284
 
285
+ if (eventType === "change" && isServerOnlyRuntimeFile(relativePath)) {
286
+ publishReload("server-runtime");
287
+ return;
288
+ }
289
+
273
290
  if (eventType !== "change" || !isMarkdownRouteFile(relativePath)) {
274
291
  return;
275
292
  }
@@ -0,0 +1,26 @@
1
+ export type RouteActionStateHandler<TState = unknown> = (
2
+ previousState: TState,
3
+ formData: FormData,
4
+ ) => Promise<TState>;
5
+
6
+ const ROUTE_ACTION_STUB_MARKER = Symbol.for("react-bun-ssr.route-action-stub");
7
+
8
+ export function markRouteActionStub<TState>(
9
+ handler: RouteActionStateHandler<TState>,
10
+ ): RouteActionStateHandler<TState> {
11
+ Object.defineProperty(handler, ROUTE_ACTION_STUB_MARKER, {
12
+ value: true,
13
+ enumerable: false,
14
+ configurable: false,
15
+ writable: false,
16
+ });
17
+ return handler;
18
+ }
19
+
20
+ export function isRouteActionStub(value: unknown): value is RouteActionStateHandler<unknown> {
21
+ if (typeof value !== "function") {
22
+ return false;
23
+ }
24
+
25
+ return (value as unknown as Record<PropertyKey, unknown>)[ROUTE_ACTION_STUB_MARKER] === true;
26
+ }
@@ -1,15 +1,10 @@
1
1
  import { hydrateRoot, type Root } from "react-dom/client";
2
2
  import {
3
- consumeTransitionChunkText,
4
- createTransitionChunkParserState,
5
- flushTransitionChunkText,
6
3
  isStaleNavigationToken,
7
4
  matchClientPageRoute,
8
5
  sanitizePrefetchCache,
9
- shouldHardNavigateForRedirectDepth,
10
6
  shouldSkipSoftNavigation,
11
7
  } from "./client-transition-core";
12
- import { isDeferredToken } from "./deferred";
13
8
  import {
14
9
  addNavigationNavigateListener,
15
10
  canNavigationNavigateWithIntercept,
@@ -21,6 +16,12 @@ import {
21
16
  RBSSR_ROUTER_SCRIPT_ID,
22
17
  } from "./runtime-constants";
23
18
  import { replaceManagedHead } from "./head-reconcile";
19
+ import {
20
+ applyRouteWireDeferredChunk,
21
+ completeRouteWireTransition,
22
+ createRouteWireProtocol,
23
+ reviveRouteWirePayload,
24
+ } from "./route-wire-protocol";
24
25
  import {
25
26
  createCatchAppTree,
26
27
  createErrorAppTree,
@@ -34,7 +35,6 @@ import type {
34
35
  RenderPayload,
35
36
  RouteModule,
36
37
  RouteModuleBundle,
37
- TransitionDeferredChunk,
38
38
  TransitionDocumentChunk,
39
39
  TransitionInitialChunk,
40
40
  TransitionRedirectChunk,
@@ -73,16 +73,6 @@ interface PrefetchEntry {
73
73
  donePromise: Promise<void>;
74
74
  }
75
75
 
76
- interface TransitionRequestOptions {
77
- onDeferredChunk?: (chunk: TransitionDeferredChunk) => void;
78
- signal?: AbortSignal;
79
- }
80
-
81
- interface TransitionRequestHandle {
82
- initialPromise: Promise<TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null>;
83
- donePromise: Promise<void>;
84
- }
85
-
86
76
  interface FrameworkNavigationInfo {
87
77
  __rbssrTransition: true;
88
78
  id: string;
@@ -173,6 +163,28 @@ function getClientRuntimeSingleton(): ClientRuntimeSingleton {
173
163
 
174
164
  const clientRuntimeSingleton = getClientRuntimeSingleton();
175
165
 
166
+ function readCurrentWindowUrl(): URL | null {
167
+ if (typeof window === "undefined") {
168
+ return null;
169
+ }
170
+
171
+ return new URL(window.location.href);
172
+ }
173
+
174
+ function getClientRouteWireProtocol() {
175
+ return createRouteWireProtocol({
176
+ getCurrentUrl: readCurrentWindowUrl,
177
+ });
178
+ }
179
+
180
+ function getDeferredRuntime(): DeferredClientRuntime | undefined {
181
+ if (typeof window === "undefined") {
182
+ return undefined;
183
+ }
184
+
185
+ return window.__RBSSR_DEFERRED__;
186
+ }
187
+
176
188
  function emitNavigation(info: NavigateResult): void {
177
189
  for (const listener of clientRuntimeSingleton.navigationListeners) {
178
190
  try {
@@ -373,31 +385,6 @@ function findPendingTransitionForEvent(event: NavigateEventLike): PendingNavigat
373
385
  return bestMatch;
374
386
  }
375
387
 
376
- function reviveDeferredPayload(payload: RenderPayload): RenderPayload {
377
- const sourceData = payload.data;
378
- if (!sourceData || Array.isArray(sourceData) || typeof sourceData !== "object") {
379
- return payload;
380
- }
381
-
382
- const runtime = window.__RBSSR_DEFERRED__;
383
- if (!runtime) {
384
- return payload;
385
- }
386
-
387
- const revivedData = { ...(sourceData as Record<string, unknown>) };
388
- for (const [key, value] of Object.entries(revivedData)) {
389
- if (!isDeferredToken(value)) {
390
- continue;
391
- }
392
- revivedData[key] = runtime.get(value.__rbssrDeferred);
393
- }
394
-
395
- return {
396
- ...payload,
397
- data: revivedData,
398
- };
399
- }
400
-
401
388
  function ensureRuntimeState(): RuntimeState {
402
389
  if (!clientRuntimeSingleton.runtimeState) {
403
390
  throw new Error("Client runtime is not initialized. Ensure hydrateInitialRoute() ran first.");
@@ -406,106 +393,6 @@ function ensureRuntimeState(): RuntimeState {
406
393
  return clientRuntimeSingleton.runtimeState;
407
394
  }
408
395
 
409
- function createTransitionUrl(toUrl: URL): URL {
410
- const transitionUrl = new URL("/__rbssr/transition", window.location.origin);
411
- transitionUrl.searchParams.set("to", toUrl.pathname + toUrl.search + toUrl.hash);
412
- return transitionUrl;
413
- }
414
-
415
- function startTransitionRequest(
416
- toUrl: URL,
417
- options: TransitionRequestOptions = {},
418
- ): TransitionRequestHandle {
419
- let resolveInitial: (
420
- value: TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null,
421
- ) => void = () => undefined;
422
- let rejectInitial: (reason?: unknown) => void = () => undefined;
423
- const initialPromise = new Promise<
424
- TransitionInitialChunk | TransitionRedirectChunk | TransitionDocumentChunk | null
425
- >((resolve, reject) => {
426
- resolveInitial = resolve;
427
- rejectInitial = reject;
428
- });
429
-
430
- const donePromise = (async () => {
431
- const endpoint = createTransitionUrl(toUrl);
432
- const response = await fetch(endpoint.toString(), {
433
- method: "GET",
434
- credentials: "same-origin",
435
- signal: options.signal,
436
- });
437
-
438
- if (!response.ok || !response.body) {
439
- throw new Error(`Transition request failed with status ${response.status}`);
440
- }
441
-
442
- const reader = response.body.getReader();
443
- const decoder = new TextDecoder();
444
- let parserState = createTransitionChunkParserState();
445
-
446
- while (true) {
447
- const { done, value } = await reader.read();
448
- if (done) {
449
- break;
450
- }
451
-
452
- const previousInitialChunk = parserState.initialChunk;
453
- const previousDeferredCount = parserState.deferredChunks.length;
454
- parserState = consumeTransitionChunkText(
455
- parserState,
456
- decoder.decode(value, { stream: true }),
457
- );
458
-
459
- if (!previousInitialChunk && parserState.initialChunk) {
460
- resolveInitial(parserState.initialChunk);
461
- }
462
-
463
- for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
464
- options.onDeferredChunk?.(chunk);
465
- }
466
- }
467
-
468
- const previousInitialChunk = parserState.initialChunk;
469
- const previousDeferredCount = parserState.deferredChunks.length;
470
- parserState = flushTransitionChunkText(parserState);
471
-
472
- if (!previousInitialChunk && parserState.initialChunk) {
473
- resolveInitial(parserState.initialChunk);
474
- }
475
-
476
- for (const chunk of parserState.deferredChunks.slice(previousDeferredCount)) {
477
- options.onDeferredChunk?.(chunk);
478
- }
479
-
480
- if (!parserState.initialChunk) {
481
- resolveInitial(null);
482
- }
483
- })();
484
-
485
- donePromise.catch(error => {
486
- rejectInitial(error);
487
- });
488
-
489
- return {
490
- initialPromise,
491
- donePromise,
492
- };
493
- }
494
-
495
- function applyDeferredChunk(chunk: TransitionDeferredChunk): void {
496
- const runtime = window.__RBSSR_DEFERRED__;
497
- if (!runtime) {
498
- return;
499
- }
500
-
501
- if (chunk.ok) {
502
- runtime.resolve(chunk.id, chunk.value);
503
- return;
504
- }
505
-
506
- runtime.reject(chunk.id, chunk.error ?? "Deferred value rejected");
507
- }
508
-
509
396
  async function ensureRouteModuleLoaded(routeId: string, snapshot: ClientRouterSnapshot): Promise<void> {
510
397
  if (clientRuntimeSingleton.moduleRegistry.has(routeId)) {
511
398
  return;
@@ -538,8 +425,11 @@ function getOrCreatePrefetchEntry(
538
425
  ? ensureRouteModuleLoaded(routeId, snapshot).catch(() => undefined)
539
426
  : Promise.resolve();
540
427
 
541
- const transitionRequest = startTransitionRequest(toUrl, {
542
- onDeferredChunk: applyDeferredChunk,
428
+ const transitionRequest = getClientRouteWireProtocol().startTransition({
429
+ to: toUrl,
430
+ onDeferredChunk: chunk => {
431
+ applyRouteWireDeferredChunk(chunk, getDeferredRuntime());
432
+ },
543
433
  signal,
544
434
  });
545
435
  const initialPromise = transitionRequest.initialPromise.catch(() => {
@@ -574,7 +464,7 @@ async function renderTransitionInitial(
574
464
  options: NavigateOptions & { prefetched: boolean; fromPath: string },
575
465
  ): Promise<NavigateResult> {
576
466
  const state = ensureRuntimeState();
577
- const revivedPayload = reviveDeferredPayload(chunk.payload);
467
+ const revivedPayload = reviveRouteWirePayload(chunk.payload, getDeferredRuntime());
578
468
  let modules: RouteModuleBundle | null = null;
579
469
  let tree = null as ReturnType<typeof createPageAppTree> | ReturnType<typeof createNotFoundAppTree>;
580
470
 
@@ -713,7 +603,7 @@ async function navigateToInternal(
713
603
  matchedModules,
714
604
  {
715
605
  routeId: matched.route.id,
716
- data: null,
606
+ loaderData: null,
717
607
  params: matched.params,
718
608
  url: toUrl.toString(),
719
609
  },
@@ -733,43 +623,34 @@ async function navigateToInternal(
733
623
  throw new Error("Transition response did not include an initial payload.");
734
624
  }
735
625
 
736
- if (initialChunk.type === "document") {
737
- hardNavigate(new URL(initialChunk.location, window.location.origin));
738
- return null;
739
- }
740
-
741
- if (initialChunk.type === "redirect") {
742
- const redirectUrl = new URL(initialChunk.location, window.location.origin);
743
- if (!isInternalUrl(redirectUrl)) {
744
- hardNavigate(redirectUrl);
745
- return null;
746
- }
747
-
748
- const depth = (options.redirectDepth ?? 0) + 1;
749
- if (shouldHardNavigateForRedirectDepth(depth)) {
750
- hardNavigate(redirectUrl);
751
- return null;
752
- }
753
-
754
- return navigateToInternal(redirectUrl, {
755
- ...options,
756
- replace: true,
757
- redirected: true,
758
- redirectDepth: depth,
759
- // The intercepted navigation has already committed the source URL.
760
- // The redirected target must update history explicitly.
761
- historyManagedByNavigationApi: false,
762
- });
763
- }
764
-
765
- const result = await renderTransitionInitial(initialChunk, toUrl, {
766
- ...options,
767
- prefetched: usedPrefetch,
768
- fromPath: currentPath,
626
+ return completeRouteWireTransition(initialChunk, {
627
+ currentUrl: new URL(window.location.href),
628
+ redirectDepth: options.redirectDepth,
629
+ render: async chunk => {
630
+ const result = await renderTransitionInitial(chunk, toUrl, {
631
+ ...options,
632
+ prefetched: usedPrefetch,
633
+ fromPath: currentPath,
634
+ });
635
+ options.onNavigate?.(result);
636
+ emitNavigation(result);
637
+ return result;
638
+ },
639
+ softNavigate: async (location, redirectInfo) => {
640
+ return navigateToInternal(new URL(location, window.location.href), {
641
+ ...options,
642
+ replace: redirectInfo.replace,
643
+ redirected: redirectInfo.redirected,
644
+ redirectDepth: redirectInfo.redirectDepth,
645
+ // The intercepted navigation has already committed the source URL.
646
+ // The redirected target must update history explicitly.
647
+ historyManagedByNavigationApi: false,
648
+ });
649
+ },
650
+ hardNavigate: location => {
651
+ hardNavigate(new URL(location, window.location.href));
652
+ },
769
653
  });
770
- options.onNavigate?.(result);
771
- emitNavigation(result);
772
- return result;
773
654
  } catch {
774
655
  hardNavigate(toUrl);
775
656
  return null;
@@ -1060,7 +941,10 @@ export function hydrateInitialRoute(routeId: string): void {
1060
941
  return;
1061
942
  }
1062
943
 
1063
- const payload = reviveDeferredPayload(getScriptJson<RenderPayload>(RBSSR_PAYLOAD_SCRIPT_ID));
944
+ const payload = reviveRouteWirePayload(
945
+ getScriptJson<RenderPayload>(RBSSR_PAYLOAD_SCRIPT_ID),
946
+ getDeferredRuntime(),
947
+ );
1064
948
  const routerSnapshot = getScriptJson<ClientRouterSnapshot>(RBSSR_ROUTER_SCRIPT_ID);
1065
949
  const modules = clientRuntimeSingleton.moduleRegistry.get(routeId);
1066
950
  if (!modules) {
@@ -1,5 +1,6 @@
1
- import type { FrameworkConfig, RedirectResult } from "./types";
1
+ import type { FrameworkConfig, RedirectResult, RequestContext } from "./types";
2
2
  import { defer as deferValue } from "./deferred";
3
+ import { routeError } from "./route-errors";
3
4
 
4
5
  export function json(data: unknown, init: ResponseInit = {}): Response {
5
6
  const headers = new Headers(init.headers);
@@ -30,6 +31,79 @@ export function defineConfig(config: FrameworkConfig): FrameworkConfig {
30
31
 
31
32
  export const defer = deferValue;
32
33
 
34
+ function toNormalizedFallback(value: string): string {
35
+ const trimmed = value.trim();
36
+ if (!trimmed) {
37
+ return "/";
38
+ }
39
+ if (trimmed.startsWith("/")) {
40
+ return trimmed.startsWith("//") ? "/" : trimmed;
41
+ }
42
+ if (trimmed.startsWith("?") || trimmed.startsWith("#")) {
43
+ return `/${trimmed}`;
44
+ }
45
+ if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed)) {
46
+ return "/";
47
+ }
48
+ return `/${trimmed.replace(/^\/+/, "")}`;
49
+ }
50
+
51
+ export function sanitizeRedirectTarget(value: string | null | undefined, fallback = "/"): string {
52
+ const normalizedFallback = toNormalizedFallback(fallback);
53
+ if (typeof value !== "string") {
54
+ return normalizedFallback;
55
+ }
56
+
57
+ const trimmed = value.trim();
58
+ if (!trimmed) {
59
+ return normalizedFallback;
60
+ }
61
+
62
+ if (trimmed.startsWith("//") || trimmed.startsWith("\\\\")) {
63
+ return normalizedFallback;
64
+ }
65
+
66
+ let parsed: URL;
67
+ try {
68
+ parsed = new URL(trimmed, "http://rbssr.local");
69
+ } catch {
70
+ return normalizedFallback;
71
+ }
72
+
73
+ if (parsed.origin !== "http://rbssr.local") {
74
+ return normalizedFallback;
75
+ }
76
+
77
+ const normalizedTarget = `${parsed.pathname}${parsed.search}${parsed.hash}`;
78
+ if (!normalizedTarget.startsWith("/")) {
79
+ return normalizedFallback;
80
+ }
81
+
82
+ return normalizedTarget;
83
+ }
84
+
85
+ export function assertSameOriginAction(ctx: Pick<RequestContext, "request" | "url">): void {
86
+ if (ctx.request.method.toUpperCase() === "GET" || ctx.request.method.toUpperCase() === "HEAD") {
87
+ return;
88
+ }
89
+
90
+ const originHeader = ctx.request.headers.get("origin");
91
+ if (!originHeader) {
92
+ return;
93
+ }
94
+
95
+ let origin: URL;
96
+ try {
97
+ origin = new URL(originHeader);
98
+ } catch {
99
+ throw routeError(403, { message: "Invalid Origin header." });
100
+ }
101
+
102
+ if (origin.origin !== ctx.url.origin) {
103
+ throw routeError(403, { message: "Cross-origin form submissions are not allowed." });
104
+ }
105
+ }
106
+
33
107
  export function isRedirectResult(value: unknown): value is RedirectResult {
34
108
  return Boolean(
35
109
  value &&
@@ -1,29 +1,59 @@
1
- export type {
2
- Action,
3
- ActionContext,
4
- ActionResult,
5
- ApiRouteModule,
6
- BuildManifest,
7
- BuildRouteAsset,
8
- DeferredLoaderResult,
9
- DeferredToken,
10
- FrameworkConfig,
11
- Loader,
12
- LoaderContext,
13
- LoaderResult,
14
- Middleware,
15
- Params,
16
- RedirectResult,
17
- ResponseHeaderRule,
18
- RequestContext,
19
- RouteCatchContext,
20
- RouteErrorContext,
21
- RouteErrorResponse,
22
- RouteModule,
1
+ import type {
2
+ Action as RuntimeAction,
3
+ ActionContext as RuntimeActionContext,
4
+ ActionResult as RuntimeActionResult,
5
+ ApiRouteModule as RuntimeApiRouteModule,
6
+ BuildManifest as RuntimeBuildManifest,
7
+ BuildRouteAsset as RuntimeBuildRouteAsset,
8
+ DeferredLoaderResult as RuntimeDeferredLoaderResult,
9
+ DeferredToken as RuntimeDeferredToken,
10
+ FrameworkConfig as RuntimeFrameworkConfig,
11
+ Loader as RuntimeLoader,
12
+ LoaderContext as RuntimeLoaderContext,
13
+ LoaderResult as RuntimeLoaderResult,
14
+ Middleware as RuntimeMiddleware,
15
+ Params as RuntimeParams,
16
+ RedirectResult as RuntimeRedirectResult,
17
+ RequestContext as RuntimeRequestContext,
18
+ ResponseContext as RuntimeResponseContext,
19
+ ResponseCookies as RuntimeResponseCookies,
20
+ ResponseCookieOptions as RuntimeResponseCookieOptions,
21
+ ResponseHeaderRule as RuntimeResponseHeaderRule,
22
+ RouteCatchContext as RuntimeRouteCatchContext,
23
+ RouteErrorContext as RuntimeRouteErrorContext,
24
+ RouteErrorResponse as RuntimeRouteErrorResponse,
25
+ RouteModule as RuntimeRouteModule,
23
26
  } from "./types";
24
27
 
28
+ export interface AppRouteLocals extends Record<string, unknown> {}
29
+
30
+ export type Action = RuntimeAction<AppRouteLocals>;
31
+ export type ActionContext = RuntimeActionContext<AppRouteLocals>;
32
+ export type ActionResult = RuntimeActionResult;
33
+ export type ApiRouteModule = RuntimeApiRouteModule;
34
+ export type BuildManifest = RuntimeBuildManifest;
35
+ export type BuildRouteAsset = RuntimeBuildRouteAsset;
36
+ export type DeferredLoaderResult = RuntimeDeferredLoaderResult;
37
+ export type DeferredToken = RuntimeDeferredToken;
38
+ export type FrameworkConfig = RuntimeFrameworkConfig;
39
+ export type Loader = RuntimeLoader<AppRouteLocals>;
40
+ export type LoaderContext = RuntimeLoaderContext<AppRouteLocals>;
41
+ export type LoaderResult = RuntimeLoaderResult;
42
+ export type Middleware = RuntimeMiddleware<AppRouteLocals>;
43
+ export type Params = RuntimeParams;
44
+ export type RedirectResult = RuntimeRedirectResult;
45
+ export type RequestContext = RuntimeRequestContext<AppRouteLocals>;
46
+ export type ResponseContext = RuntimeResponseContext;
47
+ export type ResponseCookies = RuntimeResponseCookies;
48
+ export type ResponseCookieOptions = RuntimeResponseCookieOptions;
49
+ export type ResponseHeaderRule = RuntimeResponseHeaderRule;
50
+ export type RouteCatchContext = RuntimeRouteCatchContext;
51
+ export type RouteErrorContext = RuntimeRouteErrorContext;
52
+ export type RouteErrorResponse = RuntimeRouteErrorResponse;
53
+ export type RouteModule = RuntimeRouteModule;
54
+
25
55
  export { createServer, startHttpServer } from "./server";
26
- export { defer, json, redirect, defineConfig } from "./helpers";
56
+ export { assertSameOriginAction, defer, defineConfig, json, redirect, sanitizeRedirectTarget } from "./helpers";
27
57
  export { isRouteErrorResponse, notFound, routeError } from "./route-errors";
28
58
  export { Link, type LinkProps } from "./link";
29
59
  export { useRouter, type Router, type RouterNavigateInfo, type RouterNavigateListener, type RouterNavigateOptions } from "./router";