@bractjs/bractjs 0.1.26 → 0.1.28

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.
Files changed (98) hide show
  1. package/README.md +283 -58
  2. package/bin/cli.ts +18 -1
  3. package/package.json +1 -1
  4. package/src/__tests__/build-path.test.ts +29 -0
  5. package/src/__tests__/codegen-write.test.ts +67 -0
  6. package/src/__tests__/codegen.test.ts +64 -1
  7. package/src/__tests__/compile-safety.test.ts +4 -0
  8. package/src/__tests__/csp.test.ts +10 -0
  9. package/src/__tests__/define-actions.test.ts +69 -0
  10. package/src/__tests__/env.test.ts +18 -0
  11. package/src/__tests__/fetcher-store.test.ts +67 -0
  12. package/src/__tests__/fixtures/app/root.tsx +7 -2
  13. package/src/__tests__/fixtures/app/routes/boom.tsx +9 -0
  14. package/src/__tests__/fixtures/app/routes/client-only.tsx +16 -0
  15. package/src/__tests__/fixtures/app/routes/counter.tsx +16 -0
  16. package/src/__tests__/fixtures/app/routes/data-only.tsx +16 -0
  17. package/src/__tests__/fixtures/app/routes/intent-demo.tsx +46 -0
  18. package/src/__tests__/fixtures/app/routes/protected-client-only.tsx +15 -0
  19. package/src/__tests__/fixtures/app/routes/search-demo.tsx +39 -0
  20. package/src/__tests__/form-data-helpers.test.ts +43 -0
  21. package/src/__tests__/integration.test.ts +56 -0
  22. package/src/__tests__/loader.test.ts +32 -1
  23. package/src/__tests__/nav-utils.test.ts +46 -0
  24. package/src/__tests__/prerender.test.ts +102 -0
  25. package/src/__tests__/programmatic-api.test.ts +20 -1
  26. package/src/__tests__/revalidation.test.ts +65 -0
  27. package/src/__tests__/route-lint.test.ts +74 -0
  28. package/src/__tests__/route-table.test.ts +33 -0
  29. package/src/__tests__/safe-validate.test.ts +96 -0
  30. package/src/__tests__/scroll-restoration.test.ts +66 -0
  31. package/src/__tests__/search-serializer.test.ts +42 -0
  32. package/src/__tests__/search-validation.test.ts +125 -0
  33. package/src/__tests__/security.test.ts +110 -1
  34. package/src/__tests__/selective-ssr.test.ts +85 -0
  35. package/src/__tests__/spa-mode.test.ts +77 -0
  36. package/src/__tests__/typed-routing.test.ts +239 -0
  37. package/src/build/bundler.ts +33 -0
  38. package/src/build/prerender.ts +88 -0
  39. package/src/build/route-lint.ts +49 -0
  40. package/src/client/ClientRouter.tsx +239 -47
  41. package/src/client/build-path.ts +24 -0
  42. package/src/client/cache.ts +8 -0
  43. package/src/client/components/Await.tsx +9 -2
  44. package/src/client/components/Form.tsx +23 -34
  45. package/src/client/components/Link.tsx +105 -11
  46. package/src/client/components/Outlet.tsx +8 -2
  47. package/src/client/components/ScrollRestoration.tsx +125 -0
  48. package/src/client/entry.tsx +39 -2
  49. package/src/client/fetcher-store.ts +61 -0
  50. package/src/client/form-utils.ts +3 -0
  51. package/src/client/hooks/useActionData.ts +7 -3
  52. package/src/client/hooks/useFetcher.ts +116 -33
  53. package/src/client/hooks/useFetchers.ts +23 -0
  54. package/src/client/hooks/useLoaderData.ts +8 -4
  55. package/src/client/hooks/useLocation.ts +27 -0
  56. package/src/client/hooks/useNavigate.ts +51 -0
  57. package/src/client/hooks/useParams.ts +15 -4
  58. package/src/client/hooks/useRevalidator.ts +26 -0
  59. package/src/client/hooks/useSearch.ts +73 -0
  60. package/src/client/hooks/useSearchParams.ts +21 -6
  61. package/src/client/nav-utils.ts +26 -0
  62. package/src/client/prefetch.ts +110 -15
  63. package/src/client/registry.ts +131 -0
  64. package/src/client/revalidation.ts +25 -0
  65. package/src/client/router.tsx +28 -1
  66. package/src/client/scroll-restoration.ts +48 -0
  67. package/src/client/search-serializer.ts +40 -0
  68. package/src/client/types.ts +6 -0
  69. package/src/codegen/route-codegen.ts +201 -29
  70. package/src/config/load.ts +21 -0
  71. package/src/dev/hmr-client.ts +3 -1
  72. package/src/dev/route-table.ts +27 -0
  73. package/src/dev/server.ts +106 -8
  74. package/src/dev/watcher.ts +25 -3
  75. package/src/index.ts +44 -3
  76. package/src/server/action-handler.ts +12 -3
  77. package/src/server/action-registry.ts +35 -0
  78. package/src/server/csp.ts +10 -1
  79. package/src/server/csrf.ts +26 -0
  80. package/src/server/env.ts +26 -5
  81. package/src/server/layout.ts +31 -1
  82. package/src/server/loader.ts +14 -8
  83. package/src/server/render.ts +18 -3
  84. package/src/server/request-handler.ts +50 -8
  85. package/src/server/search.ts +43 -0
  86. package/src/server/serve.ts +88 -1
  87. package/src/server/spa.ts +62 -0
  88. package/src/server/stream-handler.ts +10 -1
  89. package/src/server/validate.ts +85 -13
  90. package/src/shared/context.ts +5 -0
  91. package/src/shared/define-actions.ts +39 -0
  92. package/src/shared/form-data.ts +34 -0
  93. package/src/shared/route-types.ts +83 -2
  94. package/templates/new-app/app/root.tsx +2 -1
  95. package/templates/new-app/bractjs.config.ts +7 -12
  96. package/types/config.d.ts +21 -0
  97. package/types/index.d.ts +210 -10
  98. package/types/route.d.ts +62 -2
