rouzer 2.0.0 → 3.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 ADDED
@@ -0,0 +1,110 @@
1
+ # Rouzer
2
+
3
+ Rouzer lets you declare an HTTP route tree once and share its TypeScript types
4
+ and Zod validation between a Hattip-compatible server and a typed fetch client.
5
+
6
+ ## What it does
7
+
8
+ A Rouzer HTTP route tree defines URL patterns, named actions, method schemas, and
9
+ optional response types once, then reuses that contract to:
10
+
11
+ - validate client arguments before `fetch`
12
+ - match and validate server requests before handlers run
13
+ - type handler context from path, query/body, headers, and middleware
14
+ - attach typed client action functions such as `client.profiles.get(...)`
15
+
16
+ Rouzer optimizes for shared TypeScript route modules over language-agnostic API
17
+ schemas or generated SDKs.
18
+
19
+ ## Is this for you?
20
+
21
+ Use Rouzer if:
22
+
23
+ - your server and client can import the same TypeScript route tree
24
+ - you want Zod request validation on both sides of an HTTP boundary
25
+ - a Hattip-compatible handler fits your server runtime
26
+ - you prefer named resource/action functions over a generated client class
27
+
28
+ Consider something else if:
29
+
30
+ - you need OpenAPI-first workflows, schema files, or generated clients for other
31
+ languages
32
+ - you need runtime response-body validation; `response: $type<T>()` is
33
+ compile-time only
34
+ - you want a framework that owns controllers, data loading, rendering, and
35
+ deployment adapters
36
+ - you cannot use ESM or Zod v4+
37
+
38
+ ## Requirements
39
+
40
+ - ESM runtime and tooling
41
+ - Zod v4 or newer
42
+ - a Hattip adapter when using `createRouter(...)`
43
+ - a Fetch API implementation when using `createClient(...)`
44
+ - an absolute `baseURL` for generated client URLs
45
+
46
+ ## Installation
47
+
48
+ ```sh
49
+ pnpm add rouzer zod
50
+ ```
51
+
52
+ Import the primary API from the root package and declare routes through the HTTP
53
+ subpath:
54
+
55
+ ```ts
56
+ import { $type, chain, createClient, createRouter } from 'rouzer'
57
+ import * as http from 'rouzer/http'
58
+ ```
59
+
60
+ `chain` is re-exported from `alien-middleware` for typed server middleware.
61
+
62
+ ## Quick example
63
+
64
+ This example shows the core loop: one HTTP action contract defines validation,
65
+ server handler types, and the typed client call.
66
+
67
+ ```ts
68
+ import * as z from 'zod'
69
+ import { $type, createClient, createRouter } from 'rouzer'
70
+ import * as http from 'rouzer/http'
71
+
72
+ export const hello = http.get('hello/:name', {
73
+ query: z.object({
74
+ excited: z.optional(z.boolean()),
75
+ }),
76
+ response: $type<{ message: string }>(),
77
+ })
78
+
79
+ export const routes = { hello }
80
+
81
+ export const handler = createRouter({ basePath: 'api/' }).use(routes, {
82
+ hello(ctx) {
83
+ return {
84
+ message: `Hello, ${ctx.path.name}${ctx.query.excited ? '!' : '.'}`,
85
+ }
86
+ },
87
+ })
88
+
89
+ const client = createClient({
90
+ baseURL: 'https://example.com/api/',
91
+ routes,
92
+ })
93
+
94
+ const { message } = await client.hello({
95
+ path: { name: 'world' },
96
+ query: { excited: true },
97
+ })
98
+ ```
99
+
100
+ `handler` can be mounted with any Hattip adapter. Client action calls validate
101
+ route arguments before `fetch`; server handlers validate matched path, query,
102
+ headers, and JSON bodies before your handler runs.
103
+
104
+ ## Documentation
105
+
106
+ - [Concepts, API selection, and v2.0.1 migration notes](docs/context.md)
107
+ - [Runnable shared-route example](examples/basic-usage.ts)
108
+ - Generated declarations in the published package provide the exact signatures
109
+ for every public export, including the `rouzer/http` subpath.
110
+ - Public TSDoc in `src/` owns symbol-level behavior and option details.
@@ -1,73 +1,102 @@
1
1
  import { Promisable } from '../common.js';
