@tanstack/router-core
Version:
Modern and scalable routing for React applications
565 lines (564 loc) • 18.3 kB
JavaScript
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