package/types/index.d.ts CHANGED
@@ -4,8 +4,11 @@ import type { ReactNode, Context, CSSProperties } from "react";
4
4
  export type {
5
5
  LoaderArgs, ActionArgs, MetaDescriptor, MetaArgs,
6
6
  LoaderFunction, ActionFunction, MetaFunction, RouteModule,
7
- RouteFile, Segment,
7
+ RouteFile, Segment, RouterLocation,
8
+ ShouldRevalidateArgs, ShouldRevalidateFunction,
9
+ LoaderData, ActionData,
8
10
  } from "./route.d.ts";
11
+ import type { RouterLocation, LoaderData, ActionData, ActionArgs } from "./route.d.ts";
9
12
 
10
13
  // ── Config + Server ───────────────────────────────────────────────────────
11
14
  export type { BractJSConfig, ServerManifest, BuildConfig } from "./config.d.ts";
@@ -18,9 +21,15 @@ export interface RenderOptions {
18
21
  actionData: unknown;
19
22
  params: Record<string, string>;
20
23
  pathname: string;
24
+ /** Validated search params — hydrates `useSearch()` on the client. */
25
+ search?: Record<string, unknown>;
21
26
  manifest: ServerManifest;
22
27
  meta: MetaDescriptor[];
23
28
  status?: number;
29
+ routeFile?: string;
30
+ nonce?: string;
31
+ /** Set when the document did not SSR the route component (selective SSR / SPA shell). */
32
+ ssrMode?: "client-only" | "data-only" | "spa";
24
33
  }
25
34
 
26
35
  export type OnErrorHook = (err: unknown, request?: Request) => Promise<void> | void;
@@ -64,6 +73,10 @@ export interface BractJSContextValue {
64
73
  params: Record<string, string>;
65
74
  pathname: string;
66
75
  manifest: RouteManifest;
76
+ /** The request's location, so `useLocation()` works during SSR (hash is always ""). */
77
+ location?: RouterLocation;
78
+ /** Validated search params, so `useSearch()` works during SSR. */
79
+ search?: Record<string, unknown>;
67
80
  }
