Upgrade to React Router 8

This commit is contained in:
Kalle
2026-07-02 17:38:26 +03:00
parent 5239725777
commit 8f482802ba
47 changed files with 609 additions and 1156 deletions

View File

@@ -22,7 +22,7 @@ export type LoaderNotification = NonNullable<
export function useNotifications() {
const [root] = useMatches();
const notifications = (root.data as RootLoaderData | undefined)
const notifications = (root.loaderData as RootLoaderData | undefined)
?.notifications;
const unseenIds = React.useMemo(

View File

@@ -23,10 +23,9 @@ export async function wrapActionForApi(
} catch (e) {
if (e instanceof Response && e.status === 302) {
const location = e.headers.get("Location") ?? "";
if (location.includes("__error=")) {
const errorMsg = new URLSearchParams(location.replace("?", "")).get(
"__error",
);
const search = location.slice(location.indexOf("?") + 1);
const errorMsg = new URLSearchParams(search).get("__error");
if (errorMsg !== null) {
return new Response(JSON.stringify({ error: errorMsg }), {
status: 400,
headers: { "Content-Type": "application/json" },

View File

@@ -54,7 +54,7 @@ export const handle: SendouRouteHandle = {
};
export const meta: MetaFunction = (args) => {
const data = args.data as SerializeFrom<typeof loader> | null;
const data = args.loaderData as SerializeFrom<typeof loader> | null;
if (!data) return [];

View File

@@ -19,7 +19,7 @@ export { loader };
export const handle: SendouRouteHandle = {
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];
@@ -35,7 +35,7 @@ export const handle: SendouRouteHandle = {
export const meta: MetaFunction = (args) => {
invariant(args.params.slug);
const data = args.data as SerializeFrom<typeof loader> | null;
const data = args.loaderData as SerializeFrom<typeof loader> | null;
if (!data) return [];

View File

@@ -4,5 +4,5 @@ import type { RootLoaderData } from "~/root";
export function useUser() {
const [root] = useMatches();
return (root.data as RootLoaderData | undefined)?.user;
return (root.loaderData as RootLoaderData | undefined)?.user;
}

View File

@@ -18,7 +18,7 @@ export default function EditBadgePage() {
const isStaff = useHasRole("STAFF");
const matches = useMatches();
const parentMatch = matches.at(-2)!;
const data = parentMatch.data as BadgeDetailsLoaderData;
const data = parentMatch.loaderData as BadgeDetailsLoaderData;
const { badge } = useOutletContext<BadgeDetailsContext>();
const canManageBadge = useHasPermission(badge, "MANAGE");

View File

@@ -18,12 +18,12 @@ import { loader } from "../loaders/builds.$slug.popular.server";
export { loader };
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: `${args.data.weaponName} popular builds`,
ogTitle: `${args.data.weaponName} Splatoon 3 popular builds`,
description: `List of most popular ability combinations for ${args.data.weaponName}.`,
title: `${args.loaderData.weaponName} popular builds`,
ogTitle: `${args.loaderData.weaponName} Splatoon 3 popular builds`,
description: `List of most popular ability combinations for ${args.loaderData.weaponName}.`,
location: args.location,
});
};
@@ -31,7 +31,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["analyzer", "builds"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -21,12 +21,12 @@ import { MAX_AP } from "~/features/build-analyzer/analyzer-constants";
import styles from "./builds.$slug.stats.module.css";
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: `${args.data.weaponName} popular abilities`,
ogTitle: `${args.data.weaponName} Splatoon 3 popular abilities`,
description: `List of the most popular abilities for ${args.data.weaponName} in Splatoon 3.`,
title: `${args.loaderData.weaponName} popular abilities`,
ogTitle: `${args.loaderData.weaponName} Splatoon 3 popular abilities`,
description: `List of the most popular abilities for ${args.loaderData.weaponName} in Splatoon 3.`,
location: args.location,
});
};
@@ -34,7 +34,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["weapons", "builds", "analyzer"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -129,12 +129,12 @@ function filterKey(filter: ParsedFilter): string {
}
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: `${args.data.weaponName} builds`,
ogTitle: `${args.data.weaponName} Splatoon 3 builds`,
description: `Collection of ${args.data.weaponName} builds from the top competitive players. Find the best combination of abilities and level up your gameplay.`,
title: `${args.loaderData.weaponName} builds`,
ogTitle: `${args.loaderData.weaponName} Splatoon 3 builds`,
description: `Collection of ${args.loaderData.weaponName} builds from the top competitive players. Find the best combination of abilities and level up your gameplay.`,
location: args.location,
});
};
@@ -142,7 +142,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["weapons", "builds", "gear", "analyzer"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -41,7 +41,7 @@ import { loader } from "../loaders/calendar.$id.server";
export { action, loader };
export const meta: MetaFunction = (args) => {
const data = args.data as SerializeFrom<typeof loader>;
const data = args.loaderData as SerializeFrom<typeof loader>;
if (!data) return [];
@@ -57,7 +57,7 @@ export const meta: MetaFunction = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["calendar", "game-misc"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -34,12 +34,14 @@ import { loader } from "../loaders/calendar.new.server";
export { action, loader };
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
const what = args.data.isAddingTournament ? "tournament" : "calendar event";
const what = args.loaderData.isAddingTournament
? "tournament"
: "calendar event";
return metaTags({
title: args.data.eventToEdit ? `Editing ${what}` : `New ${what}`,
title: args.loaderData.eventToEdit ? `Editing ${what}` : `New ${what}`,
location: args.location,
});
};

View File

@@ -763,7 +763,7 @@ export function useCurrentRouteChatCodes(): string[] {
const matches = useMatches();
for (const match of matches) {
const matchData = match.data as
const matchData = match.loaderData as
| { chatCode?: string | string[] }
| undefined;
if (matchData?.chatCode) {

View File

@@ -45,7 +45,7 @@ export const handle: SendouRouteHandle = {
};
export const meta: MetaFunction = (args) => {
const data = args.data as SerializeFrom<typeof loader> | null;
const data = args.loaderData as SerializeFrom<typeof loader> | null;
if (!data) return [];

View File

@@ -23,7 +23,7 @@ export { loader };
export const handle: SendouRouteHandle = {
i18n: ["weapons", "common", "analyzer", "params"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];
return [
{
@@ -36,10 +36,10 @@ export const handle: SendouRouteHandle = {
};
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: `${args.data.weaponName} parameters`,
description: `${args.data.weaponName} parameters with version history compared across ${comparedAcross(args.data.kind)}.`,
title: `${args.loaderData.weaponName} parameters`,
description: `${args.loaderData.weaponName} parameters with version history compared across ${comparedAcross(args.loaderData.kind)}.`,
location: args.location,
});
};

View File

@@ -15,7 +15,7 @@ export default function PlusCommentModalPage() {
const user = useUser();
const matches = useMatches();
const params = useParams();
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
const data = matches.at(-2)!.loaderData as PlusSuggestionsLoaderData;
const targetUserId = Number(params.userId);
const tierSuggestedTo = Number(params.tier);

View File

@@ -15,7 +15,7 @@ export { action };
export default function PlusNewSuggestionModalPage() {
const user = useUser();
const matches = useMatches();
const data = matches.at(-2)!.data as PlusSuggestionsLoaderData;
const data = matches.at(-2)!.loaderData as PlusSuggestionsLoaderData;
const tierOptions = PLUS_TIERS.filter((tier) => {
// user will be redirected anyway

View File

@@ -14,7 +14,7 @@ import { loader } from "../loaders/q.match.$id.server";
export { action, loader };
export const meta: MetaFunction = (args) => {
const data = args.data as SerializeFrom<typeof loader> | null;
const data = args.loaderData as SerializeFrom<typeof loader> | null;
if (!data) return [];

View File

@@ -48,7 +48,7 @@ function ThemeSelector() {
function CustomColorSelector() {
const [root] = useMatches();
const rootData = root.data as RootLoaderData | undefined;
const rootData = root.loaderData as RootLoaderData | undefined;
const isSupporter = useHasRole("SUPPORTER");
const fetcher = useFetcher();

View File

@@ -11,7 +11,7 @@ export function TeamGoBackButton() {
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as TeamLoaderData;
const layoutData = parentRoute.loaderData as TeamLoaderData;
return (
<div className="stack">

View File

@@ -45,7 +45,7 @@ export default function TeamIndexPage() {
const { t } = useTranslation(["team"]);
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as TeamLoaderData;
const layoutData = parentRoute.loaderData as TeamLoaderData;
const members = layoutData.team.members;
const playerMembers = members.filter(
@@ -110,7 +110,7 @@ function ActionButtons() {
const isAdmin = useHasRole("ADMIN");
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as TeamLoaderData;
const layoutData = parentRoute.loaderData as TeamLoaderData;
const team = layoutData.team;
if (!isTeamMember({ user, team }) && !isAdmin) {

View File

@@ -15,15 +15,15 @@ export { loader };
import styles from "../team.module.css";
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: args.data.team.name,
description: args.data.team.bio ?? undefined,
title: args.loaderData.team.name,
description: args.loaderData.team.bio ?? undefined,
location: args.location,
image: args.data.team.avatarUrl
image: args.loaderData.team.avatarUrl
? {
url: args.data.team.avatarUrl,
url: args.loaderData.team.avatarUrl,
dimensions: {
width: 124,
height: 124,
@@ -36,7 +36,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["team"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -22,7 +22,7 @@ export { action, loader };
export const handle: SendouRouteHandle = {
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];
@@ -44,16 +44,16 @@ export const handle: SendouRouteHandle = {
};
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
const aliasesStr =
args.data.names.aliases.length > 0
? ` (Aliases: ${args.data.names.aliases.join(", ")})`
args.loaderData.names.aliases.length > 0
? ` (Aliases: ${args.loaderData.names.aliases.join(", ")})`
: "";
return metaTags({
title: `${args.data.names.primary} X Battle Top 500 Placements`,
description: `Splatoon 3 X Battle results for the player ${args.data.names.primary}${aliasesStr}`,
title: `${args.loaderData.names.primary} X Battle Top 500 Placements`,
description: `Splatoon 3 X Battle results for the player ${args.loaderData.names.primary}${aliasesStr}`,
location: args.location,
});
};

View File

@@ -55,15 +55,15 @@ import { updateIsEstablishedSchema } from "../tournament-organization-schemas";
export { action, loader };
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: args.data.organization.name,
title: args.loaderData.organization.name,
location: args.location,
description: args.data.organization.description ?? undefined,
image: args.data.organization.avatarUrl
description: args.loaderData.organization.description ?? undefined,
image: args.loaderData.organization.avatarUrl
? {
url: args.data.organization.avatarUrl,
url: args.loaderData.organization.avatarUrl,
dimensions: { width: 124, height: 124 },
}
: undefined,
@@ -73,7 +73,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["badges", "org"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];

View File

@@ -27,15 +27,14 @@ import styles from "./to.$id.info.module.css";
export { action, loader };
export const meta: MetaFunction<typeof loader> = (args) => {
const tournamentData = JSON.parse(args.matches[1].data as any)?.tournament as
| TournamentData
| undefined;
const tournamentData = JSON.parse(args.matches[1].loaderData as any)
?.tournament as TournamentData | undefined;
if (!tournamentData) return [];
return metaTags({
title: tournamentData.ctx.name,
description: args.data?.description
? removeMarkdown(args.data.description)
description: args.loaderData?.description
? removeMarkdown(args.loaderData.description)
: undefined,
image: {
url: tournamentData.ctx.logoUrl,

View File

@@ -29,12 +29,12 @@ import { useTournament } from "./to.$id";
export { loader };
export const meta: MetaFunction<typeof loader> = (args) => {
const tournamentData = JSON.parse(args.matches[1].data as any)
const tournamentData = JSON.parse(args.matches[1].loaderData as any)
?.tournament as TournamentData;
if (!args.data || !tournamentData) return [];
if (!args.loaderData || !tournamentData) return [];
const team = tournamentData.ctx.teams.find(
(t) => t.id === args.data!.tournamentTeamId,
(t) => t.id === args.loaderData!.tournamentTeamId,
)!;
const teamLogoUrl = team.team?.logoUrl ?? team.pickupAvatarUrl;

View File

@@ -31,7 +31,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
};
export const meta: MetaFunction = (args) => {
const rawData = args.data as string | undefined;
const rawData = args.loaderData as string | undefined;
if (!rawData) return [];
@@ -51,7 +51,7 @@ export const meta: MetaFunction = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["tournament", "calendar"],
breadcrumb: ({ match }) => {
const rawData = match.data as string | undefined;
const rawData = match.loaderData as string | undefined;
if (!rawData) return [];

View File

@@ -20,7 +20,7 @@ export { action, loader };
export default function UserAdminPage() {
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
return (
<div className="stack xl">

View File

@@ -35,7 +35,7 @@ export default function UserArtPage() {
});
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const hasBothArtMadeByAndMadeOf =
data.arts.some((a) => a.author) && data.arts.some((a) => !a.author);

View File

@@ -20,7 +20,7 @@ export default function NewBuildPage() {
const { defaultValues, gearIdToAbilities } = useLoaderData<typeof loader>();
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const { t } = useTranslation(["builds"]);
if (layoutData.user.buildsCount >= BUILD.MAX_COUNT) {

View File

@@ -39,7 +39,7 @@ type BuildFilter = "ALL" | "PUBLIC" | "PRIVATE" | MainWeaponId;
export default function UserBuildsPage() {
const { t } = useTranslation(["builds", "user"]);
const user = useUser();
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
const data = useLoaderData<typeof loader>();
const [weaponFilter, setWeaponFilter] = useSearchParamState<BuildFilter>({
defaultValue: "ALL",
@@ -122,7 +122,7 @@ function BuildsFilters({
const { t } = useTranslation(["weapons", "builds"]);
const data = useLoaderData<typeof loader>();
const user = useUser();
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
if (data.builds.length === 0) return null;

View File

@@ -27,7 +27,7 @@ export default function UserEditPage() {
const { t } = useTranslation(["common", "user"]);
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const data = useLoaderData<typeof loader>();
const isSupporter = useHasRole("SUPPORTER");
const isArtist = useHasRole("ARTIST");

View File

@@ -73,7 +73,7 @@ function NewUserInfoPage() {
const user = useUser();
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const { navItems } = useOutletContext<{ navItems: UserPageNavItem[] }>();
if (data.type !== "new") {
@@ -171,7 +171,7 @@ export function OldUserInfoPage() {
const data = useLoaderData<typeof loader>();
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
if (data.type !== "old") {
throw new Error("Expected old user data");

View File

@@ -25,7 +25,7 @@ export default function UserResultsPage() {
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const [searchParams, setSearchParams] = useSearchParams();
const showAll = searchParams.get("all") === "true";

View File

@@ -78,7 +78,7 @@ export default function UserSeasonsPage() {
const data = useLoaderData<typeof loader>();
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
if (!data) {
return (
@@ -325,7 +325,7 @@ function Rank({
const { t } = useTranslation(["user"]);
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const maxOrdinal = Math.max(...skills.map((s) => s.ordinal));
@@ -469,7 +469,7 @@ function Stages({
stages: NonNullable<UserSeasonsPageLoaderData["info"]["stages"]>;
}) {
const { t } = useTranslation(["user", "game-misc"]);
const layoutData = useMatches().at(-2)!.data as UserPageLoaderData;
const layoutData = useMatches().at(-2)!.loaderData as UserPageLoaderData;
return (
<div className="stack horizontal justify-center md flex-wrap">
@@ -804,7 +804,7 @@ function Results({
function GroupMatchResult({ match }: { match: SeasonGroupMatch }) {
const [, parentRoute] = useMatches();
invariant(parentRoute);
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const userId = layoutData.user.id;
// score when match has not yet been played or was canceled

View File

@@ -30,11 +30,11 @@ export { loader };
import "~/features/user-page/user-page.module.css";
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: args.data.user.username,
description: `${args.data.user.username}'s profile on sendou.ink including builds, tournament results, art and more.`,
title: args.loaderData.user.username,
description: `${args.loaderData.user.username}'s profile on sendou.ink including builds, tournament results, art and more.`,
location: args.location,
});
};
@@ -42,7 +42,7 @@ export const meta: MetaFunction<typeof loader> = (args) => {
export const handle: SendouRouteHandle = {
i18n: ["user", "badges", "game-badges"],
breadcrumb: ({ match }) => {
const data = match.data as UserPageLoaderData | undefined;
const data = match.loaderData as UserPageLoaderData | undefined;
if (!data) return [];
@@ -86,7 +86,9 @@ export default function UserPageLayout() {
const allResultsCount =
data.user.calendarEventResultsCount + data.user.tournamentResultsCount;
const isNewUserPage = matches.some((m) => (m.data as any)?.type === "new");
const isNewUserPage = matches.some(
(m) => (m.loaderData as any)?.type === "new",
);
const navItems: UserPageNavItem[] = [
{

View File

@@ -19,7 +19,7 @@ export default function UserVodsPage() {
const [, parentRoute] = useMatches();
invariant(parentRoute);
const data = useLoaderData<typeof loader>();
const layoutData = parentRoute.data as UserPageLoaderData;
const layoutData = parentRoute.loaderData as UserPageLoaderData;
const [, setSearchParams] = useSearchParams();
const setPage = (page: number) => {

View File

@@ -42,7 +42,7 @@ export { action, loader };
export const handle: SendouRouteHandle = {
i18n: ["vods"],
breadcrumb: ({ match }) => {
const data = match.data as SerializeFrom<typeof loader> | undefined;
const data = match.loaderData as SerializeFrom<typeof loader> | undefined;
if (!data) return [];
@@ -62,10 +62,10 @@ export const handle: SendouRouteHandle = {
};
export const meta: MetaFunction<typeof loader> = (args) => {
if (!args.data) return [];
if (!args.loaderData) return [];
return metaTags({
title: args.data.vod.title,
title: args.loaderData.vod.title,
description:
"Splatoon 3 VoD with timestamps to check out specific weapons as well as map and mode combinations.",
location: args.location,

View File

@@ -61,9 +61,11 @@ import { isSupporter } from "./modules/permissions/utils";
import { IS_E2E_TEST_RUN } from "./utils/e2e";
import { allI18nNamespaces } from "./utils/i18n";
import { isRevalidation, metaTags, type SerializeFrom } from "./utils/remix";
import { requestContextMiddleware } from "./utils/request-context-middleware.server";
import { APP_ICON_URL, pwaSplashScreenImageUrl } from "./utils/urls";
export const middleware: Route.MiddlewareFunction[] = [
requestContextMiddleware,
sessionIdMiddleware,
userMiddleware,
];
@@ -334,7 +336,7 @@ function useCustomThemeVars() {
const styles: Map<string, number> = new Map();
for (const match of matches) {
const data = match.data as { customTheme?: CustomTheme } | undefined;
const data = match.loaderData as { customTheme?: CustomTheme } | undefined;
if (data?.customTheme) {
for (const [key, value] of Object.entries(data.customTheme)) {

View File

@@ -8,6 +8,7 @@ import type { z } from "zod";
import type { navItems } from "~/components/layout/nav-items";
import { ServerConfig } from "~/config.server";
import { logger } from "./logger";
import { currentRequestPathname } from "./request-context.server";
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 404 });
@@ -226,7 +227,7 @@ export function canAccessLohiEndpoint(request: Request) {
}
function errorToastRedirect(message: string) {
return redirect(`?__error=${message}`);
return redirect(`${currentRequestPathname() ?? ""}?__error=${message}`);
}
/** Asserts condition is truthy. Throws a redirect triggering an error toast with given message otherwise. */
@@ -258,7 +259,7 @@ export function errorToast(message: string) {
}
export function successToast(message: string) {
return redirect(`?__success=${message}`);
return redirect(`${currentRequestPathname() ?? ""}?__success=${message}`);
}
export function successToastWithRedirect({

View File

@@ -0,0 +1,16 @@
import { runWithRequestContext } from "./request-context.server";
type MiddlewareArgs = {
request: Request;
url: URL;
context: unknown;
};
type MiddlewareFn = (
args: MiddlewareArgs,
next: () => Promise<Response>,
) => Promise<Response>;
// TODO: this is only needed for our current hacky toast setup, once a proper one in place this middleware can be deleted
export const requestContextMiddleware: MiddlewareFn = ({ url }, next) =>
runWithRequestContext({ url }, () => next());

View File

@@ -0,0 +1,26 @@
import { AsyncLocalStorage } from "node:async_hooks";
// TODO: this is only needed for our current hacky toast setup, once a proper one in place this middleware can be deleted
interface RequestContext {
/** Normalized request URL, as provided to middleware in framework mode
* (single-fetch `.data` suffix and internal search params removed). */
url: URL;
}
const requestContextAsyncLocalStorage = new AsyncLocalStorage<RequestContext>();
/** Runs `fn` with the given request context available to server-side helpers
* (e.g. toast redirects) that don't otherwise receive the request. */
export function runWithRequestContext<T>(
context: RequestContext,
fn: () => T,
): T {
return requestContextAsyncLocalStorage.run(context, fn);
}
/** Normalized pathname of the current request, or `undefined` outside a request
* context. Used to build absolute redirects from helpers lacking the request. */
export function currentRequestPathname(): string | undefined {
return requestContextAsyncLocalStorage.getStore()?.url.pathname;
}

View File

@@ -47,10 +47,10 @@
"@faker-js/faker": "10.4.0",
"@formatjs/intl-durationformat": "0.10.14",
"@internationalized/date": "3.12.2",
"@react-router/node": "7.17.0",
"@react-router/serve": "7.15.0",
"@react-router/node": "8.1.0",
"@react-router/serve": "8.1.0",
"@remix-run/form-data-parser": "0.17.3",
"@sentry/react-router": "^10.57.0",
"@sentry/react-router": "10.63.0",
"@tldraw/tldraw": "3.12.1",
"@zumer/snapdom": "2.12.8",
"better-sqlite3": "12.10.0",
@@ -84,7 +84,7 @@
"react-error-boundary": "6.1.2",
"react-flip-toolkit": "7.2.4",
"react-i18next": "17.0.8",
"react-router": "7.17.0",
"react-router": "8.1.0",
"react-use-draggable-scroll": "0.4.7",
"remeda": "2.39.0",
"remix-auth": "4.2.0",
@@ -100,7 +100,7 @@
"@babel/preset-typescript": "7.29.7",
"@biomejs/biome": "2.5.1",
"@playwright/test": "1.60.0",
"@react-router/dev": "7.17.0",
"@react-router/dev": "8.1.0",
"@types/better-sqlite3": "7.6.13",
"@types/node": "26.0.0",
"@types/node-cron": "3.0.11",

View File

@@ -1,12 +0,0 @@
diff --git a/dist/cli.js b/dist/cli.js
index 08277520abadf36c3da03e50afc08919d316d5b2..55d9a1cf1f0220a96acd9f330b5668b03e4b6ddc 100644
--- a/dist/cli.js
+++ b/dist/cli.js
@@ -127,7 +127,6 @@ async function run() {
);
app.use(build.publicPath, import_express2.default.static(build.assetsBuildDirectory));
app.use(import_express2.default.static("public", { maxAge: "1h" }));
- app.use((0, import_morgan.default)("tiny"));
if (build.fetch) {
app.all("*", (0, import_node_fetch_server.createRequestListener)(build.fetch));
} else {

View File

@@ -0,0 +1,12 @@
diff --git a/dist/cli.js b/dist/cli.js
index 7871cebe46f6df886b76364db5346adfa1838622..495960a0a12d3d027ababaacc0b92a7dc39274a6 100644
--- a/dist/cli.js
+++ b/dist/cli.js
@@ -118,7 +118,6 @@ async function run() {
}));
app.use(expressPublicPath, express.static(build.assetsBuildDirectory));
app.use(express.static("public", { maxAge: "1h" }));
- app.use(morgan("tiny"));
if (build.fetch) app.all("/{*splat}", createRequestListener(build.fetch));
else app.all("/{*splat}", createRequestHandler({
build: buildModule,

1479
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,5 +5,5 @@ allowBuilds:
core-js: false
esbuild: true
patchedDependencies:
'@react-router/serve@7.15.0': patches/@react-router__serve@7.15.0.patch
'@react-router/serve@8.1.0': patches/@react-router__serve@8.1.0.patch
kysely@0.29.0: patches/kysely@0.29.0.patch

View File

@@ -6,16 +6,7 @@ export default {
// also lazy loading causes more load on the server
// this matches old Remix v2 behavior
routeDiscovery: { mode: "initial" },
future: {
v8_middleware: true,
v8_splitRouteModules: true,
// Disabled: passing the raw request makes relative redirects (e.g. the
// successToast/errorToast `redirect("?__success=...")` pattern) resolve
// against the `.data` URL of single-fetch requests, breaking navigation.
v8_passThroughRequests: false,
v8_trailingSlashAwareDataRequests: true,
v8_viteEnvironmentApi: true,
},
splitRouteModules: true,
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
await sentryOnBuildEnd({
viteConfig: viteConfig,