@stacksjs/router
Version:
The Stacks framework router.
214 lines (213 loc) • 9.67 kB
TypeScript
import type { Server } from 'bun';
import './request-augmentation';
import { Router } from '@stacksjs/bun-router';
import type { ActionHandler, EnhancedRequest, Route, ServerOptions } from '@stacksjs/bun-router';
import type { ActionValidations, ValidationResult } from '@stacksjs/actions';
/**
* Warn (once per process) when more than one @stacksjs/router module has loaded
* (stacksjs/stacks#1975 / #1982). Routing still works — the route table and
* request context are process-global singletons — but a duplicated install is
* worth surfacing. Called at serve() boot. Returns whether a split was detected
* so callers/tests can assert on it without capturing logs.
*/
export declare function warnOnMultipleRouterInstances(): boolean;
/**
* Generate a full URL for a named route, like Laravel's route() helper.
*
* Validates path parameters at call time so a typo'd argument
* (`url('user.post', { userId: 1 })` against `/users/{id}`) throws
* immediately with a list of expected names instead of silently
* producing a URL with `{id}` left literal in the path.
*
* @example
* ```typescript
* // Define a named route
* route.get('/api/email/unsubscribe', 'Actions/UnsubscribeAction').name('email.unsubscribe')
*
* // Generate URL
* url('email.unsubscribe', { token: 'abc-123' })
* // → https://stacksjs.com/api/email/unsubscribe?token=abc-123
*
* // With path parameters
* route.get('/users/{id}/posts/{postId}', handler).name('user.post')
* url('user.post', { id: 42, postId: 7 })
* // → https://stacksjs.com/users/42/posts/7
* ```
*/
export declare function url(routeName: string, params?: Record<string, string | number>): string;
/**
* List the placeholder names a named route expects — handy for
* codegen/test cases and for detecting typos before runtime.
*/
export declare function routeParams(routeName: string): string[];
/**
* Snapshot of the registered routes — `{ method, path, name? }` per
* route. Used by `buddy route:list` and the dev-server startup banner.
*/
export declare function listRegisteredRoutes(): Array<{ method: string, path: string, name?: string }>;
/**
* Clear the middleware cache (useful for hot-reload in development).
*
* `installMiddlewareHotReload()` will wire this up automatically when
* called from the dev server — production should never invoke it.
*/
export declare function clearMiddlewareCache(): void;
/**
* Watch `app/Middleware/` and `app/Middleware.ts` and invalidate the
* cached middleware modules whenever a file changes. Intended for the
* dev server only — calling this in production is a no-op (the
* watcher handle is created but never fires anything user code cares
* about). Returns a `disposer()` to stop watching.
*
* Without this hook, editing a middleware file in dev requires a
* full server restart to see the change — the import map caches the
* old version forever.
*/
export declare function installMiddlewareHotReload(): () => void;
/**
* Resolve every middleware alias referenced by a registered route and
* report the ones that don't load. `csrf` is always checked too — it's
* auto-injected on unsafe methods even when no route lists it.
*
* Resolution is inherently lazy (`.middleware(name)` is a sync chainable
* that just records a string; the alias map and middleware modules load
* via async dynamic import), so a throw at literal registration time is
* impossible. Calling this after all routes are registered — the end of
* `importRoutes()` and the compiled-binary boot in core/server — IS
* effectively registration-time validation. See stacksjs/stacks#1957.
*/
export declare function findUnresolvableRouteMiddleware(): Promise<Array<{ alias: string, routes: string[] }>>;
/**
* Throw when any registered route references a middleware alias that
* cannot be resolved. Fail-closed boot validation: a typo'd `auth` alias
* must abort startup loudly, not serve the route unprotected (the
* request-time guard in createMiddlewareHandler 500s as a backstop).
*/
export declare function assertRouteMiddlewareResolvable(): Promise<void>;
/**
* Run an action's declarative `validations:` against the request.
*
* @internal Exported for regression coverage of path-param coercion
* (stacksjs/stacks#1865). Production callers should rely on the
* router's action-resolution path, which invokes this for you.
*/
export declare function validateActionInput(req: EnhancedRequest, validations: ActionValidations): Promise<ValidationResult>;
export declare function stream(source: ReadableStream | AsyncIterable<string | Uint8Array>, options?: StreamOptions): Response;
// Decorate the incoming request with the helpers the framework's middleware
// and actions assume are always available. Names follow Laravel's convention
// because that's the API surface Stacks userland expects.
export declare function enhanceRequest(req: EnhancedRequest): EnhancedRequest;
/**
* Create a Stacks-enhanced router
*/
export declare function createStacksRouter(config?: StacksRouterConfig): StacksRouterInstance;
/**
* Handle a server request through the router
* This is the main entry point for the Stacks server
*/
export declare function serverResponse(request: Request, _body?: string): Promise<Response>;
// Export serve function that uses the default router
export declare function serve(options?: ServerOptions): Promise<Server<unknown>>;
export declare const route: StacksRouterInstance;
declare interface StacksRouterConfig {
verbose?: boolean
apiPrefix?: string
}
declare interface GroupOptions {
prefix?: string
middleware?: string | string[]
apiResponse?: boolean
}
declare interface ResourceRouteOptions {
only?: ResourceAction[]
except?: ResourceAction[]
middleware?: string | string[]
}
/**
* Chainable route interface for middleware and naming support
*/
declare interface ChainableRoute {
middleware: (name: string) => ChainableRoute
name: (routeName: string) => ChainableRoute
skipCsrf: () => ChainableRoute
requireCsrf: () => ChainableRoute
rateLimit: (max: number, window: 'second' | 'minute' | 'hour' | 'day' | number) => ChainableRoute
}
/**
* Helper for streaming responses — wraps a `ReadableStream` or async
* generator with the right headers for the chosen content type.
*
* Common shapes:
*
* ```ts
* // Server-Sent Events
* return stream(async function* () {
* for await (const evt of source) yield `data: ${JSON.stringify(evt)}\n\n`
* }, { type: 'sse' })
*
* // Chunked JSON (NDJSON) — one JSON object per line
* return stream(async function* () {
* for await (const row of rows) yield `${JSON.stringify(row)}\n`
* }, { type: 'ndjson' })
*
* // Raw bytes — caller supplies a ReadableStream of Uint8Array chunks
* return stream(myReadable, { contentType: 'application/octet-stream' })
* ```
*
* The wrapper sets `Cache-Control: no-cache` and `Connection: keep-alive`
* for SSE — the two headers a sane proxy / browser pair won't ignore — and
* leaves backpressure / cancellation to the underlying stream.
*
* See stacksjs/stacks#1870 R-4.
*/
export declare interface StreamOptions {
type?: 'sse' | 'ndjson'
contentType?: string
headers?: HeadersInit
status?: number
}
export declare interface StacksRouterInstance {
bunRouter: Router
routes: Route[]
get: (path: string, handler: StacksHandler) => ChainableRoute
post: (path: string, handler: StacksHandler) => ChainableRoute
put: (path: string, handler: StacksHandler) => ChainableRoute
patch: (path: string, handler: StacksHandler) => ChainableRoute
delete: (path: string, handler: StacksHandler) => ChainableRoute
options: (path: string, handler: StacksHandler) => ChainableRoute
group: (options: GroupOptions, callback: () => void | Promise<void>) => StacksRouterInstance | Promise<StacksRouterInstance>
resource: (name: string, handler: string, options?: ResourceRouteOptions) => StacksRouterInstance
match: (methods: string[], path: string, handler: StacksHandler) => ChainableRoute
health: () => StacksRouterInstance
use: (middleware: ActionHandler | ((req: EnhancedRequest, next: () => Promise<Response>) => Response | Promise<Response>)) => StacksRouterInstance
register: (routePath: string, options?: { prefix?: string, middleware?: string | string[] }) => Promise<StacksRouterInstance>
serve: (options?: ServerOptions) => Promise<Server<unknown>>
handleRequest: (req: Request) => Promise<Response>
getAllowedMethods: (pathname: string, domain?: string) => string[]
importRoutes: () => Promise<void>
loadDiscoveredRoutes: () => Promise<void>
}
declare type RouteHandlerFn = (_req: EnhancedRequest) => Response | Promise<Response>;
declare type StacksHandler = string | RouteHandlerFn;
declare type ResourceAction = 'index' | 'store' | 'show' | 'update' | 'destroy';
/**
* FIFO-bounded Map. Wraps `Map` with a hard size cap; on overflow,
* the oldest entry (Map insertion order) is evicted. Used for the
* router's small framework-internal caches whose size is normally
* bounded by action count, but which had no upper limit before —
* tests that instantiate many short-lived routers would leak entries
* across `createStacksRouter()` calls (stacksjs/stacks#1863 T-8).
*
* Insertion-order LRU is appropriate here because the access pattern
* is "set once at action-load time, then many reads" — refreshing on
* get would buy nothing since reads dominate.
*/
declare class BoundedMap<K, V> {
constructor(max: number);
get(key: K): V | undefined;
has(key: K): boolean;
set(key: K, value: V): this;
delete(key: K): boolean;
clear(): void;
get size(): number;
}