UNPKG

@squirt/server

Version:
221 lines (190 loc) 6.36 kB
import * as RegexParam from "regexparam" import * as Path from "path" import { render } from "@squirt/markup/src/render" import { Loader } from "./loader" import { Server } from "bun" export default async function createRouter(root: string, loader: Loader, production: boolean) { // TODO: route precedence const routes: Route[] = [] const contextExtensions: Set<string> = new Set() return { addFile, removeFile, request, open, message, close, drain, } async function request(request: Request, server: Server): Promise<Response | null | undefined> { const url = new URL(request.url) const { route, parameters } = findRoute(url.pathname) if (route === null) return null const module = await loader.module(route.path) if (!module) return null const method = findMethod(route, module, request.method.toLowerCase()) if (method === null) return null // TODO: different Context if type is socket let context: Context = { request, route, url, } for (const extension of contextExtensions) { const module = await loader.module(extension) if (!(typeof module.default === "function")) continue const props = module.default(context) context = { ...props, ...context } } const result = (typeof method !== "function") ? method : await Promise.resolve(method(context, parameters)) if (route.type === "api") { if (result instanceof Response) { return result } else { return new Response(JSON.stringify(result), { headers: { "Content-Type": "application/json" } }) } } else if (route.type === "socket") { const handler = result as SocketHandler const upgrade = !handler.upgrade ? undefined : await Promise.resolve(handler.upgrade(request)) const data: SquirtWebSocketData<unknown> = { type: "route", url: request.url, parameters: upgrade?.parameters } if (server.upgrade(request, { data, headers: upgrade?.headers })) { return undefined } else { return new Response("Expected WebSocket connection.", { status: 400 }) } } else { const contentType = route.type === "html" ? "text/html" : "text/css" if (result instanceof Response) { return result } return new Response(render(result), { headers: { "Content-Type": contentType } }) } } async function open(ws: SquirtWebSocket) { const handler = await findSocketHandler(ws) if (handler?.open) handler.open(ws) } async function message(ws: SquirtWebSocket, message: string | Uint8Array) { const handler = await findSocketHandler(ws) if (handler?.message) handler.message(ws, message) } async function close(ws: SquirtWebSocket, code: number, message: string) { const handler = await findSocketHandler(ws) if (handler?.close) handler.close(ws, code, message) } async function drain(ws: SquirtWebSocket) { const handler = await findSocketHandler(ws) if (handler?.drain) handler.drain(ws) } async function findSocketHandler(ws: SquirtWebSocket): Promise<SocketHandler | null> { if (ws.data.type !== "route") return null const url = new URL(ws.data.url) const { route, parameters } = findRoute(url.pathname) if (route === null) throw new Error("Could not route WebSocket.") const module = await loader.module(route.path) if (!module) throw new Error("Could not route WebSocket.") const method = module.default if (!method) throw new Error("Could not route WebSocket.") return (typeof method !== "function") ? method : await Promise.resolve(method(parameters, { route, url, })) } function findMethod(route: Route, module: any, method: string) { if (route.type === "css" || route.type === "socket") { return module.default || null } else if (route.type === "html") { if (method === "get") return module.default || module.get || null return module[method] || null } else if (route.type === "api") { if (module.default) return module.default if (method === "delete") return module.del || null return module[method] || null } return null } function findRoute(pathname: string) { const parameters: any = {} let match: Route | null = null for (const route of routes) { const matches = route.pattern.exec(pathname) if (matches) { match = route for (let p = 0; p < match.keys.length; p++) { parameters[route.keys[p]] = matches[p + 1] } break } } return { route: match, parameters, } } function addFile(path: string) { const contextPattern = /\.context\.(ts|js|civet)$/ if (contextPattern.test(path)) { contextExtensions.add(absolute(path)) } const route = createRoute(path) if (!route) return routes.push(route) } function removeFile(path: string) { contextExtensions.delete(path) const routeIndex = routes.findIndex(r => r.path === path) if (routeIndex > -1) { routes.splice(routeIndex, 1) } } function createRoute(path: string): Route | null { const routePattern = /^routes(\/?.*\/([^/]+))\.(html|css|api|socket)\.(ts|js|civet)$/ const paramPattern = /\[([^\]]+)\]/g const wildPattern = /\[\.\.\.(.+)\]$/ const unroutablePattern = /\/_/ if (unroutablePattern.test(path)) return null const [, pattern, filename, type, language] = routePattern.exec(path) || [] if (!pattern) return null let url = type === "css" ? pattern + ".css" : (filename === "index" ? pattern.replace(/\/?index/, "") : pattern) const [, wildKey] = wildPattern.exec(url) || [] if (wildKey) { url = url.replace(wildPattern, "*") } const isParam = paramPattern.test(url) if (isParam) { url = url.replace(paramPattern, ":$1") } const route: Route = { path: absolute(path), type, language, isParam, isWild: !!wildKey, ...RegexParam.parse(url), } if (wildKey) { route.keys[route.keys.length - 1] = wildKey } return route } function absolute(path: string) { return Path.join(root, "src", path) } }