2
- import { Route } from '../route.js';
2
+ import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
3
3
  import type { InferRouteResponse, RouteArgs, RouteRequest, RouteSchema } from '../types.js';
4
- export type RouzerClient<TRoutes extends Record<string, Route> = Record<string, never>> = ReturnType<typeof createClient<TRoutes>>;
5
- export declare function createClient<TRoutes extends Record<string, Route> = Record<string, never>>(config: {
4
+ /** Client type inferred from an HTTP route tree passed to `createClient`. */
5
+ export type RouzerClient<TRoutes extends HttpRouteTree = Record<string, never>> = ReturnType<typeof createClient<TRoutes>>;
6
+ /**
7
+ * Create a typed fetch client for an HTTP route tree.
8
+ *
9
+ * @remarks The returned client always includes `request(...)` for raw responses
10
+ * and `json(...)` for parsed JSON. Passing `routes` also mirrors the resource
11
+ * tree and attaches direct action functions such as `client.users.list(...)`.
12
+ */
13
+ export declare function createClient<TRoutes extends HttpRouteTree = Record<string, never>>(config: {
6
14
  /**
7
- * Base URL to use for all requests.
15
+ * Absolute base URL used for generated request URLs.
16
+ *
17
+ * @remarks A trailing slash is added when missing. In browsers, derive a
18
+ * relative API path with `new URL('/api/', window.location.origin).href`.
8
19
  */
9
20
  baseURL: string;
10
21
  /**
11
- * Default headers to send with every request.
22
+ * Default headers sent with every request.
23
+ *
24
+ * @remarks Per-request headers are merged on top of these values. Undefined
25
+ * per-request headers are removed before `fetch`.
12
26
  */
13
27
  headers?: Record<string, string>;
14
28
  /**
15
- * Pass in routes to attach them as methods on the client.
29
+ * HTTP route tree to attach as direct client action functions.
30
+ *
16
31
  * @example
17
32
  * ```ts
18
- * const client = createClient({ baseURL: '/api/', routes: { helloRoute } })
19
- * client.helloRoute.GET({ path: { name: 'world' } })
33
+ * const client = createClient({ baseURL: 'https://example.com/api/', routes })
34
+ * await client.users.list({ query: { page: 1 } })
20
35
  * ```
21
36
  */
22
37
  routes?: TRoutes;
23
38
  /**
24
- * Custom handler for non-200 response to a `.json()` request. By default, the
25
- * response is always parsed as JSON, regardless of the HTTP status code.
39
+ * Custom handler for non-2xx responses from `.json()`.
40
+ *
41
+ * @remarks When provided, the return value is returned from `.json()` as-is;
42
+ * Rouzer does not automatically parse a `Response` returned by this hook.
26
43
  */
27
44
  onJsonError?: (response: Response) => Promisable<Response>;
28
- /**
29
- * Custom fetch implementation to use for requests.
30
- */
45
+ /** Custom `fetch` implementation to use for requests. */
31
46
  fetch?: typeof globalThis.fetch;
32
- }): { [K in keyof TRoutes]: TRoutes[K]["methods"] extends infer TMethods ? { [M in keyof TMethods]: RouteFunction<Extract<TMethods[M], RouteSchema>, TRoutes[K]["path"]["source"]>; } : never; } & {
47
+ }): ClientTree<TRoutes, ""> & {
33
48
  config: {
34
49
  /**
35
- * Base URL to use for all requests.
50
+ * Absolute base URL used for generated request URLs.
51
+ *
52
+ * @remarks A trailing slash is added when missing. In browsers, derive a
53
+ * relative API path with `new URL('/api/', window.location.origin).href`.
36
54
  */
37
55
  baseURL: string;
38
56
  /**
39
- * Default headers to send with every request.
57
+ * Default headers sent with every request.
58
+ *
59
+ * @remarks Per-request headers are merged on top of these values. Undefined
60
+ * per-request headers are removed before `fetch`.
40
61
  */
41
- headers?: Record<string, string> | undefined;
62
+ headers?: Record<string, string>;
42
63
  /**
43
- * Pass in routes to attach them as methods on the client.
64
+ * HTTP route tree to attach as direct client action functions.
65
+ *
44
66
  * @example
45
67
  * ```ts
46
- * const client = createClient({ baseURL: '/api/', routes: { helloRoute } })
47
- * client.helloRoute.GET({ path: { name: 'world' } })
68
+ * const client = createClient({ baseURL: 'https://example.com/api/', routes })
69
+ * await client.users.list({ query: { page: 1 } })
48
70
  * ```
49
71
  */
50
- routes?: TRoutes | undefined;
72
+ routes?: TRoutes;
51
73
  /**
52
- * Custom handler for non-200 response to a `.json()` request. By default, the
53
- * response is always parsed as JSON, regardless of the HTTP status code.
74
+ * Custom handler for non-2xx responses from `.json()`.
75
+ *
76
+ * @remarks When provided, the return value is returned from `.json()` as-is;
77
+ * Rouzer does not automatically parse a `Response` returned by this hook.
54
78
  */
55
- onJsonError?: ((response: Response) => Promisable<Response>) | undefined;
56
- /**
57
- * Custom fetch implementation to use for requests.
58
- */
59
- fetch?: typeof globalThis.fetch | undefined;
79
+ onJsonError?: (response: Response) => Promisable<Response>;
80
+ /** Custom `fetch` implementation to use for requests. */
81
+ fetch?: typeof globalThis.fetch;
60
82
  };
61
83
  request: <T extends RouteRequest>({ path: pathBuilder, method, args: { path, query, body, headers }, schema, }: T) => Promise<Response & {
62
- json(): Promise<T["$result"]>;
84
+ json(): Promise<T['$result']>;
63
85
  }>;
64
- json: <T extends RouteRequest>(props: T) => Promise<T["$result"]>;
86
+ json: <T extends RouteRequest>(props: T) => Promise<T['$result']>;
87
+ };
88
+ type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
89
+ /** Client object shape produced from an HTTP route tree. */
90
+ export type ClientTree<T extends HttpRouteTree, TPrefix extends string = ''> = {
91
+ [K in keyof T]: T[K] extends HttpResource<infer P, infer C> ? ClientTree<C, Join<TPrefix, P>> : T[K] extends HttpAction<infer P, infer S, any> ? RouteFunction<S, Join<TPrefix, P>> : never;
65
92
  };
66
93
  /**
67
- * This function sends a request to a route of the same name. Such a function is
68
- * accessible by setting the `routes` option when creating a Rouzer client,
69
- * where it will exist as a method on the client.
94
+ * Client action function attached for each HTTP action leaf.
95
+ *
96
+ * @remarks Actions whose schema has `response: $type<T>()` return parsed JSON
97
+ * as `T`. Actions without a response marker return the raw `Response`.
70
98
  */
71
99
  export type RouteFunction<T extends RouteSchema, P extends string> = (...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never) => Promise<T extends {
72
100
  response: any;
73
101
  } ? InferRouteResponse<T> : Response>;
102
+ export {};
@@ -1,4 +1,13 @@
1
- import { mapValues, shake } from '../common.js';
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { createHref } from '@remix-run/route-pattern/href';
3
+ import { shake } from '../common.js';
4
+ /**
5
+ * Create a typed fetch client for an HTTP route tree.
6
+ *
7
+ * @remarks The returned client always includes `request(...)` for raw responses
8
+ * and `json(...)` for parsed JSON. Passing `routes` also mirrors the resource
9
+ * tree and attaches direct action functions such as `client.users.list(...)`.
10
+ */
2
11
  export function createClient(config) {
3
12
  const baseURL = config.baseURL.replace(/\/?$/, '/');
4
13
  const defaultHeaders = config.headers && shake(config.headers);
@@ -8,13 +17,13 @@ export function createClient(config) {
8
17
  path = schema.path.parse(path);
9
18
  }
10
19
  let url;
11
- const href = pathBuilder.href(path);
20
+ const href = createHref(pathBuilder, path);
12
21
  if (href[0] === '/') {
13
22
  url = new URL(baseURL);
14
23
  url.pathname += href.slice(1);
15
24
  }
16
25
  else {
17
- url = new URL(href);
26
+ url = new URL(href, baseURL);
18
27
  }
19
28
  if (schema.query) {
20
29
  query = schema.query.parse(query ?? {});
@@ -31,9 +40,9 @@ export function createClient(config) {
31
40
  }
32
41
  if (headers) {
33
42
  headers = shake(headers);
34
- if (defaultHeaders) {
35
- headers = { ...defaultHeaders, ...headers };
36
- }
43
+ }
44
+ if (defaultHeaders) {
45
+ headers = headers ? { ...defaultHeaders, ...headers } : defaultHeaders;
37
46
  }
38
47
  if (schema.headers) {
39
48
  headers = schema.headers.parse(headers);
@@ -50,7 +59,7 @@ export function createClient(config) {
50
59
  if (config.onJsonError) {
51
60
  return config.onJsonError(response);
52
61
  }
53
- const error = new Error(`Request to ${props.method} ${props.path.href(props.args.path)} failed with status ${response.status}`);
62
+ const error = new Error(`Request to ${props.method} ${createHref(props.path, props.args.path)} failed with status ${response.status}`);
54
63
  const contentType = response.headers.get('content-type');
55
64
  if (contentType?.includes('application/json')) {
56
65
  Object.assign(error, await response.json());
@@ -61,19 +70,35 @@ export function createClient(config) {
61
70
  }
62
71
  return {
63
72
  ...(config.routes
64
- ? mapValues(config.routes, route => connectRoute(route, request, json))
73
+ ? connectTree(config.routes, '', request, json)
65
74
  : null),
66
75
  config,
67
76
  request,
68
77
  json,
69
78
  };
70
79
  }
71
- function connectRoute(route, request, json) {
72
- return {
73
- ...route,
74
- ...mapValues(route.methods, (schema, key) => {
75
- const fetch = schema.response ? json : request;
76
- return (args) => fetch(route[key](args));
77
- }),
78
- };
80
+ function connectTree(tree, prefix, request, json) {
81
+ return Object.fromEntries(Object.entries(tree).map(([key, node]) => {
82
+ if (node.kind === 'resource') {
83
+ return [
84
+ key,
85
+ connectTree(node.children, joinPaths(prefix, node.path.source), request, json),
86
+ ];
87
+ }
88
+ const path = RoutePattern.parse(joinPaths(prefix, node.path?.source ?? ''));
89
+ const fetch = node.schema.response ? json : request;
90
+ return [
91
+ key,
92
+ (args = {}) => fetch({
93
+ schema: node.schema,
94
+ path,
95
+ method: node.method,
96
+ args,
97
+ $result: undefined,
98
+ }),
99
+ ];
100
+ }));
101
+ }
102
+ function joinPaths(left, right) {
103
+ return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
79
104
  }
package/dist/common.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export type Promisable<T> = T | Promise<T>;
2
+ /**
3
+ * Compile-time-only marker used by `$type<T>()` to carry an unchecked response
4
+ * type through route declarations.
5
+ *
6
+ * @remarks Consumers usually use `$type<T>()` instead of constructing this type
7
+ * directly.
8
+ */
2
9
  export type Unchecked<T> = {
3
10
  __unchecked__: T;
4
11
  };
package/dist/http.d.ts ADDED
@@ -0,0 +1,67 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import type { RouteRequestFactory, RouteSchema } from './types.js';
3
+ /** HTTP methods supported by Rouzer action declarations. */
4
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
5
+ /**
6
+ * Callable endpoint leaf in an HTTP route tree.
7
+ *
8
+ * @remarks Actions declare one HTTP operation. Their property name becomes the
9
+ * client/handler name, while their optional `path` contributes URL segments.
10
+ */
11
+ export type HttpAction<P extends string = string, T extends RouteSchema = RouteSchema, M extends HttpMethod = HttpMethod> = {
12
+ /** Discriminator used internally when traversing route trees. */
13
+ kind: 'action';
14
+ /** Optional action-local path segment appended after parent resources. */
15
+ path?: RoutePattern<P>;
16
+ /** HTTP method used when the client sends this action. */
17
+ method: M;
18
+ /** Request validation and optional response type schema. */
19
+ schema: T;
20
+ /** Low-level request descriptor factory for this action. */
21
+ request: RouteRequestFactory<T, P>;
22
+ };
23
+ /**
24
+ * Path-scoped namespace in an HTTP route tree.
25
+ *
26
+ * @remarks Resources contribute URL path segments and contain child resources or
27
+ * actions. They do not have handlers of their own.
28
+ */
29
+ export type HttpResource<P extends string = string, TChildren extends HttpRouteTree = HttpRouteTree> = {
30
+ /** Discriminator used internally when traversing route trees. */
31
+ kind: 'resource';
32
+ /** Path segment contributed by this resource. */
33
+ path: RoutePattern<P>;
34
+ /** Child resources and actions exposed below this resource. */
35
+ children: TChildren;
36
+ };
37
+ /** Node type accepted inside an HTTP route tree. */
38
+ export type HttpNode = HttpAction | HttpResource;
39
+ /** Route tree accepted by HTTP clients and routers. */
40
+ export type HttpRouteTree = {
41
+ [key: string]: HttpNode;
42
+ };
43
+ /**
44
+ * Declare an HTTP resource namespace.
45
+ *
46
+ * @remarks The resource `path` is joined with any parent resource path. Child
47
+ * property names are API names only; they do not affect the URL unless the child
48
+ * is another resource or an action with an explicit path.
49
+ */
50
+ export declare function resource<const P extends string, const TChildren extends HttpRouteTree>(path: P, children: TChildren): HttpResource<P, TChildren>;
51
+ /** Declare a GET action, optionally with an action-local path segment. */
52
+ export declare function get<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'GET'>;
53
+ export declare function get<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'GET'>;
54
+ /** Declare a POST action, optionally with an action-local path segment. */
55
+ export declare function post<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'POST'>;
56
+ export declare function post<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'POST'>;
57
+ /** Declare a PUT action, optionally with an action-local path segment. */
58
+ export declare function put<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PUT'>;
59
+ export declare function put<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PUT'>;
60
+ /** Declare a PATCH action, optionally with an action-local path segment. */
61
+ export declare function patch<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'PATCH'>;
62
+ export declare function patch<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'PATCH'>;
63
+ /** Declare a DELETE action, optionally with an action-local path segment. */
64
+ export declare function del<const P extends string, const T extends RouteSchema>(path: P, schema: T): HttpAction<P, T, 'DELETE'>;
65
+ export declare function del<const T extends RouteSchema>(schema: T): HttpAction<'', T, 'DELETE'>;
66
+ /** Alias for `del(...)`, exported for namespace imports as `http.delete(...)`. */
67
+ export { del as delete };
package/dist/http.js ADDED
@@ -0,0 +1,44 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ /**
3
+ * Declare an HTTP resource namespace.
4
+ *
5
+ * @remarks The resource `path` is joined with any parent resource path. Child
6
+ * property names are API names only; they do not affect the URL unless the child
7
+ * is another resource or an action with an explicit path.
8
+ */
9
+ export function resource(path, children) {
10
+ return {
11
+ kind: 'resource',
12
+ path: RoutePattern.parse(path),
13
+ children,
14
+ };
15
+ }
16
+ export function get(pathOrSchema, schema) {
17
+ return action('GET', pathOrSchema, schema);
18
+ }
19
+ export function post(pathOrSchema, schema) {
20
+ return action('POST', pathOrSchema, schema);
21
+ }
22
+ export function put(pathOrSchema, schema) {
23
+ return action('PUT', pathOrSchema, schema);
24
+ }
25
+ export function patch(pathOrSchema, schema) {
26
+ return action('PATCH', pathOrSchema, schema);
27
+ }
28
+ export function del(pathOrSchema, schema) {
29
+ return action('DELETE', pathOrSchema, schema);
30
+ }
31
+ /** Alias for `del(...)`, exported for namespace imports as `http.delete(...)`. */
32
+ export { del as delete };
33
+ function action(method, pathOrSchema, schema) {
34
+ const path = typeof pathOrSchema === 'string' ? RoutePattern.parse(pathOrSchema) : undefined;
35
+ schema ??= typeof pathOrSchema === 'string' ? {} : pathOrSchema;
36
+ const request = ((args = {}) => ({
37
+ schema,
38
+ path: path ?? RoutePattern.parse(''),
39
+ method,
40
+ args,
41
+ $result: undefined,
42
+ }));
43
+ return { kind: 'action', path, method, schema, request };
44
+ }
package/dist/route.d.ts CHANGED
@@ -1,14 +1,56 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
2
  import { Unchecked } from './common.js';
3
3
  import type { RouteRequestFactory, RouteSchema, RouteSchemaMap } from './types.js';
4
+ /**
5
+ * Create a compile-time-only marker for a route's JSON response payload type.
6
+ *
7
+ * @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
8
+ * server handler return values and client action functions for routes whose
9
+ * responses are expected to be JSON.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { $type } from 'rouzer'
14
+ * import * as http from 'rouzer/http'
15
+ *
16
+ * const hello = http.get('hello/:name', {
17
+ * response: $type<{ message: string }>(),
18
+ * })
19
+ * ```
20
+ */
4
21
  export declare function $type<T>(): Unchecked<T>;
5
22
  export declare namespace $type {
6
23
  var symbol: symbol;
7
24
  }
25
+ /**
26
+ * Low-level route declaration produced by `route(...)`.
27
+ *
28
+ * @remarks A `Route` stores the parsed URL pattern, the method schema map, and a
29
+ * request factory for each declared method. Use those factories with
30
+ * `client.request(...)` or `client.json(...)` when you need explicit response
31
+ * handling. For shared server/client route trees, prefer `rouzer/http` actions
32
+ * and resources; `createRouter().use(...)` and `createClient({ routes })` expect
33
+ * that HTTP route tree shape.
34
+ */
8
35
  export type Route<P extends string = string, T extends RouteSchemaMap = RouteSchemaMap> = {
36
+ /** Parsed route pattern used for request URL generation. */
9
37
  path: RoutePattern<P>;
38
+ /** Method schemas declared for this route. */
10
39
  methods: T;
11
40
  } & {
12
41
  [K in keyof T]: RouteRequestFactory<Extract<T[K], RouteSchema>, P>;
13
42
  };
43
+ /**
44
+ * Declare one URL pattern and its supported HTTP method schemas.
45
+ *
46
+ * @remarks This helper creates low-level request descriptor factories. Prefer
47
+ * `rouzer/http` action helpers for routes that will be registered with
48
+ * `createRouter().use(...)` or mirrored by `createClient({ routes })`.
49
+ *
50
+ * @param pattern Route pattern parsed by `@remix-run/route-pattern`.
51
+ * @param methods Method schemas that describe request validation and optional
52
+ * response typing.
53
+ * @returns A route declaration with request factories such as `.GET(...)` and
54
+ * `.POST(...)` for the declared methods.
55
+ */
14
56
  export declare function route<P extends string, T extends RouteSchemaMap>(pattern: P, methods: T): Route<P, T>;
package/dist/route.js CHANGED
@@ -1,11 +1,41 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
2
  import { mapEntries } from './common.js';
3
+ /**
4
+ * Create a compile-time-only marker for a route's JSON response payload type.
5
+ *
6
+ * @remarks `$type<T>()` does not perform runtime validation. It lets Rouzer type
7
+ * server handler return values and client action functions for routes whose
8
+ * responses are expected to be JSON.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { $type } from 'rouzer'
13
+ * import * as http from 'rouzer/http'
14
+ *
15
+ * const hello = http.get('hello/:name', {
16
+ * response: $type<{ message: string }>(),
17
+ * })
18
+ * ```
19
+ */
3
20
  export function $type() {
4
21
  return $type.symbol;
5
22
  }
6
23
  $type.symbol = Symbol();
24
+ /**
25
+ * Declare one URL pattern and its supported HTTP method schemas.
26
+ *
27
+ * @remarks This helper creates low-level request descriptor factories. Prefer
28
+ * `rouzer/http` action helpers for routes that will be registered with
29
+ * `createRouter().use(...)` or mirrored by `createClient({ routes })`.
30
+ *
31
+ * @param pattern Route pattern parsed by `@remix-run/route-pattern`.
32
+ * @param methods Method schemas that describe request validation and optional
33
+ * response typing.
34
+ * @returns A route declaration with request factories such as `.GET(...)` and
35
+ * `.POST(...)` for the declared methods.
36
+ */
7
37
  export function route(pattern, methods) {
8
- const path = new RoutePattern(pattern);
38
+ const path = RoutePattern.parse(pattern);
9
39
  const createFetch = (method, schema) => (args = {}) => {
10
40
  return {
11
41
  schema,
@@ -1,36 +1,38 @@
1
1
  import type { HattipHandler } from '@hattip/core';
2
2
  import { ApplyMiddleware, chain, ExtractMiddleware, MiddlewareChain, MiddlewareTypes } from 'alien-middleware';
3
- import type { Routes } from '../types.js';
3
+ import type { HttpRouteTree } from '../http.js';
4
4
  import type { RouteRequestHandlerMap } from './types.js';
5
5
  export { chain };
6
+ /** Configuration for `createRouter`. */
6
7
  export type RouterConfig = {
7
8
  /**
8
- * Base path to prepend to all routes.
9
+ * Base path to prepend to all route patterns.
10
+ *
11
+ * @remarks Leading and trailing slashes are normalized so `api`, `/api`, and
12
+ * `api/` all mount routes under `/api/`.
13
+ *
9
14
  * @example
10
15
  * ```ts
11
- * basePath: 'api/',
16
+ * createRouter({ basePath: 'api/' })
12
17
  * ```
13
18
  */
14
19
  basePath?: string;
15
20
  /**
16
- * Enable debugging features.
17
- * - When a handler throws an error, include its message in the response body.
18
- * - Throw an error if a handler is not found for a route.
19
- * @example
20
- * ```ts
21
- * debug: process.env.NODE_ENV !== 'production',
22
- * ```
21
+ * Enable debug behavior for local development.
22
+ *
23
+ * @remarks Debug mode adds an `X-Route-Name` response header for matched
24
+ * routes, includes specific Zod error messages in `400` validation responses,
25
+ * and logs missing route handlers to the console.
23
26
  */
24
27
  debug?: boolean;
25
- /**
26
- * CORS configuration.
27
- */
28
+ /** CORS configuration for requests with an `Origin` header. */
28
29
  cors?: {
29
30
  /**
30
- * If defined, requests must have an `Origin` header that is in this list.
31
+ * Allowed origins for CORS requests.
31
32
  *
32
- * Origins may contain wildcards for protocol and subdomain. The protocol is
33
- * optional and defaults to `https`.
33
+ * @remarks Origins may contain wildcards for protocol and subdomain. The
34
+ * protocol is optional and defaults to `https`. Requests with an `Origin`
35
+ * header outside this list receive `403`.
34
36
  *
35
37
  * @example
36
38
  * ```ts
@@ -40,6 +42,10 @@ export type RouterConfig = {
40
42
  allowOrigins?: string[];
41
43
  };
42
44
  };
45
+ /**
46
+ * Hattip-compatible Rouzer handler with chainable middleware and route
47
+ * registration.
48
+ */
43
49
  export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T['platform']>, MiddlewareChain<T> {
44
50
  /**
45
51
  * Clone this router and add the given middleware to the end of the chain.
@@ -48,10 +54,22 @@ export interface Router<T extends MiddlewareTypes = any> extends HattipHandler<T
48
54
  */
49
55
  use<const TMiddleware extends ExtractMiddleware<this>>(middleware: TMiddleware | null): Router<ApplyMiddleware<this, TMiddleware>>;
50
56
  /**
51
- * Clone this router and add the given routes and handlers to the chain.
57
+ * Clone this router and add the given HTTP route tree and handlers to the
58
+ * chain.
59
+ *
60
+ * @remarks The handler object mirrors the resource tree. Resource nodes are
61
+ * nested objects, and action nodes are direct handler functions.
52
62
  *
53
63
  * @returns a new `Router` instance.
54
64
  */
55
- use<TRoutes extends Routes>(routes: TRoutes, handlers: RouteRequestHandlerMap<TRoutes, this>): Router<T>;
65
+ use<TRoutes extends HttpRouteTree>(routes: TRoutes, handlers: RouteRequestHandlerMap<TRoutes, this>): Router<T>;
56
66
  }
67
+ /**
68
+ * Create a Rouzer router that can be mounted by any Hattip adapter.
69
+ *
70
+ * @param config Optional router configuration for base path, debug behavior, and
71
+ * CORS origin restrictions.
72
+ * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
73
+ * and route registration.
74
+ */
57
75
  export declare function createRouter<TEnv extends object = {}, TProperties extends object = {}, TPlatform = unknown>(config?: RouterConfig): Router<MiddlewareTypes<TEnv, TProperties, TPlatform>>;