@stacksjs/router 0.74.47 → 0.74.48
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/dist/health.js +1 -1
- package/dist/stacks-router.d.ts +430 -2
- package/package.json +16 -16
package/dist/health.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export async function runHealthProbes(probes,options={}){const timeoutMs=options.timeoutMs??1500,now=options.now??Date.now,entries=await Promise.all(probes.map(async({name,run})=>{const startedAt=now();let timeout;try{await Promise.race([run(),new Promise((_resolve,reject)=>{timeout=setTimeout(()=>reject(Error("timeout")),timeoutMs)})]);return[name,{ok:!0,ms:Math.max(0,now()-startedAt)}]}catch(error){return[name,{ok:!1,ms:Math.max(0,now()-startedAt),message:error instanceof Error?error.message:String(error)}]}finally{if(timeout)clearTimeout(timeout)}})),checks=Object.fromEntries(entries);return{status:Object.values(checks).every((check)=>check.ok)?"healthy":"degraded",checks,timestamp:now()}}export async function checkApplicationHealth(options={}){return runHealthProbes([{name:"database",async run(){const{db}=await import("@stacksjs/database"),unsafe=db.unsafe;if(typeof unsafe!=="function")throw Error("database driver does not expose a raw health probe");await unsafe.call(db,"SELECT 1")}},{name:"cache",async run(){const{cache}=await import("@stacksjs/cache"),key=`__health__:${Date.now()}`;let stored=!1;try{await cache.set(key,1,5);stored=!0}finally{if(stored)await cache.del(key)}}}],options)}
|
|
1
|
+
export async function runHealthProbes(probes,options={}){const timeoutMs=options.timeoutMs??1500,now=options.now??Date.now,entries=await Promise.all(probes.map(async({name,run})=>{const startedAt=now();let timeout;try{await Promise.race([run(),new Promise((_resolve,reject)=>{timeout=setTimeout(()=>reject(Error("timeout")),timeoutMs)})]);return[name,{ok:!0,ms:Math.max(0,now()-startedAt)}]}catch(error){return[name,{ok:!1,ms:Math.max(0,now()-startedAt),message:error instanceof Error?error.message:String(error)}]}finally{if(timeout)clearTimeout(timeout)}})),checks=Object.fromEntries(entries);return{status:Object.values(checks).every((check)=>check.ok)?"healthy":"degraded",checks,timestamp:now()}}export async function checkApplicationHealth(options={}){return runHealthProbes([{name:"database",async run(){const{db}=await import("@stacksjs/database/runtime"),unsafe=db.unsafe;if(typeof unsafe!=="function")throw Error("database driver does not expose a raw health probe");await unsafe.call(db,"SELECT 1")}},{name:"cache",async run(){const{cache}=await import("@stacksjs/cache"),key=`__health__:${Date.now()}`;let stored=!1;try{await cache.set(key,1,5);stored=!0}finally{if(stored)await cache.del(key)}}}],options)}
|
package/dist/stacks-router.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { Server } from 'bun';
|
|
1
2
|
import './request-augmentation';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
3
|
+
import { response } from '@stacksjs/bun-router';
|
|
4
|
+
import { Router } from '@stacksjs/bun-router';
|
|
5
|
+
import type { ActionHandler, ActionPath, EnhancedRequest, ExtractRouteParams, KnownRouteName, MiddlewareHandler as BunMiddlewareHandler, MiddlewareReference, PathForRouteName, RequestFor, Route, ServerOptions } from '@stacksjs/bun-router';
|
|
6
|
+
import type { ActionResult, ActionValidations, ValidationResult } from '@stacksjs/actions';
|
|
4
7
|
/**
|
|
5
8
|
* Run every registered boot hook, once per process.
|
|
6
9
|
*
|
|
@@ -21,9 +24,219 @@ export declare function warnOnMultipleRouterInstances(): boolean;
|
|
|
21
24
|
* overlapping constrained, domain-scoped, or mixed-parameter route expected.
|
|
22
25
|
*/
|
|
23
26
|
export declare function shouldUseNativeRoutesByDefault(routes: readonly Route[]): boolean;
|
|
27
|
+
// False positive: this is an overload signature, which has no body for its
|
|
28
|
+
// parameters to be used in. The implementation below uses them.
|
|
29
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
30
|
+
export declare function url<TName extends KnownRouteName>(routeName: TName, ...params: RequiresUrlParams<TName> extends true ? [params: UrlParams<TName>] : [params?: UrlParams<TName>]): string;
|
|
31
|
+
/**
|
|
32
|
+
* List the placeholder names a named route expects — handy for
|
|
33
|
+
* codegen/test cases and for detecting typos before runtime.
|
|
34
|
+
*/
|
|
35
|
+
export declare function routeParams(routeName: string): string[];
|
|
36
|
+
/**
|
|
37
|
+
* Every named route, as `name → path`.
|
|
38
|
+
*
|
|
39
|
+
* `listRegisteredRoutes()` reports a name by searching the named registry for a
|
|
40
|
+
* matching path, which answers "what is this route called" and cannot answer
|
|
41
|
+
* "what routes are there names for" - two routes on one path give the first
|
|
42
|
+
* name found, and a name whose route was never registered is invisible.
|
|
43
|
+
*
|
|
44
|
+
* The type generator needs the second question: it writes this map into the
|
|
45
|
+
* router's type registry so `url('users.shwo')` stops compiling. Reading the
|
|
46
|
+
* registry directly is the only way to get every name.
|
|
47
|
+
*/
|
|
48
|
+
export declare function listNamedRoutes(): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Snapshot of the registered routes — `{ method, path, name?, handler? }` per
|
|
51
|
+
* route. Used by `buddy route:list`, the dev-server startup banner, and the
|
|
52
|
+
* OpenAPI generator.
|
|
53
|
+
*
|
|
54
|
+
* `handler` is the string a route was registered with (`'Actions/…'`), and it
|
|
55
|
+
* is absent for an inline function, which has no file to read anything out of.
|
|
56
|
+
* The OpenAPI generator needs it to find the action's `validations`: it used to
|
|
57
|
+
* guess the path by title-casing the route *name* (`posts.store` →
|
|
58
|
+
* `Actions/PostsStoreAction`), which found a schema for the handful of routes
|
|
59
|
+
* following that convention and silently described every other endpoint as
|
|
60
|
+
* taking no input at all. The registry has known the real answer the whole
|
|
61
|
+
* time; it was simply not being reported.
|
|
62
|
+
*/
|
|
63
|
+
export declare function listRegisteredRoutes(): Array<{ method: string, path: string, name?: string, handler?: string, action?: RouterAction, middleware?: string[] }>;
|
|
64
|
+
/**
|
|
65
|
+
* The merged middleware alias map: the framework defaults, with the
|
|
66
|
+
* application's own map on top.
|
|
67
|
+
*
|
|
68
|
+
* Exported for diagnostics and tests. A copy, so a caller cannot edit the
|
|
69
|
+
* cached map out from under the router.
|
|
70
|
+
*/
|
|
71
|
+
export declare function middlewareAliases(): Promise<Record<string, string>>;
|
|
72
|
+
/*.ts` as the single middleware registry instead of asking
|
|
73
|
+
* the application to define a second set of anonymous page-only functions.
|
|
74
|
+
*/
|
|
75
|
+
export declare function loadMiddlewareHandlers(): Promise<Record<string, MiddlewareHandler>>;
|
|
76
|
+
/**
|
|
77
|
+
* Clear the middleware cache (useful for hot-reload in development).
|
|
78
|
+
*
|
|
79
|
+
* `installMiddlewareHotReload()` will wire this up automatically when
|
|
80
|
+
* called from the dev server — production should never invoke it.
|
|
81
|
+
*/
|
|
82
|
+
export declare function clearMiddlewareCache(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Watch `app/Middleware/` and `app/Middleware.ts` and invalidate the
|
|
85
|
+
* cached middleware modules whenever a file changes. Intended for the
|
|
86
|
+
* dev server only — calling this in production is a no-op (the
|
|
87
|
+
* watcher handle is created but never fires anything user code cares
|
|
88
|
+
* about). Returns a `disposer()` to stop watching.
|
|
89
|
+
*
|
|
90
|
+
* Without this hook, editing a middleware file in dev requires a
|
|
91
|
+
* full server restart to see the change — the import map caches the
|
|
92
|
+
* old version forever.
|
|
93
|
+
*/
|
|
94
|
+
export declare function installMiddlewareHotReload(): () => void;
|
|
95
|
+
/**
|
|
96
|
+
* Drop every registered route-state mapping.
|
|
97
|
+
*
|
|
98
|
+
* The registry is module-scoped and lives for the life of the process, which
|
|
99
|
+
* is what `listRegisteredRoutes()` and the boot-time validators read. That is
|
|
100
|
+
* correct at runtime — routes are registered once at boot — but it means a
|
|
101
|
+
* test that registers a route keeps it visible to every later caller in the
|
|
102
|
+
* same process, including `assertRouteMiddlewareResolvable()`. A suite that
|
|
103
|
+
* deliberately registers an unresolvable alias therefore made an unrelated
|
|
104
|
+
* file's `serverResponse()` throw on routes it never declared.
|
|
105
|
+
*
|
|
106
|
+
* `clearMiddlewareCache()` deliberately does NOT do this: it flushes resolved
|
|
107
|
+
* middleware so a hot-reloaded file is re-read, while the routes themselves
|
|
108
|
+
* must survive. Clearing the route table is a separate, coarser action.
|
|
109
|
+
*/
|
|
110
|
+
export declare function clearRouteMiddlewareRegistry(): void;
|
|
111
|
+
/**
|
|
112
|
+
* Resolve every middleware alias referenced by a registered route and
|
|
113
|
+
* report the ones that don't load. `csrf` is always checked too — it's
|
|
114
|
+
* auto-injected on unsafe methods even when no route lists it.
|
|
115
|
+
*
|
|
116
|
+
* Resolution is inherently lazy (`.middleware(name)` is a sync chainable
|
|
117
|
+
* that just records a string; the alias map and middleware modules load
|
|
118
|
+
* via async dynamic import), so a throw at literal registration time is
|
|
119
|
+
* impossible. Calling this after all routes are registered — the end of
|
|
120
|
+
* `importRoutes()` and the compiled-binary boot in core/server — IS
|
|
121
|
+
* effectively registration-time validation. See stacksjs/stacks#1957.
|
|
122
|
+
*/
|
|
123
|
+
export declare function findUnresolvableRouteMiddleware(): Promise<Array<{ alias: string, routes: string[] }>>;
|
|
124
|
+
/**
|
|
125
|
+
* Throw when any registered route references a middleware alias that
|
|
126
|
+
* cannot be resolved. Fail-closed boot validation: a typo'd `auth` alias
|
|
127
|
+
* must abort startup loudly, not serve the route unprotected (the
|
|
128
|
+
* request-time guard in createMiddlewareHandler 500s as a backstop).
|
|
129
|
+
*/
|
|
130
|
+
export declare function assertRouteMiddlewareResolvable(): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Whether this request is asking "would this be accepted?" rather than
|
|
133
|
+
* "do this" (stacksjs/stacks#2226).
|
|
134
|
+
*
|
|
135
|
+
* An Action's `validations:` block was a server-only artifact: nothing could
|
|
136
|
+
* read it from a template or a client script, so every form in every app
|
|
137
|
+
* retyped the rules in the browser and the two copies drifted. The framework's
|
|
138
|
+
* own defaults demonstrated it — the browser refused a 7-character password
|
|
139
|
+
* that `POST /register` would have accepted.
|
|
140
|
+
*
|
|
141
|
+
* Rather than serialise the rules (a `schema.*` chain is a live object with
|
|
142
|
+
* no faithful JSON projection), the client asks the real endpoint to run the
|
|
143
|
+
* real rules and stops short of the side effect. Same shape as Laravel
|
|
144
|
+
* Precognition, and deliberately header-compatible with it.
|
|
145
|
+
*
|
|
146
|
+
* `?_validate=1` is accepted too: a header cannot be set on a plain form
|
|
147
|
+
* submission or a `<script client>` block using a bare fetch shorthand, and
|
|
148
|
+
* requiring one would have put this out of reach of the simplest caller.
|
|
149
|
+
*/
|
|
150
|
+
export declare function precognitionRequest(req: EnhancedRequest, knownHeader?: string | null): { only: string[] } | null;
|
|
151
|
+
/**
|
|
152
|
+
* The "nothing to report" answer to a precognition request. 204 rather than an
|
|
153
|
+
* empty 200 so a client cannot mistake it for the action's real result.
|
|
154
|
+
*
|
|
155
|
+
* `Vary` because the same URL and method now has two different answers
|
|
156
|
+
* depending on a request header — without it a shared cache is entitled to
|
|
157
|
+
* serve this 204 to a caller that meant to submit.
|
|
158
|
+
*/
|
|
159
|
+
export declare function precognitionSuccess(): Response;
|
|
160
|
+
/** Whether a route handler is an action rather than a plain function. */
|
|
161
|
+
export declare function isRouterAction(handler: unknown): handler is RouterAction;
|
|
162
|
+
/**
|
|
163
|
+
* Turn a resolved action into the function the route actually runs.
|
|
164
|
+
*
|
|
165
|
+
* Split out of the string-resolution path so registering an action by import
|
|
166
|
+
* (`createTypedRouter().get('/x', ShowAction)`) and registering it by name
|
|
167
|
+
* (`route.get('/x', 'Actions/ShowAction')`) share one runtime path. The two
|
|
168
|
+
* forms differ only in when the module is loaded and in what the compiler can
|
|
169
|
+
* see; everything below - validation, precognition, `authorize`, `before`,
|
|
170
|
+
* result formatting, error reporting - is the same code for both.
|
|
171
|
+
*
|
|
172
|
+
* `handlerKey` identifies the action for the CSRF skip cache and for error
|
|
173
|
+
* labels. It is the handler string when there is one, and the route key
|
|
174
|
+
* otherwise.
|
|
175
|
+
*/
|
|
176
|
+
export declare function wrapAction(action: RouterAction, handlerKey: string): RouteHandlerFn;
|
|
177
|
+
declare function createValidationFieldLabel(field: string): { label: string, decorate: (message: string) => string };
|
|
178
|
+
export declare function validateActionInput(req: EnhancedRequest, validations: ActionValidations): Promise<ValidationResult>;
|
|
179
|
+
export declare function stream(source: ReadableStream | AsyncIterable<string | Uint8Array>, options?: StreamOptions): Response;
|
|
180
|
+
/** Test / hot-reload seam: forget the resolved module. */
|
|
181
|
+
export declare function clearCsrfModuleCache(): void;
|
|
182
|
+
export declare function enhanceRequest(req: EnhancedRequest, initializeRequestId?: boolean): EnhancedRequest;
|
|
183
|
+
/**
|
|
184
|
+
* Create a Stacks-enhanced router
|
|
185
|
+
*/
|
|
186
|
+
export declare function createStacksRouter(config?: StacksRouterConfig): StacksRouterInstance;
|
|
187
|
+
/**
|
|
188
|
+
* Tell the file-based view renderer where this application keeps its templates.
|
|
189
|
+
*
|
|
190
|
+
* Two different programs render `.stx` in a Stacks app and they were not
|
|
191
|
+
* agreeing. `buddy dev` and the production server go through
|
|
192
|
+
* `bun-plugin-stx`'s own `serve()`, which is handed the components, layouts and
|
|
193
|
+
* partials directories explicitly. `route.serve()` goes through bun-router's
|
|
194
|
+
* file routing, which was handed nothing - so its components directory fell
|
|
195
|
+
* back to `<viewsDir>/components`, and `resources/components`, where every
|
|
196
|
+
* Stacks app actually keeps them, was never looked in.
|
|
197
|
+
*
|
|
198
|
+
* The failure is silent in the direction that hides bugs. The page still
|
|
199
|
+
* answers 200; the component is replaced inline with
|
|
200
|
+
* `[Error loading component: ENOENT ...]`. So a test that boots the router and
|
|
201
|
+
* asserts on rendered HTML cannot see any component at all, and reads as though
|
|
202
|
+
* the feature under test were missing rather than the harness.
|
|
203
|
+
*
|
|
204
|
+
* Only fills in what has not been set: an application that called
|
|
205
|
+
* `route.bunRouter.views(...)` itself has said something more specific, and
|
|
206
|
+
* this must not overwrite it.
|
|
207
|
+
*
|
|
208
|
+
* **It deliberately does not set `viewsPath`.** Naming one switches on
|
|
209
|
+
* file-based route discovery, which walks the whole views tree - so an
|
|
210
|
+
* application that never asked for file routing would start doing it because a
|
|
211
|
+
* directory happened to exist. Where the views are is already decided
|
|
212
|
+
* elsewhere; the only thing missing was where the *components* are.
|
|
213
|
+
*/
|
|
214
|
+
export declare function configureViewDirectories(bunRouter: Router): void;
|
|
215
|
+
/*.ts` works.
|
|
216
|
+
*
|
|
217
|
+
* The two functions are coupled and the coupling is bun-router's, not ours:
|
|
218
|
+
* `serve()` calls {@link configureViewDirectories} after this, and that only
|
|
219
|
+
* backs off because `disableFileRouting()` happens to leave a non-empty
|
|
220
|
+
* `_fileRoutingConfig` behind. Were that flag ever to move to a field of its
|
|
221
|
+
* own, `configureViewDirectories` would overwrite the config and re-mount every
|
|
222
|
+
* template. `api-view-routing.test.ts` drives both, in that order, so the
|
|
223
|
+
* upgrade that changes it fails a test rather than quietly restoring the site
|
|
224
|
+
* to the API port.
|
|
225
|
+
*
|
|
226
|
+
* @returns whether file routing was switched off by this call.
|
|
227
|
+
*/
|
|
228
|
+
export declare function disableViewRouting(bunRouter: Router): boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Handle a server request through the router
|
|
231
|
+
* This is the main entry point for the Stacks server
|
|
232
|
+
*/
|
|
233
|
+
export declare function serverResponse(request: Request, _body?: string): Promise<Response>;
|
|
234
|
+
// Export serve function that uses the default router
|
|
235
|
+
export declare function serve(options?: ServerOptions): Promise<Server<unknown>>;
|
|
24
236
|
declare const CSRF_DEFAULT: 0;
|
|
25
237
|
declare const CSRF_SKIPPED: 1;
|
|
26
238
|
declare const CSRF_REQUIRED: 2;
|
|
239
|
+
export declare const route: StacksRouterInstance;
|
|
27
240
|
/**
|
|
28
241
|
* Warn (once per process) when more than one @stacksjs/router module has loaded
|
|
29
242
|
* (stacksjs/stacks#1975 / #1982). Routing still works — the route table and
|
|
@@ -70,6 +283,11 @@ export declare interface StacksRouterConfig {
|
|
|
70
283
|
csrf?: boolean
|
|
71
284
|
requestContext?: boolean
|
|
72
285
|
}
|
|
286
|
+
declare interface GroupOptions {
|
|
287
|
+
prefix?: string
|
|
288
|
+
middleware?: MiddlewareReference | MiddlewareReference[]
|
|
289
|
+
apiResponse?: boolean
|
|
290
|
+
}
|
|
73
291
|
/** The file-name half each CRUD action is composed from. */
|
|
74
292
|
declare interface ResourceKindOf {
|
|
75
293
|
index: 'Index'
|
|
@@ -88,6 +306,118 @@ export declare interface ChainableRoute {
|
|
|
88
306
|
requireCsrf: () => ChainableRoute
|
|
89
307
|
rateLimit: (max: number, window: 'second' | 'minute' | 'hour' | 'day' | number) => ChainableRoute
|
|
90
308
|
}
|
|
309
|
+
/** Represents a middleware module with a handle method */
|
|
310
|
+
declare interface MiddlewareHandler {
|
|
311
|
+
handle: (req: EnhancedRequest) => Promise<void> | void
|
|
312
|
+
priority?: number
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Anything the router is willing to treat as an action.
|
|
316
|
+
*
|
|
317
|
+
* Structural rather than `instanceof Action`, because an action can also be a
|
|
318
|
+
* plain object with a `handle()` - the framework's own defaults include both
|
|
319
|
+
* shapes - and because a duplicated install would make an `instanceof` check
|
|
320
|
+
* reject a perfectly good action from the other copy of the package.
|
|
321
|
+
*/
|
|
322
|
+
export declare interface RouterAction {
|
|
323
|
+
handle: (req: any) => unknown
|
|
324
|
+
validations?: ActionValidations
|
|
325
|
+
authorize?: (req: any) => unknown
|
|
326
|
+
before?: (req: any) => unknown
|
|
327
|
+
skipCsrf?: boolean
|
|
328
|
+
csrf?: boolean
|
|
329
|
+
apiResponse?: boolean
|
|
330
|
+
model?: unknown
|
|
331
|
+
modelDefinition?: unknown
|
|
332
|
+
name?: string
|
|
333
|
+
responses?: unknown
|
|
334
|
+
responseHeaders?: unknown
|
|
335
|
+
requestHeaders?: unknown
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Helper for streaming responses — wraps a `ReadableStream` or async
|
|
339
|
+
* generator with the right headers for the chosen content type.
|
|
340
|
+
*
|
|
341
|
+
* Common shapes:
|
|
342
|
+
*
|
|
343
|
+
* ```ts
|
|
344
|
+
* // Server-Sent Events
|
|
345
|
+
* return stream(async function* () {
|
|
346
|
+
* for await (const evt of source) yield `data: ${JSON.stringify(evt)}\n\n`
|
|
347
|
+
* }, { type: 'sse' })
|
|
348
|
+
*
|
|
349
|
+
* // Chunked JSON (NDJSON) — one JSON object per line
|
|
350
|
+
* return stream(async function* () {
|
|
351
|
+
* for await (const row of rows) yield `${JSON.stringify(row)}\n`
|
|
352
|
+
* }, { type: 'ndjson' })
|
|
353
|
+
*
|
|
354
|
+
* // Raw bytes — caller supplies a ReadableStream of Uint8Array chunks
|
|
355
|
+
* return stream(myReadable, { contentType: 'application/octet-stream' })
|
|
356
|
+
* ```
|
|
357
|
+
*
|
|
358
|
+
* The wrapper sets `Cache-Control: no-cache` and `Connection: keep-alive`
|
|
359
|
+
* for SSE — the two headers a sane proxy / browser pair won't ignore — and
|
|
360
|
+
* leaves backpressure / cancellation to the underlying stream.
|
|
361
|
+
*
|
|
362
|
+
* See stacksjs/stacks#1870 R-4.
|
|
363
|
+
*/
|
|
364
|
+
export declare interface StreamOptions {
|
|
365
|
+
type?: 'sse' | 'ndjson'
|
|
366
|
+
contentType?: string
|
|
367
|
+
headers?: HeadersInit
|
|
368
|
+
status?: number
|
|
369
|
+
}
|
|
370
|
+
export declare interface StacksRouterInstance {
|
|
371
|
+
bunRouter: Router
|
|
372
|
+
routes: Route[]
|
|
373
|
+
get: {
|
|
374
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
375
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
376
|
+
}
|
|
377
|
+
post: {
|
|
378
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
379
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
380
|
+
}
|
|
381
|
+
put: {
|
|
382
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
383
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
384
|
+
}
|
|
385
|
+
patch: {
|
|
386
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
387
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
388
|
+
}
|
|
389
|
+
delete: {
|
|
390
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
391
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
392
|
+
}
|
|
393
|
+
options: {
|
|
394
|
+
<TPath extends string>(path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
395
|
+
(path: string, handler: StacksHandler): ChainableRoute
|
|
396
|
+
}
|
|
397
|
+
group: (options: GroupOptions, callback: () => void | Promise<void>) => StacksRouterInstance | Promise<StacksRouterInstance>
|
|
398
|
+
resource: <TBase extends string, TOnly extends ResourceAction = never, TExcept extends ResourceAction = never>(
|
|
399
|
+
name: string,
|
|
400
|
+
handler: TBase & ResourceBaseCheck<TBase, ActiveResourceActions<TOnly, TExcept>>,
|
|
401
|
+
options?: {
|
|
402
|
+
only?: readonly TOnly[]
|
|
403
|
+
except?: readonly TExcept[]
|
|
404
|
+
middleware?: MiddlewareReference | MiddlewareReference[]
|
|
405
|
+
},
|
|
406
|
+
) => StacksRouterInstance
|
|
407
|
+
match: {
|
|
408
|
+
<TPath extends string>(methods: string[], path: TPath, handler: TypedInlineRouteHandler<TPath>): ChainableRoute
|
|
409
|
+
(methods: string[], path: string, handler: StacksHandler): ChainableRoute
|
|
410
|
+
}
|
|
411
|
+
health: () => StacksRouterInstance
|
|
412
|
+
use: (middleware: ActionHandler | BunMiddlewareHandler) => StacksRouterInstance
|
|
413
|
+
register: (routePath: string, options?: { prefix?: string, middleware?: MiddlewareReference | MiddlewareReference[] }) => Promise<StacksRouterInstance>
|
|
414
|
+
booting: (name: string, run: () => void | Promise<void>) => StacksRouterInstance
|
|
415
|
+
serve: (options?: ServerOptions) => Promise<Server<unknown>>
|
|
416
|
+
handleRequest: (req: Request) => Promise<Response>
|
|
417
|
+
getAllowedMethods: (pathname: string, domain?: string) => string[]
|
|
418
|
+
importRoutes: () => Promise<void>
|
|
419
|
+
loadDiscoveredRoutes: () => Promise<void>
|
|
420
|
+
}
|
|
91
421
|
declare type RouteHandlerFn = (_req: EnhancedRequest) => Response | Promise<Response>;
|
|
92
422
|
declare type AsyncRouteHandlerFn = (
|
|
93
423
|
req: EnhancedRequest,
|
|
@@ -155,3 +485,101 @@ declare type ResourceBaseCheck<TBase extends string, TActive extends ResourceAct
|
|
|
155
485
|
? unknown
|
|
156
486
|
: { 'these actions do not exist': MissingResourceActions<TBase, TActive> }
|
|
157
487
|
declare type RouteCsrfMode = typeof CSRF_DEFAULT | typeof CSRF_SKIPPED | typeof CSRF_REQUIRED;
|
|
488
|
+
declare type CorsHeaderApplier = (req: Request, res: Response, cfg?: unknown) => Response;
|
|
489
|
+
declare type CompressionApplier = (req: Request, res: Response, bodySizeUpperBound?: number) => Promise<Response>;
|
|
490
|
+
/**
|
|
491
|
+
* Generate a full URL for a named route, like Laravel's route() helper.
|
|
492
|
+
*
|
|
493
|
+
* Validates path parameters at call time so a typo'd argument
|
|
494
|
+
* (`url('user.post', { userId: 1 })` against `/users/{id}`) throws
|
|
495
|
+
* immediately with a list of expected names instead of silently
|
|
496
|
+
* producing a URL with `{id}` left literal in the path.
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```typescript
|
|
500
|
+
* // Define a named route
|
|
501
|
+
* route.get('/api/email/unsubscribe', 'Actions/UnsubscribeAction').name('email.unsubscribe')
|
|
502
|
+
*
|
|
503
|
+
* // Generate URL
|
|
504
|
+
* url('email.unsubscribe', { token: 'abc-123' })
|
|
505
|
+
* // → https://stacksjs.com/api/email/unsubscribe?token=abc-123
|
|
506
|
+
*
|
|
507
|
+
* // With path parameters
|
|
508
|
+
* route.get('/users/{id}/posts/{postId}', handler).name('user.post')
|
|
509
|
+
* url('user.post', { id: 42, postId: 7 })
|
|
510
|
+
* // → https://stacksjs.com/users/42/posts/7
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
/**
|
|
514
|
+
* The params a named route needs, plus anything else that becomes query string.
|
|
515
|
+
*
|
|
516
|
+
* The required half comes from the path the name resolves to, so
|
|
517
|
+
* `url('user.post', { id: 42 })` against `/users/{id}/posts/{postId}` stops
|
|
518
|
+
* compiling instead of throwing at call time. Extra keys stay allowed: the
|
|
519
|
+
* implementation appends whatever it did not consume as a query string, which
|
|
520
|
+
* is what `url('email.unsubscribe', { token })` relies on.
|
|
521
|
+
*/
|
|
522
|
+
export type UrlParams<TName extends string> = { [K in keyof ExtractRouteParams<PathForRouteName<TName>>]: string | number }
|
|
523
|
+
& Record<string, string | number>;
|
|
524
|
+
/** The keys of `T` that are not optional. */
|
|
525
|
+
declare type RequiredParamKeys<T> = { [K in keyof T]-?: object extends Pick<T, K> ? never : K }[keyof T];
|
|
526
|
+
/**
|
|
527
|
+
* Whether `url()` must be given a second argument.
|
|
528
|
+
*
|
|
529
|
+
* Keyed on REQUIRED params: a path whose only placeholder is optional is
|
|
530
|
+
* reachable with nothing at all, and demanding an empty object for it would be
|
|
531
|
+
* the type getting in the way of the truth.
|
|
532
|
+
*/
|
|
533
|
+
declare type RequiresUrlParams<TName extends string> = [RequiredParamKeys<ExtractRouteParams<PathForRouteName<TName>>>] extends [never] ? false : true;
|
|
534
|
+
/**
|
|
535
|
+
* Run an action's declarative `validations:` against the request.
|
|
536
|
+
*
|
|
537
|
+
* The action path uses this synchronous core because input collection and the
|
|
538
|
+
* validator contract are synchronous. The exported wrapper below stays
|
|
539
|
+
* Promise-based for compatibility with callers that chain or await it.
|
|
540
|
+
*/
|
|
541
|
+
declare type ActionRuleTest = (value: unknown) => boolean;
|
|
542
|
+
declare type ActionValidationEntry = [string, ActionValidations[string], ActionRuleTest[]?, ReturnType<typeof createValidationFieldLabel>?];
|
|
543
|
+
/**
|
|
544
|
+
* Per-request read-routing context.
|
|
545
|
+
*
|
|
546
|
+
* The database package tracks writes per async context so that a request
|
|
547
|
+
* which reads back what it just wrote is never served from a read replica
|
|
548
|
+
* (replication is asynchronous, so the row may not be there yet). That
|
|
549
|
+
* tracking only works if something establishes the context at the request
|
|
550
|
+
* boundary — without this, `contextHasWritten()` has no store to consult,
|
|
551
|
+
* always reports false, and every read routes to a replica including the
|
|
552
|
+
* one immediately following a write. The safety rule would be dead code.
|
|
553
|
+
*
|
|
554
|
+
* Loaded lazily rather than imported: `@stacksjs/database` already depends
|
|
555
|
+
* on `@stacksjs/router`, so a static import here would close a package
|
|
556
|
+
* cycle. The dynamic import is resolved once and cached, and the fallback
|
|
557
|
+
* is a plain pass-through so a build without the database package (or an
|
|
558
|
+
* older one) still serves requests.
|
|
559
|
+
*/
|
|
560
|
+
declare type ContextDispatcher = <T, A>(fn: (arg: A) => T, arg: A) => T;
|
|
561
|
+
declare type ContextRunner = <T>(fn: () => T) => T;
|
|
562
|
+
declare type NativeRouteHandler = (request: Request) => Response | Promise<Response>;
|
|
563
|
+
declare type NativeRouteTable = Record<string, Record<string, NativeRouteHandler>>;
|
|
564
|
+
declare type NativeResponseFinalizer = (response: Response, request: Request) => Response | Promise<Response>;
|
|
565
|
+
/**
|
|
566
|
+
* FIFO-bounded Map. Wraps `Map` with a hard size cap; on overflow,
|
|
567
|
+
* the oldest entry (Map insertion order) is evicted. Used for the
|
|
568
|
+
* router's small framework-internal caches whose size is normally
|
|
569
|
+
* bounded by action count, but which had no upper limit before —
|
|
570
|
+
* tests that instantiate many short-lived routers would leak entries
|
|
571
|
+
* across `createStacksRouter()` calls (stacksjs/stacks#1863 T-8).
|
|
572
|
+
*
|
|
573
|
+
* Insertion-order LRU is appropriate here because the access pattern
|
|
574
|
+
* is "set once at action-load time, then many reads" — refreshing on
|
|
575
|
+
* get would buy nothing since reads dominate.
|
|
576
|
+
*/
|
|
577
|
+
declare class BoundedMap<K, V> {
|
|
578
|
+
constructor(max: number);
|
|
579
|
+
get(key: K): V | undefined;
|
|
580
|
+
has(key: K): boolean;
|
|
581
|
+
set(key: K, value: V): this;
|
|
582
|
+
delete(key: K): boolean;
|
|
583
|
+
clear(): void;
|
|
584
|
+
get size(): number;
|
|
585
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/router",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.48",
|
|
6
6
|
"description": "The Stacks framework router.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -67,27 +67,27 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@stacksjs/bun-router": "^0.1.18",
|
|
70
|
-
"@stacksjs/cache": "0.74.
|
|
71
|
-
"@stacksjs/collections": "0.74.
|
|
72
|
-
"@stacksjs/config": "0.74.
|
|
73
|
-
"@stacksjs/error-handling": "0.74.
|
|
74
|
-
"@stacksjs/logging": "0.74.
|
|
75
|
-
"@stacksjs/pagination": "0.74.
|
|
76
|
-
"@stacksjs/path": "0.74.
|
|
77
|
-
"@stacksjs/security": "0.74.
|
|
78
|
-
"@stacksjs/storage": "0.74.
|
|
70
|
+
"@stacksjs/cache": "0.74.48",
|
|
71
|
+
"@stacksjs/collections": "0.74.48",
|
|
72
|
+
"@stacksjs/config": "0.74.48",
|
|
73
|
+
"@stacksjs/error-handling": "0.74.48",
|
|
74
|
+
"@stacksjs/logging": "0.74.48",
|
|
75
|
+
"@stacksjs/pagination": "0.74.48",
|
|
76
|
+
"@stacksjs/path": "0.74.48",
|
|
77
|
+
"@stacksjs/security": "0.74.48",
|
|
78
|
+
"@stacksjs/storage": "0.74.48",
|
|
79
79
|
"ts-rate-limiter": "^0.4.10"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@stacksjs/actions": "0.74.
|
|
83
|
-
"@stacksjs/types": "0.74.
|
|
82
|
+
"@stacksjs/actions": "0.74.48",
|
|
83
|
+
"@stacksjs/types": "0.74.48",
|
|
84
84
|
"better-dx": "^0.2.24"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
|
-
"@stacksjs/api": "0.74.
|
|
88
|
-
"@stacksjs/auth": "0.74.
|
|
89
|
-
"@stacksjs/database": "0.74.
|
|
90
|
-
"@stacksjs/validation": "0.74.
|
|
87
|
+
"@stacksjs/api": "0.74.48",
|
|
88
|
+
"@stacksjs/auth": "0.74.48",
|
|
89
|
+
"@stacksjs/database": "0.74.48",
|
|
90
|
+
"@stacksjs/validation": "0.74.48"
|
|
91
91
|
},
|
|
92
92
|
"peerDependenciesMeta": {
|
|
93
93
|
"@stacksjs/api": {
|