UNPKG

@stacksjs/router

Version:
214 lines (213 loc) 9.67 kB
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; }