one
Version:
One is a new React Framework that makes Vite serve both native and web.
325 lines (275 loc) • 8.63 kB
text/typescript
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]
})
)
}