68
81
  export declare const BractJSContext: Context<BractJSContextValue>;
69
82
  export declare function BractJSProvider(props: { value: BractJSContextValue; children: ReactNode }): ReactNode;
@@ -129,18 +142,116 @@ export declare function validate<T>(
129
142
  input: FormData | Record<string, unknown>,
130
143
  ): Promise<T>;
131
144
 
145
+ export type SafeValidateResult<T> =
146
+ | { ok: true; data: T }
147
+ | { ok: false; fieldErrors: FieldErrors; firstError: string };
148
+ /** Non-throwing validate(): returns a result instead of throwing a 400. */
149
+ export declare function safeValidate<T>(
150
+ schema: { safeParse?(i: unknown): unknown } | { parse(i: unknown): T },
151
+ input: FormData | Record<string, unknown>,
152
+ ): Promise<SafeValidateResult<T>>;
153
+ /** True for the 400 Response thrown by validate()/searchSchema validation. */
154
+ export declare function isValidationResponse(value: unknown): value is Response;
155
+ /** Parse the `{ errors }` body of a validation 400 into field errors + first message. */
156
+ export declare function readValidationError(
157
+ res: Response,
158
+ ): Promise<{ fieldErrors: FieldErrors; firstError: string }>;
159
+
160
+ // ── FormData helpers ──────────────────────────────────────────────────────
161
+ /** String field from FormData; "" when missing or a File. */
162
+ export declare function formText(formData: FormData, key: string): string;
163
+ /** Collect string fields from FormData (all, or a named subset). */
164
+ export declare function formValues(formData: FormData, keys?: string[]): Record<string, string>;
165
+
166
+ // ── Search-param validation ───────────────────────────────────────────────
167
+ /** URLSearchParams → plain object; repeated keys collapse into arrays. */
168
+ export declare function searchParamsToObject(sp: URLSearchParams): Record<string, string | string[]>;
169
+ /**
170
+ * Validate a URL's search params against a route's `searchSchema`. No schema →
171
+ * the raw string record; failure → throws a 400 Response with field errors.
172
+ */
173
+ export declare function validateSearch(schema: unknown, url: URL): Promise<Record<string, unknown>>;
174
+ /** Serialize a search object back into a query string (leading `?`, or ""). */
175
+ export declare function serializeSearch(search: Record<string, unknown>): string;
176
+
177
+ // ── Typed-routing registration seam ───────────────────────────────────────
178
+ // Mirror of src/client/registry.ts. Augment `Register` (done by `bractjs codegen`
179
+ // in app/route-types.gen.ts) to make <Link>/useNavigate/useParams/useSearchParams
180
+ // type-safe. Un-augmented, everything falls back to loose `string` / Record so
181
+ // apps that never run codegen keep compiling. Keep in sync with registry.ts.
182
+ export interface Register {}
183
+ export interface RouteRegistry {
184
+ routes: string;
185
+ params: Record<string, Record<string, string>>;
186
+ search: Record<string, Record<string, string>>;
187
+ }
188
+ export interface RouteSearchParamsMap {}
189
+ export interface RouteContextMap {}
190
+ // Infer each member directly (NOT `infer R extends RouteRegistry` — a constrained
191
+ // infer fails to match the generated registry and falls back to loose). Keep in
192
+ // sync with src/client/registry.ts.
193
+ export type RegisteredRoutes =
194
+ Register extends { routes: { routes: infer R } } ? R : string;
195
+ export type RegisteredParamsMap =
196
+ Register extends { routes: { params: infer P } } ? P : Record<string, Record<string, string>>;
197
+ export type RegisteredSearchMap =
198
+ Register extends { routes: { search: infer S } } ? S : Record<string, Record<string, string>>;
199
+ export type RegisteredSearchOutputMap =
200
+ Register extends { routes: { searchOutput: infer S } } ? S : Record<string, Record<string, unknown>>;
201
+ export type ParamsFor<TTo> =
202
+ TTo extends keyof RegisteredParamsMap ? RegisteredParamsMap[TTo] : Record<string, string>;
203
+ export type SearchFor<TTo> =
204
+ TTo extends keyof RegisteredSearchMap ? RegisteredSearchMap[TTo] : Record<string, string>;
205
+ /** Validated (schema-output) search object for a specific route literal. */
206
+ export type SearchOutputFor<TTo> =
207
+ TTo extends keyof RegisteredSearchOutputMap ? RegisteredSearchOutputMap[TTo] : Record<string, unknown>;
208
+ /** Infer the output type of a Zod/Valibot-compatible schema (duck-typed z.infer). */
209
+ export type InferSchemaOutput<S> =
210
+ S extends { parse(input: unknown): infer T } ? T :
211
+ S extends { safeParse(input: unknown): infer R }
212
+ ? (Awaited<R> extends { data?: infer T } ? NonNullable<T> : Record<string, unknown>)
213
+ : Record<string, unknown>;
214
+ export declare function buildPath(pattern: string, params: Record<string, string | number>): string;
215
+
132
216
  // ── Client components ─────────────────────────────────────────────────────
