UNPKG

@tanstack/solid-start

Version:

Modern and scalable routing for Solid applications

237 lines (205 loc) 6.67 kB
import { FUNCTION_HEADER, configureServerFunctionsServer, createNoJSHandler, handleServerFunctionRequest, } from '@solidjs/web/server-functions/server' import { redirect } from '@tanstack/solid-router' import { provideRequestEvent } from '@solidjs/web/storage' import { getStartContext, runWithStartContext, } from '@tanstack/start-storage-context' import { getSolidStartServerFunctionCodec } from './solid-rpc-codec' import { collectSolidStartFlightData } from './solid-rpc-flight-server' import type { HandleServerFunctionOptions } from '@solidjs/web/server-functions/server' import type { StartStorageContext } from '@tanstack/start-storage-context' import { getServerFnById } from '#tanstack-start-server-fn-resolver' configureServerFunctionsServer({ provideEvent: provideRequestEvent, }) const solidNoJSHandler = createNoJSHandler() export interface HandleSolidServerFunctionRequestOptions extends HandleServerFunctionOptions { startContext?: Partial<StartStorageContext> } export async function handleSolidServerFunctionRequest( request: Request, options: HandleSolidServerFunctionRequestOptions = {}, ) { const serverFnId = getSolidServerFunctionId(request) if (!serverFnId) { throw new Error('Unable to resolve Solid server function id from request') } await getServerFnById(serverFnId, { origin: 'client' }) const requestWithId = withSolidServerFunctionId(request, serverFnId) const { startContext, ...solidOptions } = options const transformResult = solidOptions.transformResult const handleNoJS = solidOptions.handleNoJS const collectFlightData = solidOptions.collectFlightData const existingStartContext = getStartContext({ throwIfNotFound: false }) const handlerOptions: HandleServerFunctionOptions = { ...solidOptions, collectFlightData: collectFlightData ?? collectSolidStartFlightData, transformResult: async (event, result, context) => { const transformed = transformResult ? await transformResult(event, result, context) : result return adaptTanStackResult(transformed) }, handleNoJS: async (result, currentRequest, args, thrown) => { if (handleNoJS) { return await handleNoJS(result, currentRequest, args, thrown) } return await handleSolidStartNoJS(result, currentRequest, args, thrown) }, } const handleRequest = () => handleServerFunctionRequest(requestWithId, { codec: getSolidStartServerFunctionCodec(), ...handlerOptions, }) if (existingStartContext) { return await handleRequest() } return await runWithStartContext( { getRouter: unavailableRouter, request, startOptions: {}, contextAfterGlobalMiddlewares: {}, executedRequestMiddlewares: new Set(), handlerType: 'serverFn', ...startContext, }, handleRequest, ) } function getSolidServerFunctionId(request: Request) { const headerId = request.headers.get(FUNCTION_HEADER) if (headerId) { return headerId } const url = new URL(request.url) const queryId = url.searchParams.get('id') if (queryId) { return queryId } const serverFnBase = process.env.TSS_SERVER_FN_BASE if (serverFnBase && url.pathname.startsWith(serverFnBase)) { return url.pathname.slice(serverFnBase.length).split('/')[0] } return undefined } function withSolidServerFunctionId(request: Request, serverFnId: string) { if (request.headers.has(FUNCTION_HEADER)) { return request } try { request.headers.set(FUNCTION_HEADER, serverFnId) return request } catch { const clonedRequest = request.clone() clonedRequest.headers.set(FUNCTION_HEADER, serverFnId) return clonedRequest } } function serializeTanStackRedirect(result: unknown) { if (!isObject(result) || !isTanStackRedirect(result.error)) { return result } const redirectResponse = result.error return { ...result, error: { ...redirectResponse.options, headers: Object.fromEntries(redirectResponse.headers), isSerializedRedirect: true, }, } } function adaptTanStackResult(result: unknown) { const serialized = serializeTanStackRedirect(result) if (!isTanStackResultEnvelope(serialized)) { return serialized } return Object.fromEntries( Object.entries(serialized).filter(([, value]) => value !== undefined), ) } async function handleSolidStartNoJS( result: unknown, request: Request, args: Array<unknown>, thrown?: boolean, ) { if (isObject(result) && isSerializedRedirect(result.error)) { const startContext = getStartContext({ throwIfNotFound: false }) if (startContext) { const router = await startContext.getRouter() return router.resolveRedirect(redirect(result.error as never)) } } if (isObject(result)) { const unwrapped = result.result ?? result.error if (isResponseLike(unwrapped)) { return normalizeResponse(unwrapped) } if (typeof unwrapped === 'string') { return new Response(unwrapped) } if (unwrapped !== undefined) { return Response.json(unwrapped) } } return solidNoJSHandler(result, request, args, thrown) } function isTanStackRedirect( value: unknown, ): value is Response & { options: Record<string, unknown> } { return isResponseLike(value) && isObject(value.options) } function isTanStackResultEnvelope( value: unknown, ): value is Record<string, unknown> { return ( isObject(value) && ('result' in value || 'error' in value || 'context' in value) ) } interface ResponseLike { body: ReadableStream<Uint8Array> | null headers: Headers options?: unknown status: number statusText: string } function isResponseLike(value: unknown): value is ResponseLike { return ( isObject(value) && typeof value.status === 'number' && isObject(value.headers) && typeof value.headers.forEach === 'function' && ('body' in value || typeof value.text === 'function') ) } function normalizeResponse(response: ResponseLike) { if (response instanceof Response) { return response } return new Response(response.body, { headers: response.headers, status: response.status, statusText: response.statusText, }) } function isSerializedRedirect(value: unknown) { return isObject(value) && value.isSerializedRedirect === true } function isObject(value: unknown): value is Record<string, any> { return !!value && typeof value === 'object' } function unavailableRouter(): never { throw new Error( 'Router context is not available in handleSolidServerFunctionRequest. Use it inside createStartHandler or pass startContext.getRouter.', ) }