@stacksjs/bun-router 0.1.2 → 0.1.3

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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * One `request.cookies`, built one way.
3
+ *
4
+ * There were three constructions of this object - one in the router, one in
5
+ * `testing/test-request`, one in `testing/auth-testing` - and the server built
6
+ * a fourth, different one through a macro accessor. Only the macro's was the
7
+ * real shape: callable, carrying the name→value entries directly, AND holding
8
+ * `get`/`set`/`delete`/`getAll`. The other three were plain method bags, so a
9
+ * handler written against a real request (`req.cookies()`, `req.cookies.session`)
10
+ * broke the moment it met a test request, and the declared type described none
11
+ * of them completely.
12
+ *
13
+ * This is the one builder. The differences that are real - where a read comes
14
+ * from, where a write goes - are parameters.
15
+ */
16
+ import type { CookieAccessor, CookieOptions } from '../types';
17
+ export interface CookieAccessorSinks {
18
+ /** The current cookie map. Called on every read, so it can stay lazy. */
19
+ read: () => Record<string, string>;
20
+ /** Where a `set` goes. Omitted, `set` is a no-op. */
21
+ write?: (name: string, value: string, options: CookieOptions) => void;
22
+ /** Where a `delete` goes. Omitted, `delete` is a no-op. */
23
+ remove?: (name: string, options: CookieOptions) => void;
24
+ }
25
+ export declare function createCookieAccessor(sinks: CookieAccessorSinks): CookieAccessor;
26
+ /**
27
+ * A cookie accessor backed by a mutable in-memory map.
28
+ *
29
+ * What a test request wants: `set` and `delete` take effect immediately and are
30
+ * visible to the next read, rather than being queued onto a response that does
31
+ * not exist.
32
+ */
33
+ export declare function createInMemoryCookieAccessor(initial?: Record<string, string>): CookieAccessor;
@@ -1,4 +1,4 @@
1
- import type { MiddlewareHandler } from '../types';
1
+ import type { MiddlewareHandler, MiddlewareReference } from '../types';
2
2
  /**
3
3
  * Middleware Group Registry
4
4
  *
@@ -20,19 +20,19 @@ export declare class MiddlewareGroupRegistry {
20
20
  /**
21
21
  * Define a middleware group
22
22
  */
23
- define(name: string, middleware: (string | MiddlewareHandler)[]): this;
23
+ define(name: string, middleware: (MiddlewareReference | MiddlewareHandler)[]): this;
24
24
  /**
25
25
  * Extend an existing group with additional middleware
26
26
  */
27
- extend(name: string, middleware: (string | MiddlewareHandler)[]): this;
27
+ extend(name: string, middleware: (MiddlewareReference | MiddlewareHandler)[]): this;
28
28
  /**
29
29
  * Prepend middleware to an existing group
30
30
  */
31
- prepend(name: string, middleware: (string | MiddlewareHandler)[]): this;
31
+ prepend(name: string, middleware: (MiddlewareReference | MiddlewareHandler)[]): this;
32
32
  /**
33
33
  * Get middleware for a group
34
34
  */
35
- get(name: string): (string | MiddlewareHandler)[];
35
+ get(name: string): (MiddlewareReference | MiddlewareHandler)[];
36
36
  /**
37
37
  * Check if a group exists
38
38
  */
