UNPKG

fets

Version:

TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience

240 lines (239 loc) • 9.71 kB
import { parse as qsParse } from 'qs'; import * as DefaultFetchAPI from '@whatwg-node/fetch'; import { createServerAdapter, isPromise, useErrorHandling } from '@whatwg-node/server'; import landingPageRaw from './landing-page-html.js'; import { useDefineRoutes } from './plugins/define-routes.js'; import { useOpenAPI } from './plugins/openapi.js'; import { useTypeBox } from './plugins/typebox.js'; import { EMPTY_OBJECT } from './plugins/utils.js'; import { asyncIterationUntilReturn } from './utils.js'; export function createRouterBase({ fetchAPI: givenFetchAPI, base: basePath = '/', plugins = [], openAPI, swaggerUI, landingPage = true, } = {}, openAPIDocument) { const fetchAPI = givenFetchAPI ? { ...DefaultFetchAPI, ...givenFetchAPI, } : DefaultFetchAPI; const __onRouterInitHooks = []; const onRouteHooks = []; const onRouteHandleHooks = []; for (const plugin of plugins) { if (plugin.onRouterInit) { __onRouterInitHooks.push(plugin.onRouterInit); } if (plugin.onRoute) { onRouteHooks.push(plugin.onRoute); } if (plugin.onRouteHandle) { onRouteHandleHooks.push(plugin.onRouteHandle); } } const routeByPatternByMethod = new Map(); const routeByPathByMethod = new Map(); const __routes = []; function handleUnhandledRoute(requestPath) { if (landingPage) { return new fetchAPI.Response(landingPageRaw .replaceAll('__BASE_PATH__', basePath) .replaceAll('__OAS_PATH__', openAPI?.endpoint || '/openapi.json') .replaceAll('__SWAGGER_UI_PATH__', swaggerUI?.endpoint || '/docs') .replaceAll('__REQUEST_PATH__', requestPath), { headers: { 'Content-Type': 'text/html', }, }); } return undefined; } return { openAPIDocument, handle(request, context) { let url = new Proxy(EMPTY_OBJECT, { get(_target, prop, _receiver) { url = new fetchAPI.URL(request.url, 'http://localhost'); return Reflect.get(url, prop, url); }, }); let parsedQuery; Object.defineProperties(request, { parsedUrl: { get() { return url; }, configurable: true, }, query: { get() { if (parsedQuery == null) { const searchPart = request.url.split('?')[1]; if (searchPart == null) { return EMPTY_OBJECT; } parsedQuery = qsParse(searchPart); } return parsedQuery; }, configurable: true, }, }); const pathPatternMapByMethod = routeByPathByMethod.get(request.method); if (pathPatternMapByMethod) { const route = pathPatternMapByMethod.get(url.pathname); if (route) { for (const onRouteHandleHook of onRouteHandleHooks) { onRouteHandleHook({ route, // @ts-expect-error - We know it's a TypedRequest request, }); } // @ts-expect-error - We know it's a TypedRequest const handlerResult$ = route.handler(request, context); if (isPromise(handlerResult$)) { return handlerResult$.then(handlerResult => { if (handlerResult) { return handlerResult; } return handleUnhandledRoute(request.url); }); } if (handlerResult$) { return handlerResult$; } } } const methodPatternMaps = routeByPatternByMethod.get(request.method); if (methodPatternMaps) { const patternHandlerResult$ = asyncIterationUntilReturn(methodPatternMaps.entries(), ([pattern, route]) => { // Do not parse URL if not needed const match = pattern.exec(url); if (match != null) { let paramsProxy; Object.defineProperty(request, 'params', { get() { if (paramsProxy == null) { paramsProxy = new Proxy(match.pathname.groups, { get(_, prop) { const value = match.pathname.groups[prop.toString()]; if (value != null) { return decodeURIComponent(value); } return value; }, }); } return paramsProxy; }, configurable: true, }); for (const onRouteHandleHook of onRouteHandleHooks) { onRouteHandleHook({ route, // @ts-expect-error - We know it's a TypedRequest request, }); } // @ts-expect-error - We know it's a TypedRequest return route.handler(request, context); } }); if (isPromise(patternHandlerResult$)) { return patternHandlerResult$.then(patternHandlerResult => { if (patternHandlerResult) { return patternHandlerResult; } return handleUnhandledRoute(request.url); }); } if (patternHandlerResult$) { return patternHandlerResult$; } } return handleUnhandledRoute(request.url); }, route(route) { if (!route.internal) { __routes.push(route); } for (const onRouteHook of onRouteHooks) { onRouteHook({ basePath, route, routeByPathByMethod, routeByPatternByMethod, openAPIDocument, fetchAPI, }); } return this; }, use(prefixOrSubRouter, subRouter) { let prefix = ''; let actualSubRouter; if (typeof prefixOrSubRouter === 'string') { prefix = prefixOrSubRouter === '/' ? '' : prefixOrSubRouter; actualSubRouter = subRouter; } else { actualSubRouter = prefixOrSubRouter; } const subBase = actualSubRouter.__base === '/' ? '' : actualSubRouter.__base; for (const subRoute of actualSubRouter.__routes) { const subPath = subRoute.path === '/' ? '' : subRoute.path; const newPath = `${prefix}${subBase}${subPath}` || '/'; this.route({ ...subRoute, path: newPath }); } return this; }, __client: {}, __onRouterInitHooks, __routes, __base: basePath, }; } export function createRouter(options = {}) { const { openAPI: { endpoint: oasEndpoint = '/openapi.json', includeValidationErrors, ...openAPIDocument } = {}, swaggerUI: { endpoint: swaggerUIEndpoint = '/docs', ...swaggerUIOpts } = {}, plugins: userPlugins = [], base = '/', } = options; openAPIDocument.openapi = openAPIDocument.openapi || '3.0.1'; const oasInfo = (openAPIDocument.info ||= {}); oasInfo.title ||= 'feTS API'; oasInfo.description ||= 'An API written with feTS'; oasInfo.version ||= '1.0.0'; if (base !== '/') { openAPIDocument.servers = openAPIDocument.servers || [ { url: base, }, ]; } const plugins = [ ...(oasEndpoint || swaggerUIEndpoint ? [ useOpenAPI({ oasEndpoint, swaggerUIEndpoint, swaggerUIOpts, includeValidationErrors, }), ] : []), useTypeBox(), useErrorHandling(options?.onError), useDefineRoutes(), ...userPlugins, ]; const finalOpts = { ...options, swaggerUI: { endpoint: swaggerUIEndpoint, ...swaggerUIOpts, }, base, plugins, }; const routerBaseObject = createRouterBase(finalOpts, openAPIDocument); const router = createServerAdapter(routerBaseObject, finalOpts); for (const onRouterInitHook of routerBaseObject.__onRouterInitHooks) { onRouterInitHook(router); } return router; }