@bractjs/bractjs 0.1.28 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -17
- package/package.json +8 -7
- package/src/__tests__/fixtures/app/routes/features-demo.tsx +28 -0
- package/src/__tests__/headers.test.ts +111 -0
- package/src/__tests__/integration.test.ts +34 -0
- package/src/__tests__/layout-registry.test.ts +7 -3
- package/src/__tests__/matcher.test.ts +29 -0
- package/src/__tests__/module-registry.test.ts +2 -3
- package/src/__tests__/route-lint.test.ts +5 -0
- package/src/__tests__/route-middleware.test.ts +84 -0
- package/src/__tests__/scanner.test.ts +46 -1
- package/src/__tests__/security-fixes.test.ts +201 -0
- package/src/__tests__/use-matches.test.ts +54 -0
- package/src/build/route-lint.ts +3 -3
- package/src/client/ClientRouter.tsx +118 -18
- package/src/client/hooks/useMatches.ts +32 -0
- package/src/client/router.tsx +7 -1
- package/src/client/rpc.ts +11 -1
- package/src/codegen/module-registry.ts +13 -21
- package/src/codegen/route-codegen.ts +8 -3
- package/src/config/load.ts +1 -0
- package/src/index.ts +11 -3
- package/src/server/action-handler.ts +1 -20
- package/src/server/adapter.ts +16 -0
- package/src/server/api-route.ts +47 -0
- package/src/server/csp.ts +9 -3
- package/src/server/csrf.ts +10 -3
- package/src/server/headers.ts +49 -0
- package/src/server/layout.ts +12 -19
- 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 +34 -16
- package/src/server/request-handler.ts +67 -27
- package/src/server/scanner.ts +45 -3
- package/src/server/search.ts +5 -1
- package/src/server/serve.ts +28 -3
- package/src/server/session.ts +12 -1
- package/src/server/validate.ts +4 -1
- package/src/shared/context.ts +3 -1
- package/src/shared/route-types.ts +108 -0
- package/types/config.d.ts +3 -0
- package/types/index.d.ts +17 -0
- package/types/route.d.ts +76 -1
|
@@ -75,6 +75,41 @@ export type MetaFunction<T = unknown> = (
|
|
|
75
75
|
args: MetaArgs<T>
|
|
76
76
|
) => MetaDescriptor[];
|
|
77
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
|
+
|
|
78
113
|
export interface BeforeLoadArgs {
|
|
79
114
|
params: Record<string, string>;
|
|
80
115
|
context: Record<string, unknown>;
|
|
@@ -105,10 +140,64 @@ export interface ShouldRevalidateArgs {
|
|
|
105
140
|
|
|
106
141
|
export type ShouldRevalidateFunction = (args: ShouldRevalidateArgs) => boolean;
|
|
107
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
|
+
|
|
108
178
|
export interface RouteModule<TLoader = unknown, TAction = unknown> {
|
|
109
179
|
loader?: LoaderFunction<TLoader>;
|
|
110
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>;
|
|
111
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[];
|
|
112
201
|
beforeLoad?: BeforeLoadFunction;
|
|
113
202
|
shouldRevalidate?: ShouldRevalidateFunction;
|
|
114
203
|
/**
|
|
@@ -141,3 +230,22 @@ export interface RouteDefinition {
|
|
|
141
230
|
parentId?: string;
|
|
142
231
|
index?: boolean;
|
|
143
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
|
+
}
|
package/types/config.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface BractJSConfig {
|
|
|
23
23
|
plugins?: BunPlugin[];
|
|
24
24
|
/** Directory for the transformed-image cache. Default: ".bract-image-cache". */
|
|
25
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;
|
|
26
29
|
/** WebSocket port for dev HMR (used by `bractjs dev` only). Default 3001. */
|
|
27
30
|
hmrPort?: number;
|
|
28
31
|
/** Custom server adapter (Cloudflare Workers, Deno, Node, etc.). Defaults to Bun.serve(). */
|
package/types/index.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ import type { MiddlewareFn, MiddlewareContext } from "./middleware.d.ts";
|
|
|
93
93
|
|
|
94
94
|
export declare class MiddlewarePipeline {
|
|
95
95
|
use(fn: MiddlewareFn): this;
|
|
96
|
+
clear(): this;
|
|
96
97
|
run(ctx: MiddlewareContext, handler: () => Promise<Response>): Promise<Response>;
|
|
97
98
|
}
|
|
98
99
|
export declare const pipeline: MiddlewarePipeline;
|
|
@@ -106,16 +107,24 @@ export declare function authGuard(options: AuthGuardOptions): MiddlewareFn;
|
|
|
106
107
|
|
|
107
108
|
// ── API routes (C1) ───────────────────────────────────────────────────────
|
|
108
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
|
+
}
|
|
109
116
|
export interface ApiRouteDefinition<TMethod extends HttpMethod, TPath extends string, TInput, TOutput> {
|
|
110
117
|
method: TMethod;
|
|
111
118
|
path: TPath;
|
|
112
119
|
handler: (input: TInput, request: Request) => TOutput | Promise<TOutput>;
|
|
120
|
+
csrf: boolean;
|
|
113
121
|
}
|
|
114
122
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
123
|
export declare function route<TMethod extends HttpMethod, TPath extends string, TInput, TOutput>(
|
|
116
124
|
method: TMethod,
|
|
117
125
|
path: TPath,
|
|
118
126
|
handler: (input: TInput, request: Request) => TOutput | Promise<TOutput>,
|
|
127
|
+
options?: ApiRouteOptions,
|
|
119
128
|
): ApiRouteDefinition<TMethod, TPath, TInput, TOutput>;
|
|
120
129
|
export type AppApiRoutes = never; // users extend this via codegen
|
|
121
130
|
|
|
@@ -163,6 +172,14 @@ export declare function formText(formData: FormData, key: string): string;
|
|
|
163
172
|
/** Collect string fields from FormData (all, or a named subset). */
|
|
164
173
|
export declare function formValues(formData: FormData, keys?: string[]): Record<string, string>;
|
|
165
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
|
+
|
|
166
183
|
// ── Search-param validation ───────────────────────────────────────────────
|
|
167
184
|
/** URLSearchParams → plain object; repeated keys collapse into arrays. */
|
|
168
185
|
export declare function searchParamsToObject(sp: URLSearchParams): Record<string, string | string[]>;
|
package/types/route.d.ts
CHANGED
|
@@ -63,6 +63,32 @@ export type ActionFunction<T = unknown> = (
|
|
|
63
63
|
|
|
64
64
|
export type MetaFunction<T = unknown> = (args: MetaArgs<T>) => MetaDescriptor[];
|
|
65
65
|
|
|
66
|
+
export interface HeadersArgs<T = unknown> {
|
|
67
|
+
loaderData: T;
|
|
68
|
+
params: Record<string, string>;
|
|
69
|
+
request: Request;
|
|
70
|
+
/** Headers merged from ancestors in the chain (root → layout → this route). */
|
|
71
|
+
parentHeaders: Headers;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A module's optional `headers` export — set response headers (`Cache-Control`,
|
|
76
|
+
* `ETag`, `Vary`, …) on the document and `/_data` responses. Runs in chain
|
|
77
|
+
* order (root → layout → route); innermost wins per key.
|
|
78
|
+
*/
|
|
79
|
+
export type HeadersFunction<T = unknown> = (args: HeadersArgs<T>) => HeadersInit;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A nested route-middleware function. Runs on the server in chain order
|
|
83
|
+
* (root → layout → route) before `beforeLoad`/action/loaders, with a shared
|
|
84
|
+
* mutable `context`. Return a `Response` to short-circuit; call `next()` to
|
|
85
|
+
* continue.
|
|
86
|
+
*/
|
|
87
|
+
export type RouteMiddlewareFunction = (
|
|
88
|
+
ctx: { request: Request; params: Record<string, string>; context: Record<string, unknown> },
|
|
89
|
+
next: () => Promise<Response>,
|
|
90
|
+
) => Promise<Response>;
|
|
91
|
+
|
|
66
92
|
export interface BeforeLoadArgs {
|
|
67
93
|
params: Record<string, string>;
|
|
68
94
|
context: Record<string, unknown>;
|
|
@@ -90,10 +116,38 @@ export interface ShouldRevalidateArgs {
|
|
|
90
116
|
|
|
91
117
|
export type ShouldRevalidateFunction = (args: ShouldRevalidateArgs) => boolean;
|
|
92
118
|
|
|
119
|
+
/** A route's browser-side loader (RR7-style). See the package docs. */
|
|
120
|
+
export interface ClientLoaderFunction<T = unknown> {
|
|
121
|
+
(args: {
|
|
122
|
+
request: Request;
|
|
123
|
+
params: Record<string, string>;
|
|
124
|
+
search: Record<string, unknown>;
|
|
125
|
+
serverLoader: () => Promise<unknown>;
|
|
126
|
+
}): Promise<T> | T;
|
|
127
|
+
/** Run on initial hydration too (default: only on client navigation). */
|
|
128
|
+
hydrate?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A route's browser-side action (RR7-style). See the package docs. */
|
|
132
|
+
export type ClientActionFunction<T = unknown> = (args: {
|
|
133
|
+
request: Request;
|
|
134
|
+
params: Record<string, string>;
|
|
135
|
+
formData: FormData;
|
|
136
|
+
serverAction: () => Promise<unknown>;
|
|
137
|
+
}) => Promise<T> | T;
|
|
138
|
+
|
|
93
139
|
export interface RouteModule<TLoader = unknown, TAction = unknown> {
|
|
94
140
|
loader?: LoaderFunction<TLoader>;
|
|
95
141
|
action?: ActionFunction<TAction>;
|
|
142
|
+
/** Browser-side loader; see {@link ClientLoaderFunction}. */
|
|
143
|
+
clientLoader?: ClientLoaderFunction<TLoader>;
|
|
144
|
+
/** Browser-side action; see {@link ClientActionFunction}. */
|
|
145
|
+
clientAction?: ClientActionFunction<TAction>;
|
|
96
146
|
meta?: MetaFunction<TLoader>;
|
|
147
|
+
/** Set response headers (Cache-Control/ETag/Vary/…) for this route's document and `/_data` responses. Chain order, innermost wins. */
|
|
148
|
+
headers?: HeadersFunction<TLoader>;
|
|
149
|
+
/** Nested middleware (root → layout → route), shared mutable `context`, runs before beforeLoad/action/loaders. A single fn or an array. */
|
|
150
|
+
middleware?: RouteMiddlewareFunction | RouteMiddlewareFunction[];
|
|
97
151
|
beforeLoad?: BeforeLoadFunction;
|
|
98
152
|
shouldRevalidate?: ShouldRevalidateFunction;
|
|
99
153
|
/** Zod/Valibot-compatible schema validating search params before loaders run (400 on failure). */
|
|
@@ -111,7 +165,28 @@ export interface RouteModule<TLoader = unknown, TAction = unknown> {
|
|
|
111
165
|
default?: ComponentType;
|
|
112
166
|
}
|
|
113
167
|
|
|
114
|
-
|
|
168
|
+
/**
|
|
169
|
+
* One entry in the matched route chain (see `useMatches`), outermost → innermost:
|
|
170
|
+
* root, layouts, then the leaf route.
|
|
171
|
+
*/
|
|
172
|
+
export interface RouteMatch<TData = unknown, THandle = Record<string, unknown>> {
|
|
173
|
+
/** Stable id of the matched module — its appDir-relative file path. */
|
|
174
|
+
id: string;
|
|
175
|
+
/** The active URL pathname (shared across the chain). */
|
|
176
|
+
pathname: string;
|
|
177
|
+
/** The matched route params (shared across the chain). */
|
|
178
|
+
params: Record<string, string>;
|
|
179
|
+
/** This module's loader data slice. */
|
|
180
|
+
data: TData;
|
|
181
|
+
/** This module's static `handle` export, or `undefined`. */
|
|
182
|
+
handle: THandle | undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type Segment =
|
|
186
|
+
| string
|
|
187
|
+
| { param: string }
|
|
188
|
+
| { optional: string }
|
|
189
|
+
| { catchAll: string };
|
|
115
190
|
|
|
116
191
|
export interface RouteFile {
|
|
117
192
|
filePath: string;
|