Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions packages/void/src/runtime/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ declare module 'hono' {
export type CloudContext = Context<CloudEnv>;

type HandlerFn = (c: CloudContext) => unknown | Promise<unknown>;
// `R` is the loader's data, so `defineHandler<Props>(async (c) => ...)` works.
type LoaderFn<R> = (c: CloudContext) => R | Response | Promise<R | Response>;
// For loaders that LoaderFn cannot infer, such as `cond ? loadA() : loadB()`.
type LoaderOutput<F extends HandlerFn> = Exclude<Awaited<ReturnType<F>>, Response>;
type MiddlewareFn = MiddlewareHandler<CloudEnv>;
type BasicAuthOptionValue = string | (() => string);

Expand Down Expand Up @@ -209,35 +213,68 @@ export type TypedHandler<V extends ValidatorSlots = {}, R = unknown> = {
* Multiple arguments: middleware chain + final handler, composed at runtime.
*/
export function defineHandler<R>(
handler: (c: CloudContext) => R | Response | Promise<R | Response>,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler(firstMiddleware: MiddlewareFn, handler: HandlerFn): TypedHandler;
export function defineHandler(
export function defineHandler<R>(
firstMiddleware: MiddlewareFn,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler<F extends HandlerFn>(
firstMiddleware: MiddlewareFn,
handler: F,
): TypedHandler<{}, LoaderOutput<F>>;
export function defineHandler<R>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler<F extends HandlerFn>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
handler: F,
): TypedHandler<{}, LoaderOutput<F>>;
export function defineHandler<R>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
thirdMiddleware: MiddlewareFn,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler<F extends HandlerFn>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
handler: HandlerFn,
): TypedHandler;
export function defineHandler(
thirdMiddleware: MiddlewareFn,
handler: F,
): TypedHandler<{}, LoaderOutput<F>>;
export function defineHandler<R>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
thirdMiddleware: MiddlewareFn,
handler: HandlerFn,
): TypedHandler;
export function defineHandler(
fourthMiddleware: MiddlewareFn,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler<F extends HandlerFn>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
thirdMiddleware: MiddlewareFn,
fourthMiddleware: MiddlewareFn,
handler: HandlerFn,
): TypedHandler;
export function defineHandler(
handler: F,
): TypedHandler<{}, LoaderOutput<F>>;
export function defineHandler<R>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
thirdMiddleware: MiddlewareFn,
fourthMiddleware: MiddlewareFn,
fifthMiddleware: MiddlewareFn,
handler: LoaderFn<R>,
): TypedHandler<{}, Exclude<Awaited<R>, Response>>;
export function defineHandler<F extends HandlerFn>(
firstMiddleware: MiddlewareFn,
secondMiddleware: MiddlewareFn,
thirdMiddleware: MiddlewareFn,
fourthMiddleware: MiddlewareFn,
fifthMiddleware: MiddlewareFn,
handler: HandlerFn,
): TypedHandler;
handler: F,
): TypedHandler<{}, LoaderOutput<F>>;
export function defineHandler(...args: Array<MiddlewareFn | HandlerFn>): TypedHandler {
if (args.length === 1) {
return args[0] as TypedHandler;
Expand Down
49 changes: 48 additions & 1 deletion packages/void/test/unit/infer-props.typecheck.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { describe, expectTypeOf, it } from 'vite-plus/test';
import { z } from 'zod';
import type { InferProps } from '../../src/pages/defer.ts';
import { defineHandler } from '../../src/runtime/handler.ts';
import { defineHandler, defineMiddleware } from '../../src/runtime/handler.ts';

type Book = { id: string; title: string };
type Author = { id: string; name: string };

declare function findBook(id: string): Promise<Book | undefined>;
declare function findAuthor(id: string): Promise<Author>;
declare const showAuthor: boolean;

const validators = { params: z.object({ id: z.string() }) };

const middleware = defineMiddleware(async (_c, next) => {
await next();
});

describe('InferProps', () => {
it('unwraps an async plain loader and excludes Response', () => {
const loader = defineHandler(async (c) => {
Expand Down Expand Up @@ -76,4 +83,44 @@ describe('InferProps', () => {
book: Promise<Book | undefined>;
}>();
});
it('unwraps an async loader with middleware and excludes Response', () => {
const loader = defineHandler(middleware, async (c) => {
const book = await findBook('1');
if (!book) {
return c.notFound();
}
return { book };
});

expectTypeOf<InferProps<typeof loader>>().toEqualTypeOf<{ book: Book }>();
});

it('infers a loader with several middlewares', () => {
const loader = defineHandler(middleware, middleware, middleware, (_c) => ({ id: '1' }));

expectTypeOf<InferProps<typeof loader>>().toEqualTypeOf<{ id: string }>();
});

it('keeps every promise a non-async loader with middleware can return', () => {
const loader = defineHandler(middleware, (_c) =>
showAuthor
? findAuthor('1').then((author) => ({ author }))
: findBook('1').then((book) => ({ book })),
);

expectTypeOf<InferProps<typeof loader>>().toEqualTypeOf<
{ author: Author } | { book: Book | undefined }
>();
});

it('takes the props type as an explicit type argument', () => {
type Props = { title: string };
const loader = defineHandler<Props>(async () => ({ title: 'a' }));
const guarded = defineHandler<Props>(middleware, async (c) =>
c.req.query('missing') ? c.notFound() : { title: 'a' },
);

expectTypeOf<InferProps<typeof loader>>().toEqualTypeOf<Props>();
expectTypeOf<InferProps<typeof guarded>>().toEqualTypeOf<Props>();
});
});
Loading