Skip to content

Commit

Permalink
refactor!: improve router (#817)
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Jul 8, 2024
1 parent a3c9c6d commit 8d124ed
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 148 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"cookie-es": "^1.1.0",
"iron-webcrypto": "^1.2.1",
"ohash": "^1.1.3",
"rou3": "^0.1.0",
"rou3": "^0.2.0",
"ufo": "^1.5.3",
"uncrypto": "^0.1.3"
},
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions src/app/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
WebSocketOptions,
App,
EventHandler,
HTTPMethod,
} from "../types";
import { _kRaw } from "../event";
import { defineLazyEventHandler } from "../handler";
Expand Down Expand Up @@ -44,7 +45,7 @@ export function use(
}

export function createResolver(stack: Stack): EventHandlerResolver {
return async (path: string) => {
return async (method, path) => {
let _layerPath: string;
for (const layer of stack) {
if (layer.route === "/" && !layer.handler.__resolve__) {
Expand All @@ -59,7 +60,7 @@ export function createResolver(stack: Stack): EventHandlerResolver {
}
let res = { route: layer.route, handler: layer.handler };
if (res.handler.__resolve__) {
const _res = await res.handler.__resolve__(_layerPath);
const _res = await res.handler.__resolve__(method, _layerPath);
if (!_res) {
continue;
}
Expand Down Expand Up @@ -94,14 +95,15 @@ export function normalizeLayer(input: InputLayer) {
}

export function resolveWebsocketOptions(
evResolver: EventHandlerResolver,
resolver: EventHandlerResolver,
appOptions: AppOptions,
): WebSocketOptions {
return {
...appOptions.websocket,
async resolve(info) {
const pathname = getPathname(info.url || "/");
const resolved = await evResolver(pathname);
const method = ((info as any).method || "GET") as HTTPMethod;
const resolved = await resolver(method, pathname);
return resolved?.handler?.__websocket__ || {};
},
};
Expand Down
204 changes: 108 additions & 96 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,109 @@
import {
createRouter as _createRouter,
findRoute as _findRoute,
addRoute as _addRoute,
} from "rou3";
import type {
CreateRouterOptions,
RouterOptions,
EventHandler,
RouteNode,
HTTPMethod,
RouterEntry,
Router,
RouterMethod,
H3Event,
} from "./types";
import {
type RouterContext,
createRouter as _createRouter,
findRoute,
addRoute,
} from "rou3";
import { createError } from "./error";
import { withLeadingSlash } from "./utils/internal/path";

const RouterMethods: RouterMethod[] = [
"connect",
"delete",
"get",
"head",
"options",
"post",
"put",
"trace",
"patch",
];

/**
* Create a new h3 router instance.
*/
export function createRouter(opts: CreateRouterOptions = {}): Router {
const _router = _createRouter<RouteNode>();
export function createRouter(opts: RouterOptions = {}): Router {
return new H3Router(opts);
}

const router: Router = {} as Router;
class H3Router implements Router {
_router: RouterContext<RouterEntry>;
_options: RouterOptions;
constructor(opts: RouterOptions = {}) {
this._router = _createRouter();
this._options = opts;
this.handler = this.handler.bind(this);
(this.handler as EventHandler).__resolve__ = this._resolveRoute.bind(this);
}

// Utility to add a new route
function addRoute(
path: string,
handler: EventHandler,
method: RouterMethod | RouterMethod[] | "" | undefined,
) {
if (Array.isArray(method)) {
for (const _method of method) {
addRoute(path, handler, _method);
}
} else {
const _method = (method || "").toLowerCase();
_addRoute(_router, path, _method, <RouteNode>{
handler,
path,
method: _method,
});
}
return router;
all(path: string, handler: EventHandler) {
return this.add("", path, handler);
}

// Shortcuts
router.use = router.add = (path, handler, method) =>
addRoute(path, handler as EventHandler, method);
for (const method of RouterMethods) {
router[method] = (path, handle) => router.add(path, handle, method);
use(path: string, handler: EventHandler) {
return this.all(path, handler);
}

// Handler matcher
function matchRoute(
path = "/",
method: RouterMethod = "get",
): { error: Error } | { data: RouteNode; params?: Record<string, string> } {
// Remove query parameters for matching
const qIndex = path.indexOf("?");
if (qIndex !== -1) {
path = path.slice(0, Math.max(0, qIndex));
}
// Match route
const match = _findRoute(_router, path, method);
if (!match) {
return {
error: createError({
statusCode: 404,
name: "Not Found",
statusMessage: `Cannot find any route matching [${method}] ${path || "/"}.`,
}),
};
}
return match as { data: RouteNode; params?: Record<string, string> };
get(path: string, handler: EventHandler) {
return this.add("GET", path, handler);
}

post(path: string, handler: EventHandler) {
return this.add("POST", path, handler);
}

put(path: string, handler: EventHandler) {
return this.add("PUT", path, handler);
}

delete(path: string, handler: EventHandler) {
return this.add("DELETE", path, handler);
}

patch(path: string, handler: EventHandler) {
return this.add("PATCH", path, handler);
}

// Main handle
router.handler = (event) => {
head(path: string, handler: EventHandler) {
return this.add("HEAD", path, handler);
}

options(path: string, handler: EventHandler) {
return this.add("OPTIONS", path, handler);
}

connect(path: string, handler: EventHandler) {
return this.add("CONNECT", path, handler);
}

trace(path: string, handler: EventHandler) {
return this.add("TRACE", path, handler);
}

add(
method: HTTPMethod | Lowercase<HTTPMethod> | "",
path: string,
handler: EventHandler,
): this {
const _method = (method || "").toUpperCase();
addRoute(this._router, _method, path, <RouterEntry>{
method: _method,
route: path,
handler,
});
return this;
}

handler(event: H3Event) {
// Match handler
const match = matchRoute(
const match = this._findRoute(
event.method.toUpperCase() as HTTPMethod,
event.path,
event.method.toLowerCase() as RouterMethod,
);

// No match (method or route)
if ("error" in match) {
if (opts.preemptive) {
throw match.error;
if (!match) {
if (this._options.preemptive) {
throw createError({
statusCode: 404,
name: "Not Found",
statusMessage: `Cannot find any route matching [${event.method}] ${event.path || "/"}`,
});
} else {
return; // Let app match other handlers
}
Expand All @@ -108,33 +115,38 @@ export function createRouter(opts: CreateRouterOptions = {}): Router {

// Call handler
return Promise.resolve(match.data.handler(event)).then((res) => {
if (res === undefined && opts.preemptive) {
if (res === undefined && this._options.preemptive) {
return null; // Send empty content
}
return res;
});
};
}

_findRoute(method: HTTPMethod = "GET", path = "/") {
// Remove query parameters for matching
const qIndex = path.indexOf("?");
if (qIndex !== -1) {
path = path.slice(0, Math.max(0, qIndex));
}
return findRoute(this._router, method, path) as
| { data: RouterEntry; params?: Record<string, string> }
| undefined;
}

// Resolver
router.handler.__resolve__ = async (path) => {
path = withLeadingSlash(path);
const match = matchRoute(path);
if ("error" in match) {
async _resolveRoute(method: HTTPMethod = "GET", path: string) {
const match = this._findRoute(method, path);
if (!match) {
return;
}
let res = {
route: match.data.path,
const resolved = {
route: match.data.route,
handler: match.data.handler,
params: match.params,
};
if (match.data.handler.__resolve__) {
const _res = await match.data.handler.__resolve__(path);
if (!_res) {
return;
}
res = { ...res, ..._res };
if (resolved.handler.__resolve__) {
const _resolved = await resolved.handler.__resolve__(method, path);
return { ...resolved, ..._resolved };
}
return res;
};

return router;
return resolved;
}
}
4 changes: 2 additions & 2 deletions src/types/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Session } from "./utils/session";
import type { RouteNode } from "./router";
import type { RouterEntry } from "./router";

export interface H3EventContext extends Record<string, any> {
/* Matched router parameters */
Expand All @@ -9,7 +9,7 @@ export interface H3EventContext extends Record<string, any> {
*
* @experimental The object structure may change in non-major version.
*/
matchedRoute?: RouteNode;
matchedRoute?: RouterEntry;
/* Cached session data */
sessions?: Record<string, Session>;
/* Trusted IP Address of client */
Expand Down
7 changes: 6 additions & 1 deletion src/types/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Readable as NodeReadableStream } from "node:stream";
import type { QueryObject } from "ufo";
import type { H3Event } from "./event";
import type { Hooks as WSHooks } from "crossws";
import type { HTTPMethod } from "./http";
import type { RouterEntry } from "./router";

export type ResponseBody =
| undefined // middleware pass
Expand All @@ -26,8 +28,11 @@ export type InferEventInput<
type MaybePromise<T> = T | Promise<T>;

export type EventHandlerResolver = (
method: HTTPMethod,
path: string,
) => MaybePromise<undefined | { route?: string; handler: EventHandler }>;
) => MaybePromise<
undefined | (Partial<RouterEntry> & { params?: Record<string, string> })
>;

export interface EventHandler<
Request extends EventHandlerRequest = EventHandlerRequest,
Expand Down
9 changes: 1 addition & 8 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,7 @@ export type {
} from "./web";

// Router
export type {
RouteNode,
Router,
RouterMethod,
RouterUse,
AddRouteShortcuts,
CreateRouterOptions,
} from "./router";
export type { Router, RouterOptions, RouterEntry } from "./router";

// Context
export type { H3EventContext } from "./context";
Expand Down
Loading

0 comments on commit 8d124ed

Please sign in to comment.