UNPKG

one

Version:

One is a new React Framework that makes Vite serve both native and web.

325 lines (275 loc) 8.63 kB
import { LOADER_JS_POSTFIX_UNCACHED } from './constants' import type { Middleware, MiddlewareContext } from './createMiddleware' import type { RouteNode } from './router/Route' import type { RouteInfoCompiled } from './server/createRoutesManifest' import type { LoaderProps } from './types' import { getPathFromLoaderPath } from './utils/cleanUrl' import { isResponse } from './utils/isResponse' import { getManifest } from './vite/getManifest' import { resolveAPIEndpoint, resolveResponse } from './vite/resolveResponse' import type { RouteInfo } from './vite/types' export type RequestHandlers = { handlePage?: (props: RequestHandlerProps) => Promise<any> handleLoader?: (props: RequestHandlerProps) => Promise<any> handleAPI?: (props: RequestHandlerProps) => Promise<any> loadMiddleware?: (route: RouteNode) => Promise<any> } type RequestHandlerProps<RouteExtraProps extends Object = {}> = { request: Request route: RouteInfo<string> & RouteExtraProps url: URL loaderProps?: LoaderProps } type RequestHandlerResponse = null | string | Response export async function runMiddlewares( handlers: RequestHandlers, request: Request, route: RouteInfo, getResponse: () => Promise<Response> ): Promise<Response> { const middlewares = route.middlewares if (!middlewares?.length) { return await getResponse() } if (!handlers.loadMiddleware) { throw new Error(`No middleware handler configured`) } const context: MiddlewareContext = {} async function dispatch(index: number): Promise<Response> { const middlewareModule = middlewares![index] // no more middlewares, finish if (!middlewareModule) { return await getResponse() } const exported = (await handlers.loadMiddleware!(middlewareModule))?.default as | Middleware | undefined if (!exported) { throw new Error(`No valid export found in middleware: ${middlewareModule.contextKey}`) } // go to next middleware const next = async () => { return dispatch(index + 1) } // run middlewares, if response returned, exit early const response = await exported({ request, next, context }) if (response) { return response } // If the middleware returns null/void, keep going return dispatch(index + 1) } // Start with the first middleware (index 0). return dispatch(0) } export async function resolveAPIRoute( handlers: RequestHandlers, request: Request, url: URL, route: RouteInfoCompiled ) { const { pathname } = url const params = getRouteParams(pathname, route) try { return resolveAPIEndpoint( () => handlers.handleAPI!({ request, route, url, loaderProps: { path: pathname, params, }, }), request, params || {} ) } catch (err) { if (isResponse(err)) { return err } if (process.env.NODE_ENV === 'development') { console.error(`\n [one] Error importing API route at ${pathname}: ${err} If this is an import error, you can likely fix this by adding this dependency to the "optimizeDeps.include" array in your vite.config.ts. `) } throw err } } export async function resolveLoaderRoute( handlers: RequestHandlers, request: Request, url: URL, route: RouteInfoCompiled ) { return await runMiddlewares(handlers, request, route, async () => { return await resolveResponse(async () => { const headers = new Headers() headers.set('Content-Type', 'text/javascript') try { const loaderResponse = await handlers.handleLoader!({ request, route, url, loaderProps: { path: url.pathname, request: route.type === 'ssr' ? request : undefined, params: getLoaderParams(url, route), }, }) return new Response(loaderResponse, { headers, }) } catch (err) { // allow throwing a response in a loader if (isResponse(err)) { return err } console.error(`Error running loader: ${err}`) throw err } }) }) } export async function resolvePageRoute( handlers: RequestHandlers, request: Request, url: URL, route: RouteInfoCompiled ) { const { pathname, search } = url return resolveResponse(async () => { const resolved = await runMiddlewares(handlers, request, route, async () => { return await handlers.handlePage!({ request, route, url, loaderProps: { path: pathname + search, // Ensure SSR loaders receive the original request request: route.type === 'ssr' ? request : undefined, params: getLoaderParams(url, route), }, }) }) return resolved }) } export function getURLfromRequestURL(request: Request) { const urlString = request.url || '' return new URL( urlString || '', request.headers.get('host') ? `http://${request.headers.get('host')}` : '' ) } function compileRouteRegex(route: RouteInfo): RouteInfoCompiled { return { ...route, compiledRegex: new RegExp(route.namedRegex), } } export function compileManifest(manifest: { pageRoutes: RouteInfo[]; apiRoutes: RouteInfo[] }): { pageRoutes: RouteInfoCompiled[] apiRoutes: RouteInfoCompiled[] } { return { pageRoutes: manifest.pageRoutes.map(compileRouteRegex), apiRoutes: manifest.apiRoutes.map(compileRouteRegex), } } // in dev mode we do it more simply: export function createHandleRequest( handlers: RequestHandlers, { routerRoot }: { routerRoot: string } ) { const manifest = getManifest({ routerRoot }) if (!manifest) { throw new Error(`No routes manifest`) } const compiledManifest = compileManifest(manifest) return { manifest, handler: async function handleRequest(request: Request): Promise<RequestHandlerResponse> { const url = getURLfromRequestURL(request) const { pathname, search } = url if (pathname === '/__vxrnhmr' || pathname.startsWith('/@')) { return null } if (handlers.handleAPI) { const apiRoute = compiledManifest.apiRoutes.find((route) => { return route.compiledRegex.test(pathname) }) if (apiRoute) { return await resolveAPIRoute(handlers, request, url, apiRoute) } } if (request.method !== 'GET') { return null } if (handlers.handleLoader) { const isClientRequestingNewRoute = pathname.endsWith(LOADER_JS_POSTFIX_UNCACHED) if (isClientRequestingNewRoute) { const originalUrl = getPathFromLoaderPath(pathname) for (const route of compiledManifest.pageRoutes) { if (route.file === '') { // ignore not found route continue } const finalUrl = new URL(originalUrl, url.origin) finalUrl.search = url.search if (!route.compiledRegex.test(finalUrl.pathname)) { continue } return resolveLoaderRoute(handlers, request, finalUrl, route) } if (process.env.NODE_ENV === 'development') { console.error(`No matching route found for loader!`, { originalUrl, pathname, routes: manifest.pageRoutes, }) } // error no match! return Response.error() } } if (handlers.handlePage) { for (const route of compiledManifest.pageRoutes) { if (!route.compiledRegex.test(pathname)) { continue } return resolvePageRoute(handlers, request, url, route) } } return null }, } } function getLoaderParams( url: URL, config: { compiledRegex: RegExp; routeKeys: Record<string, string> } ) { const params: Record<string, string> = {} const match = new RegExp(config.compiledRegex).exec(url.pathname) if (match?.groups) { for (const [key, value] of Object.entries(match.groups)) { const namedKey = config.routeKeys[key] params[namedKey] = value as string } } return params } // Add this helper function function getRouteParams(pathname: string, route: RouteInfo<string>) { const regex = new RegExp(route.namedRegex) const match = regex.exec(pathname) if (!match) return {} return Object.fromEntries( Object.entries(route.routeKeys).map(([key, value]) => { return [value, (match.groups?.[key] || '') as string] }) ) }