import { createRootRouteWithContext, createRoute, createRouter, } from '@tanstack/react-router'; import { useEffect, useRef, useState } from 'react'; import { App } from '#/app.jsx'; import { Link } from '#/components/atoms/link.jsx'; import { Skeleton } from '#/components/atoms/skeleton.jsx'; import { PageNotice } from '#/components/molecules/page-notice.jsx'; import { RepoPageSkeleton } from '#/components/molecules/repo-page-skeleton.jsx'; import { loadAccount, primeOnce } from '#/lib/account.js'; import { loadAuthors } from '#/lib/authors.js'; import { loadChecks } from '#/lib/checks.js'; import { loadPulls, PULL_COMMIT_LIMIT, parsePullPath, pullCommits, pullStatements, } from '#/lib/collab.js'; import { account, defaultRef, isEmpty, loadRepoSummary, readChange, repoRecord, spaceOf, splitRefPath, } from '#/lib/git.js'; import { loadIssueStatements } from '#/lib/issues.js'; import { useNavigate, useRepoRecord, useSpace } from '#/lib/navigation.jsx'; import { clearRenewal, completeSignIn } from '#/lib/oauth.js'; import { loadPackages } from '#/lib/packages.js'; import { useSession } from '#/lib/session.jsx'; import { publishedPackages } from '#/lib/settings.js'; import { directoryKey, fileKey, historyKey, logKey, readDirectory, readHistory, readLog, readSource, } from '#/lib/source.js'; import { CheckPage } from '#/pages/check.jsx'; import { ChecksPage } from '#/pages/checks.jsx'; import { CommitPage } from '#/pages/commit.jsx'; import { CommitsPage } from '#/pages/commits.jsx'; import { FilePage } from '#/pages/file.jsx'; import { IssuePage } from '#/pages/issue.jsx'; import { NewIssuePage } from '#/pages/issue-new.jsx'; import { IssuesPage } from '#/pages/issues.jsx'; import { PackagesPage } from '#/pages/packages.jsx'; import { PullPage } from '#/pages/pull.jsx'; import { PullsPage } from '#/pages/pulls.jsx'; import { NewRepoPage } from '#/pages/repo-new.jsx'; import { ReposPage } from '#/pages/repos.jsx'; import { SettingsPage } from '#/pages/settings.jsx'; import { TreePage } from '#/pages/tree.jsx'; /** * The routes, and what each one reads before it renders. * * A route's loader warms the query the page then subscribes to, under the * same key, so the page renders with the answer in hand and a link starts * the read when the pointer settles on it. The page keeps its own query: * that is what re-renders it when a publish invalidates the key. * * The account gate stays in the shell. The last visit's account and * repository rows are read synchronously there, so a loader below can ask * the server for a repository on the first frame. */ /** @typedef {{queryClient: import('@tanstack/react-query').QueryClient}} RouterContext */ const rootRoute = createRootRouteWithContext()({ component: App, // Every loader below asks the account's server, so the server has to be // known first. Primed from the last visit, that is immediate and the fresh // discovery runs behind the paint; on a first visit the routes wait for it. beforeLoad: async ({ context }) => { const query = { queryKey: ['account'], queryFn: () => loadAccount(context.queryClient), }; if (primeOnce()) { void context.queryClient.prefetchQuery(query); return; } await context.queryClient.ensureQueryData(query); }, }); /** * Where the authorization server sends the browser back. The code is spent * once, so the address bar is cleaned before anything else renders: a reload * of this URL would otherwise present a code the server has already refused. */ function CallbackPage() { const navigate = useNavigate(); const { setSession } = useSession(); const [error, setError] = useState(null); // The exchange consumes the pending sign-in the moment it starts, so it // must run once however many times the effect fires. StrictMode fires it // twice in development, and a second run would find nothing pending and // report a failure over a sign-in that succeeded. The ref survives the // simulated remount; a cleanup-cancelled flag would not, and would also // cancel the first run's redirect. const ran = useRef(false); useEffect(() => { if (ran.current) return; ran.current = true; // Read from the address bar rather than from a prop: the code is spent // once and this effect must not re-run, so it depends on nothing a render // makes anew. completeSignIn(new URLSearchParams(window.location.search)) .then(({ session, returnTo }) => { clearRenewal(); setSession(session); navigate(returnTo); }) .catch((err) => setError(err.message)); }, [navigate, setSession]); if (error) { return ( {error} Start again ); } return (
); } /** * Warm one query. What a loader does: the page subscribes to the same key. * @param {RouterContext} context * @param {unknown[]} queryKey * @param {() => Promise} queryFn */ const warm = ({ queryClient }, queryKey, queryFn) => queryClient.ensureQueryData({ queryKey, queryFn }); /** * Start one query without waiting on it. The route renders at once and the * page's own query, on the same key, shows its placeholder and streams the * answer in. A loader that computes from the result still awaits; one that * only fills the page's cache does not. * @param {RouterContext} context * @param {unknown[]} queryKey * @param {() => Promise} queryFn */ const prefetch = ({ queryClient }, queryKey, queryFn) => void queryClient.prefetchQuery({ queryKey, queryFn }); /** * The DID whose copy an address names, where it names one, and the rest. * A commit on a contributor's branch is addressed by whose copy holds it, * so this account's own commits keep the shorter address they always had. * @param {string} splat */ function copyOf(splat) { const parts = splat.split('/').filter(Boolean); const from = parts[0]?.startsWith('did:') ? parts[0] : ''; return { from, rest: from ? parts.slice(1) : parts }; } const indexRoute = createRoute({ getParentRoute: () => rootRoute, path: '/', loader: ({ context }) => prefetch(context, ['repo-summary'], loadRepoSummary), component: ReposPage, }); const callbackRoute = createRoute({ getParentRoute: () => rootRoute, path: '/callback', component: CallbackPage, }); /** Creating a repository, a page of its own like the file-issue one. */ const newRoute = createRoute({ getParentRoute: () => rootRoute, path: '/new', component: NewRepoPage, }); /** * One repository. Every section below reads its refs out of the record, and * a name the account does not hold has no sections. A private repository is * addressed by its name like any other; the listing says which space holds * it, and every read below takes the space from there. */ const repoRoute = createRoute({ getParentRoute: () => rootRoute, path: '/$repo', loader: ({ context }) => prefetch(context, ['authors'], loadAuthors), }); /** * What a directory listing shows: its entries and the ref's history, which * the latest-commit line and the gauges read. An empty repository has * neither and asks for nothing. * @param {RouterContext} context * @param {string} repo @param {string} refName @param {string} path * @param {string} space */ function warmTree(context, repo, refName, path, space) { const record = repoRecord(repo, space); if (!record || isEmpty(record)) return; prefetch(context, directoryKey(repo, refName, path, space), () => readDirectory(repo, refName, path, space), ); prefetch(context, historyKey(repo, refName, space), () => readHistory(repo, refName, space), ); } const treeIndexRoute = createRoute({ getParentRoute: () => repoRoute, path: '/', loader: ({ context, params }) => { const space = spaceOf(params.repo); const record = repoRecord(params.repo, space); if (!record) return; warmTree(context, params.repo, defaultRef(record), '', space); }, component: function TreeIndex() { const { repo } = treeIndexRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ( ); }, }); /** * A ref name may hold slashes, so where the ref ends and the path begins is * read against the repository's own refs. */ const treeRoute = createRoute({ getParentRoute: () => repoRoute, path: '/tree/$', loader: ({ context, params }) => { const space = spaceOf(params.repo); const record = repoRecord(params.repo, space); if (!record) return; const { ref, path } = splitRefPath(record, params._splat ?? ''); warmTree(context, params.repo, ref, path, space); }, component: function Tree() { const { repo, _splat } = treeRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; const { ref, path } = splitRefPath(record, _splat ?? ''); return ; }, }); const blobRoute = createRoute({ getParentRoute: () => repoRoute, path: '/blob/$', loader: ({ context, params }) => { const space = spaceOf(params.repo); const record = repoRecord(params.repo, space); if (!record) return; const { ref, path } = splitRefPath(record, params._splat ?? ''); prefetch(context, fileKey(params.repo, ref, path, space), () => readSource(params.repo, ref, path, space), ); }, component: function Blob() { const { repo, _splat } = blobRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; const { ref, path } = splitRefPath(record, _splat ?? ''); return ; }, }); /** * A history: a ref's, or the commits across a range, in this account's copy * or another's. A segment carrying `..` is a range rather than a branch, * since git forbids `..` in a ref name. */ const commitsRoute = createRoute({ getParentRoute: () => repoRoute, path: '/commits/$', loader: ({ context, params }) => { const space = spaceOf(params.repo); const record = repoRecord(params.repo, space); if (!record || isEmpty(record)) return; const { from, rest } = copyOf(params._splat ?? ''); const tail = rest.join('/'); const split = tail.includes('..') ? null : splitRefPath(record, tail); const what = split ? { refName: split.ref, path: split.path } : { range: tail, from: from || undefined }; prefetch( context, logKey( params.repo, what.refName, what.range, what.from, space, what.path, ), () => readLog(params.repo, { ...what, limit: PULL_COMMIT_LIMIT, space }), ); }, component: function Commits() { const { repo, _splat } = commitsRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; const { from, rest } = copyOf(_splat ?? ''); const tail = rest.join('/'); if (tail.includes('..')) { return ; } const { ref, path } = splitRefPath(record, tail); return ; }, }); const commitsIndexRoute = createRoute({ getParentRoute: () => repoRoute, path: '/commits', loader: ({ context, params }) => { const space = spaceOf(params.repo); const record = repoRecord(params.repo, space); if (!record || isEmpty(record)) return; const refName = splitRefPath(record, '').ref; prefetch( context, logKey(params.repo, refName, undefined, undefined, space), () => readLog(params.repo, { refName, limit: PULL_COMMIT_LIMIT, space }), ); }, component: function CommitsIndex() { const { repo } = commitsIndexRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ( ); }, }); const commitRoute = createRoute({ getParentRoute: () => repoRoute, path: '/commit/$', loader: ({ context, params }) => { const space = spaceOf(params.repo); if (!repoRecord(params.repo, space)) return; if (isEmpty(repoRecord(params.repo, space))) return; const { from, rest } = copyOf(params._splat ?? ''); const sha = rest.join('/'); prefetch(context, ['commit', params.repo, sha, from, space], () => readChange(params.repo, sha, from, space), ); }, component: function Commit() { const { repo, _splat } = commitRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; const { from, rest } = copyOf(_splat ?? ''); return ( ); }, }); const checksRoute = createRoute({ getParentRoute: () => repoRoute, path: '/checks', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['checks', params.repo, space], () => loadChecks(params.repo, space), ); }, component: function Checks() { const { repo } = checksRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const checkRoute = createRoute({ getParentRoute: () => repoRoute, path: '/checks/$rkey', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['checks', params.repo, space], () => loadChecks(params.repo, space), ); }, component: function Check() { const { repo, rkey } = checkRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const pullsRoute = createRoute({ getParentRoute: () => repoRoute, path: '/pulls', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['pulls', params.repo, space], () => loadPulls(params.repo, space), ); }, component: function Pulls() { const { repo } = pullsRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const pullsTabRoute = createRoute({ getParentRoute: () => repoRoute, path: '/pulls/$tab', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['pulls', params.repo, space], () => loadPulls(params.repo, space), ); }, component: function PullsTab() { const { repo, tab } = pullsTabRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); /** * A pull request is addressed by the account whose copy holds the work and * the branch, so the address survives the rebases that move its commits. * parsePullPath reads that address, beside the diffHref that writes it. * * The list names the pull request, so the loader reads the list and then * warms what its page shows: the diff across it, its commits, and the * statements about it, together rather than one after the other. None of it * holds the render: the page paints its placeholder and each answer lands * in it, the way every other route streams. */ const pullRoute = createRoute({ getParentRoute: () => repoRoute, path: '/pull/$', loader: ({ context, params }) => { const { repo } = params; const space = spaceOf(params.repo); const { did, branch, commit } = parsePullPath( (params._splat ?? '').split('/').filter(Boolean), ); void warm(context, ['pulls', repo, space], () => loadPulls(repo, space)) .then((listed) => { const pulls = /** @type {import('#/lib/collab.js').Pulls} */ (listed); const pull = [...pulls.open, ...pulls.merged].find( (one) => one.author.did === did && one.branch === branch, ); if (!pull) return; const from = pull.author.did === account.did ? '' : pull.author.did; const sha = commit || (pull.base ? `${pull.base}..${pull.sha}` : pull.sha); prefetch(context, ['commit', repo, sha, from, space], () => readChange(repo, sha, from, space), ); if (!commit) { prefetch(context, ['commits', repo, '', sha, from, space], () => pullCommits(repo, pull.base, pull.sha, from, 50), ); } prefetch(context, ['statements', repo, space], () => pullStatements(repo, space), ); }) .catch(() => { // The page's own query reports the failure where the reader is. }); }, component: function Pull() { const { repo, _splat } = pullRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; const { did, branch, tab, commit } = parsePullPath( (_splat ?? '').split('/').filter(Boolean), ); return ( ); }, }); const issuesRoute = createRoute({ getParentRoute: () => repoRoute, path: '/issues', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['issues', params.repo, space], () => loadIssueStatements(params.repo, space), ); }, component: function Issues() { const { repo } = issuesRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); /** * Filing is a page of its own. A DID is never the word `new`, so this and a * thread cannot be mistaken for each other; it is listed before the tab * route so the word is not read as a tab. */ const issueNewRoute = createRoute({ getParentRoute: () => repoRoute, path: '/issues/new', component: function IssueNew() { const { repo } = issueNewRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const issuesTabRoute = createRoute({ getParentRoute: () => repoRoute, path: '/issues/$tab', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['issues', params.repo, space], () => loadIssueStatements(params.repo, space), ); }, component: function IssuesTab() { const { repo, tab } = issuesTabRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); /** * An issue thread is addressed by its opener: whose repo holds it, and the * record key there. The pull requests are read too, for the ones whose * commits claim to fix it. */ const issueRoute = createRoute({ getParentRoute: () => repoRoute, path: '/issues/$did/$rkey', loader: ({ context, params }) => { const space = spaceOf(params.repo); prefetch(context, ['issues', params.repo, space], () => loadIssueStatements(params.repo, space), ); prefetch(context, ['pulls', params.repo, space], () => loadPulls(params.repo, space), ); }, component: function Issue() { const { repo, did, rkey } = issueRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const packagesRoute = createRoute({ getParentRoute: () => repoRoute, path: '/packages', loader: ({ context, params }) => prefetch(context, ['packages', params.repo], () => loadPackages(params.repo), ), component: function Packages() { const { repo } = packagesRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); const packagesTabRoute = createRoute({ getParentRoute: () => repoRoute, path: '/packages/$registry', loader: ({ context, params }) => prefetch(context, ['packages', params.repo], () => loadPackages(params.repo), ), component: function PackagesTab() { const { repo, registry } = packagesTabRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); /** * The repository's own settings. Every field is already in hand from the * listing, so the loader reads only what the form offers to link: the * packages this account publishes. */ const settingsRoute = createRoute({ getParentRoute: () => repoRoute, path: '/settings', loader: ({ context }) => prefetch(context, ['published-packages'], publishedPackages), component: function Settings() { const { repo } = settingsRoute.useParams(); const space = useSpace(); const { record, settled } = useRepoRecord(repo, space); if (!record) return settled ? : ; return ; }, }); function NoSuchRepository() { return No such repository.; } const routeTree = rootRoute.addChildren([ indexRoute, newRoute, callbackRoute, repoRoute.addChildren([ treeIndexRoute, treeRoute, blobRoute, commitsIndexRoute, commitsRoute, commitRoute, checksRoute, checkRoute, pullsRoute, pullsTabRoute, pullRoute, issuesRoute, issueNewRoute, issuesTabRoute, issueRoute, packagesRoute, packagesTabRoute, settingsRoute, ]), ]); /** * @param {import('@tanstack/react-query').QueryClient} queryClient */ export function createAppRouter(queryClient) { return createRouter({ routeTree, context: { queryClient }, // A link's page is read when the pointer settles on it. The query // client says whether the answer is fresh, so the router never re-reads // what the client already holds. defaultPreload: 'intent', defaultPreloadStaleTime: 0, scrollRestoration: false, }); }