rouzer 1.0.0-beta.2 → 1.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ export declare function createClient(config: {
2
+ /**
3
+ * Base URL to use for all requests.
4
+ */
5
+ baseURL: string;
6
+ /**
7
+ * Default headers to send with every request.
8
+ */
9
+ headers?: Record<string, string>;
10
+ /**
11
+ * Custom handler for non-200 response to a `.json()` request. By default, the
12
+ * response is always parsed as JSON, regardless of the HTTP status code.
13
+ */
14
+ onJsonError?: (response: Response) => Response;
15
+ }): {
16
+ config: {
17
+ /**
18
+ * Base URL to use for all requests.
19
+ */
20
+ baseURL: string;
21
+ /**
22
+ * Default headers to send with every request.
23
+ */
24
+ headers?: Record<string, string>;
25
+ /**
26
+ * Custom handler for non-200 response to a `.json()` request. By default, the
27
+ * response is always parsed as JSON, regardless of the HTTP status code.
28
+ */
29
+ onJsonError?: (response: Response) => Response;
30
+ };
31
+ request<T extends RouteRequest>({ pathPattern, method, args: { path, query, body, headers }, route, }: T): Promise<Response & {
32
+ json(): Promise<T["$result"]>;
33
+ }>;
34
+ json<T extends RouteRequest>(request: T): Promise<T["$result"]>;
35
+ };
@@ -0,0 +1,46 @@
1
+ import { shake } from '../common';
2
+ export function createClient(config) {
3
+ return {
4
+ config,
5
+ request({ pathPattern, method, args: { path, query, body, headers }, route, }) {
6
+ if (route.path) {
7
+ path = route.path.parse(path);
8
+ }
9
+ const url = new URL(pathPattern.href(path), config.baseURL);
10
+ if (route.query) {
11
+ query = route.query.parse(query ?? {});
12
+ url.search = new URLSearchParams(query).toString();
13
+ }
14
+ else if (query) {
15
+ throw new Error('Unexpected query parameters');
16
+ }
17
+ if (route.body) {
18
+ body = route.body.parse(body !== undefined ? body : {});
19
+ }
20
+ else if (body !== undefined) {
21
+ throw new Error('Unexpected body');
22
+ }
23
+ if (config.headers || headers) {
24
+ headers = {
25
+ ...config.headers,
26
+ ...(headers && shake(headers)),
27
+ };
28
+ }
29
+ if (route.headers) {
30
+ headers = route.headers.parse(headers);
31
+ }
32
+ return fetch(url, {
33
+ method,
34
+ body: body !== undefined ? JSON.stringify(body) : undefined,
35
+ headers,
36
+ });
37
+ },
38
+ async json(request) {
39
+ const response = await this.request(request);
40
+ if (!response.ok && config.onJsonError) {
41
+ return config.onJsonError(response);
42
+ }
43
+ return response.json();
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Map over all the keys to create a new object.
3
+ *
4
+ * @see https://radashi.js.org/reference/object/mapEntries
5
+ * @example
6
+ * ```ts
7
+ * const a = { a: 1, b: 2, c: 3 }
8
+ * mapEntries(a, (key, value) => [value, key])
9
+ * // => { 1: 'a', 2: 'b', 3: 'c' }
10
+ * ```
11
+ * @version 12.1.0
12
+ */
13
+ export declare function mapEntries<TKey extends string | number | symbol, TValue, TNewKey extends string | number | symbol, TNewValue>(obj: Record<TKey, TValue>, toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]): Record<TNewKey, TNewValue>;
14
+ /**
15
+ * Removes (shakes out) undefined entries from an object. Optional
16
+ * second argument shakes out values by custom evaluation.
17
+ *
18
+ * Note that non-enumerable keys are never shaken out.
19
+ *
20
+ * @see https://radashi.js.org/reference/object/shake
21
+ * @example
22
+ * ```ts
23
+ * const a = { a: 1, b: undefined, c: 3 }
24
+ * shake(a)
25
+ * // => { a: 1, c: 3 }
26
+ * ```
27
+ * @version 12.1.0
28
+ */
29
+ export declare function shake<T extends object>(obj: T): {
30
+ [K in keyof T]: Exclude<T[K], undefined>;
31
+ };
32
+ export declare function shake<T extends object>(obj: T, filter: ((value: unknown) => boolean) | undefined): T;
33
+ /**
34
+ * Map over all the keys to create a new object.
35
+ *
36
+ * @see https://radashi.js.org/reference/object/mapValues
37
+ * @example
38
+ * ```ts
39
+ * const a = { a: 1, b: 2, c: 3 }
40
+ * mapValues(a, (value, key) => value * 2)
41
+ * // => { a: 2, b: 4, c: 6 }
42
+ * ```
43
+ * @version 12.1.0
44
+ */
45
+ export declare function mapValues<T extends object, U>(obj: T, mapFunc: (value: Required<T>[keyof T], key: keyof T) => U): {
46
+ [K in keyof T]: U;
47
+ };
package/dist/common.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Map over all the keys to create a new object.
3
+ *
4
+ * @see https://radashi.js.org/reference/object/mapEntries
5
+ * @example
6
+ * ```ts
7
+ * const a = { a: 1, b: 2, c: 3 }
8
+ * mapEntries(a, (key, value) => [value, key])
9
+ * // => { 1: 'a', 2: 'b', 3: 'c' }
10
+ * ```
11
+ * @version 12.1.0
12
+ */
13
+ export function mapEntries(obj, toEntry) {
14
+ if (!obj) {
15
+ return {};
16
+ }
17
+ return Object.entries(obj).reduce((acc, [key, value]) => {
18
+ const [newKey, newValue] = toEntry(key, value);
19
+ acc[newKey] = newValue;
20
+ return acc;
21
+ }, {});
22
+ }
23
+ export function shake(obj, filter = value => value === undefined) {
24
+ if (!obj) {
25
+ return {};
26
+ }
27
+ return Object.keys(obj).reduce((acc, key) => {
28
+ if (!filter(obj[key])) {
29
+ acc[key] = obj[key];
30
+ }
31
+ return acc;
32
+ }, {});
33
+ }
34
+ /**
35
+ * Map over all the keys to create a new object.
36
+ *
37
+ * @see https://radashi.js.org/reference/object/mapValues
38
+ * @example
39
+ * ```ts
40
+ * const a = { a: 1, b: 2, c: 3 }
41
+ * mapValues(a, (value, key) => value * 2)
42
+ * // => { a: 2, b: 4, c: 6 }
43
+ * ```
44
+ * @version 12.1.0
45
+ */
46
+ export function mapValues(obj, mapFunc) {
47
+ return Object.keys(obj).reduce((acc, key) => {
48
+ acc[key] = mapFunc(obj[key], key);
49
+ return acc;
50
+ }, {});
51
+ }
@@ -0,0 +1,5 @@
1
+ export * from './route';
2
+ export * from './server/router';
3
+ export * from './client/index';
4
+ export type * from './types';
5
+ export * from 'alien-middleware';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './route';
2
+ export * from './server/router';
3
+ export * from './client/index';
4
+ export * from 'alien-middleware';
@@ -0,0 +1,8 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import type { Routes } from './types';
3
+ export declare function $type<T>(): Unchecked<T>;
4
+ export declare function route<P extends string, T extends Routes>(path: P, routes: T): {
5
+ path: P;
6
+ pathPattern: RoutePattern<string>;
7
+ routes: T;
8
+ } & { [K in keyof T]: RouteFunction<any>; };
package/dist/route.js ADDED
@@ -0,0 +1,18 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { mapEntries } from './common';
3
+ export function $type() {
4
+ return null;
5
+ }
6
+ export function route(path, routes) {
7
+ const pathPattern = new RoutePattern(path);
8
+ const createFetch = (method, route) => (args) => {
9
+ return {
10
+ route,
11
+ pathPattern,
12
+ method,
13
+ args,
14
+ $result: undefined,
15
+ };
16
+ };
17
+ return Object.assign({ path, pathPattern, routes }, mapEntries(routes, (method, route) => [method, createFetch(method, route)]));
18
+ }
@@ -0,0 +1,54 @@
1
+ import type { AdapterRequestContext } from '@hattip/core';
2
+ import { type Params } from '@remix-run/route-pattern';
3
+ import { chain, MiddlewareChain, type MiddlewareContext } from 'alien-middleware';
4
+ import * as z from 'zod/mini';
5
+ import type { Routes } from '../types';
6
+ export { chain };
7
+ type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
8
+ initial: {
9
+ env: {};
10
+ properties: {};
11
+ };
12
+ current: {
13
+ env: {};
14
+ properties: {};
15
+ };
16
+ platform: TPlatform;
17
+ }>;
18
+ export type RouterConfig<TRoutes extends Record<string, {
19
+ path: string;
20
+ routes: Routes;
21
+ }> = any, TMiddleware extends MiddlewareChain = any> = {
22
+ routes: TRoutes;
23
+ middlewares?: TMiddleware;
24
+ debug?: boolean;
25
+ };
26
+ export declare function createRouter<TRoutes extends Record<string, {
27
+ path: string;
28
+ routes: Routes;
29
+ }>, TMiddleware extends MiddlewareChain = EmptyMiddlewareChain>(config: {
30
+ routes: TRoutes;
31
+ middlewares?: TMiddleware;
32
+ debug?: boolean;
33
+ }): (handlers: { [K in keyof TRoutes]: { [M in keyof TRoutes[K]["routes"]]: TRoutes[K]["routes"][M] extends infer T ? T extends TRoutes[K]["routes"][M] ? T extends QueryRoute ? (context: MiddlewareContext<TMiddleware> & {
34
+ query: z.infer<T["query"]>;
35
+ params: Params<TRoutes[K]["path"]>;
36
+ headers: z.infer<T["headers"]>;
37
+ }) => Promisable<TResult | Response> : T extends MutationRoute ? (context: MiddlewareContext<TMiddleware> & {
38
+ body: z.infer<T["body"]>;
39
+ params: Params<TRoutes[K]["path"]>;
40
+ headers: z.infer<T["headers"]>;
41
+ }) => Promisable<TResult | Response> : never : never : never; }; }) => import("alien-middleware").ApplyMiddleware<TMiddleware, (context: AdapterRequestContext<TMiddleware extends MiddlewareChain<infer T extends {
42
+ initial: {
43
+ env: object;
44
+ properties: object;
45
+ };
46
+ current: {
47
+ env: object;
48
+ properties: object;
49
+ };
50
+ platform: unknown;
51
+ }> ? T["platform"] : never> & {
52
+ url?: URL;
53
+ params?: {};
54
+ }) => Promise<Response>>;
@@ -0,0 +1,115 @@
1
+ import { RoutePattern } from '@remix-run/route-pattern';
2
+ import { chain, } from 'alien-middleware';
3
+ import { mapValues } from '../common';
4
+ import * as z from 'zod/mini';
5
+ export { chain };
6
+ export function createRouter(config) {
7
+ const keys = Object.keys(config.routes);
8
+ const middlewares = config.middlewares ?? chain();
9
+ const patterns = mapValues(config.routes, ({ path }) => new RoutePattern(path));
10
+ return (handlers) => middlewares.use(async function (context) {
11
+ const request = context.request;
12
+ const method = request.method.toUpperCase();
13
+ const url = (context.url ??= new URL(request.url));
14
+ for (let i = 0; i < keys.length; i++) {
15
+ const pattern = patterns[keys[i]];
16
+ const match = pattern.match(url);
17
+ if (!match) {
18
+ continue;
19
+ }
20
+ const route = config.routes[keys[i]].routes[method];
21
+ if (!route) {
22
+ continue;
23
+ }
24
+ if (route.headers) {
25
+ const error = parseHeaders(context, enableStringParsing(route.headers));
26
+ if (error) {
27
+ return httpClientError(error, 'Invalid request headers', config);
28
+ }
29
+ }
30
+ if (route.query) {
31
+ const error = parseQueryString(context, enableStringParsing(route.query));
32
+ if (error) {
33
+ return httpClientError(error, 'Invalid query string', config);
34
+ }
35
+ }
36
+ if (route.body) {
37
+ const error = await parseRequestBody(context, route.body);
38
+ if (error) {
39
+ return httpClientError(error, 'Invalid request body', config);
40
+ }
41
+ }
42
+ const handler = handlers[keys[i]][method];
43
+ if (!handler) {
44
+ continue;
45
+ }
46
+ context.params = match.params;
47
+ const result = await handler(context);
48
+ if (result instanceof Response) {
49
+ return result;
50
+ }
51
+ return Response.json(result);
52
+ }
53
+ });
54
+ }
55
+ function httpClientError(error, message, config) {
56
+ return Response.json({
57
+ ...error,
58
+ message: config.debug ? `${message}: ${error.message}` : message,
59
+ }, { status: 400 });
60
+ }
61
+ function parseHeaders(context, schema) {
62
+ const headers = Object.fromEntries(context.request.headers);
63
+ const result = schema.safeParse(headers);
64
+ if (!result.success) {
65
+ return result.error;
66
+ }
67
+ context.headers = result.data;
68
+ return null;
69
+ }
70
+ function parseQueryString(context, schema) {
71
+ const result = schema.safeParse(Object.fromEntries(context.url.searchParams));
72
+ if (!result.success) {
73
+ return result.error;
74
+ }
75
+ context.query = result.data;
76
+ return null;
77
+ }
78
+ async function parseRequestBody(context, schema) {
79
+ const result = await context.request.json().then(body => schema.safeParse(body), error => ({ success: false, error }));
80
+ if (!result.success) {
81
+ return result.error;
82
+ }
83
+ context.body = result.data;
84
+ return null;
85
+ }
86
+ const seen = new WeakMap();
87
+ /**
88
+ * Traverse object and array schemas, finding schemas that expect a number or
89
+ * boolean, and replace those schemas with a new schema that parses the input
90
+ * value as a number or boolean.
91
+ */
92
+ function enableStringParsing(schema) {
93
+ if (schema.type === 'number') {
94
+ return z.pipe(z.transform(Number), schema);
95
+ }
96
+ if (schema.type === 'boolean') {
97
+ return z.pipe(z.transform(toBooleanStrict), schema);
98
+ }
99
+ if (schema.type === 'object') {
100
+ const cached = seen.get(schema);
101
+ if (cached) {
102
+ return cached;
103
+ }
104
+ const modified = z.object(mapValues(schema.def.shape, enableStringParsing));
105
+ seen.set(schema, modified);
106
+ return modified;
107
+ }
108
+ if (schema.type === 'array') {
109
+ return z.array(enableStringParsing(schema.def.element));
110
+ }
111
+ return schema;
112
+ }
113
+ function toBooleanStrict(value) {
114
+ return value === 'true' || (value === 'false' ? false : value);
115
+ }
@@ -0,0 +1,79 @@
1
+ import { Params, RoutePattern } from '@remix-run/route-pattern';
2
+ import * as z from 'zod/mini';
3
+ export type Promisable<T> = T | Promise<T>;
4
+ export type Unchecked<T> = {
5
+ __unchecked__: T;
6
+ };
7
+ export type QueryRoute = {
8
+ path?: z.ZodMiniObject<any>;
9
+ query?: z.ZodMiniObject<any>;
10
+ body?: never;
11
+ headers?: z.ZodMiniObject<any>;
12
+ response: Unchecked<any>;
13
+ };
14
+ export type MutationRoute = {
15
+ path?: z.ZodMiniObject<any>;
16
+ query?: never;
17
+ body: z.ZodMiniType<any, any>;
18
+ headers?: z.ZodMiniObject<any>;
19
+ response?: Unchecked<any>;
20
+ };
21
+ export type Routes = {
22
+ GET?: QueryRoute;
23
+ POST?: MutationRoute;
24
+ PUT?: MutationRoute;
25
+ PATCH?: MutationRoute;
26
+ DELETE?: MutationRoute;
27
+ };
28
+ declare class Any {
29
+ private isAny;
30
+ }
31
+ type PathArgs<T> = T extends {
32
+ path: infer TPath extends string;
33
+ } ? Params<TPath> extends infer TParams ? {} extends TParams ? {
34
+ path?: TParams;
35
+ } : {
36
+ path: TParams;
37
+ } : unknown : unknown;
38
+ type QueryArgs<T> = T extends QueryRoute & {
39
+ query: infer TQuery;
40
+ } ? {} extends z.infer<TQuery> ? {
41
+ query?: z.infer<TQuery>;
42
+ } : {
43
+ query: z.infer<TQuery>;
44
+ } : unknown;
45
+ type MutationArgs<T> = T extends MutationRoute & {
46
+ body: infer TBody;
47
+ } ? {} extends z.infer<TBody> ? {
48
+ body?: z.infer<TBody>;
49
+ } : {
50
+ body: z.infer<TBody>;
51
+ } : unknown;
52
+ export type RouteArgs<T extends QueryRoute | MutationRoute = any> = ([
53
+ T
54
+ ] extends [Any] ? {
55
+ query?: any;
56
+ body?: any;
57
+ path?: any;
58
+ } : QueryArgs<T> & MutationArgs<T> & PathArgs<T>) & Omit<RequestInit, 'method' | 'body' | 'headers'> & {
59
+ headers?: Record<string, string | undefined>;
60
+ };
61
+ export type RouteRequest<TResult = any> = {
62
+ route: QueryRoute | MutationRoute;
63
+ pathPattern: RoutePattern;
64
+ method: string;
65
+ args: RouteArgs;
66
+ $result: TResult;
67
+ };
68
+ export type RouteResponse<TResult = any> = Response & {
69
+ json(): Promise<TResult>;
70
+ };
71
+ export type InferRouteResponse<T extends QueryRoute | MutationRoute> = T extends {
72
+ response: Unchecked<infer TResponse>;
73
+ } ? TResponse : void;
74
+ export type RouteFunction<T extends QueryRoute | MutationRoute> = {
75
+ (args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>;
76
+ $args: RouteArgs<T>;
77
+ $response: InferRouteResponse<T>;
78
+ };
79
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "rouzer",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.3",
4
+ "type": "module",
4
5
  "exports": {
5
6
  ".": {
6
7
  "types": "./dist/index.d.ts",
@@ -27,6 +28,10 @@
27
28
  "type": "git",
28
29
  "url": "https://github.com/alloc/rouzer.git"
29
30
  },
31
+ "files": [
32
+ "dist",
33
+ "!*.tsbuildinfo"
34
+ ],
30
35
  "scripts": {
31
36
  "build": "tsgo -b tsconfig.json"
32
37
  }
@@ -1,5 +0,0 @@
1
- {
2
- "editor.formatOnSave": true,
3
- "editor.defaultFormatter": "esbenp.prettier-vscode",
4
- "rewrap.wrappingColumn": 80
5
- }
@@ -1,70 +0,0 @@
1
- import { shake } from '../common'
2
- import type { RouteRequest } from '../types'
3
-
4
- export function createClient(config: {
5
- /**
6
- * Base URL to use for all requests.
7
- */
8
- baseURL: string
9
- /**
10
- * Default headers to send with every request.
11
- */
12
- headers?: Record<string, string>
13
- /**
14
- * Custom handler for non-200 response to a `.json()` request. By default, the
15
- * response is always parsed as JSON, regardless of the HTTP status code.
16
- */
17
- onJsonError?: (response: Response) => Response
18
- }) {
19
- return {
20
- config,
21
- request<T extends RouteRequest>({
22
- pathPattern,
23
- method,
24
- args: { path, query, body, headers },
25
- route,
26
- }: T) {
27
- if (route.path) {
28
- path = route.path.parse(path)
29
- }
30
-
31
- const url = new URL(pathPattern.href(path), config.baseURL)
32
-
33
- if (route.query) {
34
- query = route.query.parse(query ?? {})
35
- url.search = new URLSearchParams(query).toString()
36
- } else if (query) {
37
- throw new Error('Unexpected query parameters')
38
- }
39
- if (route.body) {
40
- body = route.body.parse(body !== undefined ? body : {})
41
- } else if (body !== undefined) {
42
- throw new Error('Unexpected body')
43
- }
44
-
45
- if (config.headers || headers) {
46
- headers = {
47
- ...config.headers,
48
- ...(headers && shake(headers)),
49
- }
50
- }
51
-
52
- if (route.headers) {
53
- headers = route.headers.parse(headers) as any
54
- }
55
-
56
- return fetch(url, {
57
- method,
58
- body: body !== undefined ? JSON.stringify(body) : undefined,
59
- headers,
60
- }) as Promise<Response & { json(): Promise<T['$result']> }>
61
- },
62
- async json<T extends RouteRequest>(request: T): Promise<T['$result']> {
63
- const response = await this.request(request)
64
- if (!response.ok && config.onJsonError) {
65
- return config.onJsonError(response)
66
- }
67
- return response.json()
68
- },
69
- }
70
- }
package/src/common.ts DELETED
@@ -1,99 +0,0 @@
1
- /**
2
- * Map over all the keys to create a new object.
3
- *
4
- * @see https://radashi.js.org/reference/object/mapEntries
5
- * @example
6
- * ```ts
7
- * const a = { a: 1, b: 2, c: 3 }
8
- * mapEntries(a, (key, value) => [value, key])
9
- * // => { 1: 'a', 2: 'b', 3: 'c' }
10
- * ```
11
- * @version 12.1.0
12
- */
13
- export function mapEntries<
14
- TKey extends string | number | symbol,
15
- TValue,
16
- TNewKey extends string | number | symbol,
17
- TNewValue,
18
- >(
19
- obj: Record<TKey, TValue>,
20
- toEntry: (key: TKey, value: TValue) => [TNewKey, TNewValue]
21
- ): Record<TNewKey, TNewValue> {
22
- if (!obj) {
23
- return {} as Record<TNewKey, TNewValue>
24
- }
25
- return Object.entries(obj).reduce(
26
- (acc, [key, value]) => {
27
- const [newKey, newValue] = toEntry(key as TKey, value as TValue)
28
- acc[newKey] = newValue
29
- return acc
30
- },
31
- {} as Record<TNewKey, TNewValue>
32
- )
33
- }
34
-
35
- /**
36
- * Removes (shakes out) undefined entries from an object. Optional
37
- * second argument shakes out values by custom evaluation.
38
- *
39
- * Note that non-enumerable keys are never shaken out.
40
- *
41
- * @see https://radashi.js.org/reference/object/shake
42
- * @example
43
- * ```ts
44
- * const a = { a: 1, b: undefined, c: 3 }
45
- * shake(a)
46
- * // => { a: 1, c: 3 }
47
- * ```
48
- * @version 12.1.0
49
- */
50
- export function shake<T extends object>(
51
- obj: T
52
- ): {
53
- [K in keyof T]: Exclude<T[K], undefined>
54
- }
55
-
56
- export function shake<T extends object>(
57
- obj: T,
58
- filter: ((value: unknown) => boolean) | undefined
59
- ): T
60
-
61
- export function shake<T extends object>(
62
- obj: T,
63
- filter: (value: unknown) => boolean = value => value === undefined
64
- ): T {
65
- if (!obj) {
66
- return {} as T
67
- }
68
- return (Object.keys(obj) as (keyof T)[]).reduce((acc, key) => {
69
- if (!filter(obj[key])) {
70
- acc[key] = obj[key]
71
- }
72
- return acc
73
- }, {} as T)
74
- }
75
-
76
- /**
77
- * Map over all the keys to create a new object.
78
- *
79
- * @see https://radashi.js.org/reference/object/mapValues
80
- * @example
81
- * ```ts
82
- * const a = { a: 1, b: 2, c: 3 }
83
- * mapValues(a, (value, key) => value * 2)
84
- * // => { a: 2, b: 4, c: 6 }
85
- * ```
86
- * @version 12.1.0
87
- */
88
- export function mapValues<T extends object, U>(
89
- obj: T,
90
- mapFunc: (value: Required<T>[keyof T], key: keyof T) => U
91
- ): { [K in keyof T]: U } {
92
- return (Object.keys(obj) as (keyof T)[]).reduce(
93
- (acc, key) => {
94
- acc[key] = mapFunc(obj[key], key)
95
- return acc
96
- },
97
- {} as { [K in keyof T]: U }
98
- )
99
- }
package/src/index.ts DELETED
@@ -1,6 +0,0 @@
1
- export * from './route'
2
- export * from './server/router'
3
- export * from './client/index'
4
- export type * from './types'
5
-
6
- export * from 'alien-middleware'
package/src/route.ts DELETED
@@ -1,44 +0,0 @@
1
- import { RoutePattern } from '@remix-run/route-pattern'
2
- import { mapEntries } from './common'
3
- import type {
4
- MutationRoute,
5
- QueryRoute,
6
- RouteArgs,
7
- RouteFunction,
8
- RouteRequest,
9
- Routes,
10
- Unchecked,
11
- } from './types'
12
-
13
- export function $type<T>() {
14
- return null as unknown as Unchecked<T>
15
- }
16
-
17
- export function route<P extends string, T extends Routes>(path: P, routes: T) {
18
- const pathPattern = new RoutePattern(path)
19
- const createFetch =
20
- (method: string, route: QueryRoute | MutationRoute) =>
21
- (args: RouteArgs): RouteRequest => {
22
- return {
23
- route,
24
- pathPattern,
25
- method,
26
- args,
27
- $result: undefined!,
28
- }
29
- }
30
-
31
- return Object.assign(
32
- { path, pathPattern, routes },
33
- mapEntries(
34
- routes as Record<string, QueryRoute | MutationRoute>,
35
- (method, route) => [method, createFetch(method, route)]
36
- )
37
- ) as unknown as {
38
- path: P
39
- pathPattern: RoutePattern
40
- routes: T
41
- } & {
42
- [K in keyof T]: RouteFunction<Extract<T[K], QueryRoute | MutationRoute>>
43
- }
44
- }
@@ -1,248 +0,0 @@
1
- import type { AdapterRequestContext } from '@hattip/core'
2
- import { RoutePattern, type Params } from '@remix-run/route-pattern'
3
- import {
4
- chain,
5
- MiddlewareChain,
6
- type MiddlewareContext,
7
- } from 'alien-middleware'
8
- import { mapValues } from '../common'
9
- import * as z from 'zod/mini'
10
- import type {
11
- InferRouteResponse,
12
- MutationRoute,
13
- Promisable,
14
- QueryRoute,
15
- Routes,
16
- } from '../types'
17
-
18
- export { chain }
19
-
20
- type EmptyMiddlewareChain<TPlatform = unknown> = MiddlewareChain<{
21
- initial: {
22
- env: {}
23
- properties: {}
24
- }
25
- current: {
26
- env: {}
27
- properties: {}
28
- }
29
- platform: TPlatform
30
- }>
31
-
32
- export type RouterConfig<
33
- TRoutes extends Record<string, { path: string; routes: Routes }> = any,
34
- TMiddleware extends MiddlewareChain = any,
35
- > = {
36
- routes: TRoutes
37
- middlewares?: TMiddleware
38
- debug?: boolean
39
- }
40
-
41
- export function createRouter<
42
- TRoutes extends Record<string, { path: string; routes: Routes }>,
43
- TMiddleware extends MiddlewareChain = EmptyMiddlewareChain,
44
- >(config: { routes: TRoutes; middlewares?: TMiddleware; debug?: boolean }) {
45
- const keys = Object.keys(config.routes)
46
- const middlewares = config.middlewares ?? (chain() as TMiddleware)
47
- const patterns = mapValues(
48
- config.routes,
49
- ({ path }) => new RoutePattern(path)
50
- )
51
-
52
- type RequestContext = MiddlewareContext<TMiddleware>
53
-
54
- type RequestHandler<TArgs extends object, TResult> = (
55
- context: RequestContext & TArgs
56
- ) => Promisable<TResult | Response>
57
-
58
- type InferRequestHandler<T, P extends string> = T extends QueryRoute
59
- ? RequestHandler<
60
- {
61
- query: z.infer<T['query']>
62
- params: Params<P>
63
- headers: z.infer<T['headers']>
64
- },
65
- InferRouteResponse<T>
66
- >
67
- : T extends MutationRoute
68
- ? RequestHandler<
69
- {
70
- body: z.infer<T['body']>
71
- params: Params<P>
72
- headers: z.infer<T['headers']>
73
- },
74
- InferRouteResponse<T>
75
- >
76
- : never
77
-
78
- type RequestHandlers = {
79
- [K in keyof TRoutes]: {
80
- [M in keyof TRoutes[K]['routes']]: InferRequestHandler<
81
- TRoutes[K]['routes'][M],
82
- TRoutes[K]['path']
83
- >
84
- }
85
- }
86
-
87
- type TPlatform =
88
- TMiddleware extends MiddlewareChain<infer T> ? T['platform'] : never
89
-
90
- return (handlers: RequestHandlers) =>
91
- middlewares.use(async function (
92
- context: AdapterRequestContext<TPlatform> & {
93
- url?: URL
94
- params?: {}
95
- }
96
- ) {
97
- const request = context.request as Request
98
- const method = request.method.toUpperCase() as keyof Routes
99
- const url: URL = (context.url ??= new URL(request.url))
100
-
101
- for (let i = 0; i < keys.length; i++) {
102
- const pattern = patterns[keys[i]]
103
-
104
- const match = pattern.match(url)
105
- if (!match) {
106
- continue
107
- }
108
-
109
- const route = config.routes[keys[i]].routes[method]
110
- if (!route) {
111
- continue
112
- }
113
-
114
- if (route.headers) {
115
- const error = parseHeaders(
116
- context,
117
- enableStringParsing(route.headers)
118
- )
119
- if (error) {
120
- return httpClientError(error, 'Invalid request headers', config)
121
- }
122
- }
123
-
124
- if (route.query) {
125
- const error = parseQueryString(
126
- context,
127
- enableStringParsing(route.query)
128
- )
129
- if (error) {
130
- return httpClientError(error, 'Invalid query string', config)
131
- }
132
- }
133
-
134
- if (route.body) {
135
- const error = await parseRequestBody(context, route.body)
136
- if (error) {
137
- return httpClientError(error, 'Invalid request body', config)
138
- }
139
- }
140
-
141
- const handler = handlers[keys[i]][method]
142
- if (!handler) {
143
- continue
144
- }
145
-
146
- context.params = match.params
147
-
148
- const result = await handler(context as any)
149
- if (result instanceof Response) {
150
- return result
151
- }
152
- return Response.json(result)
153
- }
154
- })
155
- }
156
-
157
- function httpClientError(
158
- error: any,
159
- message: string,
160
- config: { debug?: boolean }
161
- ) {
162
- return Response.json(
163
- {
164
- ...error,
165
- message: config.debug ? `${message}: ${error.message}` : message,
166
- },
167
- { status: 400 }
168
- )
169
- }
170
-
171
- function parseHeaders(
172
- context: AdapterRequestContext & { headers?: {} },
173
- schema: z.ZodMiniType<any, any>
174
- ) {
175
- const headers = Object.fromEntries(context.request.headers as any)
176
- const result = schema.safeParse(headers)
177
- if (!result.success) {
178
- return result.error
179
- }
180
- context.headers = result.data
181
- return null
182
- }
183
-
184
- function parseQueryString(
185
- context: AdapterRequestContext & { url?: URL; query?: {} },
186
- schema: z.ZodMiniType<any, any>
187
- ) {
188
- const result = schema.safeParse(
189
- Object.fromEntries(context.url!.searchParams as any)
190
- )
191
- if (!result.success) {
192
- return result.error
193
- }
194
- context.query = result.data
195
- return null
196
- }
197
-
198
- async function parseRequestBody(
199
- context: AdapterRequestContext & { body?: {} },
200
- schema: z.ZodMiniType<any, any>
201
- ) {
202
- const result = await context.request.json().then(
203
- body => schema.safeParse(body),
204
- error => ({ success: false, error }) as const
205
- )
206
- if (!result.success) {
207
- return result.error
208
- }
209
- context.body = result.data
210
- return null
211
- }
212
-
213
- const seen = new WeakMap<z.ZodMiniType<any, any>, z.ZodMiniType<any, any>>()
214
-
215
- /**
216
- * Traverse object and array schemas, finding schemas that expect a number or
217
- * boolean, and replace those schemas with a new schema that parses the input
218
- * value as a number or boolean.
219
- */
220
- function enableStringParsing(schema: z.ZodMiniType<any, any>): typeof schema {
221
- if (schema.type === 'number') {
222
- return z.pipe(z.transform(Number), schema)
223
- }
224
- if (schema.type === 'boolean') {
225
- return z.pipe(z.transform(toBooleanStrict), schema)
226
- }
227
- if (schema.type === 'object') {
228
- const cached = seen.get(schema)
229
- if (cached) {
230
- return cached
231
- }
232
- const modified = z.object(
233
- mapValues((schema as z.ZodMiniObject<any>).def.shape, enableStringParsing)
234
- )
235
- seen.set(schema, modified)
236
- return modified
237
- }
238
- if (schema.type === 'array') {
239
- return z.array(
240
- enableStringParsing((schema as z.ZodMiniArray<any>).def.element)
241
- )
242
- }
243
- return schema
244
- }
245
-
246
- function toBooleanStrict(value: string) {
247
- return value === 'true' || (value === 'false' ? false : value)
248
- }
package/src/types.ts DELETED
@@ -1,89 +0,0 @@
1
- import { Params, RoutePattern } from '@remix-run/route-pattern'
2
- import * as z from 'zod/mini'
3
-
4
- export type Promisable<T> = T | Promise<T>
5
-
6
- export type Unchecked<T> = { __unchecked__: T }
7
-
8
- export type QueryRoute = {
9
- path?: z.ZodMiniObject<any>
10
- query?: z.ZodMiniObject<any>
11
- body?: never
12
- headers?: z.ZodMiniObject<any>
13
- response: Unchecked<any>
14
- }
15
-
16
- export type MutationRoute = {
17
- path?: z.ZodMiniObject<any>
18
- query?: never
19
- body: z.ZodMiniType<any, any>
20
- headers?: z.ZodMiniObject<any>
21
- response?: Unchecked<any>
22
- }
23
-
24
- export type Routes = {
25
- GET?: QueryRoute
26
- POST?: MutationRoute
27
- PUT?: MutationRoute
28
- PATCH?: MutationRoute
29
- DELETE?: MutationRoute
30
- }
31
-
32
- declare class Any {
33
- private isAny: true
34
- }
35
-
36
- type PathArgs<T> = T extends { path: infer TPath extends string }
37
- ? Params<TPath> extends infer TParams
38
- ? {} extends TParams
39
- ? { path?: TParams }
40
- : { path: TParams }
41
- : unknown
42
- : unknown
43
-
44
- type QueryArgs<T> = T extends QueryRoute & { query: infer TQuery }
45
- ? {} extends z.infer<TQuery>
46
- ? { query?: z.infer<TQuery> }
47
- : { query: z.infer<TQuery> }
48
- : unknown
49
-
50
- type MutationArgs<T> = T extends MutationRoute & { body: infer TBody }
51
- ? {} extends z.infer<TBody>
52
- ? { body?: z.infer<TBody> }
53
- : { body: z.infer<TBody> }
54
- : unknown
55
-
56
- export type RouteArgs<T extends QueryRoute | MutationRoute = any> = ([
57
- T,
58
- ] extends [Any]
59
- ? { query?: any; body?: any; path?: any }
60
- : QueryArgs<T> & MutationArgs<T> & PathArgs<T>) &
61
- Omit<RequestInit, 'method' | 'body' | 'headers'> & {
62
- headers?: Record<string, string | undefined>
63
- }
64
-
65
- export type RouteRequest<TResult = any> = {
66
- route: QueryRoute | MutationRoute
67
- pathPattern: RoutePattern
68
- method: string
69
- args: RouteArgs
70
- $result: TResult
71
- }
72
-
73
- export type RouteResponse<TResult = any> = Response & {
74
- json(): Promise<TResult>
75
- }
76
-
77
- export type InferRouteResponse<T extends QueryRoute | MutationRoute> =
78
- T extends {
79
- response: Unchecked<infer TResponse>
80
- }
81
- ? TResponse
82
- : void
83
-
84
- export type RouteFunction<T extends QueryRoute | MutationRoute> = {
85
- (args: RouteArgs<T>): RouteRequest<InferRouteResponse<T>>
86
-
87
- $args: RouteArgs<T>
88
- $response: InferRouteResponse<T>
89
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "dist",
4
- "declaration": true,
5
- "module": "nodenext",
6
- "moduleResolution": "nodenext",
7
- "lib": ["dom", "es2019"],
8
- "target": "esnext"
9
- }
10
- }