Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!: improve router #817

Merged
merged 3 commits into from
Jul 8, 2024
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
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
Loading