@bractjs/bractjs 0.1.27 → 0.1.29

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 (117) hide show
  1. package/bin/cli.ts +18 -1
  2. package/package.json +3 -2
  3. package/src/__tests__/codegen-write.test.ts +67 -0
  4. package/src/__tests__/codegen.test.ts +29 -2
  5. package/src/__tests__/compile-safety.test.ts +4 -0
  6. package/src/__tests__/csp.test.ts +10 -0
  7. package/src/__tests__/define-actions.test.ts +69 -0
  8. package/src/__tests__/env.test.ts +18 -0
  9. package/src/__tests__/fetcher-store.test.ts +67 -0
  10. package/src/__tests__/fixtures/app/root.tsx +7 -2
  11. package/src/__tests__/fixtures/app/routes/boom.tsx +9 -0
  12. package/src/__tests__/fixtures/app/routes/client-only.tsx +16 -0
  13. package/src/__tests__/fixtures/app/routes/counter.tsx +16 -0
  14. package/src/__tests__/fixtures/app/routes/data-only.tsx +16 -0
  15. package/src/__tests__/fixtures/app/routes/features-demo.tsx +28 -0
  16. package/src/__tests__/fixtures/app/routes/intent-demo.tsx +46 -0
  17. package/src/__tests__/fixtures/app/routes/protected-client-only.tsx +15 -0
  18. package/src/__tests__/fixtures/app/routes/search-demo.tsx +39 -0
  19. package/src/__tests__/form-data-helpers.test.ts +43 -0
  20. package/src/__tests__/headers.test.ts +111 -0
  21. package/src/__tests__/integration.test.ts +90 -0
  22. package/src/__tests__/layout-registry.test.ts +7 -3
  23. package/src/__tests__/loader.test.ts +32 -1
  24. package/src/__tests__/matcher.test.ts +29 -0
  25. package/src/__tests__/module-registry.test.ts +2 -3
  26. package/src/__tests__/nav-utils.test.ts +46 -0
  27. package/src/__tests__/prerender.test.ts +102 -0
  28. package/src/__tests__/programmatic-api.test.ts +20 -1
  29. package/src/__tests__/revalidation.test.ts +65 -0
  30. package/src/__tests__/route-lint.test.ts +79 -0
  31. package/src/__tests__/route-middleware.test.ts +84 -0
  32. package/src/__tests__/route-table.test.ts +33 -0
  33. package/src/__tests__/safe-validate.test.ts +96 -0
  34. package/src/__tests__/scanner.test.ts +46 -1
  35. package/src/__tests__/scroll-restoration.test.ts +66 -0
  36. package/src/__tests__/search-serializer.test.ts +42 -0
  37. package/src/__tests__/search-validation.test.ts +125 -0
  38. package/src/__tests__/security-fixes.test.ts +201 -0
  39. package/src/__tests__/security.test.ts +110 -1
  40. package/src/__tests__/selective-ssr.test.ts +85 -0
  41. package/src/__tests__/spa-mode.test.ts +77 -0
  42. package/src/__tests__/typed-routing.test.ts +51 -1
  43. package/src/__tests__/use-matches.test.ts +54 -0
  44. package/src/build/bundler.ts +33 -0
  45. package/src/build/prerender.ts +88 -0
  46. package/src/build/route-lint.ts +49 -0
  47. package/src/client/ClientRouter.tsx +339 -47
  48. package/src/client/cache.ts +8 -0
  49. package/src/client/components/Await.tsx +9 -2
  50. package/src/client/components/Form.tsx +23 -34
  51. package/src/client/components/Link.tsx +80 -9
  52. package/src/client/components/Outlet.tsx +8 -2
  53. package/src/client/components/ScrollRestoration.tsx +125 -0
  54. package/src/client/entry.tsx +39 -2
  55. package/src/client/fetcher-store.ts +61 -0
  56. package/src/client/form-utils.ts +3 -0
  57. package/src/client/hooks/useActionData.ts +7 -3
  58. package/src/client/hooks/useFetcher.ts +116 -33
  59. package/src/client/hooks/useFetchers.ts +23 -0
  60. package/src/client/hooks/useLoaderData.ts +8 -4
  61. package/src/client/hooks/useLocation.ts +27 -0
  62. package/src/client/hooks/useMatches.ts +32 -0
  63. package/src/client/hooks/useNavigate.ts +11 -6
  64. package/src/client/hooks/useRevalidator.ts +26 -0
  65. package/src/client/hooks/useSearch.ts +73 -0
  66. package/src/client/hooks/useSearchParams.ts +7 -2
  67. package/src/client/nav-utils.ts +26 -0
  68. package/src/client/prefetch.ts +110 -15
  69. package/src/client/registry.ts +24 -0
  70. package/src/client/revalidation.ts +25 -0
  71. package/src/client/router.tsx +34 -1
  72. package/src/client/rpc.ts +11 -1
  73. package/src/client/scroll-restoration.ts +48 -0
  74. package/src/client/search-serializer.ts +40 -0
  75. package/src/client/types.ts +6 -0
  76. package/src/codegen/module-registry.ts +13 -21
  77. package/src/codegen/route-codegen.ts +148 -10
  78. package/src/config/load.ts +22 -0
  79. package/src/dev/hmr-client.ts +3 -1
  80. package/src/dev/route-table.ts +27 -0
  81. package/src/dev/server.ts +106 -8
  82. package/src/dev/watcher.ts +25 -3
  83. package/src/index.ts +38 -6
  84. package/src/server/action-handler.ts +3 -13
  85. package/src/server/action-registry.ts +35 -0
  86. package/src/server/adapter.ts +16 -0
  87. package/src/server/api-route.ts +47 -0
  88. package/src/server/csp.ts +19 -4
  89. package/src/server/csrf.ts +36 -3
  90. package/src/server/env.ts +26 -5
  91. package/src/server/headers.ts +49 -0
  92. package/src/server/layout.ts +43 -20
  93. package/src/server/loader.ts +14 -8
  94. package/src/server/matcher.ts +29 -2
  95. package/src/server/matches.ts +50 -0
  96. package/src/server/middleware.ts +66 -0
  97. package/src/server/proto-guard.ts +56 -0
  98. package/src/server/render.ts +51 -18
  99. package/src/server/request-handler.ts +111 -29
  100. package/src/server/scanner.ts +45 -3
  101. package/src/server/search.ts +47 -0
  102. package/src/server/serve.ts +116 -4
  103. package/src/server/session.ts +12 -1
  104. package/src/server/spa.ts +62 -0
  105. package/src/server/stream-handler.ts +10 -1
  106. package/src/server/validate.ts +89 -14
  107. package/src/shared/context.ts +7 -0
  108. package/src/shared/define-actions.ts +39 -0
  109. package/src/shared/form-data.ts +34 -0
  110. package/src/shared/route-types.ts +191 -2
  111. package/templates/new-app/app/root.tsx +2 -1
  112. package/templates/new-app/bractjs.config.ts +7 -12
  113. package/types/config.d.ts +24 -0
  114. package/types/index.d.ts +182 -9
  115. package/types/route.d.ts +138 -3
  116. package/LICENSE +0 -21
  117. package/README.md +0 -1125
