fets
Version:
TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience
240 lines (239 loc) • 9.71 kB
JavaScript
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;
}