This commit is contained in:
Joe Chen
2026-05-28 16:40:47 -04:00
parent bc6c1ddf07
commit 8f9a1cf2c1
4 changed files with 82 additions and 76 deletions
+2 -2
View File
@@ -96,10 +96,10 @@ export function DiffToolbar({
type="button"
onClick={onToggleTreeDesktop}
aria-label={desktopTreeOpen ? t("repo.hide_file_tree") : t("repo.show_file_tree")}
aria-pressed={desktopTreeOpen}
aria-pressed={!!desktopTreeOpen}
// `pl-1` nudges the icon right so it visually aligns with
// the sidebar's collapsed-rail edge on desktop.
className="hidden size-6 cursor-pointer place-items-center rounded text-(--color-muted-foreground) hover:bg-(--color-surface) hover:text-(--color-foreground) lg:grid pl-1"
className="hidden size-6 cursor-pointer place-items-center rounded text-(--color-muted-foreground) hover:bg-(--color-surface) hover:text-(--color-foreground) lg:grid lg:pl-1"
>
{desktopTreeOpen ? (
<PanelLeftClose className="size-4" aria-hidden />
+3 -70
View File
@@ -1,34 +1,17 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
Outlet,
RouterProvider,
createRootRouteWithContext,
createRoute,
createRouter,
notFound,
} from "@tanstack/react-router";
import { Outlet, RouterProvider, createRootRouteWithContext, createRoute, createRouter } from "@tanstack/react-router";
import { Footer } from "@/components/Footer";
import { Navbar } from "@/components/Navbar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { webContext } from "@/lib/context";
import { LoaderResponseError, loaderResponseError } from "@/lib/loader-error";
import { repoHeaderQuery } from "@/lib/queries/repo";
import { subUrl } from "@/lib/url";
import type { UserInfo } from "@/lib/user-info";
import { Landing } from "@/pages/Landing";
import { NotFound } from "@/pages/NotFound";
import { ServerError } from "@/pages/ServerError";
import { RepoCommit, type RepoCommitPage } from "@/pages/repo/Commit";
import { validateRepoCommitSearch } from "@/pages/repo/Commit.search";
import { createRepoRoutes } from "@/routes/repo";
import { createUserRoutes } from "@/routes/user";
// Match the legacy server-side route constraint (see `web.go` near the
// `/commit/:sha([a-f0-9]{7,40})$` declaration). The server enforces the same
// shape for SEO and to skip the SPA shell for malformed paths; this client
// check short-circuits the loader so we render 404 without a wasted fetch.
const SHA_RE = /^[a-f0-9]{7,40}$/;
interface RouterContext {
user: UserInfo | null;
queryClient: QueryClient;
@@ -55,57 +38,7 @@ const landingRoute = createRoute({
component: Landing,
});
const repoCommitRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/$owner/$repo/commit/$sha",
validateSearch: validateRepoCommitSearch,
// Reject malformed SHA at parse time so the route doesn't match for paths
// like `/owner/repo/commit/garbage`. The thrown `notFound()` bubbles to the
// root route's NotFound component.
params: {
parse: (raw: { owner: string; repo: string; sha: string }) => {
if (!SHA_RE.test(raw.sha)) {
// eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
throw notFound();
}
return raw;
},
stringify: (params: { owner: string; repo: string; sha: string }) => params,
},
loaderDeps: ({ search }) => ({ whitespace: search.whitespace }),
loader: async ({ params, deps, context }): Promise<RepoCommitPage> => {
const metaURL = subUrl(`/api/web/${params.owner}/${params.repo}/commit/${params.sha}`);
const rawBase = subUrl(`/${params.owner}/${params.repo}/commit/${params.sha}.diff`);
const rawURL = deps.whitespace ? `${rawBase}?whitespace=${encodeURIComponent(deps.whitespace)}` : rawBase;
// Three requests in parallel: repo header (via Query cache for cross-page
// reuse), commit metadata JSON, raw patch text. Splitting the patch out
// skips JSON-string escaping and lets the browser cache the (often large)
// patch separately from the metadata.
try {
const [, meta, patch] = await Promise.all([
context.queryClient.ensureQueryData(repoHeaderQuery(params.owner, params.repo)),
fetch(metaURL, { credentials: "same-origin" }).then(async (res) => {
if (!res.ok) throw await loaderResponseError(res);
return (await res.json()) as Omit<RepoCommitPage, "patch">;
}),
fetch(rawURL, { credentials: "same-origin" }).then(async (res) => {
if (!res.ok) throw await loaderResponseError(res);
return res.text();
}),
]);
return { ...meta, patch };
} catch (err) {
if (err instanceof LoaderResponseError && err.status === 404) {
// eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
throw notFound();
}
throw err;
}
},
component: RepoCommit,
});
const routeTree = rootRoute.addChildren([landingRoute, ...createUserRoutes(rootRoute), repoCommitRoute]);
const routeTree = rootRoute.addChildren([landingRoute, ...createUserRoutes(rootRoute), ...createRepoRoutes(rootRoute)]);
function makeRouter(context: RouterContext) {
return createRouter({
+73
View File
@@ -0,0 +1,73 @@
import type { QueryClient } from "@tanstack/react-query";
import { type AnyRoute, createRoute, notFound } from "@tanstack/react-router";
import { LoaderResponseError, loaderResponseError } from "@/lib/loader-error";
import { repoHeaderQuery } from "@/lib/queries/repo";
import { subUrl } from "@/lib/url";
import { RepoCommit, type RepoCommitPage } from "@/pages/repo/Commit";
import { type RepoCommitSearch, validateRepoCommitSearch } from "@/pages/repo/Commit.search";
// Match the legacy server-side route constraint (see `web.go` near the
// `/commit/:sha([a-f0-9]{7,40})$` declaration). The server enforces the same
// shape for SEO and to skip the SPA shell for malformed paths; this client
// check short-circuits the loader so we render 404 without a wasted fetch.
const SHA_RE = /^[a-f0-9]{7,40}$/;
interface RouterContext {
queryClient: QueryClient;
}
export function createRepoRoutes(rootRoute: AnyRoute) {
const repoCommitRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/$owner/$repo/commit/$sha",
validateSearch: validateRepoCommitSearch,
// Reject malformed SHA at parse time so the route doesn't match for paths
// like `/owner/repo/commit/garbage`. The thrown `notFound()` bubbles to the
// root route's NotFound component.
params: {
parse: (raw: { owner: string; repo: string; sha: string }) => {
if (!SHA_RE.test(raw.sha)) {
// eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
throw notFound();
}
return raw;
},
stringify: (params: { owner: string; repo: string; sha: string }) => params,
},
loaderDeps: ({ search }: { search: RepoCommitSearch }) => ({ whitespace: search.whitespace }),
loader: async ({ params, deps, context }): Promise<RepoCommitPage> => {
const metaURL = subUrl(`/api/web/${params.owner}/${params.repo}/commit/${params.sha}`);
const rawBase = subUrl(`/${params.owner}/${params.repo}/commit/${params.sha}.diff`);
const rawURL = deps.whitespace ? `${rawBase}?whitespace=${encodeURIComponent(deps.whitespace)}` : rawBase;
const routerContext = context as RouterContext;
// Three requests in parallel: repo header (via Query cache for cross-page
// reuse), commit metadata JSON, raw patch text. Splitting the patch out
// skips JSON-string escaping and lets the browser cache the (often large)
// patch separately from the metadata.
try {
const [, meta, patch] = await Promise.all([
routerContext.queryClient.ensureQueryData(repoHeaderQuery(params.owner, params.repo)),
fetch(metaURL, { credentials: "same-origin" }).then(async (res) => {
if (!res.ok) throw await loaderResponseError(res);
return (await res.json()) as Omit<RepoCommitPage, "patch">;
}),
fetch(rawURL, { credentials: "same-origin" }).then(async (res) => {
if (!res.ok) throw await loaderResponseError(res);
return res.text();
}),
]);
return { ...meta, patch };
} catch (err) {
if (err instanceof LoaderResponseError && err.status === 404) {
// eslint-disable-next-line @typescript-eslint/only-throw-error -- `notFound()` is the documented TanStack Router signal for 404, not an Error subclass.
throw notFound();
}
throw err;
}
},
component: RepoCommit,
});
return [repoCommitRoute];
}
+4 -4
View File
@@ -13,10 +13,10 @@ export default defineConfig({
},
server: {
port: 5173,
// The dev page is served by the gogs Go server (e.g. https://gogs.localhost)
// which reverse-proxies HTTP to this Vite dev server. That proxy is HTTP-only,
// so the HMR client's WebSocket can't tunnel through it. Point HMR's WS
// directly at the Vite dev port instead, bypassing gogs entirely.
// The dev page is served by the Go server (e.g., https://gogs.localhost)
// which reverse-proxies HTTP to this Vite dev server. That proxy is
// HTTP-only, so the HMR client's WebSocket can't tunnel through it. Point
// HMR's WS directly at the Vite dev port instead, bypassing gogs entirely.
hmr: {
protocol: "ws",
host: "localhost",