UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

437 lines (430 loc) 18 kB
import { requestHandler } from "./request-response.js"; import { getStartManifest } from "./router-manifest.js"; import { handleServerAction } from "./server-functions-handler.js"; import { createEarlyHintsCollector } from "./early-hints.js"; import { createCachedBaseManifestLoader, createFinalManifestResolver } from "./finalManifest.js"; import { HEADERS } from "./constants.js"; import { ServerFunctionSerializationAdapter } from "./serializer/ServerFunctionSerializationAdapter.js"; import { createMemoryHistory } from "@tanstack/history"; import { createCsrfMiddleware, createNullProtoObject, csrfSymbol, flattenMiddlewares, mergeHeaders, safeObjectMerge } from "@tanstack/start-client-core"; import { executeRewriteInput, isRedirect, isResolvedRedirect } from "@tanstack/router-core"; import { attachRouterServerSsrUtils, getNormalizedURL, getOrigin, isSsrResponse, normalizeSsrResponse, replaceSsrResponse, stripSsrResponseBody } from "@tanstack/router-core/ssr/server"; import { getStartContext, runWithStartContext } from "@tanstack/start-storage-context"; //#region src/createStartHandler.ts function getStartResponseHeaders(opts) { return mergeHeaders({ "Content-Type": "text/html; charset=utf-8" }, ...opts.router.stores.matches.get().map((match) => { return match.headers; })); } var entriesPromise; var hasWarnedMissingCsrfMiddleware = false; var defaultCsrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === "serverFn" }); var getCachedBaseManifest = createCachedBaseManifestLoader(() => getStartManifest()); var getProdBaseManifest = () => getCachedBaseManifest(); var getBaseManifest = process.env.TSS_DEV_SERVER === "true" ? getStartManifest : getProdBaseManifest; var createEarlyHintsForRequest = process.env.TSS_DEV_SERVER === "true" ? () => void 0 : createEarlyHintsCollector; async function loadEntries() { const [routerEntry, startEntry, pluginAdapters] = await Promise.all([ import("#tanstack-router-entry"), import("#tanstack-start-entry"), import("#tanstack-start-plugin-adapters") ]); return { routerEntry, startEntry, pluginAdapters }; } function getEntries() { if (!entriesPromise) entriesPromise = loadEntries(); return entriesPromise; } function hasCsrfMiddleware(middlewares) { return middlewares.some((middleware) => csrfSymbol in middleware); } function warnMissingCsrfMiddlewareOnce() { if (hasWarnedMissingCsrfMiddleware) return; hasWarnedMissingCsrfMiddleware = true; console.warn(`TanStack Start server functions are not protected by the CSRF middleware. Server functions are same-origin RPC endpoints and should be protected from cross-site requests. Add the CSRF middleware in src/start.ts: const csrfMiddleware = createCsrfMiddleware({ filter: (ctx) => ctx.handlerType === 'serverFn', }) export const startInstance = createStart(() => ({ requestMiddleware: [csrfMiddleware], })) If you intentionally handle CSRF another way, disable this warning: tanstackStart({ serverFns: { disableCsrfMiddlewareWarning: true, }, })`); } var ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || "/"; var SERVER_FN_BASE = process.env.TSS_SERVER_FN_BASE; var IS_PRERENDERING = process.env.TSS_PRERENDERING === "true"; var IS_SHELL_ENV = process.env.TSS_SHELL === "true"; var IS_DEV = process.env.NODE_ENV === "development"; var ERR_NO_RESPONSE = IS_DEV ? `It looks like you forgot to return a response from your server route handler. If you want to defer to the app router, make sure to have a component set in this route.` : "Internal Server Error"; var ERR_NO_DEFER = IS_DEV ? `You cannot defer to the app router if there is no component defined on this route.` : "Internal Server Error"; function throwRouteHandlerError() { throw new Error(ERR_NO_RESPONSE); } function throwIfMayNotDefer() { throw new Error(ERR_NO_DEFER); } /** * Check if a value is a special response (Response or Redirect) */ function isSpecialResponse(value) { return value instanceof Response || isRedirect(value); } /** * Normalize middleware result to context shape */ function handleCtxResult(result) { if (isSsrResponse(result) || isSpecialResponse(result)) return { response: result }; return result; } /** * Execute a middleware chain */ async function executeMiddleware(middlewares, ctx) { let index = -1; let streamResponse; const setResponse = (response) => { if (isSsrResponse(response)) { if (response.serverSsrCleanup === "stream") streamResponse = response; ctx.response = response.response; return; } ctx.response = response; }; const disposeStreamResponse = async (reason) => { const response = streamResponse; if (!response) return; streamResponse = void 0; const currentResponse = ctx.response; if (currentResponse === response.response || currentResponse instanceof Response && response.response.body !== null && currentResponse.body === response.response.body) ctx.response = void 0; await response.dispose(reason); }; const getFinalResponse = async () => { const response = ctx.response; if (!response) throwRouteHandlerError(); if (!streamResponse) return response; if (response === streamResponse.response) return streamResponse; if (streamResponse.response.body !== null && response.body === streamResponse.response.body) return { ...streamResponse, response }; await disposeStreamResponse("middleware response replaced"); return response; }; const next = async (nextCtx) => { if (nextCtx) { if (nextCtx.context) ctx.context = safeObjectMerge(ctx.context, nextCtx.context); for (const key of Object.keys(nextCtx)) if (key === "response") setResponse(nextCtx.response); else if (key !== "context") ctx[key] = nextCtx[key]; } index++; const middleware = middlewares[index]; if (!middleware) return ctx; let result; try { result = await middleware({ ...ctx, next }); } catch (err) { if (isSpecialResponse(err)) { setResponse(err); return ctx; } await disposeStreamResponse("middleware error"); throw err; } const normalized = handleCtxResult(result); if (normalized) { if (normalized.response !== void 0) setResponse(normalized.response); if (normalized.context) ctx.context = safeObjectMerge(ctx.context, normalized.context); } return ctx; }; await next(); return { ctx, response: await getFinalResponse() }; } /** * Wrap a route handler as middleware */ function handlerToMiddleware(handler, mayDefer = false) { if (mayDefer) return handler; return async (ctx) => { const response = await handler({ ...ctx, next: throwIfMayNotDefer }); if (!response) throwRouteHandlerError(); return response; }; } /** * Creates the TanStack Start request handler. * * @example Backwards-compatible usage (handler callback only): * ```ts * export default createStartHandler(defaultStreamHandler) * ``` * * @example With CDN URL rewriting: * ```ts * export default createStartHandler({ * handler: defaultStreamHandler, * transformAssets: 'https://cdn.example.com', * }) * ``` * * @example With per-request URL rewriting: * ```ts * export default createStartHandler({ * handler: defaultStreamHandler, * transformAssets: { * transform: ({ url }) => { * const cdnBase = getRequest().headers.get('x-cdn-base') || '' * return { href: `${cdnBase}${url}` } * }, * cache: false, * }, * }) * ``` */ function createStartHandler(cbOrOptions) { const handlerOptions = typeof cbOrOptions === "function" ? {} : cbOrOptions; const cb = typeof cbOrOptions === "function" ? cbOrOptions : cbOrOptions.handler; const finalManifestResolver = createFinalManifestResolver({ ...handlerOptions, cacheCreateTransform: process.env.TSS_DEV_SERVER !== "true" }); const resolveManifestForRequest = process.env.TSS_DEV_SERVER === "true" ? finalManifestResolver.resolveUncached : finalManifestResolver.resolveCached; if (process.env.TSS_DEV_SERVER !== "true") finalManifestResolver.warmup({ getBaseManifest: () => getBaseManifest(void 0) }); const startRequestResolver = async (request, requestOpts) => { let router = null; let responseOwnsCleanup = false; try { const { url, handledProtocolRelativeURL } = getNormalizedURL(request.url); const href = url.pathname + url.search + url.hash; const origin = getOrigin(request); if (handledProtocolRelativeURL) return Response.redirect(url, 308); const entries = await getEntries(); const hasStartInstance = !!entries.startEntry.startInstance; const startOptions = await entries.startEntry.startInstance?.getOptions() || {}; const { hasPluginAdapters, pluginSerializationAdapters } = entries.pluginAdapters; const serializationAdapters = [ ...startOptions.serializationAdapters || [], ...hasPluginAdapters ? pluginSerializationAdapters : [], ServerFunctionSerializationAdapter ]; const requestStartOptions = { ...startOptions, requestMiddleware: hasStartInstance ? startOptions.requestMiddleware : [defaultCsrfMiddleware], serializationAdapters }; const flattenedRequestMiddlewares = requestStartOptions.requestMiddleware ? flattenMiddlewares(requestStartOptions.requestMiddleware) : []; const executedRequestMiddlewares = new Set(flattenedRequestMiddlewares); const getRouter = async () => { if (router) return router; router = await entries.routerEntry.getRouter(); let isShell = IS_SHELL_ENV; if (IS_PRERENDERING && !isShell) isShell = request.headers.get(HEADERS.TSS_SHELL) === "true"; const history = createMemoryHistory({ initialEntries: [href] }); router.update({ history, isShell, isPrerendering: IS_PRERENDERING, origin: router.options.origin ?? origin, defaultSsr: requestStartOptions.defaultSsr, serializationAdapters: [...requestStartOptions.serializationAdapters, ...router.options.serializationAdapters || []], basepath: ROUTER_BASEPATH }); return router; }; if (SERVER_FN_BASE && url.pathname.startsWith(SERVER_FN_BASE)) { if (process.env.NODE_ENV !== "production" && process.env.TSS_DISABLE_CSRF_MIDDLEWARE_WARNING !== "true" && !hasCsrfMiddleware(flattenedRequestMiddlewares)) warnMissingCsrfMiddlewareOnce(); const serverFnId = url.pathname.slice(SERVER_FN_BASE.length).split("/")[0]; if (!serverFnId) throw new Error("Invalid server action param for serverFnId"); const serverFnHandler = async ({ context }) => { return runWithStartContext({ getRouter, startOptions: requestStartOptions, contextAfterGlobalMiddlewares: context, request, executedRequestMiddlewares, handlerType: "serverFn" }, () => handleServerAction({ request, context: requestOpts?.context, serverFnId })); }; const { response: middlewareResponse } = await executeMiddleware([...flattenedRequestMiddlewares.map((d) => d.options.server), serverFnHandler], { request, pathname: url.pathname, handlerType: "serverFn", context: createNullProtoObject(requestOpts?.context) }); const result = await handleRedirectResponse(middlewareResponse, request, getRouter); responseOwnsCleanup = result.serverSsrCleanup === "stream"; return result.response; } const executeRouter = async (serverContext, matchedRoutes) => { const acceptParts = (request.headers.get("Accept") || "*/*").split(","); if (!["*/*", "text/html"].some((mimeType) => acceptParts.some((part) => part.trim().startsWith(mimeType)))) return normalizeSsrResponse(Response.json({ error: "Only HTML requests are supported here" }, { status: 500 })); const manifest = await resolveManifestForRequest({ request, requestInlineCss: requestOpts?.inlineCss, getBaseManifest: () => getBaseManifest(matchedRoutes) }); const earlyHints = createEarlyHintsForRequest({ onEarlyHints: requestOpts?.onEarlyHints, responseLinkHeader: requestOpts?.responseLinkHeader }); earlyHints?.collectStatic({ manifest, matchedRoutes }); const routerInstance = await getRouter(); attachRouterServerSsrUtils({ router: routerInstance, manifest, getRequestAssets: () => getStartContext({ throwIfNotFound: false })?.requestAssets }); routerInstance.update({ additionalContext: { serverContext } }); await routerInstance.load(); if (routerInstance.state.redirect) return normalizeSsrResponse(routerInstance.state.redirect); earlyHints?.collectDynamic(routerInstance.stores.matches.get()); const ctx = getStartContext({ throwIfNotFound: false }); await routerInstance.serverSsr.dehydrate({ requestAssets: ctx?.requestAssets }); const responseHeaders = getStartResponseHeaders({ router: routerInstance }); earlyHints?.appendResponseHeaders(responseHeaders); return normalizeSsrResponse(await cb({ request, router: routerInstance, responseHeaders })); }; const requestHandlerMiddleware = async ({ context }) => { return runWithStartContext({ getRouter, startOptions: requestStartOptions, contextAfterGlobalMiddlewares: context, request, executedRequestMiddlewares, handlerType: "router" }, async () => { try { return await handleServerRoutes({ getRouter, request, url, executeRouter, context, executedRequestMiddlewares }); } catch (err) { if (err instanceof Response) return err; throw err; } }); }; const { response: middlewareResponse } = await executeMiddleware([...flattenedRequestMiddlewares.map((d) => d.options.server), requestHandlerMiddleware], { request, pathname: url.pathname, handlerType: "router", context: createNullProtoObject(requestOpts?.context) }); const response = await handleRedirectResponse(middlewareResponse, request, getRouter); responseOwnsCleanup = response.serverSsrCleanup === "stream"; return response.response; } finally { if (router?.serverSsr && !responseOwnsCleanup) router.serverSsr.cleanup(); router = null; } }; return requestHandler(startRequestResolver); } async function handleRedirectResponse(response, request, getRouter) { const ssrResponse = normalizeSsrResponse(response); if (!isRedirect(ssrResponse.response)) return ssrResponse; if (isResolvedRedirect(ssrResponse.response)) { if (request.headers.get("x-tsr-serverFn") === "true") return replaceSsrResponse(ssrResponse, Response.json({ ...ssrResponse.response.options, isSerializedRedirect: true }, { headers: ssrResponse.response.headers }), "redirect response replaced"); return ssrResponse; } const opts = ssrResponse.response.options; if (opts.to && typeof opts.to === "string" && !opts.to.startsWith("/")) throw new Error(`Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(opts)}`); if ([ "params", "search", "hash" ].some((d) => typeof opts[d] === "function")) throw new Error(`Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(opts).filter((d) => typeof opts[d] === "function").map((d) => `"${d}"`).join(", ")}`); const redirect = (await getRouter()).resolveRedirect(ssrResponse.response); if (request.headers.get("x-tsr-serverFn") === "true") return replaceSsrResponse(ssrResponse, Response.json({ ...ssrResponse.response.options, isSerializedRedirect: true }, { headers: ssrResponse.response.headers }), "redirect response replaced"); return replaceSsrResponse(ssrResponse, redirect, "redirect response replaced"); } async function handleServerRoutes({ getRouter, request, url, executeRouter, context, executedRequestMiddlewares }) { const router = await getRouter(); const pathname = executeRewriteInput(router.rewrite, url).pathname; const { matchedRoutes, foundRoute, routeParams } = router.getMatchedRoutes(pathname); const isExactMatch = foundRoute && routeParams["**"] === void 0; const routeMiddlewares = []; for (const route of matchedRoutes) { const serverMiddleware = route.options.server?.middleware; if (serverMiddleware) { const flattened = flattenMiddlewares(serverMiddleware); for (const m of flattened) if (!executedRequestMiddlewares.has(m)) routeMiddlewares.push(m.options.server); } } const server = foundRoute?.options.server; let isHeadFallback = false; if (server?.handlers && isExactMatch) { const handlers = typeof server.handlers === "function" ? server.handlers({ createHandlers: (d) => d }) : server.handlers; const requestMethod = request.method.toUpperCase(); const handler = requestMethod === "HEAD" ? handlers["HEAD"] ?? handlers["GET"] ?? handlers["ANY"] : handlers[requestMethod] ?? handlers["ANY"]; isHeadFallback = requestMethod === "HEAD" && handler !== void 0 && !handlers["HEAD"]; if (handler) { const mayDefer = !!foundRoute.options.component; if (typeof handler === "function") routeMiddlewares.push(handlerToMiddleware(handler, mayDefer)); else { if (handler.middleware?.length) { const handlerMiddlewares = flattenMiddlewares(handler.middleware); for (const m of handlerMiddlewares) routeMiddlewares.push(m.options.server); } if (handler.handler) routeMiddlewares.push(handlerToMiddleware(handler.handler, mayDefer)); } } } routeMiddlewares.push(((ctx) => executeRouter(ctx.context, matchedRoutes))); const { ctx, response } = await executeMiddleware(routeMiddlewares, { request, context, params: routeParams, pathname, handlerType: "router" }); if (isHeadFallback) { if (!ctx.response) throwRouteHandlerError(); return stripSsrResponseBody(await handleRedirectResponse(response, request, getRouter), "HEAD body stripped"); } return normalizeSsrResponse(response); } //#endregion export { createStartHandler }; //# sourceMappingURL=createStartHandler.js.map