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.
@@ -1,4 +1,5 @@
1
1
  import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { createMatcher } from '@remix-run/route-pattern/match';
2
3
  import { chain, MiddlewareChain, } from 'alien-middleware';
3
4
  import * as z from 'zod';
4
5
  import { mapValues } from '../common.js';
@@ -13,14 +14,14 @@ class RouterObject extends MiddlewareChain {
13
14
  this.basePath = config.basePath?.replace(/\/?$/, '/');
14
15
  const allowOrigins = config.cors?.allowOrigins?.map(createOriginPattern);
15
16
  if (allowOrigins) {
16
- super.use((ctx) => {
17
+ super.use(((ctx) => {
17
18
  const origin = ctx.request.headers.get('Origin');
18
19
  if (origin &&
19
20
  allowOrigins &&
20
21
  !allowOrigins.some(pattern => pattern.test(origin))) {
21
22
  return new Response(null, { status: 403 });
22
23
  }
23
- });
24
+ }));
24
25
  }
25
26
  }
26
27
  use(...args) {
@@ -31,28 +32,13 @@ class RouterObject extends MiddlewareChain {
31
32
  /** @internal */
32
33
  useRoutes(routeSchemas, handlers) {
33
34
  const { config, basePath } = this;
34
- const routes = Object.entries(routeSchemas).map(([name, route]) => ({
35
- name,
36
- path: basePath
37
- ? new RoutePattern(route.path.source.replace(/^\/?/, basePath))
38
- : route.path,
39
- methods: mapValues(route.methods, (schema, method) => {
40
- const handler = handlers[name][method];
41
- if (!handler && config.debug) {
42
- console.error(`Handler missing for route: ${method} ${name}`);
43
- }
44
- return {
45
- schema,
46
- handler,
47
- };
48
- }),
49
- }));
35
+ const routes = flattenRoutes(routeSchemas, handlers, basePath ?? '', config.debug);
50
36
  const addDebugHeaders = config.debug
51
37
  ? (context, route) => {
52
38
  context.setHeader('X-Route-Name', route.name);
53
39
  }
54
40
  : null;
55
- return super.use(async function (context) {
41
+ return super.use((async function (context) {
56
42
  const request = context.request;
57
43
  const origin = request.headers.get('Origin');
58
44
  const url = (context.url ??= new URL(request.url));
@@ -65,28 +51,18 @@ class RouterObject extends MiddlewareChain {
65
51
  'GET';
66
52
  }
67
53
  for (const route of routes) {
68
- const props = route.methods.hasOwnProperty(method)
69
- ? route.methods[method]
70
- : route.methods.ALL;
71
- if (!props) {
54
+ if (route.method !== method) {
72
55
  continue;
73
56
  }
74
- const { schema, handler } = props;
57
+ const { schema, handler } = route;
75
58
  if (!handler) {
76
59
  continue;
77
60
  }
78
- const match = route.path.match(url);
61
+ const match = route.matcher.match(url);
79
62
  if (!match) {
80
63
  continue;
81
64
  }
82
65
  if (isPreflight) {
83
- const optionsHandler = handlers[route.name].OPTIONS;
84
- if (optionsHandler) {
85
- const response = await optionsHandler(context);
86
- if (response) {
87
- return response;
88
- }
89
- }
90
66
  return new Response(null, {
91
67
  headers: {
92
68
  'Access-Control-Allow-Origin': origin ?? '',
@@ -136,15 +112,49 @@ class RouterObject extends MiddlewareChain {
136
112
  }
137
113
  return Response.json(result);
138
114
  }
139
- });
115
+ }));
140
116
  }
141
117
  }
118
+ /**
119
+ * Create a Rouzer router that can be mounted by any Hattip adapter.
120
+ *
121
+ * @param config Optional router configuration for base path, debug behavior, and
122
+ * CORS origin restrictions.
123
+ * @returns A Hattip-compatible handler with `.use(...)` methods for middleware
124
+ * and route registration.
125
+ */
142
126
  export function createRouter(config = {}) {
143
127
  const router = new RouterObject(config);
144
128
  const handler = router.toHandler();
145
129
  Object.setPrototypeOf(handler, router);
146
130
  return handler;
147
131
  }
132
+ function flattenRoutes(tree, handlers, prefix, debug) {
133
+ const routes = [];
134
+ for (const [name, node] of Object.entries(tree)) {
135
+ if (node.kind === 'resource') {
136
+ routes.push(...flattenRoutes(node.children, handlers[name], joinPaths(prefix, node.path.source), debug));
137
+ }
138
+ else {
139
+ const handler = handlers[name];
140
+ if (!handler && debug) {
141
+ console.error(`Handler missing for route: ${node.method} ${name}`);
142
+ }
143
+ routes.push({
144
+ name,
145
+ path: RoutePattern.parse(joinPaths(prefix, node.path?.source ?? '')),
146
+ matcher: createMatcher(joinPaths(prefix, node.path?.source ?? '')),
147
+ method: node.method,
148
+ schema: node.schema,
149
+ handler,
150
+ });
151
+ }
152
+ }
153
+ return routes;
154
+ }
155
+ function joinPaths(left, right) {
156
+ return [left, right].filter(Boolean).join('/').replace(/\/+/g, '/');
157
+ }
148
158
  function httpClientError(error, message, config) {
149
159
  return Response.json({
150
160
  ...error,
@@ -1,38 +1,42 @@
1
- import type { Params } from '@remix-run/route-pattern';
1
+ import type { MatchParams } from '@remix-run/route-pattern/match';
2
2
  import type { AnyMiddlewareChain, MiddlewareChain, MiddlewareContext } from 'alien-middleware';
3
3
  import type * as z from 'zod';
4
4
  import { Promisable } from '../common.js';
5
- import type { InferRouteResponse, Routes, RouteSchema } from '../types.js';
5
+ import type { HttpAction, HttpResource, HttpRouteTree } from '../http.js';
6
+ import type { InferRouteResponse, RouteSchema } from '../types.js';
6
7
  type RequestContext<TMiddleware extends AnyMiddlewareChain> = MiddlewareContext<TMiddleware>;
7
8
  type RouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TArgs extends object, TResult> = (context: RequestContext<TMiddleware> & TArgs) => Promisable<TResult | Response>;
8
- type InferRouteRequestHandler<TMiddleware extends AnyMiddlewareChain, TSchema extends RouteSchema, TMethod extends string, TPath extends string> = TMethod extends 'GET' ? RouteRequestHandler<TMiddleware, {
9
- path: TSchema extends {
9
+ type InferActionHandler<TMiddleware extends AnyMiddlewareChain, TAction extends HttpAction, TPath extends string> = TAction['method'] extends 'GET' ? RouteRequestHandler<TMiddleware, {
10
+ path: TAction['schema'] extends {
10
11
  path: any;
11
- } ? z.infer<TSchema['path']> : Params<TPath>;
12
- query: TSchema extends {
12
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
13
+ query: TAction['schema'] extends {
13
14
  query: any;
14
- } ? z.infer<TSchema['query']> : undefined;
15
- headers: TSchema extends {
15
+ } ? z.infer<TAction['schema']['query']> : undefined;
16
+ headers: TAction['schema'] extends {
16
17
  headers: any;
17
- } ? z.infer<TSchema['headers']> : undefined;
18
- }, InferRouteResponse<TSchema>> : RouteRequestHandler<TMiddleware, {
19
- path: TSchema extends {
18
+ } ? z.infer<TAction['schema']['headers']> : undefined;
19
+ }, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>> : RouteRequestHandler<TMiddleware, {
20
+ path: TAction['schema'] extends {
20
21
  path: any;
21
- } ? z.infer<TSchema['path']> : Params<TPath>;
22
- body: TSchema extends {
22
+ } ? z.infer<TAction['schema']['path']> : MatchParams<TPath>;
23
+ body: TAction['schema'] extends {
23
24
  body: any;
24
- } ? z.infer<TSchema['body']> : undefined;
25
- headers: TSchema extends {
25
+ } ? z.infer<TAction['schema']['body']> : undefined;
26
+ headers: TAction['schema'] extends {
26
27
  headers: any;
27
- } ? z.infer<TSchema['headers']> : undefined;
28
- }, InferRouteResponse<TSchema>>;
29
- export type RouteRequestHandlerMap<TRoutes extends Routes = Routes, TMiddleware extends AnyMiddlewareChain = MiddlewareChain> = {
30
- [K in keyof TRoutes]: {
31
- [TMethod in keyof TRoutes[K]['methods']]: InferRouteRequestHandler<TMiddleware, Extract<TRoutes[K]['methods'][TMethod], RouteSchema>, Extract<TMethod, string>, TRoutes[K]['path']['source']>;
32
- } & {
33
- OPTIONS?: RouteRequestHandler<TMiddleware, {
34
- path: Params<TRoutes[K]['path']['source']>;
35
- }, void>;
36
- };
28
+ } ? z.infer<TAction['schema']['headers']> : undefined;
29
+ }, InferRouteResponse<Extract<TAction['schema'], RouteSchema>>>;
30
+ type Join<A extends string, B extends string> = A extends '' ? B : B extends '' ? A : `${A}/${B}`;
31
+ /**
32
+ * Handler map shape required by `createRouter().use(routes, handlers)`.
33
+ *
34
+ * @remarks The handler object mirrors the HTTP route tree. Resource nodes become
35
+ * nested handler objects, while action nodes become direct handler functions.
36
+ * Handler context is inferred from middleware plus accumulated path params,
37
+ * query/body schemas, and header schemas.
38
+ */
39
+ export type RouteRequestHandlerMap<TRoutes extends HttpRouteTree = HttpRouteTree, TMiddleware extends AnyMiddlewareChain = MiddlewareChain, TPrefix extends string = ''> = {
40
+ [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;
37
41
  };
38
42
  export {};
package/dist/types.d.ts CHANGED
@@ -1,21 +1,47 @@
1
- import { Params, RoutePattern } from '@remix-run/route-pattern';
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import type { MatchParams } from '@remix-run/route-pattern/match';
2
3
  import * as z from 'zod';
3
4
  import { Unchecked } from './common.js';
5
+ /**
6
+ * Compile-time-only marker used by `$type<T>()` for unchecked response types.
7
+ *
8
+ * @remarks Application code should usually call `$type<T>()` instead of naming
9
+ * this marker directly.
10
+ */
4
11
  export type { Unchecked };
12
+ /** Schema shape for `GET` route methods. */
5
13
  export type QueryRouteSchema = {
14
+ /** Optional Zod object used to validate path params. */
6
15
  path?: z.ZodObject<any>;
16
+ /** Optional Zod object used to validate URL query params. */
7
17
  query?: z.ZodObject<any>;
18
+ /** `GET` routes do not accept request bodies. */
8
19
  body?: never;
20
+ /** Optional Zod object used to validate request headers. */
9
21
  headers?: z.ZodObject<any>;
22
+ /** Optional compile-time-only JSON response type marker. */
10
23
  response?: Unchecked<any>;
11
24
  };
25
+ /** Schema shape for mutation route methods. */
12
26
  export type MutationRouteSchema = {
27
+ /** Optional Zod object used to validate path params. */
13
28
  path?: z.ZodObject<any>;
29
+ /** Mutation routes do not accept query schemas. */
14
30
  query?: never;
31
+ /** Optional Zod schema used to validate the JSON request body. */
15
32
  body?: z.ZodType<any, any>;
33
+ /** Optional Zod object used to validate request headers. */
16
34
  headers?: z.ZodObject<any>;
35
+ /** Optional compile-time-only JSON response type marker. */
17
36
  response?: Unchecked<any>;
18
37
  };
38
+ /**
39
+ * Method schema map accepted by the low-level `route(...)` helper.
40
+ *
41
+ * @remarks `GET` validates query input and mutation methods validate JSON body
42
+ * input. Prefer `rouzer/http` actions for route trees registered with
43
+ * `createRouter().use(...)` or `createClient({ routes })`.
44
+ */
19
45
  export type RouteSchemaMap = {
20
46
  GET?: QueryRouteSchema;
21
47
  POST?: MutationRouteSchema;
@@ -23,14 +49,27 @@ export type RouteSchemaMap = {
23
49
  PATCH?: MutationRouteSchema;
24
50
  DELETE?: MutationRouteSchema;
25
51
  ALL?: {
52
+ /** Optional Zod object used to validate path params. */
26
53
  path?: z.ZodObject<any>;
54
+ /** Optional Zod object used to validate URL query params. */
27
55
  query?: z.ZodObject<any>;
56
+ /** `ALL` fallback routes do not accept request bodies. */
28
57
  body?: never;
58
+ /** Optional Zod object used to validate request headers. */
29
59
  headers?: z.ZodObject<any>;
60
+ /** `ALL` fallback routes do not define typed JSON responses. */
30
61
  response?: never;
31
62
  };
32
63
  };
64
+ /** Any route method schema Rouzer can execute. */
33
65
  export type RouteSchema = QueryRouteSchema | MutationRouteSchema;
66
+ /**
67
+ * Low-level route map shape produced from `route(...)` declarations.
68
+ *
69
+ * @remarks The router and client shorthand registration APIs now expect
70
+ * `HttpRouteTree` values from the `rouzer/http` subpath. Use this type only for
71
+ * code that still works directly with low-level `route(...)` descriptors.
72
+ */
34
73
  export type Routes = {
35
74
  [key: string]: {
36
75
  path: RoutePattern;
@@ -46,7 +85,7 @@ type PathArgs<T, P extends string> = T extends {
46
85
  [K in keyof T as 'path']?: z.infer<TPath>;
47
86
  } : {
48
87
  [K in keyof T as 'path']: z.infer<TPath>;
49
- } : Params<P> extends infer TParams ? {} extends TParams ? {
88
+ } : MatchParams<P> extends infer TParams ? {} extends TParams ? {
50
89
  [K in keyof T as 'path']?: TParams;
51
90
  } : {
52
91
  [K in keyof T as 'path']: TParams;
@@ -67,23 +106,47 @@ type MutationArgs<T> = T extends MutationRouteSchema ? T extends {
67
106
  } : {
68
107
  body?: unknown;
69
108
  } : unknown;
109
+ /**
110
+ * Arguments accepted by a request factory such as an HTTP action's `.request(...)`
111
+ * or a low-level `route.GET(...)` factory.
112
+ *
113
+ * @remarks The type is derived from a method schema and route pattern. `path`,
114
+ * `query`, `body`, and `headers` are validated by the client before `fetch` when
115
+ * a matching schema exists. The client forwards the HTTP method, JSON body, and
116
+ * headers; extra `RequestInit` fields are accepted by the type surface but are
117
+ * not forwarded.
118
+ */
70
119
  export type RouteArgs<T extends RouteSchema = any, P extends string = string> = ([T] extends [Any] ? {
71
120
  query?: any;
72
121
  body?: any;
73
122
  path?: any;
74
123
  } : QueryArgs<T> & MutationArgs<T> & PathArgs<T, P>) & Omit<RequestInit, 'method' | 'body' | 'headers'> & {
124
+ /** Headers for this request. Undefined values are removed before `fetch`. */
75
125
  headers?: Record<string, string | undefined>;
76
126
  };
127
+ /**
128
+ * Request descriptor produced by an HTTP action or route request factory.
129
+ *
130
+ * @remarks Pass this object to `client.request(...)` for a raw `Response` or
131
+ * `client.json(...)` for parsed JSON handling.
132
+ */
77
133
  export type RouteRequest<TResult = any> = {
134
+ /** Method schema used for client-side validation. */
78
135
  schema: RouteSchema;
136
+ /** Parsed route pattern used to generate the request URL. */
79
137
  path: RoutePattern;
138
+ /** HTTP method to send. */
80
139
  method: string;
140
+ /** Validated route arguments and request options. */
81
141
  args: RouteArgs;
142
+ /** Phantom result type consumed by `client.json(...)`. */
82
143
  $result: TResult;
83
144
  };
145
+ /** `Response` whose `.json()` method resolves to a known payload type. */
84
146
  export type RouteResponse<TResult = any> = Response & {
85
147
  json(): Promise<TResult>;
86
148
  };
149
+ /** Infer the JSON response payload type from a method schema. */
87
150
  export type InferRouteResponse<T extends RouteSchema> = T extends {
88
151
  response: Unchecked<infer TResponse>;
89
152
  } ? TResponse : void;
@@ -93,12 +156,35 @@ type InferRouteSchemaBody<TSchema> = TSchema extends MutationRouteSchema ? TSche
93
156
  type InferRouteArgsBody<TArgs> = TArgs extends {
94
157
  body?: infer TBody;
95
158
  } ? TBody : never;
159
+ /**
160
+ * Infer the request body type from a schema or request factory.
161
+ *
162
+ * @remarks HTTP action schemas can be inspected with
163
+ * `InferRouteBody<typeof action.schema>`. Request factories for mutation methods
164
+ * infer their `body` argument type. Schemas without a body schema infer
165
+ * `unknown`.
166
+ */
96
167
  export type InferRouteBody<T> = T extends RouteRequestFactory<any, any> ? InferRouteArgsBody<T['$args']> : T extends RouteSchema ? InferRouteSchemaBody<T> : never;
168
+ /**
169
+ * Infer the request body type for a named method on a low-level `Route`.
170
+ *
171
+ * @remarks `GET` and `ALL` infer `never` because they do not accept request
172
+ * bodies. For `rouzer/http` actions, prefer
173
+ * `InferRouteBody<typeof action.schema>`.
174
+ */
97
175
  export type InferRouteMethodBody<TRoute extends {
98
176
  methods: RouteSchemaMap;
99
177
  }, TMethod extends keyof TRoute['methods']> = TMethod extends 'GET' | 'ALL' ? never : TMethod extends keyof TRoute ? InferRouteBody<TRoute[TMethod]> : InferRouteBody<Extract<TRoute['methods'][TMethod], RouteSchema>>;
178
+ /**
179
+ * Callable factory attached to an HTTP action or low-level `Route` method.
180
+ *
181
+ * @remarks Calling a factory validates no data by itself; it creates a typed
182
+ * `RouteRequest` descriptor for `createClient` to validate and send.
183
+ */
100
184
  export type RouteRequestFactory<T extends RouteSchema, P extends string> = {
101
185
  (...p: RouteArgs<T, P> extends infer TArgs ? {} extends TArgs ? [args?: TArgs] : [args: TArgs] : never): RouteRequest<InferRouteResponse<T>>;
186
+ /** Inferred argument type for this request factory. */
102
187
  $args: RouteArgs<T, P>;
188
+ /** Inferred JSON response type for this request factory. */
103
189
  $response: InferRouteResponse<T>;
104
190
  };