Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,7 @@ dist
docs/dist
docs/dist-ssr
docs/*.local
docs/pages/README.md
docs/pages/README.md

# Vitest output
*.vitest-temp.*
9 changes: 2 additions & 7 deletions example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ import {
error,
} from '../src'

// declare a custom Router type with used methods
interface CustomRouter extends RouterType {
puppy: Route,
}

// declare a custom Request type to allow request injection from middleware
type RequestWithAuthors = {
authors?: string[]
Expand All @@ -25,11 +20,11 @@ const withAuthors = (request: IRequest) => {

const { corsify, preflight } = createCors()

const router = Router({ base: '/' })
const router = Router<'/', 'puppy'>({ base: '/' })

router
.all('*', preflight)
.get<CustomRouter>('/authors', withAuthors, (request: RequestWithAuthors) => {
.get('/authors', withAuthors, (request: RequestWithAuthors) => {
return request.authors?.[0]
})
.puppy('/:name', (request) => {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"docs:serve": "vite preview",
"lint": "npx eslint src",
"test": "vitest --coverage --reporter verbose",
"test:types": "vitest typecheck --coverage --reporter verbose",
"test:once": "vitest run",
"coverage": "vitest run --coverage",
"coveralls": "yarn coverage && cat ./coverage/lcov.info | coveralls",
Expand Down Expand Up @@ -67,7 +68,7 @@
"rollup-plugin-multi-input": "^1.3.3",
"typescript": "^4.8.4",
"vite": "^2.8.6",
"vitest": "^0.24.3",
"vitest": "0.29.2",
"vue": "^3.2.31",
"yarn": "^1.22.18",
"yarn-release": "^1.10.3"
Expand Down
43 changes: 43 additions & 0 deletions src/Router.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ParseRouteParameters } from 'Router';
import { it, describe, expectTypeOf } from 'vitest';

describe('route param parsing', () => {
it('parses route without params correctly', () => {
const route = '/foo/bar';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<{}>();
});

it('parses route with params correctly', () => {
const route = '/foo/:bar/:id';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<{ bar: string; id: string }>();
});

it('parses route with optional param correctly', () => {
const route = '/foo/:bar/:id?';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<{ bar: string; id: undefined | string }>();
});

it('parses route with catch-all param correctly', () => {
const route = '/foo/:bar/:rest+';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<{ bar: string; rest: undefined | string }>();
});

it('ignores inference with wildcard', () => {
const route = '*';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<{ [key: string]: string | undefined }>();
});

it('infers params but allows any param with slash wildcard', () => {
const route = '/foo/:bar/:buzz/*';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<
{ bar: string; buzz: string; } & { [key: string]: string | undefined }
>();
});

it('infers params but allows any param with slash-less wildcard', () => {
const route = '/foo/:bar/:buzz*';
expectTypeOf<ParseRouteParameters<typeof route>>().toEqualTypeOf<
{ bar: string; buzz: string; } & { [key: string]: string | undefined }
>();
});
});
130 changes: 83 additions & 47 deletions src/Router.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,99 @@
export type GenericTraps = {
[key: string]: any
}
[key: string]: any;
};

export type RequestLike = {
method: string,
url: string,
} & GenericTraps

export type IRequest = {
method: string,
url: string,
params: {
[key: string]: string,
},
method: string;
url: string;
} & GenericTraps;

type Modifier = '?' | '+';
type EatModifiers<Input extends string> = Input extends `${infer Remainder}${Modifier}`
? EatModifiers<Remainder>
: Input;

type CleanRouteParameters<Params extends { [key: string]: any }> = {
[Entry in keyof Params as Entry extends string ? EatModifiers<Entry> : Entry]: Params[Entry];
};

type UnknownRouteParameters = { [key: string]: string | undefined };

type InferParameterType<Entry> = string | (Entry extends `${string}${Modifier}` ? undefined : never);

type InferRouteParameters<Route> = Route extends `${string}/:${infer Param}/${infer Rest}*`
? {
[Entry in Param | keyof InferRouteParameters<`/${Rest}`>]: InferParameterType<Entry>;
} & UnknownRouteParameters
: Route extends `${string}/:${infer Param}*`
? {
[Entry in Param]: InferParameterType<Entry>;
} & UnknownRouteParameters
: Route extends `${string}*`
? UnknownRouteParameters
: Route extends `${string}/:${infer Param}/${infer Rest}`
? {
[Entry in Param | keyof InferRouteParameters<`/${Rest}`>]: InferParameterType<Entry>;
}
: Route extends `${string}/:${infer Param}`
? { [Entry in Param]: InferParameterType<Entry> }
: {};

export type ParseRouteParameters<Route> = CleanRouteParameters<InferRouteParameters<Route>>;

export type IRequest<TBaseRoute extends string | undefined = undefined, TRoute extends string = string> = {
method: string;
url: string;
params: string | undefined extends TRoute
? UnknownRouteParameters
: ParseRouteParameters<`${TBaseRoute extends `/${string}` ? TBaseRoute : ''}${TRoute}`>;
query: {
[key: string]: string | string[] | undefined,
},
proxy?: any,
} & GenericTraps

export interface RouterOptions {
base?: string
routes?: RouteEntry[]
[key: string]: string | string[] | undefined;
};
proxy?: any;
} & GenericTraps;

export interface RouterOptions<TBaseRoute extends string | undefined> {
base?: TBaseRoute;
routes?: RouteEntry[];
}

export interface RouteHandler {
(request: IRequest, ...args: any): any
export interface RouteHandler<TBaseRoute extends string | undefined = undefined, TRoute extends string = string> {
(request: IRequest<TBaseRoute, TRoute>, ...args: any): any;
}

export type RouteEntry = [string, RegExp, RouteHandler[]]

export type Route = <T extends RouterType>(
path: string,
...handlers: RouteHandler[]
) => T
export type RouteEntry = [string, RegExp, RouteHandler[]];

export type RouterHints = {
all: Route,
delete: Route,
get: Route,
options: Route,
patch: Route,
post: Route,
put: Route,
}
type ExtractBaseRoute<T extends RouterType> = T extends RouterType<infer TBaseRoute> ? TBaseRoute : undefined;

export type RouterType = {
__proto__: RouterType,
routes: RouteEntry[],
handle: (request: RequestLike, ...extra: any) => Promise<any>
} & RouterHints
export type Route = <T extends RouterType, TRoute extends string = string, TBaseRoute extends string | undefined = ExtractBaseRoute<T>>(
this: T,
path: TRoute,
...handlers: RouteHandler<TBaseRoute, TRoute>[]
) => T;

export const Router = ({ base = '', routes = [] }: RouterOptions = {}): RouterType =>
export type RouterHints = {
all: Route;
delete: Route;
get: Route;
options: Route;
patch: Route;
post: Route;
put: Route;
};

export type RouterType<TBaseRoute extends string | undefined = undefined, TMethods extends string | undefined = undefined> = {
__proto__: RouterType<TBaseRoute>;
routes: RouteEntry[];
handle: (request: RequestLike, ...extra: any) => Promise<any>;
} & RouterHints & ( TMethods extends string ? Record<TMethods, Route> : {});

export const Router = <
TBaseRoute extends string | undefined,
TMethods extends string | undefined = undefined,
>({ base = '', routes = [] }: RouterOptions<TBaseRoute> = {}): RouterType<TBaseRoute, TMethods> =>
// @ts-expect-error TypeScript doesn't know that Proxy makes this work
({
__proto__: new Proxy({} as RouterType, {
__proto__: new Proxy({} as RouterType<TBaseRoute>, {
get: (target, prop: string, receiver) => (route: string, ...handlers: RouteHandler[]) =>
routes.push([
prop.toUpperCase(),
Expand Down Expand Up @@ -94,7 +133,6 @@ export const Router = ({ base = '', routes = [] }: RouterOptions = {}): RouterTy

// // router.foo()


// type RequestWithAuthors = {
// authors?: string[]
// } & IRequest
Expand All @@ -104,7 +142,6 @@ export const Router = ({ base = '', routes = [] }: RouterOptions = {}): RouterTy
// request.authors = ['foo', 'bar']
// }


// const router = Router()

// type BooksResponse = {
Expand Down Expand Up @@ -136,7 +173,6 @@ export const Router = ({ base = '', routes = [] }: RouterOptions = {}): RouterTy

// const router = Router() as RouterType & MyTraps;


/*

{
Expand Down
Loading