133
217
  export declare function Scripts(): null;
134
218
  export declare function LiveReload(): ReactNode;
135
219
  export declare function Outlet(): ReactNode;
136
220
 
137
- export interface LinkProps { to: string; prefetch?: "hover" | "none"; viewTransition?: boolean; children?: ReactNode; className?: string; [key: string]: unknown; }
138
- export declare function Link(props: LinkProps): ReactNode;
221
+ export type LinkProps<TTo extends RegisteredRoutes = RegisteredRoutes> = {
222
+ to: TTo | (string & {});
223
+ params?: ParamsFor<TTo>;
224
+ /** Search params for the target, typed by its `searchSchema` (replaces any query in `to`). */
225
+ search?: Partial<SearchOutputFor<TTo>>;
226
+ /** When to prefetch the target's chunk + loader data. Default "none". */
227
+ prefetch?: "none" | "intent" | "hover" | "viewport" | "render";
228
+ viewTransition?: boolean;
229
+ /** Replace the current history entry instead of pushing. */
230
+ replace?: boolean;
231
+ children?: ReactNode;
232
+ className?: string;
233
+ [key: string]: unknown;
234
+ };
235
+ export declare function Link<TTo extends RegisteredRoutes = RegisteredRoutes>(props: LinkProps<TTo>): ReactNode;
139
236
 
140
- export interface FormProps { method?: "post" | "put" | "delete"; action?: string; children?: ReactNode; [key: string]: unknown; }
237
+ export interface ScrollRestorationProps {
238
+ /** Derive the storage key for a location. Default: `location.key`. */
239
+ getKey?: (location: RouterLocation) => string;
240
+ storageKey?: string;
241
+ }
242
+ /** Restores scroll on back/forward, scrolls to top (or `#hash`) on new navigations. Render once in root.tsx. */
243
+ export declare function ScrollRestoration(props?: ScrollRestorationProps): null;
244
+
245
+ export interface FormProps { method?: "post" | "put" | "delete"; action?: string; /** Renders a hidden `intent` input (pairs with defineActions()). */ intent?: string; children?: ReactNode; [key: string]: unknown; }
141
246
  export declare function Form(props: FormProps): ReactNode;
142
247
 
143
- export interface AwaitProps<T> { resolve: Promise<T>; fallback: ReactNode; children: (data: T) => ReactNode; }
248
+ // ── defineActions (intent dispatch) ───────────────────────────────────────
249
+ /** Compose a route action from per-intent handlers, dispatching on the form's `intent` field. */
250
+ export declare function defineActions<M extends Record<string, (args: ActionArgs) => unknown>>(
251
+ handlers: M,
252
+ ): (args: ActionArgs) => Promise<Awaited<ReturnType<M[keyof M]>> | Response>;
253
+
254
+ export interface AwaitProps<T> { resolve: Promise<T> | Deferred<T>; fallback: ReactNode; children: (data: T) => ReactNode; }
144
255
  export declare function Await<T>(props: AwaitProps<T>): ReactNode;
