UNPKG

@tanstack/router-core

Version:

Modern and scalable routing for React applications

565 lines (564 loc) 18.3 kB
const require_not_found = require("./not-found.cjs"); const require_root = require("./root.cjs"); const require_redirect = require("./redirect.cjs"); const require_load_client = require("./load-client.cjs"); const require_router = require("./router.cjs"); const require_await_signal = require("./await-signal.cjs"); //#region src/load-server.ts const SUCCESS = 0; const ERROR = 1; const NOT_FOUND = 2; const REDIRECTED = 3; const SKIPPED = 4; function getRoute(router, match) { return router.routesById[match.routeId]; } function normalize(value, rejected) { if (require_redirect.isRedirect(value)) return [REDIRECTED, value]; if (require_not_found.isNotFound(value)) return [NOT_FOUND, value]; if (rejected && typeof value?.then === "function") value = new Error("A Promise was thrown", { cause: value }); return rejected ? [ERROR, value] : [SUCCESS, value]; } function normalizeError(route, cause) { let outcome = normalize(cause, true); if (outcome[0] !== ERROR) return outcome; try { route.options.onError?.(outcome[1]); } catch (onErrorCause) { outcome = normalize(onErrorCause, true); } return outcome; } function maybe(value, cause) { if (cause !== void 0) return { status: "error", error: cause }; return { status: "success", value }; } function navigateFrom(router, location) { return (options) => router.navigate({ ...options, _fromLocation: location }); } function waitFor(value, signal) { return signal ? require_await_signal.waitForReason(value, signal) : value; } async function resolveSsr(router, lane, index) { const match = lane.matches[index]; const route = getRoute(router, match); const parentSsr = lane.matches[index - 1]?.ssr; if (router.isShell()) return route.id === require_root.rootRouteId; if (parentSsr === false) return false; const inherit = (value) => { return value === true && parentSsr === "data-only" ? "data-only" : value; }; const defaultSsr = router.options.defaultSsr ?? true; const inheritedDefault = inherit(defaultSsr); match.ssr = inheritedDefault; const option = route.options.ssr; if (option === void 0) return inheritedDefault; if (typeof option !== "function") return inherit(option); return inherit(await option({ search: maybe(match.search, match.searchError), params: maybe(match.params, match.paramsError), location: lane.location, matches: lane.matches.map((candidate) => ({ index: candidate.index, pathname: candidate.pathname, fullPath: candidate.fullPath, staticData: candidate.staticData, id: candidate.id, routeId: candidate.routeId, search: maybe(candidate.search, candidate.searchError), params: maybe(candidate.params, candidate.paramsError), ssr: candidate.ssr })) }) ?? defaultSsr); } function stampNotFound(match, outcome) { if (outcome[0] === NOT_FOUND && !outcome[1].routeId) outcome[1].routeId = match.routeId; return outcome; } async function contextualize(router, lane, signal) { const globalBoundary = lane.matches.findIndex((match) => match._notFound); let end = globalBoundary < 0 ? lane.matches.length : globalBoundary + 1; let failure; let parentContext = { ...router.options.context ?? {} }; for (let index = 0; index < end; index++) { const match = lane.matches[index]; const route = getRoute(router, match); try { match.ssr = await resolveSsr(router, lane, index); } catch (cause) { signal?.throwIfAborted(); failure = [index, stampNotFound(match, normalizeError(route, cause))]; end = index; } signal?.throwIfAborted(); if (failure?.[1][0] === REDIRECTED) break; match.__beforeLoadContext = void 0; let context = parentContext; try { let routeContext; if (route.options.context) { const routeContextOptions = { deps: match.loaderDeps, params: match.params, context: parentContext, location: lane.location, navigate: navigateFrom(router, lane.location), buildLocation: router.buildLocation, cause: match.cause, abortController: match.abortController, preload: false, matches: lane.matches, routeId: route.id }; routeContext = route.options.context(routeContextOptions) ?? void 0; } context = { ...parentContext, ...routeContext }; match.context = context; } catch (cause) { signal?.throwIfAborted(); if (!failure) failure = [index, stampNotFound(match, normalizeError(route, cause))]; end = index; break; } signal?.throwIfAborted(); if (failure) break; const validationError = match.paramsError ?? match.searchError; if (validationError !== void 0) { failure = [index, stampNotFound(match, normalizeError(route, validationError))]; end = index; break; } signal?.throwIfAborted(); if (match.ssr === false || !route.options.beforeLoad) { parentContext = context; continue; } const abortController = match.abortController; const options = { search: match.search, abortController, params: match.params, preload: false, context, location: lane.location, navigate: navigateFrom(router, lane.location), buildLocation: router.buildLocation, cause: match.cause, matches: lane.matches, routeId: route.id, ...router.options.additionalContext }; try { const beforeLoadContext = await route.options.beforeLoad(options); signal?.throwIfAborted(); const outcome = stampNotFound(match, normalize(beforeLoadContext, false)); if (outcome[0] !== SUCCESS) { failure = [index, outcome]; end = index; break; } match.__beforeLoadContext = beforeLoadContext; match.context = { ...context, ...beforeLoadContext }; parentContext = match.context; } catch (cause) { signal?.throwIfAborted(); failure = [index, stampNotFound(match, normalizeError(route, cause))]; end = index; break; } } return { location: lane.location, matches: lane.matches, end, failure }; } function getLoaderContext(router, lane, match, route, index, tasks) { return { params: match.params, deps: match.loaderDeps, preload: false, parentMatchPromise: tasks[index - 1]?.match, abortController: match.abortController, context: match.context, location: lane.location, navigate: navigateFrom(router, lane.location), cause: match.cause, route, ...router.options.additionalContext }; } function createLoaderTask(router, lane, index, tasks, signal) { const match = lane.matches[index]; const route = getRoute(router, match); let outcome; if (match.ssr === false) outcome = Promise.resolve([SKIPPED]); else { const routeLoader = route.options.loader; const loader = typeof routeLoader === "function" ? routeLoader : routeLoader?.handler; if (!loader) outcome = Promise.resolve([SUCCESS, void 0]); else outcome = Promise.resolve().then(() => loader(getLoaderContext(router, lane, match, route, index, tasks))).then((result) => normalize(result, false), (cause) => normalize(cause, true)).then((result) => { if (result[0] !== REDIRECTED && (signal?.aborted || match.abortController.signal.reason === lane)) return [SKIPPED]; if (result[0] === ERROR) result = normalizeError(route, result[1]); return stampNotFound(match, result); }); } const parentMatch = outcome.then((result) => { const snapshot = { ...match }; if (result[0] === SUCCESS) { snapshot.loaderData = result[1]; snapshot.status = "success"; snapshot.error = void 0; snapshot.invalid = false; snapshot.isFetching = false; } else if (result[0] === ERROR) { snapshot.status = "error"; snapshot.error = result[1]; } else if (result[0] === NOT_FOUND) { snapshot.status = "notFound"; snapshot.error = result[1]; } return snapshot; }); return { index, outcome, match: parentMatch }; } async function getNotFoundBoundary(router, matches, indexed, signal, fallback = 0) { const cause = indexed?.[1][1]; let index = cause?.routeId ? matches.findIndex((match) => match.routeId === cause.routeId) : indexed?.[0] ?? matches.length - 1; if (index < 0) index = 0; for (let candidate = index; candidate >= 0; candidate--) { const route = getRoute(router, matches[candidate]); const loading = require_load_client.loadRouteChunk(route, false); if (loading) try { await loading; } catch { signal?.throwIfAborted(); } signal?.throwIfAborted(); if (route.options.notFoundComponent) return candidate; } return cause?.routeId ? index : fallback; } function abortMatches(matches, start = 0, reason) { for (let index = start; index < matches.length; index++) matches[index].abortController.abort(reason); } function resolveServerRedirect(router, location, value) { value.options._fromLocation = location; return { type: "redirect", redirect: router.resolveRedirect(value) }; } async function applyFailure(router, lane, indexed, signal) { if (!indexed) { const boundary = lane.matches.findIndex((match) => match._notFound); if (boundary >= 0) { abortMatches(lane.matches, boundary + 1); return { status: 404, boundary, kind: NOT_FOUND }; } return { status: 200 }; } const [index, outcome] = indexed; if (outcome[0] === ERROR) { const match = lane.matches[index]; match._notFound = void 0; match.status = "error"; match.error = outcome[1]; match.isFetching = false; abortMatches(lane.matches, index + 1); return { status: 500, boundary: index, kind: ERROR }; } const boundary = indexed[2] ?? await getNotFoundBoundary(router, lane.matches, indexed, signal); const match = lane.matches[boundary]; const cause = outcome[1]; cause.routeId = match.routeId; match._notFound = void 0; if (match.routeId === router.routeTree.id) { match.status = "success"; match._notFound = true; match.error = cause; } else { match.status = "notFound"; match.error = cause; } match.isFetching = false; abortMatches(lane.matches, boundary + 1); return { status: 404, boundary, kind: NOT_FOUND }; } async function loadNormalChunks(router, lane, end, signal) { const chunks = []; for (let index = 0; index < lane.matches.length; index++) { const match = lane.matches[index]; if (index >= end || match.ssr !== true || match.status !== "success") continue; const route = getRoute(router, match); try { const loading = require_load_client.loadRouteChunk(route); if (loading) { const chunk = loading.then(() => { signal?.throwIfAborted(); }, (cause) => { signal?.throwIfAborted(); return [index, stampNotFound(match, normalizeError(route, cause))]; }); chunk.catch(() => {}); chunks.push(chunk); } } catch (cause) { signal?.throwIfAborted(); chunks.push([index, stampNotFound(match, normalizeError(route, cause))]); } } for (const chunk of chunks) { const indexed = Array.isArray(chunk) ? chunk : await chunk; if (indexed) return indexed; } } async function projectLane(router, lane, signal) { for (const match of lane.matches) { const routeOptions = getRoute(router, match).options; if (routeOptions.head || routeOptions.scripts || routeOptions.headers) { const context = { ssr: router.options.ssr, matches: lane.matches, match, params: match.params, loaderData: match.loaderData }; try { const [head, scripts, headers] = await Promise.all([ routeOptions.head?.(context), routeOptions.scripts?.(context), routeOptions.headers?.(context) ]); signal?.throwIfAborted(); match.meta = head?.meta; match.links = head?.links; match.headScripts = head?.scripts; match.styles = head?.styles; match.scripts = scripts; match.headers = headers; } catch (cause) { signal?.throwIfAborted(); console.error(cause); } } if (match.ssr === false || match.status !== "success" || match._notFound) break; } } async function executeServerLane(router, location, matchedMatches, signal) { const matched = { location, matches: matchedMatches.map((match) => ({ ...match, __beforeLoadContext: void 0, context: {}, isFetching: false, abortController: new AbortController() })) }; const abortLane = () => abortMatches(matched.matches, 0, signal?.reason); if (signal?.aborted) { abortLane(); signal.throwIfAborted(); } signal?.addEventListener("abort", abortLane, { once: true }); try { const plannedGlobalBoundary = matched.matches.findIndex((match) => match._notFound); if (router.options.notFoundMode !== "root" && plannedGlobalBoundary >= 0) { const boundary = await getNotFoundBoundary(router, matched.matches, void 0, signal, plannedGlobalBoundary); if (boundary !== plannedGlobalBoundary) { matched.matches[plannedGlobalBoundary]._notFound = void 0; matched.matches[boundary]._notFound = true; } } const lane = await contextualize(router, matched, signal); signal?.throwIfAborted(); let loaderEnd = lane.end; if (lane.failure?.[1][0] === REDIRECTED) loaderEnd = 0; else if (lane.failure?.[1][0] === NOT_FOUND) { lane.failure[2] = await getNotFoundBoundary(router, lane.matches, lane.failure, signal); loaderEnd = Math.min(loaderEnd, lane.failure[2] + 1); } const tasks = []; for (let index = 0; index < loaderEnd; index++) { const task = createLoaderTask(router, lane, index, tasks, signal); tasks.push(task); } let loaderFailure; let control = lane.failure?.[1][0] === REDIRECTED ? lane.failure : void 0; try { await Promise.all(tasks.map((task) => task.outcome.then((loadedOutcome) => { const match = lane.matches[task.index]; const outcome = loadedOutcome; if (outcome[0] === SUCCESS) { match.loaderData = outcome[1]; match.status = "success"; match.error = void 0; match.invalid = false; match.isFetching = false; match.updatedAt = Date.now(); } else if (outcome[0] === REDIRECTED) { control = [task.index, outcome]; throw control; } else { if (match.ssr !== false) { match.status = "success"; match.error = void 0; match.invalid = true; match.isFetching = false; } if (!loaderFailure && outcome[0] !== SKIPPED) loaderFailure = [task.index, outcome]; } }))); } catch (cause) { if (!Array.isArray(cause)) throw cause; control = cause; } signal?.throwIfAborted(); if (control?.[1][0] === REDIRECTED) { abortMatches(lane.matches, 0, lane); return resolveServerRedirect(router, location, control[1][1]); } let failure = lane.failure ?? loaderFailure; const plannedBoundary = lane.matches.findIndex((match) => match._notFound); let readinessEnd; if (failure) { const outcomeEnd = failure[2] ??= failure[1][0] === NOT_FOUND ? await getNotFoundBoundary(router, lane.matches, failure, signal) : failure[0]; for (const task of tasks) { if (task.index >= outcomeEnd) break; const outcome = await task.outcome; if (outcome[0] !== SUCCESS && outcome[0] < REDIRECTED && !("loaderData" in lane.matches[task.index])) { failure = [task.index, outcome]; failure[2] = outcome[0] === NOT_FOUND ? await getNotFoundBoundary(router, lane.matches, failure, signal) : task.index; break; } } readinessEnd = failure[2]; } else readinessEnd = plannedBoundary < 0 ? lane.matches.length : plannedBoundary; const requiredFailure = await loadNormalChunks(router, lane, readinessEnd, signal); signal?.throwIfAborted(); if (requiredFailure) { if (requiredFailure[1][0] === REDIRECTED) { abortMatches(lane.matches); return resolveServerRedirect(router, location, requiredFailure[1][1]); } failure = requiredFailure; } const terminal = await applyFailure(router, lane, failure, signal); if (terminal.boundary !== void 0) { const match = lane.matches[terminal.boundary]; if (match.ssr === true) { const route = getRoute(router, match); try { if (terminal.kind === ERROR) await require_load_client.loadRouteChunk(route, "errorComponent"); else if (match._notFound) await Promise.all([require_load_client.loadRouteChunk(route), require_load_client.loadRouteChunk(route, "notFoundComponent")]); else await require_load_client.loadRouteChunk(route, "notFoundComponent"); } catch {} signal?.throwIfAborted(); } } signal?.throwIfAborted(); await projectLane(router, { location: lane.location, matches: lane.matches }, signal); signal?.throwIfAborted(); router.serverSsr?.onCleanup(abortLane); return { type: "render", status: terminal.status, matches: lane.matches }; } finally { signal?.removeEventListener("abort", abortLane); } } async function loadServerRoute(router, opts) { router.updateLatestLocation(); const next = router.latestLocation; const previous = router._committed; let result; try { const canonical = router.buildLocation({ to: next.pathname, search: true, params: true, hash: true, state: true, _includeValidateSearch: true }); if (next.publicHref !== canonical.publicHref) { const href = canonical.publicHref || "/"; throw canonical.external ? require_redirect.redirect({ href }) : require_redirect.redirect({ href, _builtLocation: canonical }); } const changeInfo = require_router.getLocationChangeInfo(next, router.stores.resolvedLocation.get()); router.emit({ type: "onBeforeNavigate", ...changeInfo }); router.emit({ type: "onBeforeLoad", ...changeInfo }); opts?._signal?.throwIfAborted(); result = await waitFor(executeServerLane(router, next, router.matchRoutes(next), opts?._signal), opts?._signal); opts?._signal?.throwIfAborted(); } catch (cause) { opts?._signal?.throwIfAborted(); if (!require_redirect.isRedirect(cause)) throw cause; result = resolveServerRedirect(router, next, cause); } router._serverResult = result; router.batch(() => { router.stores.location.set(next); router.stores.status.set("idle"); if (result.type === "render") { router.stores.setMatches(result.matches); router.stores.resolvedLocation.set(next); } }); if (result.type === "render") { router._committed = result.matches; require_router.runRouteLifecycle(router, previous, result.matches); } router._commitPromise?.resolve(); router._commitPromise = void 0; } //#endregion exports.loadServerRoute = loadServerRoute; //# sourceMappingURL=load-server.cjs.map