@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.
- package/bin/cli.ts +18 -1
- package/package.json +3 -2
- package/src/__tests__/codegen-write.test.ts +67 -0
- package/src/__tests__/codegen.test.ts +29 -2
- package/src/__tests__/compile-safety.test.ts +4 -0
- package/src/__tests__/csp.test.ts +10 -0
- package/src/__tests__/define-actions.test.ts +69 -0
- package/src/__tests__/env.test.ts +18 -0
- package/src/__tests__/fetcher-store.test.ts +67 -0
- package/src/__tests__/fixtures/app/root.tsx +7 -2
- package/src/__tests__/fixtures/app/routes/boom.tsx +9 -0
- package/src/__tests__/fixtures/app/routes/client-only.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/counter.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/data-only.tsx +16 -0
- package/src/__tests__/fixtures/app/routes/features-demo.tsx +28 -0
- package/src/__tests__/fixtures/app/routes/intent-demo.tsx +46 -0
- package/src/__tests__/fixtures/app/routes/protected-client-only.tsx +15 -0
- package/src/__tests__/fixtures/app/routes/search-demo.tsx +39 -0
- package/src/__tests__/form-data-helpers.test.ts +43 -0
- package/src/__tests__/headers.test.ts +111 -0
- package/src/__tests__/integration.test.ts +90 -0
- package/src/__tests__/layout-registry.test.ts +7 -3
- package/src/__tests__/loader.test.ts +32 -1
- package/src/__tests__/matcher.test.ts +29 -0
- package/src/__tests__/module-registry.test.ts +2 -3
- package/src/__tests__/nav-utils.test.ts +46 -0
- package/src/__tests__/prerender.test.ts +102 -0
- package/src/__tests__/programmatic-api.test.ts +20 -1
- package/src/__tests__/revalidation.test.ts +65 -0
- package/src/__tests__/route-lint.test.ts +79 -0
- package/src/__tests__/route-middleware.test.ts +84 -0
- package/src/__tests__/route-table.test.ts +33 -0
- package/src/__tests__/safe-validate.test.ts +96 -0
- package/src/__tests__/scanner.test.ts +46 -1
- package/src/__tests__/scroll-restoration.test.ts +66 -0
- package/src/__tests__/search-serializer.test.ts +42 -0
- package/src/__tests__/search-validation.test.ts +125 -0
- package/src/__tests__/security-fixes.test.ts +201 -0
- package/src/__tests__/security.test.ts +110 -1
- package/src/__tests__/selective-ssr.test.ts +85 -0
- package/src/__tests__/spa-mode.test.ts +77 -0
- package/src/__tests__/typed-routing.test.ts +51 -1
- package/src/__tests__/use-matches.test.ts +54 -0
- package/src/build/bundler.ts +33 -0
- package/src/build/prerender.ts +88 -0
- package/src/build/route-lint.ts +49 -0
- package/src/client/ClientRouter.tsx +339 -47
- package/src/client/cache.ts +8 -0
- package/src/client/components/Await.tsx +9 -2
- package/src/client/components/Form.tsx +23 -34
- package/src/client/components/Link.tsx +80 -9
- package/src/client/components/Outlet.tsx +8 -2
- package/src/client/components/ScrollRestoration.tsx +125 -0
- package/src/client/entry.tsx +39 -2
- package/src/client/fetcher-store.ts +61 -0
- package/src/client/form-utils.ts +3 -0
- package/src/client/hooks/useActionData.ts +7 -3
- package/src/client/hooks/useFetcher.ts +116 -33
- package/src/client/hooks/useFetchers.ts +23 -0
- package/src/client/hooks/useLoaderData.ts +8 -4
- package/src/client/hooks/useLocation.ts +27 -0
- package/src/client/hooks/useMatches.ts +32 -0
- package/src/client/hooks/useNavigate.ts +11 -6
- package/src/client/hooks/useRevalidator.ts +26 -0
- package/src/client/hooks/useSearch.ts +73 -0
- package/src/client/hooks/useSearchParams.ts +7 -2
- package/src/client/nav-utils.ts +26 -0
- package/src/client/prefetch.ts +110 -15
- package/src/client/registry.ts +24 -0
- package/src/client/revalidation.ts +25 -0
- package/src/client/router.tsx +34 -1
- package/src/client/rpc.ts +11 -1
- package/src/client/scroll-restoration.ts +48 -0
- package/src/client/search-serializer.ts +40 -0
- package/src/client/types.ts +6 -0
- package/src/codegen/module-registry.ts +13 -21
- package/src/codegen/route-codegen.ts +148 -10
- package/src/config/load.ts +22 -0
- package/src/dev/hmr-client.ts +3 -1
- package/src/dev/route-table.ts +27 -0
- package/src/dev/server.ts +106 -8
- package/src/dev/watcher.ts +25 -3
- package/src/index.ts +38 -6
- package/src/server/action-handler.ts +3 -13
- package/src/server/action-registry.ts +35 -0
- package/src/server/adapter.ts +16 -0
- package/src/server/api-route.ts +47 -0
- package/src/server/csp.ts +19 -4
- package/src/server/csrf.ts +36 -3
- package/src/server/env.ts +26 -5
- package/src/server/headers.ts +49 -0
- package/src/server/layout.ts +43 -20
- package/src/server/loader.ts +14 -8
- package/src/server/matcher.ts +29 -2
- package/src/server/matches.ts +50 -0
- package/src/server/middleware.ts +66 -0
- package/src/server/proto-guard.ts +56 -0
- package/src/server/render.ts +51 -18
- package/src/server/request-handler.ts +111 -29
- package/src/server/scanner.ts +45 -3
- package/src/server/search.ts +47 -0
- package/src/server/serve.ts +116 -4
- package/src/server/session.ts +12 -1
- package/src/server/spa.ts +62 -0
- package/src/server/stream-handler.ts +10 -1
- package/src/server/validate.ts +89 -14
- package/src/shared/context.ts +7 -0
- package/src/shared/define-actions.ts +39 -0
- package/src/shared/form-data.ts +34 -0
- package/src/shared/route-types.ts +191 -2
- package/templates/new-app/app/root.tsx +2 -1
- package/templates/new-app/bractjs.config.ts +7 -12
- package/types/config.d.ts +24 -0
- package/types/index.d.ts +182 -9
- package/types/route.d.ts +138 -3
- package/LICENSE +0 -21
- package/README.md +0 -1125
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
import type { Deferred } from "./deferred.ts";
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
|
1
|
+
import { defineConfig } from "@bractjs/bractjs";
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
export declare function
|
|
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> {
|
|
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:
|
|
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
|
|
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>;
|