@stacksjs/bun-router 0.0.27 → 0.1.1
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/chunk-03m2j6h7.js +2 -0
- package/dist/chunk-08cw7ejr.js +3 -0
- package/dist/chunk-0e4g0fyv.js +62 -0
- package/dist/chunk-0mmn02yw.js +2 -0
- package/dist/chunk-126c9qm3.js +2 -0
- package/dist/chunk-12p13817.js +2 -0
- package/dist/chunk-14v65c37.js +4 -0
- package/dist/chunk-1b6a7t3v.js +231 -0
- package/dist/chunk-1wenkmk0.js +2 -0
- package/dist/chunk-2152npfd.js +2 -0
- package/dist/chunk-26w23krx.js +2 -0
- package/dist/chunk-2g8m379z.js +10 -0
- package/dist/chunk-2rr178et.js +1654 -0
- package/dist/chunk-2w8f3swz.js +29 -0
- package/dist/chunk-3y4gaz7v.js +82 -0
- package/dist/chunk-4r1x4afz.js +12 -0
- package/dist/chunk-4wyw66zk.js +2 -0
- package/dist/chunk-4z5sf8ag.js +2 -0
- package/dist/chunk-58tfvayh.js +4 -0
- package/dist/chunk-5be9x5cq.js +3 -0
- package/dist/chunk-5dp83qcx.js +397 -0
- package/dist/chunk-5f0d9j9a.js +2 -0
- package/dist/chunk-5z52b0fc.js +28 -0
- package/dist/chunk-5zrnk2kq.js +4 -0
- package/dist/chunk-6bbmkwng.js +3 -0
- package/dist/chunk-6dmqtdnq.js +2 -0
- package/dist/chunk-6e91956t.js +3 -0
- package/dist/chunk-6rv8w5jk.js +2 -0
- package/dist/chunk-6x1feysm.js +3 -0
- package/dist/chunk-7qnxdedn.js +321 -0
- package/dist/chunk-7y72hm1x.js +2 -0
- package/dist/chunk-8hbhfh6b.js +194 -0
- package/dist/chunk-8kfzygg5.js +3 -0
- package/dist/chunk-8wpj3fqe.js +351 -0
- package/dist/chunk-8wydx2we.js +2 -0
- package/dist/chunk-8zjnjvv7.js +2 -0
- package/dist/chunk-9805tt8n.js +341 -0
- package/dist/chunk-988h4bzr.js +2 -0
- package/dist/chunk-9b6a58ac.js +2 -0
- package/dist/chunk-a8fcaywb.js +2 -0
- package/dist/chunk-bfjwn7yg.js +2 -0
- package/dist/chunk-bp2522mm.js +2 -0
- package/dist/chunk-brpgjyqg.js +3 -0
- package/dist/chunk-bswe62q4.js +48 -0
- package/dist/chunk-c3v796c8.js +943 -0
- package/dist/chunk-ccqmwdry.js +2 -0
- package/dist/chunk-ck0crwnx.js +2 -0
- package/dist/chunk-cqgkxcb4.js +2 -0
- package/dist/chunk-dntnakjy.js +2 -0
- package/dist/chunk-dxg045wj.js +5 -0
- package/dist/chunk-e3qej0jr.js +4 -0
- package/dist/chunk-ettdpnrw.js +60 -0
- package/dist/chunk-ezqwq17j.js +185 -0
- package/dist/chunk-ezx3qsez.js +2 -0
- package/dist/chunk-f72nnjy4.js +15413 -0
- package/dist/chunk-fna5n0ey.js +246 -0
- package/dist/chunk-fqhckdt8.js +2 -0
- package/dist/chunk-fr6hbx25.js +2 -0
- package/dist/chunk-g9cwng6w.js +3 -0
- package/dist/chunk-gd3za5x0.js +2 -0
- package/dist/chunk-gf28608k.js +2 -0
- package/dist/chunk-h1dj6v8q.js +3 -0
- package/dist/chunk-hjxe7k2g.js +2 -0
- package/dist/chunk-hrkgznbn.js +2 -0
- package/dist/chunk-j6h5v0br.js +2 -0
- package/dist/chunk-kjtjbcq4.js +71 -0
- package/dist/chunk-mgb01qdv.js +14 -0
- package/dist/chunk-n24c9whp.js +2 -0
- package/dist/chunk-n2j24b0h.js +4540 -0
- package/dist/chunk-nf3kv7kv.js +3 -0
- package/dist/chunk-nhmtg01n.js +84 -0
- package/dist/chunk-p0qhpnqh.js +70 -0
- package/dist/chunk-p0w1x8y2.js +3 -0
- package/dist/chunk-p3dk1w2w.js +216 -0
- package/dist/chunk-q4e9pxs7.js +3 -0
- package/dist/chunk-q5dv1kyy.js +2 -0
- package/dist/chunk-qnqgf9tq.js +2 -0
- package/dist/chunk-qvwy1808.js +898 -0
- package/dist/chunk-r18f77vc.js +12 -0
- package/dist/chunk-r63za1mr.js +2 -0
- package/dist/chunk-rft8gw92.js +7851 -0
- package/dist/chunk-ryvmyskt.js +4211 -0
- package/dist/chunk-sa6mx308.js +2 -0
- package/dist/chunk-sw0chtw4.js +4 -0
- package/dist/chunk-va0pc7hx.js +2 -0
- package/dist/chunk-vcp8dnjq.js +412 -0
- package/dist/chunk-vg1erq1q.js +2 -0
- package/dist/chunk-vsh4390q.js +358 -0
- package/dist/chunk-w0jv49ch.js +11 -0
- package/dist/chunk-wm805tte.js +35 -0
- package/dist/chunk-xg9xscqz.js +1 -0
- package/dist/chunk-xj9h6mc9.js +2 -0
- package/dist/chunk-ytvzwrq3.js +51 -0
- package/dist/chunk-zde7ebc4.js +288 -0
- package/dist/chunk-zmqsv2xy.js +2 -0
- package/dist/cli.js +51 -3233
- package/dist/container/index.js +1 -321
- package/dist/index.d.ts +1 -0
- package/dist/index.js +80 -13987
- package/dist/typed/client.d.ts +89 -0
- package/dist/typed/contract.d.ts +34 -0
- package/dist/typed/endpoint.d.ts +90 -0
- package/dist/typed/index.d.ts +14 -0
- package/dist/typed/router.d.ts +141 -0
- package/package.json +1 -1
- package/dist/chunk-1ahs68ys.js +0 -18
- package/dist/chunk-cgptvjdf.js +0 -18131
- package/dist/chunk-g3ybefhg.js +0 -1923
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A client that knows what every endpoint takes and returns, with no
|
|
3
|
+
* generation step between changing a route and seeing the type change.
|
|
4
|
+
*
|
|
5
|
+
* Reads the route map straight out of `createTypedRouter()`'s type, so editing
|
|
6
|
+
* a handler's return shape or its `validations` is a compile error at every
|
|
7
|
+
* call site that disagrees - immediately, with no CLI run and no generated
|
|
8
|
+
* file that could still be holding yesterday's answer.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import type { AppRoutes } from './routes'
|
|
12
|
+
* import { createTypedClient } from '@stacksjs/bun-router'
|
|
13
|
+
*
|
|
14
|
+
* const client = createTypedClient<AppRoutes>({ baseUrl: 'https://api.example.com' })
|
|
15
|
+
*
|
|
16
|
+
* const projects = await client.get('/v1/projects')
|
|
17
|
+
* const one = await client.get('/v1/projects/{id}', { params: { id: '42' } })
|
|
18
|
+
* const created = await client.post('/v1/projects', { name: 'apollo', budget: 1200 })
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* A path the API does not serve is a compile error. A body that does not match
|
|
22
|
+
* the handler's declared input is a compile error. The awaited result is the
|
|
23
|
+
* handler's own return type.
|
|
24
|
+
*
|
|
25
|
+
* ## What it does not promise
|
|
26
|
+
*
|
|
27
|
+
* The output type is the handler's return type as TypeScript sees it, not a
|
|
28
|
+
* model of what JSON does to it - a `Date` in a response body arrives as a
|
|
29
|
+
* string, and the type will still say `Date`. A handler that returns a
|
|
30
|
+
* `Response` or a stream has taken over the wire format itself and is typed
|
|
31
|
+
* `unknown`, which is the honest answer.
|
|
32
|
+
*
|
|
33
|
+
* It speaks HTTP and nothing else: no router import, no server code, nothing
|
|
34
|
+
* that stops it being bundled for a browser.
|
|
35
|
+
*/
|
|
36
|
+
import type { PathsForMethod, RouteMapOf, TypedRouteMap } from './contract';
|
|
37
|
+
/** A response the server refused. Carries the parsed body when there was one. */
|
|
38
|
+
export declare class TypedClientError extends Error {
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly response: Response;
|
|
41
|
+
readonly body: unknown;
|
|
42
|
+
constructor(response: Response, body: unknown);
|
|
43
|
+
}
|
|
44
|
+
export interface TypedClientOptions {
|
|
45
|
+
/** Where the API lives. A trailing slash is fine. */
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
/**
|
|
48
|
+
* Headers on every request. A function is called per request, so an auth
|
|
49
|
+
* token that rotates does not have to be a new client.
|
|
50
|
+
*/
|
|
51
|
+
headers?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
52
|
+
/** Swap the fetch implementation - a test double, or one that retries. */
|
|
53
|
+
fetch?: typeof globalThis.fetch;
|
|
54
|
+
credentials?: RequestInit['credentials'];
|
|
55
|
+
/**
|
|
56
|
+
* Called instead of throwing when the server answers 4xx/5xx. Return a value
|
|
57
|
+
* to have it become the call's result; throw to keep the default behaviour.
|
|
58
|
+
*/
|
|
59
|
+
onError?: (error: TypedClientError) => unknown;
|
|
60
|
+
}
|
|
61
|
+
/** Per-call options. `params` is required when the path has any. */
|
|
62
|
+
export interface TypedRequestOptions<TParams extends Record<string, string>> {
|
|
63
|
+
params?: TParams;
|
|
64
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
65
|
+
headers?: Record<string, string>;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}
|
|
68
|
+
type Route<T, M extends string, P extends string> = RouteMapOf<T> extends infer R ? (R extends TypedRouteMap ? (`${M} ${P}` extends keyof R ? R[`${M} ${P}`] : never) : never) : never;
|
|
69
|
+
type Input<T, M extends string, P extends string> = Route<T, M, P>['input'];
|
|
70
|
+
type Output<T, M extends string, P extends string> = Route<T, M, P>['output'];
|
|
71
|
+
type Params<T, M extends string, P extends string> = Route<T, M, P>['params'];
|
|
72
|
+
type Paths<T, M extends string> = RouteMapOf<T> extends infer R ? (R extends TypedRouteMap ? PathsForMethod<R, M> : never) : never;
|
|
73
|
+
export interface TypedClient<T> {
|
|
74
|
+
get: <P extends Paths<T, 'GET'>>(path: P, options?: TypedRequestOptions<Params<T, 'GET', P>>) => Promise<Output<T, 'GET', P>>;
|
|
75
|
+
post: <P extends Paths<T, 'POST'>>(path: P, body: Input<T, 'POST', P>, options?: TypedRequestOptions<Params<T, 'POST', P>>) => Promise<Output<T, 'POST', P>>;
|
|
76
|
+
put: <P extends Paths<T, 'PUT'>>(path: P, body: Input<T, 'PUT', P>, options?: TypedRequestOptions<Params<T, 'PUT', P>>) => Promise<Output<T, 'PUT', P>>;
|
|
77
|
+
patch: <P extends Paths<T, 'PATCH'>>(path: P, body: Input<T, 'PATCH', P>, options?: TypedRequestOptions<Params<T, 'PATCH', P>>) => Promise<Output<T, 'PATCH', P>>;
|
|
78
|
+
delete: <P extends Paths<T, 'DELETE'>>(path: P, options?: TypedRequestOptions<Params<T, 'DELETE', P>>) => Promise<Output<T, 'DELETE', P>>;
|
|
79
|
+
/**
|
|
80
|
+
* The untyped escape hatch, for when you need the `Response` itself -
|
|
81
|
+
* a redirect to follow by hand, a header to read, a body to stream.
|
|
82
|
+
*/
|
|
83
|
+
raw: (method: string, path: string, init?: RequestInit & {
|
|
84
|
+
params?: Record<string, string>;
|
|
85
|
+
query?: Record<string, unknown>;
|
|
86
|
+
}) => Promise<Response>;
|
|
87
|
+
}
|
|
88
|
+
export declare function createTypedClient<T>(options: TypedClientOptions): TypedClient<T>;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The contract between a typed router and a typed client.
|
|
3
|
+
*
|
|
4
|
+
* One route map, keyed `"METHOD /path"`, describing what each endpoint takes,
|
|
5
|
+
* what it answers with, and what its path needs. `createTypedRouter()`
|
|
6
|
+
* accumulates one of these into its own type as routes are registered;
|
|
7
|
+
* `createTypedClient()` reads it back. Nothing else needs to know it exists.
|
|
8
|
+
*
|
|
9
|
+
* It lives in its own module, with no imports, because both halves depend on
|
|
10
|
+
* it and neither should have to depend on the other.
|
|
11
|
+
*/
|
|
12
|
+
/** One route: what a client sends, what it gets back, what the path needs. */
|
|
13
|
+
export interface TypedRoute {
|
|
14
|
+
input: unknown;
|
|
15
|
+
output: unknown;
|
|
16
|
+
params: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
/** A whole API, keyed `"METHOD /path"` (e.g. `"GET /v1/projects"`). */
|
|
19
|
+
export type TypedRouteMap = Record<string, TypedRoute>;
|
|
20
|
+
/**
|
|
21
|
+
* The route map behind a typed router, or the map itself.
|
|
22
|
+
*
|
|
23
|
+
* `createTypedRouter()` accumulates its map into a phantom `__routes`
|
|
24
|
+
* property, so `typeof api` carries it and this reads it back out. Passing a
|
|
25
|
+
* map type directly works too, which is what an application that would rather
|
|
26
|
+
* name its API explicitly will export.
|
|
27
|
+
*/
|
|
28
|
+
export type RouteMapOf<T> = T extends {
|
|
29
|
+
__routes?: infer R;
|
|
30
|
+
} ? ([R] extends [TypedRouteMap | undefined] ? NonNullable<R> : never) : (T extends TypedRouteMap ? T : never);
|
|
31
|
+
/** The paths one method serves, as a union of path literals. */
|
|
32
|
+
export type PathsForMethod<R extends TypedRouteMap, M extends string> = Extract<keyof R, `${M} ${string}`> extends `${M} ${infer P}` ? P : never;
|
|
33
|
+
/** The HTTP methods a typed router registers. */
|
|
34
|
+
export type TypedMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a handler's input and output types.
|
|
3
|
+
*
|
|
4
|
+
* A typed route map is only as good as what can be inferred about the handler
|
|
5
|
+
* behind it, and handlers come in more than one shape. Three are understood
|
|
6
|
+
* here, in order:
|
|
7
|
+
*
|
|
8
|
+
* 1. A plain function. Its return type is the output; the input is unknown,
|
|
9
|
+
* because a function that reads `req.body` says nothing about what it
|
|
10
|
+
* expects to find there.
|
|
11
|
+
* 2. An object with `handle()`. Same output inference, and if it also
|
|
12
|
+
* carries a `validations` map, the input comes from that - the same
|
|
13
|
+
* object the validator runs, so the two cannot drift.
|
|
14
|
+
* 3. Anything declaring an explicit `__input` phantom, which wins over
|
|
15
|
+
* `validations`. The escape hatch for a handler that validates somewhere
|
|
16
|
+
* this cannot see.
|
|
17
|
+
*
|
|
18
|
+
* Shape 2 is why a framework built on this router does not need its own
|
|
19
|
+
* builder: an "action" with `handle` and `validations` is structurally exactly
|
|
20
|
+
* what this reads.
|
|
21
|
+
*/
|
|
22
|
+
/** A validation rule, as `validations` maps are written. */
|
|
23
|
+
export interface TypedRule<T = unknown> {
|
|
24
|
+
/**
|
|
25
|
+
* Names the value the rule accepts. Matches the `Validator<T>` shape that
|
|
26
|
+
* ts-validation (and anything else with a `test(value: T)` predicate) uses,
|
|
27
|
+
* which is how the input type is read without depending on a validator
|
|
28
|
+
* library.
|
|
29
|
+
*/
|
|
30
|
+
test?: (value: T) => boolean;
|
|
31
|
+
validate: (value: any) => any;
|
|
32
|
+
}
|
|
33
|
+
/** A `validations:` map: one rule per field, optionally with a message. */
|
|
34
|
+
export type TypedValidations = Record<string, {
|
|
35
|
+
rule: TypedRule<any>;
|
|
36
|
+
message?: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
/** What a single rule accepts. */
|
|
39
|
+
type RuleInput<R> = R extends {
|
|
40
|
+
test: (value: infer T) => boolean;
|
|
41
|
+
} ? T : unknown;
|
|
42
|
+
/** The body shape a `validations` map describes. */
|
|
43
|
+
export type InputOfValidations<V> = V extends TypedValidations ? {
|
|
44
|
+
[K in keyof V]: RuleInput<V[K]['rule']>;
|
|
45
|
+
} : Record<string, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* What a client has to send to reach this handler.
|
|
48
|
+
*
|
|
49
|
+
* An explicit `__input` wins; otherwise `validations`; otherwise unknown-ish,
|
|
50
|
+
* which is the honest answer rather than a guess.
|
|
51
|
+
*/
|
|
52
|
+
export type InputOf<H> = H extends {
|
|
53
|
+
__input?: infer I;
|
|
54
|
+
} ? ([I] extends [undefined] ? InputOfHandlerValidations<H> : NonNullable<I>) : InputOfHandlerValidations<H>;
|
|
55
|
+
type InputOfHandlerValidations<H> = H extends {
|
|
56
|
+
validations: infer V;
|
|
57
|
+
} ? InputOfValidations<V> : Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* What a client gets back.
|
|
60
|
+
*
|
|
61
|
+
* A handler that returns a `Response` or a stream has taken over the wire
|
|
62
|
+
* format itself, so its shape is genuinely unknown here - saying so is more
|
|
63
|
+
* useful than pretending the client knows.
|
|
64
|
+
*/
|
|
65
|
+
export type OutputOf<H> = H extends {
|
|
66
|
+
handle: (..._args: any[]) => infer R;
|
|
67
|
+
} ? NormalizeOutput<Awaited<R>> : (H extends (..._args: any[]) => infer R ? NormalizeOutput<Awaited<R>> : unknown);
|
|
68
|
+
type NormalizeOutput<R> = [R] extends [Response | ReadableStream] ? unknown : R;
|
|
69
|
+
/** Anything this module can read types out of. */
|
|
70
|
+
export type TypedHandlerLike = ((..._args: any[]) => any) | {
|
|
71
|
+
handle: (..._args: any[]) => any;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Declare a handler's input and output explicitly.
|
|
75
|
+
*
|
|
76
|
+
* For the case shape 1 cannot cover: a plain function that validates its own
|
|
77
|
+
* body, where the router has no `validations` map to read. The returned value
|
|
78
|
+
* is the same function with an `__input` phantom attached, so it registers
|
|
79
|
+
* exactly as it would have.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* const createUser = defineEndpoint<{ name: string }, { id: number }>(async (req) => {
|
|
83
|
+
* const body = await req.json()
|
|
84
|
+
* return { id: 1 }
|
|
85
|
+
* })
|
|
86
|
+
*/
|
|
87
|
+
export declare function defineEndpoint<TInput, TOutput>(handle: (..._args: any[]) => TOutput | Promise<TOutput>): ((..._args: any[]) => TOutput | Promise<TOutput>) & {
|
|
88
|
+
__input?: TInput;
|
|
89
|
+
};
|
|
90
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed routes and the client that reads them.
|
|
3
|
+
*
|
|
4
|
+
* Three modules, deliberately separate: the contract both halves agree on, the
|
|
5
|
+
* builder that produces it, and the client that consumes it. The client
|
|
6
|
+
* imports only the contract, so it stays bundleable for a browser.
|
|
7
|
+
*/
|
|
8
|
+
export type { PathsForMethod, RouteMapOf, TypedMethod, TypedRoute, TypedRouteMap } from './contract';
|
|
9
|
+
export { createTypedClient, TypedClientError } from './client';
|
|
10
|
+
export type { TypedClient, TypedClientOptions, TypedRequestOptions } from './client';
|
|
11
|
+
export { defineEndpoint } from './endpoint';
|
|
12
|
+
export type { InputOf, InputOfValidations, OutputOf, TypedHandlerLike, TypedRule, TypedValidations } from './endpoint';
|
|
13
|
+
export { createTypedRouter } from './router';
|
|
14
|
+
export type { RoutesOf, TypedRouteParams, TypedRegistrar, TypedRouteOptions, TypedRouter } from './router';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes a TypeScript consumer can see the shape of, with no generation step.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is for
|
|
5
|
+
*
|
|
6
|
+
* `router.get('/v1/projects', handler)` tells the compiler nothing a client
|
|
7
|
+
* could use. The path is a string, the handler returns a `Response`, and the
|
|
8
|
+
* only route-aware client you could offer was one generated from an OpenAPI
|
|
9
|
+
* document by running a CLI command - which has to be re-run, and whose output
|
|
10
|
+
* goes stale the moment a route changes.
|
|
11
|
+
*
|
|
12
|
+
* This builder registers through the router it is given, exactly as you would
|
|
13
|
+
* by hand, while accumulating a route map into its own type as you chain:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* export const api = createTypedRouter(router)
|
|
17
|
+
* .get('/v1/projects', listProjects)
|
|
18
|
+
* .post('/v1/projects', createProject)
|
|
19
|
+
*
|
|
20
|
+
* export type AppRoutes = typeof api
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* // any TypeScript consumer, same repo or a package away
|
|
25
|
+
* import { createTypedClient } from '@stacksjs/bun-router'
|
|
26
|
+
*
|
|
27
|
+
* const client = createTypedClient<AppRoutes>({ baseUrl: 'https://api.example.com' })
|
|
28
|
+
* const projects = await client.get('/v1/projects') // typed, no CLI step
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Registration goes through the ordinary router, so there is exactly one
|
|
32
|
+
* runtime dispatch path and the difference from a hand-written route is
|
|
33
|
+
* entirely at compile time.
|
|
34
|
+
*
|
|
35
|
+
* ## What it reads
|
|
36
|
+
*
|
|
37
|
+
* See `./endpoint.ts`. A plain function gives its return type; an object with
|
|
38
|
+
* `handle()` gives the same, plus its input from a `validations` map when it
|
|
39
|
+
* has one. That second shape is deliberate: a framework built on this router
|
|
40
|
+
* whose "actions" are `{ handle, validations }` objects gets typed routes from
|
|
41
|
+
* this builder without needing one of its own.
|
|
42
|
+
*
|
|
43
|
+
* ## What it is not
|
|
44
|
+
*
|
|
45
|
+
* Not a replacement for `router.get(...)`, which stays exactly as it is for
|
|
46
|
+
* every route that would rather be lazy than inferable, and not a replacement
|
|
47
|
+
* for OpenAPI, which remains the answer for consumers that are not TypeScript
|
|
48
|
+
* in this repo.
|
|
49
|
+
*
|
|
50
|
+
* ## Groups
|
|
51
|
+
*
|
|
52
|
+
* There is deliberately no `.group()`. A prefix applied only at runtime makes
|
|
53
|
+
* every accumulated path type wrong, and one applied only in the type is a
|
|
54
|
+
* second place for the URL to be written down. Write the full path; it is what
|
|
55
|
+
* the client will show you anyway.
|
|
56
|
+
*/
|
|
57
|
+
import type { ExtractRouteParams } from '../types';
|
|
58
|
+
import type { TypedRouteMap } from './contract';
|
|
59
|
+
import type { InputOf, OutputOf, TypedHandlerLike } from './endpoint';
|
|
60
|
+
/**
|
|
61
|
+
* A path's params, normalized.
|
|
62
|
+
*
|
|
63
|
+
* `ExtractRouteParams` answers `object` for a path with no parameters, which
|
|
64
|
+
* would let a client pass anything. An empty record says what is meant.
|
|
65
|
+
*/
|
|
66
|
+
export type TypedRouteParams<P extends string> = keyof ExtractRouteParams<P> extends never ? Record<string, never> : ExtractRouteParams<P>;
|
|
67
|
+
type Entry<P extends string, H> = {
|
|
68
|
+
input: InputOf<H>;
|
|
69
|
+
output: OutputOf<H>;
|
|
70
|
+
params: TypedRouteParams<P>;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Per-route settings.
|
|
74
|
+
*
|
|
75
|
+
* An argument rather than a chained call, because chaining `.middleware()`
|
|
76
|
+
* would return the route and lose the accumulated map. Applied by calling the
|
|
77
|
+
* matching method on whatever the registrar returned, so a router whose
|
|
78
|
+
* `get()` returns a chainable route supports all of them; see
|
|
79
|
+
* {@link TypedRegistrar}.
|
|
80
|
+
*/
|
|
81
|
+
export interface TypedRouteOptions {
|
|
82
|
+
middleware?: string | readonly string[];
|
|
83
|
+
name?: string;
|
|
84
|
+
skipCsrf?: boolean;
|
|
85
|
+
requireCsrf?: boolean;
|
|
86
|
+
rateLimit?: {
|
|
87
|
+
max: number;
|
|
88
|
+
window?: 'second' | 'minute' | 'hour' | 'day' | number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Anything that can register a route.
|
|
93
|
+
*
|
|
94
|
+
* `Router` satisfies this, and so does any wrapper around it. The return value
|
|
95
|
+
* is whatever that router hands back: when it is a chainable route,
|
|
96
|
+
* {@link TypedRouteOptions} are applied to it.
|
|
97
|
+
*/
|
|
98
|
+
export interface TypedRegistrar {
|
|
99
|
+
get: (path: string, handler: any) => unknown;
|
|
100
|
+
post: (path: string, handler: any) => unknown;
|
|
101
|
+
put: (path: string, handler: any) => unknown;
|
|
102
|
+
patch: (path: string, handler: any) => unknown;
|
|
103
|
+
delete: (path: string, handler: any) => unknown;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The builder. Each method returns the same object at runtime and a wider type
|
|
107
|
+
* at compile time, which is what puts the route map in `typeof api` without
|
|
108
|
+
* anything being written down twice.
|
|
109
|
+
*/
|
|
110
|
+
export interface TypedRouter<R extends TypedRouteMap = {}> {
|
|
111
|
+
/**
|
|
112
|
+
* Phantom. Never set at runtime; it is where `RouteMapOf` reads the
|
|
113
|
+
* accumulated map from.
|
|
114
|
+
*/
|
|
115
|
+
readonly __routes?: R;
|
|
116
|
+
get: <P extends string, H extends TypedHandlerLike>(path: P, handler: H, options?: TypedRouteOptions) => TypedRouter<R & {
|
|
117
|
+
[K in `GET ${P}`]: Entry<P, H>;
|
|
118
|
+
}>;
|
|
119
|
+
post: <P extends string, H extends TypedHandlerLike>(path: P, handler: H, options?: TypedRouteOptions) => TypedRouter<R & {
|
|
120
|
+
[K in `POST ${P}`]: Entry<P, H>;
|
|
121
|
+
}>;
|
|
122
|
+
put: <P extends string, H extends TypedHandlerLike>(path: P, handler: H, options?: TypedRouteOptions) => TypedRouter<R & {
|
|
123
|
+
[K in `PUT ${P}`]: Entry<P, H>;
|
|
124
|
+
}>;
|
|
125
|
+
patch: <P extends string, H extends TypedHandlerLike>(path: P, handler: H, options?: TypedRouteOptions) => TypedRouter<R & {
|
|
126
|
+
[K in `PATCH ${P}`]: Entry<P, H>;
|
|
127
|
+
}>;
|
|
128
|
+
delete: <P extends string, H extends TypedHandlerLike>(path: P, handler: H, options?: TypedRouteOptions) => TypedRouter<R & {
|
|
129
|
+
[K in `DELETE ${P}`]: Entry<P, H>;
|
|
130
|
+
}>;
|
|
131
|
+
}
|
|
132
|
+
/** The accumulated route map, pulled back out of a builder's type. */
|
|
133
|
+
export type RoutesOf<T> = T extends TypedRouter<infer R> ? R : never;
|
|
134
|
+
/**
|
|
135
|
+
* Build a typed route group on top of a router.
|
|
136
|
+
*
|
|
137
|
+
* Routes land in that router's table exactly as if they had been registered by
|
|
138
|
+
* hand; the only difference is that the builder's own type remembers them.
|
|
139
|
+
*/
|
|
140
|
+
export declare function createTypedRouter(router: TypedRegistrar): TypedRouter;
|
|
141
|
+
export {};
|
package/package.json
CHANGED
package/dist/chunk-1ahs68ys.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __returnValue = (v) => v;
|
|
4
|
-
function __exportSetter(name, newValue) {
|
|
5
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
-
}
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, {
|
|
10
|
-
get: all[name],
|
|
11
|
-
enumerable: true,
|
|
12
|
-
configurable: true,
|
|
13
|
-
set: __exportSetter.bind(all, name)
|
|
14
|
-
});
|
|
15
|
-
};
|
|
16
|
-
var __require = import.meta.require;
|
|
17
|
-
|
|
18
|
-
export { __export, __require };
|