145
256
 
146
257
  export type ImageFormat = "webp" | "avif" | "jpeg" | "png";
@@ -161,29 +272,97 @@ export interface ImageProps {
161
272
  export declare function Image(props: ImageProps): ReactNode;
162
273
 
163
274
  // ── Client hooks ──────────────────────────────────────────────────────────
164
- export declare function useLoaderData<T = unknown>(): T;
165
- export declare function useActionData<T = unknown>(): T | null;
166
- export declare function useParams(): Record<string, string>;
275
+ // Pass the loader/action function type to infer the data — useLoaderData<typeof loader>().
276
+ export declare function useLoaderData<T = unknown>(): LoaderData<T>;
277
+ export declare function useActionData<T = unknown>(): ActionData<T> | null;
278
+ /** The current location — reactive on the client, request-derived during SSR. */
279
+ export declare function useLocation(): RouterLocation;
280
+ export declare function useParams<TTo extends string>(): ParamsFor<TTo>;
281
+ export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
167
282
  export type NavigationState = "idle" | "loading" | "submitting";
168
283
  export declare function useNavigation(): { state: NavigationState };
284
+
285
+ export interface NavigateOptions<TTo extends RegisteredRoutes = RegisteredRoutes> {
286
+ params?: ParamsFor<TTo>;
287
+ /** Search params for the target, typed by its `searchSchema` (replaces any query in `to`). */
288
+ search?: Partial<SearchOutputFor<TTo>>;
289
+ /** Replace the current history entry instead of pushing a new one. */
290
+ replace?: boolean;
291
+ /** Arbitrary history state, readable via `useLocation().state` after navigating. */
292
+ state?: unknown;
293
+ }
294
+ export interface NavigateFn {
295
+ <TTo extends RegisteredRoutes>(to: TTo | (string & {}), options?: NavigateOptions<TTo>): Promise<void>;
296
+ }
297
+ export declare function useNavigate(): NavigateFn;
298
+
299
+ // ── Fetchers ──────────────────────────────────────────────────────────────
300
+ export type FetcherState = "idle" | "loading" | "submitting";
301
+ export interface FetcherEntry {
302
+ key: string;
303
+ state: FetcherState;
304
+ data: unknown;
305
+ /** The submitted form data while a submission is in flight — the optimistic-UI source. */
306
+ formData?: FormData;
307
+ formMethod?: string;
308
+ }
309
+ export interface FetcherFormProps {
310
+ method?: "post" | "put" | "delete";
311
+ action?: string;
312
+ /** Renders a hidden `intent` input (pairs with defineActions()). */
313
+ intent?: string;
314
+ children?: ReactNode;
315
+ [key: string]: unknown;
316
+ }
169
317
  export interface FetcherResult {
170
318
  data: unknown;
171
- state: NavigationState;
319
+ state: FetcherState;
320
+ formData?: FormData;
321
+ formMethod?: string;
322
+ key: string;
172
323
  load(path: string): Promise<void>;
173
324
  submit(path: string, opts: { method: string; body: FormData | Record<string, string> }): Promise<void>;
325
+ /** A form that submits through this fetcher (no navigation, no history). */
326
+ Form: (props: FetcherFormProps) => ReactNode;
174
327
  }
175
328
  export interface StreamFetcherResult<T = unknown> {
329
+ /** @deprecated Never emitted — call `connect(actionId)` instead. Removed in 0.2. */
176
330
  events: AsyncGenerator<T>;
177
331
  connect(actionId: string): AsyncGenerator<T>;
178
332
  }
179
- export declare function useFetcher(): FetcherResult;
333
+ export interface UseFetcherOptions { key?: string; stream?: boolean }
334
+ export declare function useFetcher(opts?: { key?: string }): FetcherResult;
180
335
  export declare function useFetcher<T>(opts: { stream: true }): StreamFetcherResult<T>;
336
+ /** Every active fetcher — the cross-component view for optimistic UI. */
337
+ export declare function useFetchers(): FetcherEntry[];
338
+
339
+ // ── Revalidation ──────────────────────────────────────────────────────────
340
+ export interface Revalidator {
341
+ revalidate(): Promise<void>;
342
+ state: "idle" | "loading";
343
+ }
344
+ /** Manually re-run the active route's loaders (respects `shouldRevalidate`). */
345
+ export declare function useRevalidator(): Revalidator;
346
+
347
+ // ── Typed search ──────────────────────────────────────────────────────────
348
+ /** The current route's VALIDATED search params (its `searchSchema` output). */
349
+ export declare function useSearch<TTo extends string>(): SearchOutputFor<TTo>;
350
+ export declare function useSearch<T extends Record<string, unknown>>(): T;
351
+ export interface SetSearchOptions { replace?: boolean }
352
+ export type SetSearchFn<T extends Record<string, unknown>> = (
353
+ updater: Partial<T> | ((prev: T) => Partial<T>),
354
+ options?: SetSearchOptions,
355
+ ) => Promise<void>;
356
+ /** Merge a patch into the current search params and soft-navigate (loaders re-run). */
357
+ export declare function useSetSearch<TTo extends string>(): SetSearchFn<SearchOutputFor<TTo>>;
358
+ export declare function useSetSearch<T extends Record<string, unknown>>(): SetSearchFn<T>;
181
359
 
182
360
  export interface SearchParamsResult<T extends Record<string, string> = Record<string, string>> {
183
361
  searchParams: URLSearchParams;
184
362
  getParam<K extends keyof T & string>(key: K): T[K] | null;
185
363
  setSearchParams(updater: Record<string, string> | ((prev: URLSearchParams) => URLSearchParams)): void;
186
364
  }
365
+ export declare function useSearchParams<TTo extends string>(): SearchParamsResult<SearchFor<TTo>>;
187
366
  export declare function useSearchParams<T extends Record<string, string> = Record<string, string>>(): SearchParamsResult<T>;
188
367
 
189
368
  // ── Typed route context ───────────────────────────────────────────────────
@@ -272,3 +451,24 @@ export interface DevServer {
272
451
  export declare function createDevServer(options?: DevServerOptions): Promise<DevServer>;
273
452
 
274
453
  export declare function loadUserConfig(): Promise<Partial<BractJSConfig>>;
454
+
455
+ /** Identity helper for bractjs.config.ts — wrap your default export for autocomplete + type-checking. */
456
+ export declare function defineConfig(config: Partial<BractJSConfig>): Partial<BractJSConfig>;
457
+
458
+ // ── Prerendering / SPA shell ──────────────────────────────────────────────
459
+ export interface PrerenderOptions {
460
+ prerender: string[] | (() => string[] | Promise<string[]>);
461
+ appDir?: string;
462
+ publicDir?: string;
463
+ buildDir?: string;
464
+ manifest?: ServerManifest;
465
+ }
466
+ export interface PrerenderResult { written: string[] }
467
+ /** Build-time prerendering (SSG): write HTML + /_data payloads under `<buildDir>/client/_prerender/`. */
468
+ export declare function runPrerender(options: PrerenderOptions): Promise<PrerenderResult>;
469
+ /** Render the SPA-mode document shell (config `ssr: false`). */
470
+ export declare function renderSpaShell(
471
+ appDir: string,
472
+ manifest: ServerManifest,
473
+ registry?: ModuleRegistry,
474
+ ): Promise<string>;
package/types/route.d.ts CHANGED
@@ -1,15 +1,47 @@
1
1
  import type { ComponentType } from "react";
2
2
 
3
- export interface LoaderArgs {
3
+ /** A parsed navigation location (see `useLocation`). `hash` is always "" during SSR. */
4
+ export interface RouterLocation {
5
+ pathname: string;
6
+ /** Raw query string including the leading `?`, or `""`. */
7
+ search: string;
8
+ /** Fragment including the leading `#`, or `""`. */
9
+ hash: string;
10
+ state: unknown;
11
+ key: string;
12
+ }
13
+
14
+ export interface LoaderArgs<TSearch extends Record<string, unknown> = Record<string, unknown>> {
4
15
  request: Request;
5
16
  params: Record<string, string>;
6
17
  context: Record<string, unknown>;
18
+ /**
19
+ * The request's search params, validated/coerced by the route's
20
+ * `searchSchema` export when present; otherwise the raw string record
21
+ * (repeated keys become arrays). Parameterize to skip the cast:
22
+ * `loader({ search }: LoaderArgs<BoardSearch>)`.
23
+ */
24
+ search: TSearch;
7
25
  }
8
26
 
9
- export interface ActionArgs extends LoaderArgs {
27
+ export interface ActionArgs<TSearch extends Record<string, unknown> = Record<string, unknown>>
28
+ extends LoaderArgs<TSearch> {
10
29
  formData: FormData;
11
30
  }
12
31
 
32
+ /**
33
+ * The data a route's loader resolves to, for typing `useLoaderData`. Pass the
34
+ * loader function type to infer it (`useLoaderData<typeof loader>()` →
35
+ * awaited return, `Response` excluded, `Deferred` fields preserved). A plain
36
+ * object type is returned as-is (back-compat).
37
+ */
38
+ export type LoaderData<T> = T extends (...args: never[]) => unknown
39
+ ? Exclude<Awaited<ReturnType<T>>, Response>
40
+ : T;
41
+
42
+ /** The data a route's action resolves to, for typing `useActionData`. See {@link LoaderData}. */
43
+ export type ActionData<T> = LoaderData<T>;
44
+
13
45
  export type MetaDescriptor =
14
46
  | { title: string }
15
47
  | { name: string; content: string }
@@ -35,17 +67,45 @@ export interface BeforeLoadArgs {
35
67
  params: Record<string, string>;
36
68
  context: Record<string, unknown>;
37
69
  location: { pathname: string; search: string };
70
+ /** Validated search params (server-side only; absent in the client-side guard). */
71
+ search?: Record<string, unknown>;
38
72
  }
39
73
 
40
74
  export type BeforeLoadFunction = (
41
75
  args: BeforeLoadArgs,
42
76
  ) => void | Response | Promise<void | Response>;
43
77
 
78
+ /**
79
+ * Decide whether loader data should be refetched (SWR background refetch and
80
+ * post-mutation revalidation). Return `args.defaultShouldRevalidate` (true)
81
+ * for the default behavior.
82
+ */
83
+ export interface ShouldRevalidateArgs {
84
+ currentUrl: URL;
85
+ nextUrl: URL;
86
+ formMethod?: string;
87
+ actionStatus?: number;
88
+ defaultShouldRevalidate: boolean;
89
+ }
90
+
91
+ export type ShouldRevalidateFunction = (args: ShouldRevalidateArgs) => boolean;
92
+
44
93
  export interface RouteModule<TLoader = unknown, TAction = unknown> {
45
94
  loader?: LoaderFunction<TLoader>;
46
95
  action?: ActionFunction<TAction>;
47
96
  meta?: MetaFunction<TLoader>;
48
97
  beforeLoad?: BeforeLoadFunction;
98
+ shouldRevalidate?: ShouldRevalidateFunction;
99
+ /** Zod/Valibot-compatible schema validating search params before loaders run (400 on failure). */
100
+ searchSchema?: unknown;
101
+ /**
102
+ * Selective SSR: `true` (default) full SSR; `"data-only"` loaders run on the
103
+ * server but the component renders client-only; `false` neither the route
104
+ * loader nor the component runs during document SSR (beforeLoad still does).
105
+ */
106
+ ssr?: boolean | "data-only";
107
+ /** SSR'd in the component's place for `ssr: false` / `"data-only"` routes. */
108
+ Fallback?: ComponentType;
49
109
  handle?: Record<string, unknown>;
50
110
  ErrorBoundary?: ComponentType<{ error: unknown }>;
51
111
  default?: ComponentType;