rouzer 3.2.0 → 5.0.0
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/README.md +8 -7
- package/dist/client/index.d.ts +18 -24
- package/dist/client/index.js +48 -52
- package/dist/http.d.ts +4 -4
- package/dist/http.js +8 -8
- package/dist/response.d.ts +12 -2
- package/dist/server/router.d.ts +1 -1
- package/dist/server/router.js +2 -1
- package/dist/types/args.d.ts +28 -37
- package/dist/types/handler.d.ts +6 -7
- package/dist/types/index.d.ts +0 -1
- package/dist/types/infer.d.ts +3 -8
- package/dist/types/schema.d.ts +6 -2
- package/dist/types/server.d.ts +2 -2
- package/docs/context.md +19 -44
- package/examples/basic-usage.ts +2 -9
- package/examples/error-responses.ts +4 -4
- package/package.json +1 -1
- package/dist/types/request.d.ts +0 -35
- package/dist/types/request.js +0 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Rouzer lets you declare an HTTP route tree once and share its TypeScript types
|
|
4
4
|
and Zod validation between a Hattip-compatible server and a typed fetch client.
|
|
5
|
+
The client is always created from that route tree.
|
|
5
6
|
|
|
6
7
|
## What it does
|
|
7
8
|
|
|
@@ -45,7 +46,7 @@ Consider something else if:
|
|
|
45
46
|
- Zod v4 or newer
|
|
46
47
|
- a Hattip adapter when using `createRouter(...)`
|
|
47
48
|
- a Fetch API implementation when using `createClient(...)`
|
|
48
|
-
- an absolute `baseURL` for generated client URLs
|
|
49
|
+
- an absolute `baseURL` and shared `routes` tree for generated client URLs
|
|
49
50
|
|
|
50
51
|
## Installation
|
|
51
52
|
|
|
@@ -96,14 +97,14 @@ const client = createClient({
|
|
|
96
97
|
})
|
|
97
98
|
|
|
98
99
|
const { message } = await client.hello({
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
name: 'world',
|
|
101
|
+
excited: true,
|
|
101
102
|
})
|
|
102
103
|
```
|
|
103
104
|
|
|
104
|
-
`handler` can be mounted with any Hattip adapter.
|
|
105
|
-
route arguments before `fetch`; server handlers validate matched path,
|
|
106
|
-
headers, and JSON bodies before your handler runs.
|
|
105
|
+
`handler` can be mounted with any Hattip adapter. Generated client action calls
|
|
106
|
+
validate route arguments before `fetch`; server handlers validate matched path,
|
|
107
|
+
query, headers, and JSON bodies before your handler runs.
|
|
107
108
|
|
|
108
109
|
### Typed status responses
|
|
109
110
|
|
|
@@ -142,7 +143,7 @@ const client = createClient({
|
|
|
142
143
|
routes,
|
|
143
144
|
})
|
|
144
145
|
|
|
145
|
-
const [error, user, status] = await client.getUser({
|
|
146
|
+
const [error, user, status] = await client.getUser({ id: '42' })
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
Success entries resolve as `[null, value, status]`; declared error entries
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Promisable } from '../common.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type HttpAction, type HttpResource, type HttpRouteTree } from '../http.js';
|
|
3
3
|
import { type ClientResponsePlugin } from '../response.js';
|
|
4
|
-
import type {
|
|
5
|
-
import type { RouteRequest } from '../types/request.js';
|
|
4
|
+
import type { RouteInput, RouteOptions } from '../types/args.js';
|
|
6
5
|
import type { InferRouteResponse } from '../types/response.js';
|
|
7
6
|
import type { RouteSchema } from '../types/schema.js';
|
|
8
7
|
/** Client type inferred from an HTTP route tree passed to `createClient`. */
|
|
@@ -10,9 +9,8 @@ export type RouzerClient<TRoutes extends HttpRouteTree = Record<string, never>>
|
|
|
10
9
|
/**
|
|
11
10
|
* Create a typed fetch client for an HTTP route tree.
|
|
12
11
|
*
|
|
13
|
-
* @remarks The returned client
|
|
14
|
-
*
|
|
15
|
-
* tree and attaches direct action functions such as `client.users.list(...)`.
|
|
12
|
+
* @remarks The returned client mirrors the resource tree and attaches direct
|
|
13
|
+
* action functions such as `client.users.list(...)`.
|
|
16
14
|
*/
|
|
17
15
|
export declare function createClient<TRoutes extends HttpRouteTree = Record<string, never>>(config: {
|
|
18
16
|
/**
|
|
@@ -38,22 +36,22 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
38
36
|
* await client.users.list({ query: { page: 1 } })
|
|
39
37
|
* ```
|
|
40
38
|
*/
|
|
41
|
-
routes
|
|
39
|
+
routes: TRoutes;
|
|
42
40
|
/** Response codec plugins used by generated action functions. */
|
|
43
41
|
plugins?: readonly ClientResponsePlugin[];
|
|
44
42
|
/**
|
|
45
|
-
* Custom handler for non-2xx responses from
|
|
46
|
-
*
|
|
43
|
+
* Custom handler for non-2xx responses from generated client action
|
|
44
|
+
* functions.
|
|
47
45
|
*
|
|
48
|
-
* @remarks When provided, the return value is returned from the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
46
|
+
* @remarks When provided, the return value is returned from the client action
|
|
47
|
+
* as-is; Rouzer does not automatically parse a `Response` returned by this
|
|
48
|
+
* hook.
|
|
51
49
|
*/
|
|
52
50
|
onJsonError?: (response: Response) => Promisable<unknown>;
|
|
53
51
|
/** Custom `fetch` implementation to use for requests. */
|
|
54
52
|
fetch?: typeof globalThis.fetch;
|
|
55
53
|
}): ClientTree<TRoutes, ""> & {
|
|
56
|
-
|
|
54
|
+
clientConfig: {
|
|
57
55
|
/**
|
|
58
56
|
* Absolute base URL used for generated request URLs.
|
|
59
57
|
*
|
|
@@ -77,25 +75,21 @@ export declare function createClient<TRoutes extends HttpRouteTree = Record<stri
|
|
|
77
75
|
* await client.users.list({ query: { page: 1 } })
|
|
78
76
|
* ```
|
|
79
77
|
*/
|
|
80
|
-
routes
|
|
78
|
+
routes: TRoutes;
|
|
81
79
|
/** Response codec plugins used by generated action functions. */
|
|
82
80
|
plugins?: readonly ClientResponsePlugin[];
|
|
83
81
|
/**
|
|
84
|
-
* Custom handler for non-2xx responses from
|
|
85
|
-
*
|
|
82
|
+
* Custom handler for non-2xx responses from generated client action
|
|
83
|
+
* functions.
|
|
86
84
|
*
|
|
87
|
-
* @remarks When provided, the return value is returned from the
|
|
88
|
-
*
|
|
89
|
-
*
|
|
85
|
+
* @remarks When provided, the return value is returned from the client action
|
|
86
|
+
* as-is; Rouzer does not automatically parse a `Response` returned by this
|
|
87
|
+
* hook.
|
|
90
88
|
*/
|
|
91
89
|
onJsonError?: (response: Response) => Promisable<unknown>;
|
|
92
90
|
/** Custom `fetch` implementation to use for requests. */
|
|
93
91
|
fetch?: typeof globalThis.fetch;
|
|
94
92
|
};
|
|
95
|
-
request: <T extends RouteRequest>({ path: pathBuilder, method, args, schema, }: T) => Promise<Response & {
|
|
96
|
-
json(): Promise<T['$result']>;
|
|
97
|
-
}>;
|
|
98
|
-
json: <T extends RouteRequest>(props: T) => Promise<T['$result']>;
|
|
99
93
|
};
|
|
100
94
|
type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
|
|
101
95
|
/** Client object shape produced from an HTTP route tree. */
|
|
@@ -112,7 +106,7 @@ export type ClientTree<T extends HttpRouteTree, TPrefix extends string = ''> = {
|
|
|
112
106
|
* plugin's client result type. Actions without a response marker return the raw
|
|
113
107
|
* `Response`.
|
|
114
108
|
*/
|
|
115
|
-
export type RouteFunction<T extends RouteSchema, P extends string> = (...p:
|
|
109
|
+
export type RouteFunction<T extends RouteSchema, P extends string> = (...p: RouteInput<T, P> extends infer TInput ? {} extends TInput ? [input?: TInput, options?: RouteOptions<T>] : [input: TInput, options?: RouteOptions<T>] : never) => Promise<T extends {
|
|
116
110
|
response: any;
|
|
117
111
|
} ? InferRouteResponse<T> : Response>;
|
|
118
112
|
export {};
|
package/dist/client/index.js
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
2
|
import { createHref } from '@remix-run/route-pattern/href';
|
|
3
3
|
import { shake } from '../common.js';
|
|
4
|
-
import {
|
|
4
|
+
import { isRawBodySchema, } from '../http.js';
|
|
5
5
|
import { getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
|
|
6
|
+
import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
|
|
6
7
|
/**
|
|
7
8
|
* Create a typed fetch client for an HTTP route tree.
|
|
8
9
|
*
|
|
9
|
-
* @remarks The returned client
|
|
10
|
-
*
|
|
11
|
-
* tree and attaches direct action functions such as `client.users.list(...)`.
|
|
10
|
+
* @remarks The returned client mirrors the resource tree and attaches direct
|
|
11
|
+
* action functions such as `client.users.list(...)`.
|
|
12
12
|
*/
|
|
13
13
|
export function createClient(config) {
|
|
14
14
|
const baseURL = config.baseURL.replace(/\/?$/, '/');
|
|
15
15
|
const defaultHeaders = config.headers && shake(config.headers);
|
|
16
16
|
const fetch = config.fetch ?? globalThis.fetch;
|
|
17
17
|
const responsePlugins = createResponsePluginMap(config.plugins, 'client response');
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (schema.path) {
|
|
24
|
-
path = schema.path.parse(path);
|
|
25
|
-
}
|
|
18
|
+
validateClientResponsePlugins(config.routes, responsePlugins);
|
|
19
|
+
async function plainRequest({ path: pathPattern, method, input = {}, options: { body: rawBody, headers, ...init } = {}, schema, }) {
|
|
20
|
+
const path = schema.path
|
|
21
|
+
? schema.path.parse(pickObjectSchemaFields(schema.path, input))
|
|
22
|
+
: input;
|
|
26
23
|
let url;
|
|
27
|
-
const href = createHref(
|
|
24
|
+
const href = createHref(pathPattern, path);
|
|
28
25
|
if (href[0] === '/') {
|
|
29
26
|
url = new URL(baseURL);
|
|
30
27
|
url.pathname += href.slice(1);
|
|
@@ -33,17 +30,14 @@ export function createClient(config) {
|
|
|
33
30
|
url = new URL(href, baseURL);
|
|
34
31
|
}
|
|
35
32
|
if (schema.query) {
|
|
36
|
-
query = schema.query.parse(query
|
|
33
|
+
const query = schema.query.parse(pickObjectSchemaFields(schema.query, input));
|
|
37
34
|
url.search = new URLSearchParams(shake(query)).toString();
|
|
38
35
|
}
|
|
39
|
-
|
|
40
|
-
throw new Error('Unexpected query parameters');
|
|
41
|
-
}
|
|
36
|
+
let body;
|
|
42
37
|
if (schema.body) {
|
|
43
|
-
body = schema.body
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
throw new Error('Unexpected body');
|
|
38
|
+
body = isRawBodySchema(schema.body)
|
|
39
|
+
? rawBody
|
|
40
|
+
: schema.body.parse(pickObjectSchemaFields(schema.body, input));
|
|
47
41
|
}
|
|
48
42
|
if (headers) {
|
|
49
43
|
headers = shake(headers);
|
|
@@ -57,27 +51,24 @@ export function createClient(config) {
|
|
|
57
51
|
return fetch(url, {
|
|
58
52
|
...init,
|
|
59
53
|
method,
|
|
60
|
-
body:
|
|
54
|
+
body: isRawBodySchema(schema.body)
|
|
55
|
+
? body
|
|
56
|
+
: body !== undefined
|
|
57
|
+
? JSON.stringify(body)
|
|
58
|
+
: undefined,
|
|
61
59
|
headers: (headers ?? defaultHeaders),
|
|
62
60
|
});
|
|
63
61
|
}
|
|
64
|
-
async function
|
|
65
|
-
const response = await
|
|
66
|
-
if (!response.ok) {
|
|
67
|
-
return handleResponseError(response, props);
|
|
68
|
-
}
|
|
69
|
-
return response.json();
|
|
70
|
-
}
|
|
71
|
-
async function response(props) {
|
|
72
|
-
const httpResponse = await request(props);
|
|
62
|
+
async function parsedRequest(props) {
|
|
63
|
+
const response = await plainRequest(props);
|
|
73
64
|
const responseSchema = props.schema.response;
|
|
74
65
|
// Handle status-keyed response maps
|
|
75
66
|
if (isResponseMap(responseSchema)) {
|
|
76
|
-
const status =
|
|
67
|
+
const status = response.status;
|
|
77
68
|
if (status in responseSchema) {
|
|
78
69
|
const marker = responseSchema[status];
|
|
79
70
|
if (isErrorMarker(marker)) {
|
|
80
|
-
return [await
|
|
71
|
+
return [await response.json(), null, status];
|
|
81
72
|
}
|
|
82
73
|
const pluginId = getResponsePluginMarkerId(marker);
|
|
83
74
|
if (pluginId) {
|
|
@@ -87,20 +78,20 @@ export function createClient(config) {
|
|
|
87
78
|
}
|
|
88
79
|
return [
|
|
89
80
|
null,
|
|
90
|
-
await plugin.decode(
|
|
81
|
+
await plugin.decode(response, {
|
|
91
82
|
marker: marker,
|
|
92
83
|
request: props,
|
|
93
84
|
}),
|
|
94
85
|
status,
|
|
95
86
|
];
|
|
96
87
|
}
|
|
97
|
-
return [null, await
|
|
88
|
+
return [null, await response.json(), status];
|
|
98
89
|
}
|
|
99
90
|
// Undeclared status — reject
|
|
100
|
-
return handleResponseError(
|
|
91
|
+
return handleResponseError(response, props);
|
|
101
92
|
}
|
|
102
|
-
if (!
|
|
103
|
-
return handleResponseError(
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
return handleResponseError(response, props);
|
|
104
95
|
}
|
|
105
96
|
const pluginId = getResponsePluginMarkerId(responseSchema);
|
|
106
97
|
if (pluginId) {
|
|
@@ -108,18 +99,18 @@ export function createClient(config) {
|
|
|
108
99
|
if (!plugin) {
|
|
109
100
|
throw missingClientResponsePlugin(pluginId);
|
|
110
101
|
}
|
|
111
|
-
return plugin.decode(
|
|
102
|
+
return plugin.decode(response, {
|
|
112
103
|
marker: responseSchema,
|
|
113
104
|
request: props,
|
|
114
105
|
});
|
|
115
106
|
}
|
|
116
|
-
return
|
|
107
|
+
return response.json();
|
|
117
108
|
}
|
|
118
109
|
async function handleResponseError(response, props) {
|
|
119
110
|
if (config.onJsonError) {
|
|
120
111
|
return config.onJsonError(response);
|
|
121
112
|
}
|
|
122
|
-
const error = new Error(`Request to ${props.method} ${createHref(props.path, props.
|
|
113
|
+
const error = new Error(`Request to ${props.method} ${createHref(props.path, props.input)} failed with status ${response.status}`);
|
|
123
114
|
const contentType = response.headers.get('content-type');
|
|
124
115
|
if (contentType?.includes('application/json')) {
|
|
125
116
|
Object.assign(error, await response.json());
|
|
@@ -127,31 +118,28 @@ export function createClient(config) {
|
|
|
127
118
|
throw error;
|
|
128
119
|
}
|
|
129
120
|
return {
|
|
130
|
-
...(config.routes
|
|
131
|
-
|
|
132
|
-
: null),
|
|
133
|
-
config,
|
|
134
|
-
request,
|
|
135
|
-
json,
|
|
121
|
+
...connectTree(config.routes, '', plainRequest, parsedRequest),
|
|
122
|
+
clientConfig: config,
|
|
136
123
|
};
|
|
137
124
|
}
|
|
138
|
-
function connectTree(tree, prefix,
|
|
125
|
+
function connectTree(tree, prefix, plainRequest, parsedRequest) {
|
|
139
126
|
return Object.fromEntries(Object.entries(tree).map(([key, node]) => {
|
|
140
127
|
if (node.kind === 'resource') {
|
|
141
128
|
return [
|
|
142
129
|
key,
|
|
143
|
-
connectTree(node.children, joinPaths(prefix, node.path.source),
|
|
130
|
+
connectTree(node.children, joinPaths(prefix, node.path.source), plainRequest, parsedRequest),
|
|
144
131
|
];
|
|
145
132
|
}
|
|
146
133
|
const path = RoutePattern.parse(joinPaths(prefix, node.path?.source ?? ''));
|
|
147
|
-
const fetch = node.schema.response ?
|
|
134
|
+
const fetch = node.schema.response ? parsedRequest : plainRequest;
|
|
148
135
|
return [
|
|
149
136
|
key,
|
|
150
|
-
(
|
|
137
|
+
(input, options) => fetch({
|
|
151
138
|
schema: node.schema,
|
|
152
139
|
path,
|
|
153
140
|
method: node.method,
|
|
154
|
-
|
|
141
|
+
input,
|
|
142
|
+
options,
|
|
155
143
|
$result: undefined,
|
|
156
144
|
}),
|
|
157
145
|
];
|
|
@@ -177,6 +165,14 @@ function validateClientResponsePlugins(tree, plugins) {
|
|
|
177
165
|
function missingClientResponsePlugin(pluginId) {
|
|
178
166
|
return new Error(`Missing client response plugin for ${pluginId}`);
|
|
179
167
|
}
|
|
168
|
+
function pickObjectSchemaFields(schema, input) {
|
|
169
|
+
if (typeof input !== 'object' || input === null) {
|
|
170
|
+
return input;
|
|
171
|
+
}
|
|
172
|
+
return Object.fromEntries(Object.keys(schema.shape)
|
|
173
|
+
.filter(key => key in input)
|
|
174
|
+
.map(key => [key, input[key]]));
|
|
175
|
+
}
|
|
180
176
|
function joinPaths(left, right) {
|
|
181
177
|
return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
|
|
182
178
|
}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
-
import type {
|
|
3
|
-
import type { RouteSchema } from './types/schema.js';
|
|
2
|
+
import type { RawBodySchema, RouteSchema } from './types/schema.js';
|
|
4
3
|
/** HTTP methods supported by Rouzer action declarations. */
|
|
5
4
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
6
5
|
/**
|
|
@@ -18,8 +17,6 @@ export type HttpAction<P extends string = string, T extends RouteSchema = RouteS
|
|
|
18
17
|
method: M;
|
|
19
18
|
/** Request validation and optional response type schema. */
|
|
20
19
|
schema: T;
|
|
21
|
-
/** Low-level request descriptor factory for this action. */
|
|
22
|
-
request: RouteRequestFactory<T, P>;
|
|
23
20
|
};
|
|
24
21
|
/**
|
|
25
22
|
* Path-scoped namespace in an HTTP route tree.
|
|
@@ -65,3 +62,6 @@ export declare function patch<const T extends RouteSchema>(schema: T): HttpActio
|
|
|
65
62
|
declare function deleteAction<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'DELETE'>;
|
|
66
63
|
declare function deleteAction<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'DELETE'>;
|
|
67
64
|
export { deleteAction as delete };
|
|
65
|
+
/** Declare a request body that is passed through to `fetch` without JSON encoding. */
|
|
66
|
+
export declare function rawBody(): RawBodySchema;
|
|
67
|
+
export declare function isRawBodySchema(schema: unknown): schema is RawBodySchema;
|
package/dist/http.js
CHANGED
|
@@ -29,17 +29,17 @@ function deleteAction(pathOrSchema, schema) {
|
|
|
29
29
|
return action('DELETE', pathOrSchema, schema);
|
|
30
30
|
}
|
|
31
31
|
export { deleteAction as delete };
|
|
32
|
+
/** Declare a request body that is passed through to `fetch` without JSON encoding. */
|
|
33
|
+
export function rawBody() {
|
|
34
|
+
return { __rawBody__: Symbol('rouzer.rawBody') };
|
|
35
|
+
}
|
|
36
|
+
export function isRawBodySchema(schema) {
|
|
37
|
+
return Boolean(schema && typeof schema === 'object' && '__rawBody__' in schema);
|
|
38
|
+
}
|
|
32
39
|
function action(method, pathOrSchema, schema) {
|
|
33
40
|
const path = typeof pathOrSchema === 'string'
|
|
34
41
|
? RoutePattern.parse(pathOrSchema)
|
|
35
42
|
: undefined;
|
|
36
43
|
schema ??= typeof pathOrSchema === 'string' ? {} : pathOrSchema;
|
|
37
|
-
|
|
38
|
-
schema,
|
|
39
|
-
path: path ?? RoutePattern.parse(''),
|
|
40
|
-
method,
|
|
41
|
-
args,
|
|
42
|
-
$result: undefined,
|
|
43
|
-
}));
|
|
44
|
-
return { kind: 'action', path, method, schema, request };
|
|
44
|
+
return { kind: 'action', path, method, schema };
|
|
45
45
|
}
|
package/dist/response.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Promisable } from './common.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { RoutePattern } from '@remix-run/route-pattern';
|
|
3
|
+
import type { RouteOptions } from './types/args.js';
|
|
4
|
+
import type { RouteSchema } from './types/schema.js';
|
|
3
5
|
/** Runtime key carried by response plugin markers. */
|
|
4
6
|
export declare const responsePluginMarker: unique symbol;
|
|
5
7
|
/**
|
|
@@ -24,9 +26,17 @@ export type ClientResponsePlugin = {
|
|
|
24
26
|
/** Decode a successful `Response` into the client action result. */
|
|
25
27
|
decode(response: Response, context: {
|
|
26
28
|
marker: ResponsePluginMarker<any, any>;
|
|
27
|
-
request:
|
|
29
|
+
request: ClientResponsePluginRequest;
|
|
28
30
|
}): Promisable<unknown>;
|
|
29
31
|
};
|
|
32
|
+
/** Request metadata passed to client response plugins. */
|
|
33
|
+
export type ClientResponsePluginRequest = {
|
|
34
|
+
schema: RouteSchema;
|
|
35
|
+
path: RoutePattern;
|
|
36
|
+
method: string;
|
|
37
|
+
input?: unknown;
|
|
38
|
+
options?: RouteOptions;
|
|
39
|
+
};
|
|
30
40
|
/** Router-side response plugin used by `createRouter({ plugins })`. */
|
|
31
41
|
export type RouterResponsePlugin = {
|
|
32
42
|
/** Stable response codec id matched against route response markers. */
|
package/dist/server/router.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HattipHandler } from '@hattip/core';
|
|
2
2
|
import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes } from 'alien-middleware';
|
|
3
|
-
import type
|
|
3
|
+
import { type HttpRouteTree } from '../http.js';
|
|
4
4
|
import { type RouterResponsePlugin } from '../response.js';
|
|
5
5
|
import type { RouteRequestHandlerMap } from '../types/server.js';
|
|
6
6
|
export { chain };
|
package/dist/server/router.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createMatcher } from '@remix-run/route-pattern/match';
|
|
|
3
3
|
import { chain, MiddlewareChain, } from 'alien-middleware';
|
|
4
4
|
import * as z from 'zod';
|
|
5
5
|
import { mapValues } from '../common.js';
|
|
6
|
+
import { isRawBodySchema } from '../http.js';
|
|
6
7
|
import { createResponsePluginMap, getResponsePluginMarkerId, } from '../response.js';
|
|
7
8
|
import { getDefaultSuccessStatus, getResponseMapPluginIds, isErrorMarker, isResponseMap, } from '../response-map.js';
|
|
8
9
|
export { chain };
|
|
@@ -103,7 +104,7 @@ class RouterObject extends MiddlewareChain {
|
|
|
103
104
|
return httpClientError(error, 'Invalid query string', config);
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
|
-
if (schema.body) {
|
|
107
|
+
if (schema.body && !isRawBodySchema(schema.body)) {
|
|
107
108
|
const error = await parseRequestBody(context, schema.body);
|
|
108
109
|
if (error) {
|
|
109
110
|
addDebugHeaders?.(context, route);
|
package/dist/types/args.d.ts
CHANGED
|
@@ -1,51 +1,42 @@
|
|
|
1
1
|
import type { MatchParams } from '@remix-run/route-pattern/match';
|
|
2
2
|
import type * as z from 'zod';
|
|
3
|
-
import type { MutationRouteSchema, QueryRouteSchema, RouteSchema } from './schema.js';
|
|
3
|
+
import type { MutationRouteSchema, QueryRouteSchema, RawBodySchema, RouteSchema } from './schema.js';
|
|
4
4
|
declare class Any {
|
|
5
5
|
private isAny;
|
|
6
6
|
}
|
|
7
|
-
type
|
|
7
|
+
type PathInput<T, P extends string> = T extends {
|
|
8
8
|
path: infer TPath;
|
|
9
|
-
} ?
|
|
10
|
-
|
|
11
|
-
} : {
|
|
12
|
-
[K in keyof T as 'path']: z.infer<TPath>;
|
|
13
|
-
} : MatchParams<P> extends infer TParams ? {} extends TParams ? {
|
|
14
|
-
[K in keyof T as 'path']?: TParams;
|
|
15
|
-
} : {
|
|
16
|
-
[K in keyof T as 'path']: TParams;
|
|
17
|
-
} : unknown;
|
|
18
|
-
type QueryArgs<T> = T extends QueryRouteSchema & {
|
|
9
|
+
} ? z.infer<TPath> : MatchParams<P>;
|
|
10
|
+
type QueryInput<T> = T extends QueryRouteSchema & {
|
|
19
11
|
query: infer TQuery;
|
|
20
|
-
} ?
|
|
21
|
-
|
|
22
|
-
} : {
|
|
23
|
-
[K in keyof T as 'query']: z.infer<TQuery>;
|
|
24
|
-
} : unknown;
|
|
25
|
-
type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
|
|
12
|
+
} ? z.infer<TQuery> : unknown;
|
|
13
|
+
type BodyInput<T> = T extends MutationRouteSchema ? T extends {
|
|
26
14
|
body: infer TBody;
|
|
27
|
-
} ?
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} : {
|
|
32
|
-
body?: unknown;
|
|
33
|
-
} : unknown;
|
|
15
|
+
} ? TBody extends RawBodySchema ? unknown : z.infer<TBody> : unknown : unknown;
|
|
16
|
+
type HeaderInput<T> = T extends {
|
|
17
|
+
headers: infer THeaders;
|
|
18
|
+
} ? Partial<z.infer<THeaders>> : Record<string, string | undefined>;
|
|
34
19
|
/**
|
|
35
|
-
*
|
|
20
|
+
* Semantic input accepted by a generated client action function.
|
|
36
21
|
*
|
|
37
|
-
* @remarks
|
|
38
|
-
*
|
|
39
|
-
* a
|
|
40
|
-
* except `method`, `body`, and `headers`, which Rouzer derives from the action
|
|
41
|
-
* schema and call arguments.
|
|
22
|
+
* @remarks Path params, query params, and JSON body fields are flattened into a
|
|
23
|
+
* single object. Avoid declaring duplicate keys across path/query/body schemas,
|
|
24
|
+
* since a flat input cannot distinguish their source.
|
|
42
25
|
*/
|
|
43
|
-
export type
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
export type RouteInput<T extends RouteSchema = any, P extends string = string> = [T] extends [Any] ? any : PathInput<T, P> & QueryInput<T> & BodyInput<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Fetch options accepted as the second argument to a generated client action.
|
|
29
|
+
*
|
|
30
|
+
* @remarks `headers` remains optional because required route headers may be
|
|
31
|
+
* supplied by `createClient({ headers })` defaults.
|
|
32
|
+
*/
|
|
33
|
+
type RouteBodyOption<T> = T extends {
|
|
34
|
+
body: RawBodySchema;
|
|
35
|
+
} ? {
|
|
36
|
+
body: BodyInit | null;
|
|
37
|
+
} : {};
|
|
38
|
+
export type RouteOptions<T extends RouteSchema = any> = Omit<RequestInit, 'method' | 'body' | 'headers'> & RouteBodyOption<T> & {
|
|
48
39
|
/** Headers for this request. Undefined values are removed before `fetch`. */
|
|
49
|
-
headers?:
|
|
40
|
+
headers?: HeaderInput<T>;
|
|
50
41
|
};
|
|
51
42
|
export {};
|
package/dist/types/handler.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ import type * as z from 'zod';
|
|
|
4
4
|
import { Promisable } from '../common.js';
|
|
5
5
|
import type { HttpAction } from '../http.js';
|
|
6
6
|
import type { InferRouteHandlerResult, InferResponseMapErrors, InferResponseMapSuccesses } from './response.js';
|
|
7
|
-
import type { RouteResponseMap, RouteSchema } from './schema.js';
|
|
7
|
+
import type { RawBodySchema, RouteResponseMap, RouteSchema } from './schema.js';
|
|
8
|
+
type InferHandlerBody<T> = T extends {
|
|
9
|
+
body: infer TBody;
|
|
10
|
+
} ? TBody extends RawBodySchema ? undefined : z.infer<TBody> : undefined;
|
|
8
11
|
type RequestContext<TMiddleware extends AnyMiddlewareChain> = MiddlewareContext<TMiddleware>;
|
|
9
12
|
/**
|
|
10
13
|
* Error response returned by `ctx.error(status, body)` in route handlers.
|
|
@@ -51,9 +54,7 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
|
|
|
51
54
|
path: TAction['schema'] extends {
|
|
52
55
|
path: any;
|
|
53
56
|
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
54
|
-
body: TAction['schema']
|
|
55
|
-
body: any;
|
|
56
|
-
} ? z.infer<TAction['schema']['body']> : undefined;
|
|
57
|
+
body: InferHandlerBody<TAction['schema']>;
|
|
57
58
|
headers: TAction['schema'] extends {
|
|
58
59
|
headers: any;
|
|
59
60
|
} ? z.infer<TAction['schema']['headers']> : undefined;
|
|
@@ -71,9 +72,7 @@ export type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction e
|
|
|
71
72
|
path: TAction['schema'] extends {
|
|
72
73
|
path: any;
|
|
73
74
|
} ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
|
|
74
|
-
body: TAction['schema']
|
|
75
|
-
body: any;
|
|
76
|
-
} ? z.infer<TAction['schema']['body']> : undefined;
|
|
75
|
+
body: InferHandlerBody<TAction['schema']>;
|
|
77
76
|
headers: TAction['schema'] extends {
|
|
78
77
|
headers: any;
|
|
79
78
|
} ? z.infer<TAction['schema']['headers']> : undefined;
|
package/dist/types/index.d.ts
CHANGED
package/dist/types/infer.d.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import type * as z from 'zod';
|
|
2
2
|
import type { MutationRouteSchema, RouteSchema } from './schema.js';
|
|
3
|
-
import type { RouteRequestFactory } from './request.js';
|
|
4
3
|
type InferRouteSchemaBody<TSchema> = TSchema extends MutationRouteSchema ? TSchema extends {
|
|
5
4
|
body: infer TBody;
|
|
6
5
|
} ? z.infer<TBody> : unknown : never;
|
|
7
|
-
type InferRouteArgsBody<TArgs> = TArgs extends {
|
|
8
|
-
body?: infer TBody;
|
|
9
|
-
} ? TBody : never;
|
|
10
6
|
/**
|
|
11
|
-
* Infer the request body type from an action schema
|
|
7
|
+
* Infer the request body type from an action schema.
|
|
12
8
|
*
|
|
13
9
|
* @remarks HTTP action schemas can be inspected with
|
|
14
|
-
* `InferRouteBody<typeof action.schema>`.
|
|
15
|
-
* infer their `body` argument type. Schemas without a body schema infer
|
|
10
|
+
* `InferRouteBody<typeof action.schema>`. Schemas without a body schema infer
|
|
16
11
|
* `unknown`.
|
|
17
12
|
*/
|
|
18
|
-
export type InferRouteBody<T> = T extends
|
|
13
|
+
export type InferRouteBody<T> = T extends RouteSchema ? InferRouteSchemaBody<T> : never;
|
|
19
14
|
export {};
|
package/dist/types/schema.d.ts
CHANGED
|
@@ -36,14 +36,18 @@ export type QueryRouteSchema = {
|
|
|
36
36
|
/** Optional compile-time-only JSON or plugin response type marker. */
|
|
37
37
|
response?: RouteResponseSchema;
|
|
38
38
|
};
|
|
39
|
+
/** Marker for request bodies passed through to `fetch` without JSON encoding. */
|
|
40
|
+
export type RawBodySchema = {
|
|
41
|
+
readonly __rawBody__: unique symbol;
|
|
42
|
+
};
|
|
39
43
|
/** Schema shape for mutation route methods. */
|
|
40
44
|
export type MutationRouteSchema = {
|
|
41
45
|
/** Optional Zod object used to validate path params. */
|
|
42
46
|
path?: z.ZodObject<any>;
|
|
43
47
|
/** Mutation routes do not accept query schemas. */
|
|
44
48
|
query?: never;
|
|
45
|
-
/** Optional Zod schema used to validate the JSON request body. */
|
|
46
|
-
body?: z.
|
|
49
|
+
/** Optional Zod schema used to validate the JSON request body, or raw body marker. */
|
|
50
|
+
body?: z.ZodObject<any> | RawBodySchema;
|
|
47
51
|
/** Optional Zod object used to validate request headers. */
|
|
48
52
|
headers?: z.ZodObject<any>;
|
|
49
53
|
/** Optional compile-time-only JSON or plugin response type marker. */
|
package/dist/types/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyMiddlewareChain
|
|
1
|
+
import type { AnyMiddlewareChain } from 'alien-middleware';
|
|
2
2
|
import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
|
|
3
3
|
import type { InferActionHandler } from './handler.js';
|
|
4
4
|
import type { Join } from './path.js';
|
|
@@ -10,6 +10,6 @@ import type { Join } from './path.js';
|
|
|
10
10
|
* Handler context is inferred from middleware plus accumulated path params,
|
|
11
11
|
* query/body schemas, and header schemas.
|
|
12
12
|
*/
|
|
13
|
-
export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain =
|
|
13
|
+
export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain = never, TPrefix extends string = ''> = {
|
|
14
14
|
[K in keyof TRoutes]: TRoutes[K] extends HttpResource<infer P, infer C> ? RouteRequestHandlerMap<C, TMiddleware, Join<TPrefix, P>> : TRoutes[K] extends HttpAction<infer P, any, any> ? InferActionHandler<TMiddleware, TRoutes[K], Join<TPrefix, P>> : never;
|
|
15
15
|
};
|
package/docs/context.md
CHANGED
|
@@ -211,18 +211,16 @@ requests with an `Origin` header.
|
|
|
211
211
|
|
|
212
212
|
### Client
|
|
213
213
|
|
|
214
|
-
`createClient({ baseURL, routes })` creates
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
- `
|
|
219
|
-
|
|
220
|
-
- response-map support
|
|
221
|
-
|
|
222
|
-
- response plugin support
|
|
223
|
-
|
|
224
|
-
- a client tree that mirrors `routes`, with action functions such as
|
|
225
|
-
`client.profiles.get(args)` when `routes` is supplied
|
|
214
|
+
`createClient({ baseURL, routes })` creates a client tree that mirrors
|
|
215
|
+
`routes`, with action functions such as `client.profiles.get(args)`.
|
|
216
|
+
Generated action functions include:
|
|
217
|
+
|
|
218
|
+
- raw `Response` results for actions without a response schema
|
|
219
|
+
- parsed JSON and default non-2xx throwing for `$type<T>()` responses
|
|
220
|
+
- response-map support, returning `[error, value, status]` tuples for declared
|
|
221
|
+
statuses
|
|
222
|
+
- response plugin support, such as `ndjson.clientPlugin` for NDJSON response
|
|
223
|
+
streams
|
|
226
224
|
|
|
227
225
|
Prefer an absolute `baseURL` for generated client URLs:
|
|
228
226
|
|
|
@@ -235,7 +233,8 @@ const client = createClient({
|
|
|
235
233
|
|
|
236
234
|
Default headers can be supplied with `headers`, per-request headers are merged on
|
|
237
235
|
top, and a custom `fetch` implementation can be supplied for tests or non-browser
|
|
238
|
-
runtimes.
|
|
236
|
+
runtimes. The returned client exposes the original options as `clientConfig`, so
|
|
237
|
+
route actions named `config` remain available as `client.config(...)`.
|
|
239
238
|
|
|
240
239
|
## Lifecycle
|
|
241
240
|
|
|
@@ -260,9 +259,9 @@ string-coercion step.
|
|
|
260
259
|
|
|
261
260
|
## Common tasks
|
|
262
261
|
|
|
263
|
-
###
|
|
262
|
+
### Call client actions
|
|
264
263
|
|
|
265
|
-
Use client action functions for
|
|
264
|
+
Use generated client action functions for application calls:
|
|
266
265
|
|
|
267
266
|
```ts
|
|
268
267
|
await client.profiles.get({ path: { id: '42' } })
|
|
@@ -272,29 +271,6 @@ await client.profiles.update({
|
|
|
272
271
|
})
|
|
273
272
|
```
|
|
274
273
|
|
|
275
|
-
Use longhand calls when you need to choose response handling explicitly. The
|
|
276
|
-
action request factory must include the full path you want to call, so this style
|
|
277
|
-
is most convenient for top-level actions:
|
|
278
|
-
|
|
279
|
-
```ts
|
|
280
|
-
export const getProfile = http.get('profiles/:id', {
|
|
281
|
-
response: $type<Profile>(),
|
|
282
|
-
})
|
|
283
|
-
export const routes = { getProfile }
|
|
284
|
-
|
|
285
|
-
const response = await client.request(
|
|
286
|
-
routes.getProfile.request({ path: { id: '42' } })
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
const json = await client.json(
|
|
290
|
-
routes.getProfile.request({ path: { id: '42' } })
|
|
291
|
-
)
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Response maps and response plugins are applied by generated client action
|
|
295
|
-
functions. For longhand calls to mapped or plugin-backed routes, use
|
|
296
|
-
`client.request(...)` for the raw `Response` and decode the response yourself.
|
|
297
|
-
|
|
298
274
|
### Handle declared error responses
|
|
299
275
|
|
|
300
276
|
Use `$error<T>()` inside a response map when an error status is part of the route
|
|
@@ -420,7 +396,7 @@ custom headers. Return a plain value for the default `Response.json(value)` path
|
|
|
420
396
|
|
|
421
397
|
### Customize JSON errors
|
|
422
398
|
|
|
423
|
-
By default,
|
|
399
|
+
By default, generated client action functions throw for
|
|
424
400
|
non-2xx responses that are not declared in a response map. If the response body
|
|
425
401
|
is JSON, its properties are copied onto the thrown `Error`.
|
|
426
402
|
|
|
@@ -484,7 +460,7 @@ await client.profiles.update({
|
|
|
484
460
|
- Export route trees from a small shared module and import that module on both
|
|
485
461
|
server and client.
|
|
486
462
|
- Use `rouzer/http` actions for routes that are registered with
|
|
487
|
-
`createRouter().use(...)` or `createClient({ routes })
|
|
463
|
+
`createRouter().use(...)` or the required `createClient({ routes })` option.
|
|
488
464
|
- Add Zod schemas when you need runtime guarantees; rely on inferred path params
|
|
489
465
|
only when string params are sufficient.
|
|
490
466
|
- Use `response: $type<T>()` for JSON endpoints that should have typed client
|
|
@@ -513,11 +489,10 @@ await client.profiles.update({
|
|
|
513
489
|
- Pathname route patterns expect an absolute client `baseURL`.
|
|
514
490
|
- Resource and action keys are API names only; paths come from the pattern
|
|
515
491
|
strings passed to `http.resource(...)` and action helpers.
|
|
516
|
-
- Nested action `.request(...)` factories do not include parent resource paths;
|
|
517
|
-
prefer client action functions for nested resources.
|
|
518
492
|
- Extra `RequestInit` fields in route args, such as `signal` or `credentials`,
|
|
519
|
-
are forwarded by `createClient`; `method
|
|
520
|
-
|
|
493
|
+
are forwarded by `createClient`; `method` and `body` are reserved for Rouzer's
|
|
494
|
+
action metadata and validated call arguments. Use route args or client defaults
|
|
495
|
+
for request headers.
|
|
521
496
|
- The HTTP action API has no `ALL` fallback route. Declare explicit actions for
|
|
522
497
|
supported methods.
|
|
523
498
|
- Rouzer does not automatically set `Access-Control-Allow-Credentials`; set it in
|
package/examples/basic-usage.ts
CHANGED
|
@@ -101,16 +101,9 @@ export async function runBasicUsageExample() {
|
|
|
101
101
|
fetch: createLocalFetch(handler),
|
|
102
102
|
})
|
|
103
103
|
|
|
104
|
-
const fetched = await client.profiles.get({
|
|
105
|
-
path: { id: '42' },
|
|
106
|
-
query: { includePosts: false },
|
|
107
|
-
headers: { 'x-request-id': 'docs' },
|
|
108
|
-
})
|
|
104
|
+
const fetched = await client.profiles.get({ id: '42', includePosts: false }, { headers: { 'x-request-id': 'docs' } })
|
|
109
105
|
|
|
110
|
-
const updated = await client.profiles.update({
|
|
111
|
-
path: { id: '42' },
|
|
112
|
-
body: { name: 'Grace' },
|
|
113
|
-
})
|
|
106
|
+
const updated = await client.profiles.update({ id: '42', name: 'Grace' })
|
|
114
107
|
|
|
115
108
|
return { fetched, updated }
|
|
116
109
|
}
|
|
@@ -89,10 +89,10 @@ export async function runErrorResponsesExample() {
|
|
|
89
89
|
fetch: createLocalFetch(handler),
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
const found = await client.getUser({
|
|
93
|
-
const created = await client.getUser({
|
|
94
|
-
const missing = await client.getUser({
|
|
95
|
-
const unauthorized = await client.getUser({
|
|
92
|
+
const found = await client.getUser({ id: '42' })
|
|
93
|
+
const created = await client.getUser({ id: 'created' })
|
|
94
|
+
const missing = await client.getUser({ id: 'missing' })
|
|
95
|
+
const unauthorized = await client.getUser({ id: 'unauthorized' })
|
|
96
96
|
|
|
97
97
|
return { found, created, missing, unauthorized }
|
|
98
98
|
}
|
package/package.json
CHANGED
package/dist/types/request.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { RoutePattern } from '@remix-run/route-pattern';
|
|
2
|
-
import type { RouteArgs } from './args.js';
|
|
3
|
-
import type { InferRouteResponse } from './response.js';
|
|
4
|
-
import type { RouteSchema } from './schema.js';
|
|
5
|
-
/**
|
|
6
|
-
* Request descriptor produced by an HTTP action request factory.
|
|
7
|
-
*
|
|
8
|
-
* @remarks Pass this object to `client.request(...)` for a raw `Response` or
|
|
9
|
-
* `client.json(...)` for parsed JSON handling.
|
|
10
|
-
*/
|
|
11
|
-
export type RouteRequest<TResult = any> = {
|
|
12
|
-
/** Method schema used for client-side validation. */
|
|
13
|
-
schema: RouteSchema;
|
|
14
|
-
/** Parsed route pattern used to generate the request URL. */
|
|
15
|
-
path: RoutePattern;
|
|
16
|
-
/** HTTP method to send. */
|
|
17
|
-
method: string;
|
|
18
|
-
/** Validated route arguments and request options. */
|
|
19
|
-
args: RouteArgs;
|
|
20
|
-
/** Phantom result type consumed by `client.json(...)`. */
|
|
21
|
-
$result: TResult;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Callable factory attached to an HTTP action.
|
|
25
|
-
*
|
|
26
|
-
* @remarks Calling a factory validates no data by itself; it creates a typed
|
|
27
|
-
* `RouteRequest` descriptor for `createClient` to validate and send.
|
|
28
|
-
*/
|
|
29
|
-
export type RouteRequestFactory<T extends RouteSchema, P extends string> = {
|
|
30
|
-
(...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never): RouteRequest<InferRouteResponse<T>>;
|
|
31
|
-
/** Inferred argument type for this request factory. */
|
|
32
|
-
$args: RouteArgs<T, P>;
|
|
33
|
-
/** Inferred response type for this request factory. */
|
|
34
|
-
$response: InferRouteResponse<T>;
|
|
35
|
-
};
|
package/dist/types/request.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|