@@ -1,15 +1,57 @@
1
1
  import type { Deferred } from "./deferred.ts";
2
2
 
3
- export interface LoaderArgs {
3
+ /**
4
+ * A parsed navigation location. `key` is the stable identity of the history
5
+ * entry (used by scroll restoration); `state` is the value passed via
6
+ * `navigate(to, { state })`. During SSR `hash` is always `""` (the fragment
7
+ * never reaches the server) and `key` is `"default"`.
8
+ */
9
+ export interface RouterLocation {
10
+ pathname: string;
11
+ /** Raw query string including the leading `?`, or `""`. */
12
+ search: string;
13
+ /** Fragment including the leading `#`, or `""`. */
14
+ hash: string;
15
+ state: unknown;
16
+ key: string;
17
+ }
18
+
19
+ export interface LoaderArgs<TSearch extends Record<string, unknown> = Record<string, unknown>> {
4
20
  request: Request;
5
21
  params: Record<string, string>;
6
22
  context: Record<string, unknown>;
23
+ /**
24
+ * The request's search params, validated/coerced by the route's
25
+ * `searchSchema` export when present; otherwise the raw string record
26
+ * (repeated keys become arrays).
27
+ *
28
+ * Parameterize to skip the cast in routes with a schema:
29
+ * `loader({ search }: LoaderArgs<BoardSearch>)`.
30
+ */
31
+ search: TSearch;
7
32
  }
8
33
 
9
- export interface ActionArgs extends LoaderArgs {
34
+ export interface ActionArgs<TSearch extends Record<string, unknown> = Record<string, unknown>>
35
+ extends LoaderArgs<TSearch> {
10
36
  formData: FormData;
11
37
  }
12
38
 
39
+ /**
40
+ * The data a route's loader resolves to, for typing `useLoaderData`.
41
+ *
42
+ * Pass the loader FUNCTION type and it unwraps the return (awaited, with the
43
+ * `Response` redirect/throw branch removed): `useLoaderData<typeof loader>()`.
44
+ * Pass a plain object type and it's returned as-is (back-compat):
45
+ * `useLoaderData<HomeData>()`. `Deferred<V>` fields are preserved — that is the
46
+ * shape the component receives during streaming SSR (unwrap them with `<Await>`).
47
+ */
48
+ export type LoaderData<T> = T extends (...args: never[]) => unknown
49
+ ? Exclude<Awaited<ReturnType<T>>, Response>
50
+ : T;
51
+
52
+ /** The data a route's action resolves to, for typing `useActionData`. See {@link LoaderData}. */
53
+ export type ActionData<T> = LoaderData<T>;
54
+
13
55
  export type MetaDescriptor =
14
56
  | { title: string }
15
57
  | { name: string; content: string }
@@ -33,21 +75,149 @@ export type MetaFunction<T = unknown> = (
33
75
  args: MetaArgs<T>
34
76
  ) => MetaDescriptor[];
35
77
 
78
+ export interface HeadersArgs<T = unknown> {
79
+ /** This route's loader data (the route slice, already awaited). */
80
+ loaderData: T;
81
+ params: Record<string, string>;
82
+ request: Request;
83
+ /**
84
+ * The merged headers contributed by ancestors in the chain (root → layout →
85
+ * this route). Spread these to inherit, or override individual keys. Each
86
+ * `headers()` in the chain runs in order and sees what came before it.
87
+ */
88
+ parentHeaders: Headers;
89
+ }
90
+
91
+ /**
92
+ * A route/layout/root module's optional `headers` export, used to set
93
+ * response headers (e.g. `Cache-Control`, `ETag`, `Vary`) on the document and
94
+ * `/_data` responses. Runs in chain order (root → layout → route); the
95
+ * innermost value wins per key. Returns a `HeadersInit` (object, array of
96
+ * tuples, or `Headers`).
97
+ */
98
+ export type HeadersFunction<T = unknown> = (
99
+ args: HeadersArgs<T>
100
+ ) => HeadersInit;
101
+
102
+ /**
103
+ * A nested route-middleware function. Runs on the server in chain order
104
+ * (root → layout → route) before `beforeLoad`, the action, and loaders. Call
105
+ * `next()` to continue, or return a `Response` to short-circuit. The `context`
106
+ * object is shared and mutable across the whole chain (and into loaders).
107
+ */
108
+ export type RouteMiddlewareFunction = (
109
+ ctx: { request: Request; params: Record<string, string>; context: Record<string, unknown> },
110
+ next: () => Promise<Response>,
111
+ ) => Promise<Response>;
112
+
36
113
  export interface BeforeLoadArgs {
37
114
  params: Record<string, string>;
38
115
  context: Record<string, unknown>;
39
116
  location: { pathname: string; search: string };
117
+ /** Validated search params (server-side only; absent in the client-side guard). */
118
+ search?: Record<string, unknown>;
40
119
  }
41
120
 
42
121
  export type BeforeLoadFunction = (
43
122
  args: BeforeLoadArgs,
44
123
  ) => void | Response | Promise<void | Response>;
45
124
 
125
+ /**
126
+ * Decide whether loader data should be refetched. Evaluated on the CLIENT for
127
+ * (a) the stale-while-revalidate background refetch and (b) the automatic
128
+ * revalidation after a `<Form>`/fetcher mutation. Return
129
+ * `args.defaultShouldRevalidate` (true) to keep the default behavior.
130
+ */
131
+ export interface ShouldRevalidateArgs {
132
+ currentUrl: URL;
133
+ nextUrl: URL;
134
+ /** Present when the revalidation was triggered by a mutation. */
135
+ formMethod?: string;
136
+ /** HTTP status the action responded with, when mutation-triggered. */
137
+ actionStatus?: number;
138
+ defaultShouldRevalidate: boolean;
139
+ }
140
+
141
+ export type ShouldRevalidateFunction = (args: ShouldRevalidateArgs) => boolean;
142
+
143
+ /**
144
+ * A route's optional client loader (RR7-style). Runs in the browser on
145
+ * navigation to the route instead of just fetching the server loader. Call
146
+ * `serverLoader()` to get this route's server loader data (the `/_data`
147
+ * payload's route slice). Set `clientLoader.hydrate = true` to also run it
148
+ * during the initial hydration of an SSR'd document.
149
+ *
150
+ * Whatever it resolves to becomes the route's `useLoaderData()` value.
151
+ */
152
+ export interface ClientLoaderFunction<T = unknown> {
153
+ (args: {
154
+ request: Request;
155
+ params: Record<string, string>;
156
+ search: Record<string, unknown>;
157
+ /** Fetch this route's server loader data (the `/_data` route slice). */
158
+ serverLoader: () => Promise<unknown>;
159
+ }): Promise<T> | T;
160
+ /** Run on initial hydration too (default: only on client navigation). */
161
+ hydrate?: boolean;
162
+ }
163
+
164
+ /**
165
+ * A route's optional client action (RR7-style). Runs in the browser on a
166
+ * `<Form>`/fetcher submission to the route instead of POSTing directly. Call
167
+ * `serverAction()` to invoke the server action and get its data. Whatever it
168
+ * resolves to becomes the route's `useActionData()` value.
169
+ */
170
+ export type ClientActionFunction<T = unknown> = (args: {
171
+ request: Request;
172
+ params: Record<string, string>;
173
+ formData: FormData;
174
+ /** Invoke this route's server action and get its returned data. */
175
+ serverAction: () => Promise<unknown>;
176
+ }) => Promise<T> | T;
177
+
46
178
  export interface RouteModule<TLoader = unknown, TAction = unknown> {
47
179
  loader?: LoaderFunction<TLoader>;
48
180
  action?: ActionFunction<TAction>;
181
+ /** Browser-side loader; see {@link ClientLoaderFunction}. */
182
+ clientLoader?: ClientLoaderFunction<TLoader>;
183
+ /** Browser-side action; see {@link ClientActionFunction}. */
184
+ clientAction?: ClientActionFunction<TAction>;
49
185
  meta?: MetaFunction<TLoader>;
186
+ /**
187
+ * Set response headers (`Cache-Control`, `ETag`, `Vary`, CDN hints, …) for
188
+ * this route's document and `/_data` responses. Runs in chain order
189
+ * (root → layout → route); innermost wins per key, and each call receives the
190
+ * `parentHeaders` accumulated so far. Skipped for mutations and error responses.
191
+ */
192
+ headers?: HeadersFunction<TLoader>;
193
+ /**
194
+ * Nested middleware for this route/layout/root. Runs on the server in chain
195
+ * order (root → layout → route) before `beforeLoad`/action/loaders, with a
196
+ * shared mutable `context`. A single function or an array. Return a
197
+ * `Response` to short-circuit; call `next()` to continue. Runs *inside* the
198
+ * global `pipeline` middleware.
199
+ */
200
+ middleware?: RouteMiddlewareFunction | RouteMiddlewareFunction[];
50
201
  beforeLoad?: BeforeLoadFunction;
202
+ shouldRevalidate?: ShouldRevalidateFunction;
203
+ /**
204
+ * Zod/Valibot-compatible schema validating the route's search params before
205
+ * loaders run. Failure → 400; use `.catch()`/`.default()` per field for
206
+ * URLs that must tolerate junk values.
207
+ */
208
+ searchSchema?: unknown;
209
+ /**
210
+ * Selective SSR (TanStack-style):
211
+ * - `true` (default) — full document SSR with loader data.
212
+ * - `"data-only"` — loaders run on the server, but the component renders
213
+ * only on the client (`Fallback` SSRs in its place).
214
+ * - `false` — neither the route loader nor the component runs during
215
+ * document SSR; the client fetches `/_data` after hydration. `beforeLoad`
216
+ * STILL runs on the server — it is the auth gate.
217
+ */
218
+ ssr?: boolean | "data-only";
219
+ /** SSR'd in the component's place for `ssr: false` / `"data-only"` routes (HydrateFallback equivalent). */
220
+ Fallback?: React.ComponentType;
51
221
  handle?: Record<string, unknown>;
52
222
  ErrorBoundary?: React.ComponentType<{ error: unknown }>;
53
223
  default?: React.ComponentType;
@@ -60,3 +230,22 @@ export interface RouteDefinition {
60
230
  parentId?: string;
61
231
  index?: boolean;
62
232
  }
233
+
234
+ /**
235
+ * One entry in the matched route chain, as returned by `useMatches()`. The
236
+ * array runs outermost → innermost: root, then each layout, then the leaf
237
+ * route. Use it for breadcrumbs and conditional chrome driven by each route's
238
+ * `handle` export.
239
+ */
240
+ export interface RouteMatch<TData = unknown, THandle = Record<string, unknown>> {
241
+ /** Stable id of the matched module — its appDir-relative file path (e.g. "routes/blog/[id].tsx", "root.tsx"). */
242
+ id: string;
243
+ /** The active URL pathname (same for every entry — they all share the matched location). */
244
+ pathname: string;
245
+ /** The matched route params (shared across the chain). */
246
+ params: Record<string, string>;
247
+ /** This module's loader data slice (root / the matching layout / the route). */
248
+ data: TData;
249
+ /** This module's static `handle` export, or `undefined` if none. */
250
+ handle: THandle | undefined;
251
+ }
@@ -1,6 +1,6 @@
1
1
  // This is the root layout for your BractJS app.
2
2
  // Every route renders inside this component.
3
- import { Scripts, LiveReload, Outlet } from "@bractjs/bractjs";
3
+ import { Scripts, LiveReload, Outlet, ScrollRestoration } from "@bractjs/bractjs";
4
4
 
5
5
  export default function Root() {
6
6
  return (
@@ -12,6 +12,7 @@ export default function Root() {
12
12
  </head>
13
13
  <body>
14
14
  <Outlet />
15
+ <ScrollRestoration />
15
16
  <Scripts />
16
17
  <LiveReload />
17
18
  </body>
@@ -1,14 +1,9 @@
1
- import type { BractJSConfig } from "@bractjs/bractjs";
1
+ import { defineConfig } from "@bractjs/bractjs";
2
2
 
3
- const config: BractJSConfig = {
3
+ // All fields are optional and merged over BractJS defaults. `defineConfig`
4
+ // gives you autocomplete + type-checking without annotating the full type.
5
+ // (The build manifest is injected at runtime — you never set it here.)
6
+ export default defineConfig({
4
7
  port: 3000,
5
- appDir: "./app",
6
- publicDir: "./public",
7
- buildDir: "./build",
8
- manifest: { clientEntry: "", routes: {} }, // populated by `bractjs build`
9
- minify: true,
10
- sourcemap: "external",
11
- clientEnv: [],
12
- };
13
-
14
- export default config;
8
+ clientEnv: [], // process.env keys to expose to the client bundle
9
+ });
package/types/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { BunPlugin } from "bun";
2
2
  import type { RouteFile, RouteModule } from "./route.d.ts";
3
+ import type { BractAdapter, I18nConfig, OnErrorHook } from "./index.d.ts";
3
4
 
4
5
  export interface BractJSConfig {
5
6
  /** TCP port to listen on. Default: 3000. */
@@ -20,10 +21,23 @@ export interface BractJSConfig {
20
21
  clientEnv?: string[];
21
22
  /** User Bun bundler plugins appended to the client build (e.g. bun-plugin-tailwind). */
22
23
  plugins?: BunPlugin[];
24
+ /** Directory for the transformed-image cache. Default: ".bract-image-cache". */
25
+ imageCacheDir?: string;
26
+ /** Hard ceiling (bytes) on any incoming request body, enforced by the Bun
27
+ * adapter regardless of advertised Content-Length. Default 16 MiB. */
28
+ maxRequestBodySize?: number;
29
+ /** WebSocket port for dev HMR (used by `bractjs dev` only). Default 3001. */
30
+ hmrPort?: number;
31
+ /** Custom server adapter (Cloudflare Workers, Deno, Node, etc.). Defaults to Bun.serve(). */
32
+ adapter?: BractAdapter;
33
+ /** i18n locale-prefix routing config consumed by the i18n utilities. */
34
+ i18n?: I18nConfig;
23
35
  /** Called once after the server starts listening. Use to open DB connections, warm caches, etc. */
24
36
  onStart?: () => Promise<void> | void;
25
37
  /** Called before the process exits (any signal or uncaught error). Use to close DB connections, flush queues, etc. */
26
38
  onShutdown?: () => Promise<void> | void;
39
+ /** Called for every unexpected error (loader/action throws, uncaught exceptions). Redirects and HttpErrors are not reported. */
40
+ onError?: OnErrorHook;
27
41
  /**
28
42
  * Pre-scanned route list. Typically imported from `app/_generated/routes.ts`.
29
43
  * Required for `bun build --compile` binaries where the routes/ directory
@@ -41,6 +55,14 @@ export interface BractJSConfig {
41
55
  * proxy plugin hashed during the client build.
42
56
  */
43
57
  actionModules?: Array<{ relPath: string; mod: Record<string, unknown> }>;
58
+ /**
59
+ * SPA mode: `false` serves one static shell for every document GET instead
60
+ * of SSR ("no document SSR", not "no server" — /_data, actions, images and
61
+ * API routes keep working). Default `true`.
62
+ */
63
+ ssr?: boolean;
64
+ /** Paths to prerender at build time (SSG); served from disk before dynamic SSR. */
65
+ prerender?: string[] | (() => string[] | Promise<string[]>);
44
66
  }
45
67
 
46
68
  export interface ServerManifest {
@@ -60,4 +82,6 @@ export interface BuildConfig {
60
82
  minify?: boolean;
61
83
  clientEnv?: string[];
62
84
  plugins?: import("bun").BunPlugin[];
85
+ /** SPA mode: when `false`, the build also emits the static document shell. */
86
+ ssr?: boolean;
63
87
  }
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;
@@ -80,6 +93,7 @@ import type { MiddlewareFn, MiddlewareContext } from "./middleware.d.ts";
80
93
 
81
94
  export declare class MiddlewarePipeline {
82
95
  use(fn: MiddlewareFn): this;
96
+ clear(): this;
83
97
  run(ctx: MiddlewareContext, handler: () => Promise<Response>): Promise<Response>;
84
98
  }
85
99
  export declare const pipeline: MiddlewarePipeline;
@@ -93,16 +107,24 @@ export declare function authGuard(options: AuthGuardOptions): MiddlewareFn;
93
107
 
94
108
  // ── API routes (C1) ───────────────────────────────────────────────────────
95
109
  export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
110
+ export interface ApiRouteOptions {
111
+ /** CSRF protection for this route. Default `true` for mutating methods
112
+ * (POST/PUT/PATCH/DELETE). Set `false` only for endpoints that don't trust
113
+ * ambient credentials (webhooks, token-authenticated/public APIs). */
114
+ csrf?: boolean;
115
+ }
96
116
  export interface ApiRouteDefinition<TMethod extends HttpMethod, TPath extends string, TInput, TOutput> {
97
117
  method: TMethod;
98
118
  path: TPath;
99
119
  handler: (input: TInput, request: Request) => TOutput | Promise<TOutput>;
120
+ csrf: boolean;
100
121
  }
101
122
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
123
  export declare function route<TMethod extends HttpMethod, TPath extends string, TInput, TOutput>(
103
124
  method: TMethod,
104
125
  path: TPath,
105
126
  handler: (input: TInput, request: Request) => TOutput | Promise<TOutput>,
127
+ options?: ApiRouteOptions,
106
128
  ): ApiRouteDefinition<TMethod, TPath, TInput, TOutput>;
107
129
  export type AppApiRoutes = never; // users extend this via codegen
108
130
 
@@ -129,6 +151,46 @@ export declare function validate<T>(
129
151
  input: FormData | Record<string, unknown>,
130
152
  ): Promise<T>;
131
153
 
154
+ export type SafeValidateResult<T> =
155
+ | { ok: true; data: T }
156
+ | { ok: false; fieldErrors: FieldErrors; firstError: string };
157
+ /** Non-throwing validate(): returns a result instead of throwing a 400. */
158
+ export declare function safeValidate<T>(
159
+ schema: { safeParse?(i: unknown): unknown } | { parse(i: unknown): T },
160
+ input: FormData | Record<string, unknown>,
161
+ ): Promise<SafeValidateResult<T>>;
162
+ /** True for the 400 Response thrown by validate()/searchSchema validation. */
163
+ export declare function isValidationResponse(value: unknown): value is Response;
164
+ /** Parse the `{ errors }` body of a validation 400 into field errors + first message. */
165
+ export declare function readValidationError(
166
+ res: Response,
167
+ ): Promise<{ fieldErrors: FieldErrors; firstError: string }>;
168
+
169
+ // ── FormData helpers ──────────────────────────────────────────────────────
170
+ /** String field from FormData; "" when missing or a File. */
171
+ export declare function formText(formData: FormData, key: string): string;
172
+ /** Collect string fields from FormData (all, or a named subset). */
173
+ export declare function formValues(formData: FormData, keys?: string[]): Record<string, string>;
174
+
175
+ // ── Prototype-pollution guards ────────────────────────────────────────────
176
+ /** Deep-scan a parsed JSON value for `__proto__`/`constructor`/`prototype`
177
+ * keys. Fails closed past an internal depth cap. Returns true if found. */
178
+ export declare function hasForbiddenKey(value: unknown, depth?: number): boolean;
179
+ /** Build a null-prototype object from entries, so a key named `__proto__`
180
+ * lands as a plain own property instead of mutating the prototype. */
181
+ export declare function nullProtoFromEntries<V>(entries: Iterable<readonly [string, V]>): Record<string, V>;
182
+
183
+ // ── Search-param validation ───────────────────────────────────────────────
184
+ /** URLSearchParams → plain object; repeated keys collapse into arrays. */
185
+ export declare function searchParamsToObject(sp: URLSearchParams): Record<string, string | string[]>;
186
+ /**
187
+ * Validate a URL's search params against a route's `searchSchema`. No schema →
188
+ * the raw string record; failure → throws a 400 Response with field errors.
189
+ */
190
+ export declare function validateSearch(schema: unknown, url: URL): Promise<Record<string, unknown>>;
191
+ /** Serialize a search object back into a query string (leading `?`, or ""). */
192
+ export declare function serializeSearch(search: Record<string, unknown>): string;
193
+
132
194
  // ── Typed-routing registration seam ───────────────────────────────────────
133
195
  // Mirror of src/client/registry.ts. Augment `Register` (done by `bractjs codegen`
134
196
  // in app/route-types.gen.ts) to make <Link>/useNavigate/useParams/useSearchParams
@@ -151,10 +213,21 @@ export type RegisteredParamsMap =
151
213
  Register extends { routes: { params: infer P } } ? P : Record<string, Record<string, string>>;
152
214
  export type RegisteredSearchMap =
153
215
  Register extends { routes: { search: infer S } } ? S : Record<string, Record<string, string>>;
216
+ export type RegisteredSearchOutputMap =
217
+ Register extends { routes: { searchOutput: infer S } } ? S : Record<string, Record<string, unknown>>;
154
218
  export type ParamsFor<TTo> =
155
219
  TTo extends keyof RegisteredParamsMap ? RegisteredParamsMap[TTo] : Record<string, string>;
156
220
  export type SearchFor<TTo> =
157
221
  TTo extends keyof RegisteredSearchMap ? RegisteredSearchMap[TTo] : Record<string, string>;
222
+ /** Validated (schema-output) search object for a specific route literal. */
223
+ export type SearchOutputFor<TTo> =
224
+ TTo extends keyof RegisteredSearchOutputMap ? RegisteredSearchOutputMap[TTo] : Record<string, unknown>;
225
+ /** Infer the output type of a Zod/Valibot-compatible schema (duck-typed z.infer). */
226
+ export type InferSchemaOutput<S> =
227
+ S extends { parse(input: unknown): infer T } ? T :
228
+ S extends { safeParse(input: unknown): infer R }
229
+ ? (Awaited<R> extends { data?: infer T } ? NonNullable<T> : Record<string, unknown>)
230
+ : Record<string, unknown>;
158
231
  export declare function buildPath(pattern: string, params: Record<string, string | number>): string;
159
232
 
160
233
  // ── Client components ─────────────────────────────────────────────────────
@@ -165,18 +238,37 @@ export declare function Outlet(): ReactNode;
165
238
  export type LinkProps<TTo extends RegisteredRoutes = RegisteredRoutes> = {
166
239
  to: TTo | (string & {});
167
240
  params?: ParamsFor<TTo>;
168
- prefetch?: "hover" | "none";
241
+ /** Search params for the target, typed by its `searchSchema` (replaces any query in `to`). */
242
+ search?: Partial<SearchOutputFor<TTo>>;
243
+ /** When to prefetch the target's chunk + loader data. Default "none". */
244
+ prefetch?: "none" | "intent" | "hover" | "viewport" | "render";
169
245
  viewTransition?: boolean;
246
+ /** Replace the current history entry instead of pushing. */
247
+ replace?: boolean;
170
248
  children?: ReactNode;
171
249
  className?: string;
172
250
  [key: string]: unknown;
173
251
  };
174
252
  export declare function Link<TTo extends RegisteredRoutes = RegisteredRoutes>(props: LinkProps<TTo>): ReactNode;
175
253
 
176
- export interface FormProps { method?: "post" | "put" | "delete"; action?: string; children?: ReactNode; [key: string]: unknown; }
254
+ export interface ScrollRestorationProps {
255
+ /** Derive the storage key for a location. Default: `location.key`. */
256
+ getKey?: (location: RouterLocation) => string;
257
+ storageKey?: string;
258
+ }
259
+ /** Restores scroll on back/forward, scrolls to top (or `#hash`) on new navigations. Render once in root.tsx. */
260
+ export declare function ScrollRestoration(props?: ScrollRestorationProps): null;
261
+
262
+ export interface FormProps { method?: "post" | "put" | "delete"; action?: string; /** Renders a hidden `intent` input (pairs with defineActions()). */ intent?: string; children?: ReactNode; [key: string]: unknown; }
177
263
  export declare function Form(props: FormProps): ReactNode;
178
264
 
179
- export interface AwaitProps<T> { resolve: Promise<T>; fallback: ReactNode; children: (data: T) => ReactNode; }
265
+ // ── defineActions (intent dispatch) ───────────────────────────────────────
266
+ /** Compose a route action from per-intent handlers, dispatching on the form's `intent` field. */
267
+ export declare function defineActions<M extends Record<string, (args: ActionArgs) => unknown>>(
268
+ handlers: M,
269
+ ): (args: ActionArgs) => Promise<Awaited<ReturnType<M[keyof M]>> | Response>;
270
+
271
+ export interface AwaitProps<T> { resolve: Promise<T> | Deferred<T>; fallback: ReactNode; children: (data: T) => ReactNode; }
180
272
  export declare function Await<T>(props: AwaitProps<T>): ReactNode;
181
273
 
182
274
  export type ImageFormat = "webp" | "avif" | "jpeg" | "png";
@@ -197,30 +289,90 @@ export interface ImageProps {
197
289
  export declare function Image(props: ImageProps): ReactNode;
198
290
 
199
291
  // ── Client hooks ──────────────────────────────────────────────────────────
200
- export declare function useLoaderData<T = unknown>(): T;
201
- export declare function useActionData<T = unknown>(): T | null;
292
+ // Pass the loader/action function type to infer the data — useLoaderData<typeof loader>().
293
+ export declare function useLoaderData<T = unknown>(): LoaderData<T>;
294
+ export declare function useActionData<T = unknown>(): ActionData<T> | null;
295
+ /** The current location — reactive on the client, request-derived during SSR. */
296
+ export declare function useLocation(): RouterLocation;
202
297
  export declare function useParams<TTo extends string>(): ParamsFor<TTo>;
203
298
  export declare function useParams<T extends Record<string, string> = Record<string, string>>(): T;
204
299
  export type NavigationState = "idle" | "loading" | "submitting";
205
300
  export declare function useNavigation(): { state: NavigationState };
206
301
 
207
- export interface NavigateOptions<TTo extends RegisteredRoutes = RegisteredRoutes> { params?: ParamsFor<TTo>; }
302
+ export interface NavigateOptions<TTo extends RegisteredRoutes = RegisteredRoutes> {
303
+ params?: ParamsFor<TTo>;
304
+ /** Search params for the target, typed by its `searchSchema` (replaces any query in `to`). */
305
+ search?: Partial<SearchOutputFor<TTo>>;
306
+ /** Replace the current history entry instead of pushing a new one. */
307
+ replace?: boolean;
308
+ /** Arbitrary history state, readable via `useLocation().state` after navigating. */
309
+ state?: unknown;
310
+ }
208
311
  export interface NavigateFn {
209
312
  <TTo extends RegisteredRoutes>(to: TTo | (string & {}), options?: NavigateOptions<TTo>): Promise<void>;
210
313
  }
211
314
  export declare function useNavigate(): NavigateFn;
315
+
316
+ // ── Fetchers ──────────────────────────────────────────────────────────────
317
+ export type FetcherState = "idle" | "loading" | "submitting";
318
+ export interface FetcherEntry {
319
+ key: string;
320
+ state: FetcherState;
321
+ data: unknown;
322
+ /** The submitted form data while a submission is in flight — the optimistic-UI source. */
323
+ formData?: FormData;
324
+ formMethod?: string;
325
+ }
326
+ export interface FetcherFormProps {
327
+ method?: "post" | "put" | "delete";
328
+ action?: string;
329
+ /** Renders a hidden `intent` input (pairs with defineActions()). */
330
+ intent?: string;
331
+ children?: ReactNode;
332
+ [key: string]: unknown;
333
+ }
212
334
  export interface FetcherResult {
213
335
  data: unknown;
214
- state: NavigationState;
336
+ state: FetcherState;
337
+ formData?: FormData;
338
+ formMethod?: string;
339
+ key: string;
215
340
  load(path: string): Promise<void>;
216
341
  submit(path: string, opts: { method: string; body: FormData | Record<string, string> }): Promise<void>;
342
+ /** A form that submits through this fetcher (no navigation, no history). */
343
+ Form: (props: FetcherFormProps) => ReactNode;
217
344
  }
218
345
  export interface StreamFetcherResult<T = unknown> {
346
+ /** @deprecated Never emitted — call `connect(actionId)` instead. Removed in 0.2. */
219
347
  events: AsyncGenerator<T>;
220
348
  connect(actionId: string): AsyncGenerator<T>;
221
349
  }
222
- export declare function useFetcher(): FetcherResult;
350
+ export interface UseFetcherOptions { key?: string; stream?: boolean }
351
+ export declare function useFetcher(opts?: { key?: string }): FetcherResult;
223
352
  export declare function useFetcher<T>(opts: { stream: true }): StreamFetcherResult<T>;
353
+ /** Every active fetcher — the cross-component view for optimistic UI. */
354
+ export declare function useFetchers(): FetcherEntry[];
355
+
356
+ // ── Revalidation ──────────────────────────────────────────────────────────
357
+ export interface Revalidator {
358
+ revalidate(): Promise<void>;
359
+ state: "idle" | "loading";
360
+ }
361
+ /** Manually re-run the active route's loaders (respects `shouldRevalidate`). */
362
+ export declare function useRevalidator(): Revalidator;
363
+
364
+ // ── Typed search ──────────────────────────────────────────────────────────
365
+ /** The current route's VALIDATED search params (its `searchSchema` output). */
366
+ export declare function useSearch<TTo extends string>(): SearchOutputFor<TTo>;
367
+ export declare function useSearch<T extends Record<string, unknown>>(): T;
368
+ export interface SetSearchOptions { replace?: boolean }
369
+ export type SetSearchFn<T extends Record<string, unknown>> = (
370
+ updater: Partial<T> | ((prev: T) => Partial<T>),
371
+ options?: SetSearchOptions,
372
+ ) => Promise<void>;
373
+ /** Merge a patch into the current search params and soft-navigate (loaders re-run). */
374
+ export declare function useSetSearch<TTo extends string>(): SetSearchFn<SearchOutputFor<TTo>>;
375
+ export declare function useSetSearch<T extends Record<string, unknown>>(): SetSearchFn<T>;
224
376
 
225
377
  export interface SearchParamsResult<T extends Record<string, string> = Record<string, string>> {
226
378
  searchParams: URLSearchParams;
@@ -316,3 +468,24 @@ export interface DevServer {
316
468
  export declare function createDevServer(options?: DevServerOptions): Promise<DevServer>;
317
469
 
318
470
  export declare function loadUserConfig(): Promise<Partial<BractJSConfig>>;
471
+
472
+ /** Identity helper for bractjs.config.ts — wrap your default export for autocomplete + type-checking. */
473
+ export declare function defineConfig(config: Partial<BractJSConfig>): Partial<BractJSConfig>;
474
+
475
+ // ── Prerendering / SPA shell ──────────────────────────────────────────────
476
+ export interface PrerenderOptions {
477
+ prerender: string[] | (() => string[] | Promise<string[]>);
478
+ appDir?: string;
479
+ publicDir?: string;
480
+ buildDir?: string;
481
+ manifest?: ServerManifest;
482
+ }
483
+ export interface PrerenderResult { written: string[] }
484
+ /** Build-time prerendering (SSG): write HTML + /_data payloads under `<buildDir>/client/_prerender/`. */
485
+ export declare function runPrerender(options: PrerenderOptions): Promise<PrerenderResult>;
486
+ /** Render the SPA-mode document shell (config `ssr: false`). */
487
+ export declare function renderSpaShell(
488
+ appDir: string,
489
+ manifest: ServerManifest,
490
+ registry?: ModuleRegistry,
491
+ ): Promise<string>;