@@ -42,21 +42,26 @@ export declare class MiddlewareGroupRegistry {
42
42
  */
43
43
  remove(name: string): this;
44
44
  /**
45
- * Resolve a middleware reference, expanding group names into their middleware
45
+ * Resolve a middleware reference, expanding group names into their middleware.
46
+ *
47
+ * A group name is a middleware reference as far as a caller is concerned -
48
+ * you write `.middleware('web')` and get the group - so an application that
49
+ * declares its aliases in `RouterTypeRegistry` lists its group names there
50
+ * too.
46
51
  */
47
- resolve(middleware: string | MiddlewareHandler): (string | MiddlewareHandler)[];
52
+ resolve(middleware: MiddlewareReference | MiddlewareHandler): (MiddlewareReference | MiddlewareHandler)[];
48
53
  /**
49
54
  * Resolve multiple middleware references, expanding groups
50
55
  */
51
- resolveAll(middleware: (string | MiddlewareHandler)[]): (string | MiddlewareHandler)[];
56
+ resolveAll(middleware: (MiddlewareReference | MiddlewareHandler)[]): (MiddlewareReference | MiddlewareHandler)[];
52
57
  /**
53
58
  * Add middleware to the global stack (runs on every request)
54
59
  */
55
- pushGlobal(...middleware: (string | MiddlewareHandler)[]): this;
60
+ pushGlobal(...middleware: (MiddlewareReference | MiddlewareHandler)[]): this;
56
61
  /**
57
62
  * Prepend middleware to the global stack
58
63
  */
59
- prependGlobal(...middleware: (string | MiddlewareHandler)[]): this;
64
+ prependGlobal(...middleware: (MiddlewareReference | MiddlewareHandler)[]): this;
60
65
  /**
61
66
  * Remove middleware from the global stack
62
67
  */
@@ -64,7 +69,7 @@ export declare class MiddlewareGroupRegistry {
64
69
  /**
65
70
  * Get the global middleware stack
66
71
  */
67
- getGlobal(): (string | MiddlewareHandler)[];
72
+ getGlobal(): (MiddlewareReference | MiddlewareHandler)[];
68
73
  /**
69
74
  * Set execution priority for a middleware (lower = runs first)
70
75
  */
@@ -72,17 +77,17 @@ export declare class MiddlewareGroupRegistry {
72
77
  /**
73
78
  * Sort middleware by priority
74
79
  */
75
- sortByPriority(middleware: (string | MiddlewareHandler)[]): (string | MiddlewareHandler)[];
80
+ sortByPriority(middleware: (MiddlewareReference | MiddlewareHandler)[]): (MiddlewareReference | MiddlewareHandler)[];
76
81
  /**
77
82
  * Filter middleware by applying except/only rules.
78
83
  *
79
84
  * @param middleware - Full middleware list
80
85
  * @param options - except or only filter
81
86
  */
82
- filter(middleware: (string | MiddlewareHandler)[], options: {
87
+ filter(middleware: (MiddlewareReference | MiddlewareHandler)[], options: {
83
88
  except?: string[];
84
89
  only?: string[];
85
- }): (string | MiddlewareHandler)[];
90
+ }): (MiddlewareReference | MiddlewareHandler)[];
86
91
  /**
87
92
  * Clear all groups and global middleware
88
93
  */
@@ -1,6 +1,6 @@
1
1
  import type { Server } from 'bun';
2
2
  import type { MiddlewareDependency, MiddlewarePipeline, MiddlewarePipelineStats, MiddlewareSkipCondition } from '../middleware/pipeline';
3
- import type { ActionHandler, CookieOptions, EnhancedRequest, MiddlewareHandler, Route, RouteGroup, RouteHandler, RouterConfig, WebSocketConfig, WebSocketData } from '../types';
3
+ import type { ActionHandler, CookieOptions, EnhancedRequest, MiddlewareHandler, MiddlewareReference, Route, RouteGroup, RouteHandler, RouterConfig, TypedRouteHandler, WebSocketConfig, WebSocketData } from '../types';
4
4
  /**
5
5
  * Route compiler interface for pattern matching
6
6
  */
@@ -95,7 +95,7 @@ export declare class Router {
95
95
  * Internal method to add a route with full HTTP method support
96
96
  * This is the core route registration method used by get/post/put/patch/delete
97
97
  */
98
- registerRoute(method: string, path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
98
+ registerRoute(method: string, path: string, handler: ActionHandler, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
99
99
  /**
100
100
  * Resolve middleware from string or handler (synchronous)
101
101
  */
@@ -118,43 +118,51 @@ export declare class Router {
118
118
  * keep working — `ActionHandler<TPath>` is a union that accepts them.
119
119
  * See stacksjs/stacks#1851 for the broader typed-request work.
120
120
  */
121
- get<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
121
+ get<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
122
+ get<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
122
123
  /**
123
124
  * HTTP POST method. See {@link get} for `TPath`-driven param
124
125
  * narrowing on inline handlers.
125
126
  */
126
- post<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
127
+ post<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
128
+ post<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
127
129
  /**
128
130
  * HTTP PUT method. See {@link get} for `TPath`-driven param
129
131
  * narrowing on inline handlers.
130
132
  */
131
- put<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
133
+ put<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
134
+ put<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
132
135
  /**
133
136
  * HTTP PATCH method. See {@link get} for `TPath`-driven param
134
137
  * narrowing on inline handlers.
135
138
  */
136
- patch<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
139
+ patch<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
140
+ patch<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
137
141
  /**
138
142
  * HTTP DELETE method. See {@link get} for `TPath`-driven param
139
143
  * narrowing on inline handlers.
140
144
  */
141
- delete<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
145
+ delete<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
146
+ delete<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
142
147
  /**
143
148
  * HTTP OPTIONS method. See {@link get} for `TPath`-driven param
144
149
  * narrowing on inline handlers.
145
150
  */
146
- options<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
151
+ options<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
152
+ options<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
147
153
  /**
148
154
  * Register the same handler against multiple HTTP methods. The
149
155
  * handler is type-narrowed via {@link ActionHandler}'s `TPath`
150
156
  * generic, same as the single-method overloads.
151
157
  */
152
- match<TPath extends string>(methods: string[], path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
158
+ match<TPath extends string>(methods: string[], path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
159
+ match<TPath extends string>(methods: string[], path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
153
160
  /**
154
161
  * Register the handler against every HTTP method. See {@link get}
155
162
  * for `TPath`-driven param narrowing on inline handlers.
156
163
  */
157
- any<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (string | MiddlewareHandler)[]): Router;
164
+ any<TPath extends string>(path: TPath, handler: TypedRouteHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
165
+ any<TPath extends string>(path: TPath, handler: ActionHandler<TPath>, type?: 'api' | 'web', name?: string, middleware?: (MiddlewareReference | MiddlewareHandler)[]): Router;
158
166
  /**
159
167
  * Register redirect route
160
168
  */
@@ -197,7 +205,7 @@ export declare class Router {
197
205
  /**
198
206
  * Add middleware to the router
199
207
  */
200
- use(...middleware: (string | MiddlewareHandler)[]): Router;
208
+ use(...middleware: (MiddlewareReference | MiddlewareHandler)[]): Router;
201
209
  /**
202
210
  * Create a route group with prefix and middleware.
203
211
  *
@@ -208,11 +216,11 @@ export declare class Router {
208
216
  */
209
217
  group(options: {
210
218
  prefix?: string;
211
- middleware?: (string | MiddlewareHandler)[];
219
+ middleware?: (MiddlewareReference | MiddlewareHandler)[];
212
220
  }, callback: () => Promise<void>): Promise<Router>;
213
221
  group(options: {
214
222
  prefix?: string;
215
- middleware?: (string | MiddlewareHandler)[];
223
+ middleware?: (MiddlewareReference | MiddlewareHandler)[];
216
224
  }, callback: () => void): Router;
217
225
  /**
218
226
  * Add route to the router
@@ -0,0 +1,93 @@
1
+ import type { BuiltInMiddleware } from '../types';
2
+ /**
3
+ * What an application tells the router about itself.
4
+ *
5
+ * A router deals in strings that are really identifiers: `'Actions/CreateUser'`
6
+ * names a file, `'auth'` names a middleware, `'users.show'` names a route. The
7
+ * compiler has no idea which of them exist, so a typo in any of the three is a
8
+ * runtime error at best — a silently unprotected endpoint at worst, when the
9
+ * typo is in a middleware alias.
10
+ *
11
+ * The router cannot know them either. The application can, so it says so, by
12
+ * augmenting this interface:
13
+ *
14
+ * ```ts
15
+ * // types/router.d.ts
16
+ * declare module '@stacksjs/bun-router' {
17
+ * interface RouterTypeRegistry {
18
+ * actions: 'Actions/CreateUser' | 'Actions/ListUsers'
19
+ * middleware: 'auth' | 'throttle' | 'signed'
20
+ * routes: {
21
+ * 'users.index': '/users'
22
+ * 'users.show': '/users/{id}'
23
+ * }
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * From then on, `router.post('/users', 'Actions/CreateUsr')` is a compile
29
+ * error, so is `.middleware('atuh')`, and `url('users.show')` demands an `id`
30
+ * because the path says it has one.
31
+ *
32
+ * ## Nothing is required
33
+ *
34
+ * Every key is independent, and every one falls back to exactly the type it had
35
+ * before this existed when it is absent. An application that declares none of
36
+ * them sees no change at all. That is the point: this cannot be a breaking
37
+ * change, and it cannot be a thing you must do before the router is usable.
38
+ *
39
+ * ## Generating it
40
+ *
41
+ * The three unions are facts a build step already knows. A framework on top of
42
+ * this router that scans `app/Actions/` or a middleware alias map can emit the
43
+ * augmentation into a `.d.ts` and the whole surface types itself with nothing
44
+ * hand-written. Writing it by hand works just as well for a smaller app.
45
+ */
46
+ export interface RouterTypeRegistry {
47
+ }
48
+ /**
49
+ * Read a key out of the registry, or fall back.
50
+ *
51
+ * `K extends keyof RouterTypeRegistry` is `never extends never` — false — while
52
+ * the interface is empty, which is what makes every fallback the default. The
53
+ * second check keeps a malformed augmentation (say, `actions: number`) from
54
+ * poisoning the type: it falls back rather than producing something unusable.
55
+ */
56
+ export type FromRegistry<TKey extends string, TFallback> = TKey extends keyof RouterTypeRegistry ? (RouterTypeRegistry[TKey] extends TFallback ? RouterTypeRegistry[TKey] : TFallback) : TFallback;
57
+ /**
58
+ * The action paths this application has, or the shape of one when it has not
59
+ * said.
60
+ *
61
+ * The fallback is the pattern that was here before: anything under `Actions/`
62
+ * ending in `Action`, or a `Controller@method` reference. It catches a shape
63
+ * mistake and nothing else, which is why declaring the real set is worth doing.
64
+ */
65
+ export type KnownActionPath = FromRegistry<'actions', `Actions/${string}Action` | `actions/${string}Action` | `${string}Controller@${string}`>;
66
+ /**
67
+ * The middleware aliases this application registers, or any string.
68
+ *
69
+ * Middleware group names belong here too: a group name is a middleware
70
+ * reference as far as a call site is concerned - `.middleware('web')` expands
71
+ * to the group - so the union has to contain both or the group form stops
72
+ * type-checking the moment anything is declared.
73
+ */
74
+ export type KnownMiddlewareName = FromRegistry<'middleware', string>;
75
+ /**
76
+ * A middleware reference: an alias, or an alias with parameters.
77
+ *
78
+ * `'throttle:60,1'` and `'can:view,post'` are how parameters are passed, so a
79
+ * declared alias has to keep accepting its parameterised form.
80
+ *
81
+ * The built-ins are always allowed alongside whatever the application declares.
82
+ * An app listing its own aliases is saying "these are mine", not "the ones this
83
+ * package ships no longer exist" - and the router's own default middleware
84
+ * groups are built from them, so narrowing them away would make the package
85
+ * fail to describe itself.
86
+ */
87
+ export type MiddlewareReference = BuiltInMiddleware | `${BuiltInMiddleware}:${string}` | KnownMiddlewareName | `${KnownMiddlewareName}:${string}`;
88
+ /** The named routes this application registers, as `name → path`. */
89
+ export type KnownRoutes = FromRegistry<'routes', Record<string, string>>;
90
+ /** The names of those routes, or any string when none are declared. */
91
+ export type KnownRouteName = keyof KnownRoutes extends never ? string : Extract<keyof KnownRoutes, string>;
92
+ /** The path a declared route name resolves to, or any path when it is not declared. */
93
+ export type PathForRouteName<TName extends string> = TName extends keyof KnownRoutes ? Extract<KnownRoutes[TName], string> : string;
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Server } from 'bun';
2
2
  import type { Router } from './router/router';
3
+ import type { KnownActionPath, MiddlewareReference } from './types/registry';
3
4
  import type { SessionManager } from './session/index';
4
5
  import type { QueryPreservationConfig } from './utils/query-preservation';
5
6
  import type { AuthContext, AuthenticatedUser, BoundModels, CacheAdapter, CacheStats, CookieToDelete, CookieToSet, ErrorMetadata, FallbackError, FileInfo, FlashMessages, FormBody, InputValidationSchemas, InputValue, JsonBodyData, JwtHeader, JwtPayload, MetricEntry, MiddlewareParams, OAuth2Flows, OAuth2Profile, RequestContext, RequestInput, RouteMetadata, SanitizationOptions, SanitizationRule, SessionData, SessionSerializer, SessionStore, SSEConnection, StaticResponseBody, TemplateHelper, TemplateHelpers, TraceAttributes, TraceSpan, TypedServerWebSocket, User, ValidatedBody, ValidatedData, ValidationSchema, WebSocketData } from './types/core';
@@ -629,8 +630,8 @@ export interface RouterConfig {
629
630
  */
630
631
  controllersPath?: string;
631
632
  defaultMiddleware?: {
632
- api?: (string | MiddlewareHandler)[];
633
- web?: (string | MiddlewareHandler)[];
633
+ api?: (MiddlewareReference | MiddlewareHandler)[];
634
+ web?: (MiddlewareReference | MiddlewareHandler)[];
634
635
  };
635
636
  /**
636
637
  * View engine configuration
@@ -678,13 +679,26 @@ export interface CookieOptions {
678
679
  /**
679
680
  * Cookie accessor interface with utility methods
680
681
  */
682
+ /**
683
+ * `request.cookies`, in all three of the shapes it actually has.
684
+ *
685
+ * The server installs a hybrid: a function returning the parsed map, carrying
686
+ * the name→value entries as own properties, with `get`/`set`/`delete`/`getAll`
687
+ * on top. The declaration described only the last of those, so `req.cookies()`
688
+ * and `req.cookies.session` were both type errors against a runtime that
689
+ * supports them, and userland reached for `as any` to get at either.
690
+ */
681
691
  export interface CookieAccessor {
692
+ /** The whole parsed cookie map. */
693
+ (): Record<string, string>;
694
+ /** Direct access by name: `req.cookies.session`. */
695
+ [name: string]: unknown;
682
696
  get: (name: string) => string | undefined;
683
697
  set: (name: string, value: string, options?: CookieOptions) => void;
684
698
  delete: (name: string, options?: CookieOptions) => void;
685
699
  getAll: () => Record<string, string>;
686
700
  }
687
- export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip' | 'cookies' | 'route'> {
701
+ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'cookies' | 'route'> {
688
702
  /**
689
703
  * Route parameters extracted from the URL
690
704
  */
@@ -761,13 +775,10 @@ export interface EnhancedRequest extends Request, Omit<RequestMacroMethods, 'ip'
761
775
  */
762
776
  requestId?: string;
763
777
  /**
764
- * IP address of the client (string property, not function from RequestMacroMethods)
765
- */
766
- ip?: string;
767
- /**
768
- * Cookies parsed from the request with utility methods
778
+ * Cookies parsed from the request. Callable, indexable, and carrying
779
+ * get/set/delete/getAll - see {@link CookieAccessor}.
769
780
  */
770
- cookies?: CookieAccessor;
781
+ cookies: CookieAccessor;
771
782
  /**
772
783
  * Flash messages (temporary messages for the next request)
773
784
  */
@@ -833,11 +844,23 @@ export interface ActionHandlerClass {
833
844
  handle: (request: EnhancedRequest) => Promise<Response>;
834
845
  }
835
846
  /**
836
- * Action handler path with strict pattern validation
847
+ * Action handler path.
848
+ *
849
+ * The real set of actions when the application declares one through
850
+ * {@link RouterTypeRegistry}, and the shape of an action path otherwise — which
851
+ * catches `'Acton/CreateUser'` and nothing else. See `./types/registry.ts`.
837
852
  */
838
- export type ActionPath = `Actions/${string}Action` | `actions/${string}Action` | `${string}Controller@${string}`;
853
+ export type ActionPath = KnownActionPath;
839
854
  /**
840
- * Strongly typed action handler with better type safety
855
+ * Strongly typed action handler: every form a route will accept.
856
+ *
857
+ * Note that the route-registering methods do NOT take this as a single
858
+ * parameter. A parameter typed as a union containing two call signatures gives
859
+ * TypeScript nothing to contextually type an inline arrow with, so
860
+ * `router.get('/users/{id}', req => …)` left `req` implicitly `any` - the exact
861
+ * opposite of what the path generic exists for. They declare
862
+ * {@link TypedRouteHandler} as a first overload and this as the fallback, which
863
+ * types the inline case and keeps every other form working.
841
864
  */
842
865
  export type ActionHandler<TPath extends string = string> = ActionPath | TypedRouteHandler<TPath> | RouteHandler | (new () => ActionHandlerClass) | Response;
843
866
  /**
@@ -935,7 +958,7 @@ export interface AuthMiddlewareConfig {
935
958
  }
936
959
  export interface RouteGroup {
937
960
  prefix?: string;
938
- middleware?: (string | MiddlewareHandler)[];
961
+ middleware?: (MiddlewareReference | MiddlewareHandler)[];
939
962
  }
940
963
  export interface Route {
941
964
  path: string;
@@ -1112,7 +1135,21 @@ export type SecurityHeader = 'Content-Security-Policy' | 'X-Frame-Options' | 'X-
1112
1135
  /**
1113
1136
  * Built-in middleware names - extremely narrow
1114
1137
  */
1115
- export type BuiltInMiddleware = 'auth' | 'cors' | 'csrf' | 'helmet' | 'json' | 'compress' | 'static' | 'session' | 'rateLimiter' | 'requestId' | 'logger' | 'throttle';
1138
+ /**
1139
+ * The middleware this package ships, by the name it is actually registered
1140
+ * under.
1141
+ *
1142
+ * This list used to be a guess and disagreed with the package in several
1143
+ * places: it offered `json`, `rateLimiter`, `requestId`, `compress`, `static`
1144
+ * and `logger`, none of which exist, while the real `json_body`, `rate_limit`,
1145
+ * `request_id`, `security` and `ddos_protection` - the ones the default
1146
+ * middleware groups in `config.ts` are built from - were missing. Anything
1147
+ * typed against it was being checked against fiction.
1148
+ *
1149
+ * Every name here is a module under `src/middleware/`, plus the three aliases
1150
+ * `Router` registers itself.
1151
+ */
1152
+ export type BuiltInMiddleware = 'auth' | 'content_security_policy' | 'cors' | 'csrf' | 'ddos_protection' | 'file_security' | 'file_upload' | 'helmet' | 'input_validation' | 'json_body' | 'performance_alerting' | 'performance_dashboard' | 'performance_monitor' | 'rate_limit' | 'request_id' | 'request_signing' | 'request_tracer' | 'response_cache' | 'security' | 'security_suite' | 'session' | 'throttle';
1116
1153
  /**
1117
1154
  * Middleware with parameters
1118
1155
  */
@@ -1175,10 +1212,19 @@ export type ContentType = 'application/json' | 'application/xml' | 'text/html' |
1175
1212
  /**
1176
1213
  * Strongly typed route method signatures
1177
1214
  */
1215
+ /**
1216
+ * A request whose `params` are exactly the ones its path declares.
1217
+ *
1218
+ * `Omit` first, because `EnhancedRequest.params` is `Record<string, string>`
1219
+ * and an intersection with it widens the narrowed keyset straight back:
1220
+ * `req.params.slugTypo` type-checked happily and returned `undefined` at
1221
+ * runtime, which is the failure a typed router exists to prevent.
1222
+ */
1223
+ export type RequestFor<TPath extends string> = Omit<EnhancedRequest, 'params'> & {
1224
+ params: ExtractRouteParams<TPath>;
1225
+ };
1178
1226
  export interface TypedRouteHandler<TPath extends string> {
1179
- (req: EnhancedRequest & {
1180
- params: ExtractRouteParams<TPath>;
1181
- }): Response | Promise<Response>;
1227
+ (req: RequestFor<TPath>): Response | Promise<Response>;
1182
1228
  }
1183
1229
  /**
1184
1230
  * Strongly typed route definition for specific HTTP methods
@@ -1860,3 +1906,4 @@ export interface EnhancedLaravelModelBindingMethods {
1860
1906
  */
1861
1907
  scopedBindings: <Parent extends string, Child extends string>(bindings: Record<Child, Parent>) => MiddlewareHandler;
1862
1908
  }
1909
+ export type { FromRegistry, KnownActionPath, KnownMiddlewareName, KnownRouteName, KnownRoutes, MiddlewareReference, PathForRouteName, RouterTypeRegistry, } from './types/registry';
package/dist/url.d.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * resolves a registered name into a URL with path parameters substituted
7
7
  * and unmatched params appended as a query string.
8
8
  */
9
+ import type { ExtractRouteParams } from './types';
10
+ import type { KnownRouteName, PathForRouteName } from './types/registry';
9
11
  /**
10
12
  * Register a named-route path. Called by Router/FluentRouter when a route
11
13
  * gets a `.name()`.
@@ -25,6 +27,36 @@ export declare function getNamedRoutes(): ReadonlyMap<string, string>;
25
27
  * Clear the registry. Provided for tests; production code should not need it.
26
28
  */
27
29
  export declare function clearNamedRoutes(): void;
30
+ /**
31
+ * The params a named route needs, plus anything else you want in the query.
32
+ *
33
+ * The required half comes from the path the name resolves to, so
34
+ * `url('users.show')` with no `id` is a compile error once the application has
35
+ * declared its routes. Extra keys stay allowed on purpose: the implementation
36
+ * appends whatever it did not consume as a query string, which is a real
37
+ * feature and not a mistake to be typed away.
38
+ */
39
+ export type UrlParams<TName extends string> = ParamValues<ExtractRouteParams<PathForRouteName<TName>>> & Record<string, string | number | boolean>;
40
+ /**
41
+ * Path params are strings on the wire, but `url()` stringifies whatever it is
42
+ * given - so `url('users.show', { id: 42 })` is correct and should type as
43
+ * correct. Homomorphic, so an optional `{slug?}` stays optional.
44
+ */
45
+ type ParamValues<TParams> = {
46
+ [K in keyof TParams]: string | number | boolean;
47
+ };
48
+ /** The keys of `T` that are not optional. */
49
+ type RequiredKeys<T> = {
50
+ [K in keyof T]-?: object extends Pick<T, K> ? never : K;
51
+ }[keyof T];
52
+ /**
53
+ * `url()` takes no second argument when the route needs no parameters.
54
+ *
55
+ * Keyed on REQUIRED params, not all of them: a path whose only parameter is
56
+ * optional (`/posts/{slug?}`) is reachable with nothing at all, and demanding
57
+ * an empty object for it would be the type getting in the way of the truth.
58
+ */
59
+ type RequiresParams<TName extends string> = [RequiredKeys<ExtractRouteParams<PathForRouteName<TName>>>] extends [never] ? false : true;
28
60
  export interface UrlOptions {
29
61
  /**
30
62
  * Return a fully-qualified URL using `process.env.APP_URL` (falling back to
@@ -53,4 +85,5 @@ export interface UrlOptions {
53
85
  * // → 'https://app.example/users/42' (when APP_URL=https://app.example)
54
86
  * ```
55
87
  */
56
- export declare function url(name: string, params?: Record<string, string | number | boolean>, options?: UrlOptions): string;
88
+ export declare function url<TName extends KnownRouteName>(name: TName, ...rest: RequiresParams<TName> extends true ? [params: UrlParams<TName>, options?: UrlOptions] : [params?: UrlParams<TName>, options?: UrlOptions]): string;
89
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/bun-router",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "description": "A fast, type-safe router for Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",