UNPKG

@tanstack/start-server-core

Version:

Modern and scalable routing for React applications

377 lines (376 loc) 15.1 kB
import { requestHandler } from "./request-response.js"; import { getStartManifest } from "./router-manifest.js"; import { handleServerAction } from "./server-functions-handler.js"; import { buildManifestWithClientEntry, resolveTransformConfig, transformManifestUrls } from "./transformAssetUrls.js"; import { HEADERS } from "./constants.js"; import { ServerFunctionSerializationAdapter } from "./serializer/ServerFunctionSerializationAdapter.js"; import { createMemoryHistory } from "@tanstack/history"; import { createNullProtoObject, flattenMiddlewares, mergeHeaders, safeObjectMerge } from "@tanstack/start-client-core"; import { executeRewriteInput, isRedirect, isResolvedRedirect } from "@tanstack/router-core"; import { attachRouterServerSsrUtils, getNormalizedURL, getOrigin } from "@tanstack/router-core/ssr/server"; import { runWithStartContext } from "@tanstack/start-storage-context"; //#region src/createStartHandler.ts function getStartResponseHeaders(opts) { return mergeHeaders({ "Content-Type": "text/html; charset=utf-8" }, ...opts.router.state.matches.map((match) => { return match.headers; })); } var entriesPromise; var baseManifestPromise; /** * Cached final manifest (with client entry script tag). In production, * this is computed once and reused for every request when caching is enabled. */ var cachedFinalManifestPromise; async function loadEntries() { const routerEntry = await import("#tanstack-router-entry"); return { startEntry: await import("#tanstack-start-entry"), routerEntry }; } function getEntries() { if (!entriesPromise) entriesPromise = loadEntries(); return entriesPromise; } /** * Returns the raw manifest data (without client entry script tag baked in). * In dev mode, always returns fresh data. In prod, cached. */ function getBaseManifest(matchedRoutes) { if (process.env.TSS_DEV_SERVER === "true") return getStartManifest(matchedRoutes); if (!baseManifestPromise) baseManifestPromise = getStartManifest(); return baseManifestPromise; } /** * Resolves a final Manifest for a given request. * * - No transform: builds client entry script tag and returns (cached in prod). * - Cached transform: transforms all URLs + builds script tag, caches result. * - Per-request transform: deep-clones base manifest, transforms per-request. */ async function resolveManifest(matchedRoutes, transformFn, cache) { const base = await getBaseManifest(matchedRoutes); const computeFinalManifest = async () => { return transformFn ? await transformManifestUrls(base, transformFn, { clone: !cache }) : buildManifestWithClientEntry(base); }; if (process.env.TSS_DEV_SERVER === "true") return computeFinalManifest(); if (!transformFn || cache) { if (!cachedFinalManifestPromise) cachedFinalManifestPromise = computeFinalManifest(); return cachedFinalManifestPromise; } return computeFinalManifest(); } 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 (isSpecialResponse(result)) return { response: result }; return result; } /** * Execute a middleware chain */ function executeMiddleware(middlewares, ctx) { let index = -1; 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 !== "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)) { ctx.response = err; return ctx; } throw err; } const normalized = handleCtxResult(result); if (normalized) { if (normalized.response !== void 0) ctx.response = normalized.response; if (normalized.context) ctx.context = safeObjectMerge(ctx.context, normalized.context); } return ctx; }; return next(); } /** * 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, * transformAssetUrls: 'https://cdn.example.com', * }) * ``` * * @example With per-request URL rewriting: * ```ts * export default createStartHandler({ * handler: defaultStreamHandler, * transformAssetUrls: { * transform: ({ url }) => { * const cdnBase = getRequest().headers.get('x-cdn-base') || '' * return `${cdnBase}${url}` * }, * cache: false, * }, * }) * ``` */ function createStartHandler(cbOrOptions) { const cb = typeof cbOrOptions === "function" ? cbOrOptions : cbOrOptions.handler; const transformAssetUrlsOption = typeof cbOrOptions === "function" ? void 0 : cbOrOptions.transformAssetUrls; const warmupTransformManifest = !!transformAssetUrlsOption && typeof transformAssetUrlsOption === "object" && transformAssetUrlsOption.warmup === true; const resolvedTransformConfig = transformAssetUrlsOption ? resolveTransformConfig(transformAssetUrlsOption) : void 0; const cache = resolvedTransformConfig ? resolvedTransformConfig.cache : true; let cachedCreateTransformPromise; const getTransformFn = async (opts) => { if (!resolvedTransformConfig) return void 0; if (resolvedTransformConfig.type === "createTransform") { if (cache) { if (!cachedCreateTransformPromise) cachedCreateTransformPromise = Promise.resolve(resolvedTransformConfig.createTransform(opts)); return cachedCreateTransformPromise; } return resolvedTransformConfig.createTransform(opts); } return resolvedTransformConfig.transformFn; }; if (warmupTransformManifest && cache && process.env.TSS_DEV_SERVER !== "true" && !cachedFinalManifestPromise) { const warmupPromise = (async () => { const base = await getBaseManifest(void 0); const transformFn = await getTransformFn({ warmup: true }); return transformFn ? await transformManifestUrls(base, transformFn, { clone: false }) : buildManifestWithClientEntry(base); })(); cachedFinalManifestPromise = warmupPromise; warmupPromise.catch(() => { if (cachedFinalManifestPromise === warmupPromise) cachedFinalManifestPromise = void 0; cachedCreateTransformPromise = void 0; }); } const startRequestResolver = async (request, requestOpts) => { let router = null; let cbWillCleanup = 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 startOptions = await entries.startEntry.startInstance?.getOptions() || {}; const serializationAdapters = [...startOptions.serializationAdapters || [], ServerFunctionSerializationAdapter]; const requestStartOptions = { ...startOptions, serializationAdapters }; const flattenedRequestMiddlewares = startOptions.requestMiddleware ? flattenMiddlewares(startOptions.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)) { 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 }, () => handleServerAction({ request, context: requestOpts?.context, serverFnId })); }; return handleRedirectResponse((await executeMiddleware([...flattenedRequestMiddlewares.map((d) => d.options.server), serverFnHandler], { request, pathname: url.pathname, context: createNullProtoObject(requestOpts?.context) })).response, request, getRouter); } 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 Response.json({ error: "Only HTML requests are supported here" }, { status: 500 }); const manifest = await resolveManifest(matchedRoutes, await getTransformFn({ warmup: false, request }), cache); const routerInstance = await getRouter(); attachRouterServerSsrUtils({ router: routerInstance, manifest }); routerInstance.update({ additionalContext: { serverContext } }); await routerInstance.load(); if (routerInstance.state.redirect) return routerInstance.state.redirect; await routerInstance.serverSsr.dehydrate(); const responseHeaders = getStartResponseHeaders({ router: routerInstance }); cbWillCleanup = true; return cb({ request, router: routerInstance, responseHeaders }); }; const requestHandlerMiddleware = async ({ context }) => { return runWithStartContext({ getRouter, startOptions: requestStartOptions, contextAfterGlobalMiddlewares: context, request, executedRequestMiddlewares }, async () => { try { return await handleServerRoutes({ getRouter, request, url, executeRouter, context, executedRequestMiddlewares }); } catch (err) { if (err instanceof Response) return err; throw err; } }); }; return handleRedirectResponse((await executeMiddleware([...flattenedRequestMiddlewares.map((d) => d.options.server), requestHandlerMiddleware], { request, pathname: url.pathname, context: createNullProtoObject(requestOpts?.context) })).response, request, getRouter); } finally { if (router && !cbWillCleanup) router.serverSsr?.cleanup(); router = null; } }; return requestHandler(startRequestResolver); } async function handleRedirectResponse(response, request, getRouter) { if (!isRedirect(response)) return response; if (isResolvedRedirect(response)) { if (request.headers.get("x-tsr-serverFn") === "true") return Response.json({ ...response.options, isSerializedRedirect: true }, { headers: response.headers }); return response; } const opts = 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(response); if (request.headers.get("x-tsr-serverFn") === "true") return Response.json({ ...response.options, isSerializedRedirect: true }, { headers: response.headers }); return redirect; } 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; if (server?.handlers && isExactMatch) { const handlers = typeof server.handlers === "function" ? server.handlers({ createHandlers: (d) => d }) : server.handlers; const handler = handlers[request.method.toUpperCase()] ?? handlers["ANY"]; 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)); return (await executeMiddleware(routeMiddlewares, { request, context, params: routeParams, pathname })).response; } //#endregion export { createStartHandler }; //# sourceMappingURL=createStartHandler.js.map