Skip to content

Commit

Every link to where something runs opens in a new tab, and a deployment's timeline no longer shows an old Deploying status spinning after a later one

syntaqxcommitted Parentbee5a81Browse files
15 files+124−580/15 viewed
+5−0
436436 g1t.page manage their apps under **On g1t.page** on the same page.
437437 - **A deployment's page**, `g1t.sh/<owner>/<project>/deployments/<id>`,
438438 shows its statuses in order, links to its log and run, and its payload.
439+ Only the newest status can show as still under way; a `queued` or
440+ `in_progress` one that a later status followed shows as over.
441+- **Every link to where something runs**, a g1t.page address, an
442+ environment's URL, a pull request's preview or a project's homepage,
443+ opens in a new tab, wherever on the site it appears.
439444 - **The repository's code page** and **the project's overview** show a
440445 **Deployments** panel with how many there are, and each environment's
441446 latest deployment and when. It links to the Deployments page.
+12−0
11 /** Small pieces every page about deployments shares. */
2+import type { AnchorHTMLAttributes } from "react";
3+
24 import type { DeployStatus } from "@g1t/contracts";
35
46 export const DEPLOY_STATUS: Record<DeployStatus, { label: string; tone: string }> = {
2527 export function host(url: string): string {
2628 return url.replace(/^https?:\/\//, "");
2729 }
30+
31+/**
32+ * A link to where something runs: a g1t.page address, an environment's
33+ * URL, a preview, a project's homepage. It always opens in a new tab, so
34+ * the page about it stays open behind it. `rel` adds to noopener and
35+ * noreferrer (nofollow, say), never replaces them.
36+ */
37+export function DeployLink({ rel, ...props }: AnchorHTMLAttributes<HTMLAnchorElement> & { href: string }) {
38+ return <a {...props} target="_blank" rel={rel ? `noopener noreferrer ${rel}` : "noopener noreferrer"} />;
39+}
+14−2
33 * latest deployment and when, linking to the Deployments page. Also the
44 * state icon and badge every page about deployments shares.
55 */
6−import { Check, CircleDashed, Clock, Loader2, Rocket, X } from "lucide-react";
6+import { Check, CircleDashed, CircleDot, Clock, Loader2, Rocket, X } from "lucide-react";
77 import { Link } from "react-router";
88
99 import type { DeploymentEnvironments, DeploymentSource, DeploymentState } from "@g1t/contracts";
2626 };
2727
2828 /** A deployment's state as an icon: a check, a cross, a spinner, a clock. */
29−export function DeploymentStateIcon({ state, size = 16, className }: { state: DeploymentState; size?: number; className?: string }) {
29+export function DeploymentStateIcon({
30+ state,
31+ size = 16,
32+ className,
33+ over = false,
34+}: {
35+ state: DeploymentState;
36+ size?: number;
37+ className?: string;
38+ /** A past status a later one followed: under way then, over now, so it does not spin. */
39+ over?: boolean;
40+}) {
3041 const label = STATE_WORD[state];
3142 switch (state) {
3243 case "success":
4354 </span>
4455 );
4556 case "in_progress":
57+ if (over) return <CircleDot size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
4658 return <Loader2 size={size} aria-label={label} className={cn("shrink-0 animate-spin text-warn", className)} />;
4759 case "queued":
4860 return <Clock size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
+3−2
22 import { useEffect, useRef, useState } from "react";
33
44 import { cn } from "../lib/cn";
5+import { DeployLink } from "./deploy";
56
67 /**
78 * A thumbnail of production as it is now, linking to it. The screenshot is
5859 );
5960
6061 return href ? (
61− <a href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}>
62+ <DeployLink href={href} className={cn("group block", className)} aria-label={`Visit ${label}`}>
6263 {frame}
63− </a>
64+ </DeployLink>
6465 ) : (
6566 <div className={className}>{frame}</div>
6667 );
+4−3
66
77 import { cn } from "../lib/cn";
88 import { KIND_CHOICES, type KindChoice, type ShownLink, choiceOf, linksToShow } from "../lib/project-kind";
9+import { DeployLink } from "./deploy";
910 import { Input, SubmitButton } from "./ui";
1011 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog";
1112 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu";
3031 <ul className={cn("space-y-1.5 text-sm", className)}>
3132 {list.map((link) => (
3233 <li key={link.key} className="min-w-0">
33− <a
34+ <DeployLink
3435 href={link.url}
35− rel="noopener noreferrer nofollow"
36+ rel="nofollow"
3637 className="flex min-w-0 items-center gap-2 text-fg-soft hover:text-accent [&_svg]:shrink-0 [&_svg]:text-faint"
3738 >
3839 {LINK_ICON[link.type]}
3940 <span className="truncate">{link.label}</span>
40− </a>
41+ </DeployLink>
4142 </li>
4243 ))}
4344 </ul>
+7−7
1010
1111 import type { DeployStatus, Project } from "@g1t/contracts";
1212
13−import { host, StatusDot } from "./deploy";
13+import { DeployLink, host, StatusDot } from "./deploy";
1414 import { ButtonLink, Input, SubmitButton, TimeAgo } from "./ui";
1515 import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
1616 import { Hint } from "./ui/hint";
6666
6767 function BigLink({ url }: { url: string }) {
6868 return (
69− <a href={url} rel="noopener noreferrer" className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent">
69+ <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-lg font-medium hover:text-accent">
7070 <span className="truncate">{host(url).replace(/\/$/, "")}</span>
7171 <ArrowUpRight size={16} className="shrink-0 text-faint" />
72− </a>
72+ </DeployLink>
7373 );
7474 }
7575
190190 </div>
191191 {url && (
192192 <div className="flex shrink-0 items-center gap-2">
193− <ButtonLink to={url} variant="accent" reloadDocument>
193+ <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
194194 Visit
195195 <ArrowUpRight size={14} />
196196 </ButtonLink>
291291 </div>
292292 {url && (
293293 <div className="flex shrink-0 items-center gap-2">
294− <ButtonLink to={url} variant="accent" reloadDocument>
294+ <ButtonLink to={url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
295295 Read
296296 <ArrowUpRight size={14} />
297297 </ButtonLink>
329329 if (!url) return null;
330330 return (
331331 <Hint label="Its homepage">
332− <a href={url} rel="noopener noreferrer" className="inline-flex items-center gap-1 text-faint hover:text-fg">
332+ <DeployLink href={url} className="inline-flex items-center gap-1 text-faint hover:text-fg">
333333 <Globe size={12} />
334334 {host(url).replace(/\/$/, "")}
335− </a>
335+ </DeployLink>
336336 </Hint>
337337 );
338338 }
+3−4
33 import { Link } from "react-router";
44
55 import { Duration, StatusIcon } from "./actions";
6+import { DeployLink } from "./deploy";
67 import { GRAPH, type GraphJob, type EdgeState, type PlacedGroup, type PlacedNode, type Standing, layoutRun, sharedUrl, standingOf, unitStanding } from "../lib/run-graph";
78
89 /** A job calling a workflow shows as running while its jobs do. */
4647 /** Where a deployment is, under its node's name. */
4748 function Address({ url }: { url: string }) {
4849 return (
49− <a
50+ <DeployLink
5051 href={url}
51− target="_blank"
52− rel="noreferrer"
5352 className="-mt-2 flex items-center gap-1 truncate pr-3 pl-9 font-mono text-xs text-accent hover:underline"
5453 style={{ height: GRAPH.urlHeight }}
5554 >
5655 <span className="truncate">{url.replace(/^https?:\/\//, "")}</span>
5756 <ExternalLink size={11} className="shrink-0" />
58− </a>
57+ </DeployLink>
5958 );
6059 }
6160
+17−0
1212 groupBuilds,
1313 hasPayload,
1414 isFiltered,
15+ isNewestStatus,
1516 isPageBuild,
1617 orderEnvironments,
1718 pageRange,
195196 assert.equal(hasPayload({ region: "wnam" }), true);
196197 assert.equal(hasPayload(null), false);
197198 });
199+
200+test("only a deployment's newest status can still be under way", () => {
201+ const statuses = [
202+ { created_at: "2026-10-08T10:00:00.000Z" }, // Deploying, building
203+ { created_at: "2026-10-08T10:02:00.000Z" }, // Deployed
204+ ];
205+ assert.equal(isNewestStatus(statuses, 0), false);
206+ assert.equal(isNewestStatus(statuses, 1), true);
207+ // Listed out of order: the newest is still the latest written.
208+ assert.equal(isNewestStatus([...statuses].reverse(), 0), true);
209+ // Written in the same moment: the last listed is the newest.
210+ const tied = [{ created_at: "2026-10-08T10:00:00.000Z" }, { created_at: "2026-10-08T10:00:00.000Z" }];
211+ assert.equal(isNewestStatus(tied, 0), false);
212+ assert.equal(isNewestStatus(tied, 1), true);
213+ assert.equal(isNewestStatus([], 0), false);
214+});
+13−0
99 DeploymentFilter,
1010 DeploymentSource,
1111 DeploymentState,
12+ DeploymentStatus,
1213 RepoDeployment,
1314 } from "@g1t/contracts";
1415
210211 export function hasPayload(payload: Record<string, unknown> | null | undefined): boolean {
211212 return payload != null && typeof payload === "object" && Object.keys(payload).length > 0;
212213 }
214+
215+/**
216+ * Whether a status on a deployment's timeline is its newest: the latest
217+ * one written (the last listed, when two share a time). Only the newest
218+ * can still be under way; one a later status followed is over, whatever
219+ * it said, so it is never shown as running.
220+ */
221+export function isNewestStatus(statuses: readonly Pick<DeploymentStatus, "created_at">[], index: number): boolean {
222+ const at = statuses[index]?.created_at;
223+ if (at == null) return false;
224+ return statuses.every((other, i) => i === index || (i < index ? other.created_at <= at : other.created_at < at));
225+}
+4−4
77 import type { Route } from "./+types/explore";
88 import { EmptyState, Pill, TimeAgo, notACredential } from "../components/ui";
99 import { page } from "../lib/meta";
10−import { host } from "../components/deploy";
10+import { DeployLink, host } from "../components/deploy";
1111 import { projects, search } from "../lib/services.server";
1212 import { getViewer } from "../lib/session.server";
1313
6666 </span>
6767 <span className="mt-2 line-clamp-2 grow text-sm text-muted">{repo.description ?? "No description."}</span>
6868 {link && (
69− <a
69+ <DeployLink
7070 href={link}
71− rel="noopener noreferrer nofollow"
71+ rel="nofollow"
7272 className="relative z-10 mt-2 flex min-w-0 items-center gap-1 self-start font-mono text-xs text-muted hover:text-accent"
7373 >
7474 <span className="truncate">{host(link).replace(/\/$/, "")}</span>
7575 <ArrowUpRight size={11} className="shrink-0" />
76− </a>
76+ </DeployLink>
7777 )}
7878 {repo.topics.length > 0 && (
7979 <span className="mt-3 flex flex-wrap gap-1.5">
+17−12
99 import { TimeAgo } from "../../components/ui";
1010 import { Badge } from "../../components/ui/badge";
1111 import { Hint } from "../../components/ui/hint";
12−import { host } from "../../components/deploy";
12+import { DeployLink, host } from "../../components/deploy";
1313 import { DeploymentStateBadge, DeploymentStateIcon, STATE_TONE, sourceLabel } from "../../components/deployments-panel";
1414 import { deployments } from "../../lib/services.server";
1515 import { getViewer } from "../../lib/session.server";
1616 import { requireRepo } from "../../lib/access.server";
17−import { STATE_WORD, buildError, environmentLabel, hasPayload, isPageBuild, shortSha } from "../../lib/deployments";
17+import { STATE_WORD, buildError, environmentLabel, hasPayload, isNewestStatus, isPageBuild, shortSha } from "../../lib/deployments";
1818
1919 export function meta({ params, ...args }: Route.MetaArgs) {
2020 return page(args, { title: `Deployment · ${params.owner}/${params.repo} · g1t` });
8989 </p>
9090 </div>
9191 {url && (
92− <a
92+ <DeployLink
9393 href={url}
9494 className="inline-flex max-w-full items-center gap-1.5 rounded-md bg-accent px-3.5 py-2 text-sm font-medium text-bg hover:bg-accent-hover"
9595 >
9696 <span className="truncate">{host(url)}</span>
9797 <ExternalLink size={14} className="shrink-0" />
98− </a>
98+ </DeployLink>
9999 )}
100100 </header>
101101
169169 ) : (
170170 <ol className="mt-4">
171171 {deployment.statuses.map((status, index) => (
172− <StatusItem key={status.id} status={status} last={index === deployment.statuses.length - 1} />
172+ <StatusItem
173+ key={status.id}
174+ status={status}
175+ last={index === deployment.statuses.length - 1}
176+ over={!isNewestStatus(deployment.statuses, index)}
177+ />
173178 ))}
174179 </ol>
175180 )}
201206 }
202207
203208 /** One status on the timeline, oldest at the top, with a line down to the next. */
204−function StatusItem({ status, last }: { status: DeploymentStatus; last: boolean }) {
209+function StatusItem({ status, last, over }: { status: DeploymentStatus; last: boolean; over: boolean }) {
205210 return (
206211 <li className="relative flex gap-3 pb-5 last:pb-0">
207212 {!last && <span aria-hidden="true" className="absolute top-6 bottom-1 left-[9px] w-px bg-line" />}
208213 <span className="relative mt-0.5 flex size-[19px] shrink-0 items-center justify-center bg-bg">
209− <DeploymentStateIcon state={status.state} size={18} />
214+ <DeploymentStateIcon state={status.state} size={18} over={over} />
210215 </span>
211216 <div className="min-w-0 grow">
212217 <p className="flex flex-wrap items-baseline gap-x-2 text-sm">
213− <span className={`font-medium ${STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span>
218+ <span className={`font-medium ${over && status.state === "in_progress" ? "text-muted" : STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span>
214219 <span className="text-xs text-faint">
215220 {status.creator} ·{" "}
216221 <Hint label={new Date(status.created_at).toUTCString()}>
224229 {(status.environment_url || status.log_url) && (
225230 <p className="mt-1 flex flex-wrap gap-x-4 gap-y-0.5 text-xs">
226231 {status.environment_url && (
227− <a href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline">
232+ <DeployLink href={status.environment_url} className="inline-flex min-w-0 items-center gap-1 font-mono text-accent hover:underline">
228233 <span className="truncate">{host(status.environment_url)}</span>
229234 <ExternalLink size={11} className="shrink-0" />
230− </a>
235+ </DeployLink>
231236 )}
232237 {status.log_url && (
233238 <a href={status.log_url} className="inline-flex items-center gap-1 text-muted hover:text-fg">
265270 {BUILD_WORDS[build.status]} · started <TimeAgo at={build.createdAt} /> by {build.createdBy}
266271 </p>
267272 {build.status === "ready" && (
268− <a href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline">
273+ <DeployLink href={build.url} className="mt-3 inline-flex items-center gap-1.5 font-mono text-sm text-accent hover:underline">
269274 {host(build.url)}
270275 <ExternalLink size={12} />
271− </a>
276+ </DeployLink>
272277 )}
273278 </>
274279 );
+9−9
1616
1717 import type { DeploymentEnvironment, FeatureState, RepoDeployment } from "@g1t/contracts";
1818
19−import { host } from "../../components/deploy";
19+import { DeployLink, host } from "../../components/deploy";
2020 import { DeploymentStateBadge, DeploymentStateIcon, sourceLabel } from "../../components/deployments-panel";
2121 import { FilterMenu } from "../../components/labels";
2222
156156 </p>
157157 </div>
158158 {productionUrl && (
159− <a
159+ <DeployLink
160160 href={productionUrl}
161161 className="inline-flex max-w-full items-center gap-1.5 rounded-md border border-line px-3 py-1.5 font-mono text-xs text-muted hover:border-line-strong hover:text-fg"
162162 >
163163 <Globe size={13} className="shrink-0" />
164164 <span className="truncate">{host(productionUrl)}</span>
165− </a>
165+ </DeployLink>
166166 )}
167167 </header>
168168
297297 {latest && <DeploymentStateBadge state={latest.state} />}
298298 </div>
299299 {url ? (
300− <a href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
300+ <DeployLink href={url} className="mt-2 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
301301 <span className="truncate">{host(url)}</span>
302302 <ExternalLink size={12} className="shrink-0" />
303− </a>
303+ </DeployLink>
304304 ) : (
305305 <p className="mt-2 text-sm text-faint">No address</p>
306306 )}
652652 </Link>
653653 )}
654654 </span>
655− <a href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block">
655+ <DeployLink href={app.url} className="hidden min-w-0 truncate font-mono text-xs text-muted hover:text-fg sm:block">
656656 {host(app.url)}
657− </a>
657+ </DeployLink>
658658 <span className="ml-auto shrink-0">{can.run && <AppActions branch={app.branch} up compact />}</span>
659659 </li>
660660 ))}
776776 <p className="mt-0.5 text-xs text-faint">{off ? "Off for this repository." : hint}</p>
777777 {app && href ? (
778778 <>
779− <a href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
779+ <DeployLink href={href} className="mt-3 flex min-w-0 items-center gap-1.5 font-mono text-sm text-accent hover:underline">
780780 <span className="truncate">{host(href)}</span>
781781 <ExternalLink size={12} className="shrink-0" />
782− </a>
782+ </DeployLink>
783783 <p className="mt-1 text-xs text-faint">
784784 <span className="font-mono">{app.commit.slice(0, 8)}</span> · deployed <TimeAgo at={app.deployedAt} />
785785 </p>
+8−8
3939
4040 import type { Route } from "./+types/overview";
4141 import { InlineMarkdown } from "../../components/inline-markdown";
42−import { host, StatusDot } from "../../components/deploy";
42+import { DeployLink, host, StatusDot } from "../../components/deploy";
4343 import { CheckBadge } from "../../components/checks";
4444 import { Elapsed, formatCost, useLiveRefresh } from "../../components/agents";
4545 import { ActivityFeed, DeployStrip, Meter, NeedsList, Panel, Quiet, Unavailable, percent } from "../../components/mission";
822822 <div className="flex flex-wrap items-center gap-x-4 gap-y-2 border-b border-line px-5 py-2.5 text-xs sm:px-6">
823823 <KindMenu project={project} label={kindLabel(project)} canChange={canChange} />
824824 {stripLinks.map((link) => (
825− <a key={link.key} href={link.url} rel="noopener noreferrer" className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg">
825+ <DeployLink key={link.key} href={link.url} className="inline-flex min-w-0 items-center gap-1.5 text-muted hover:text-fg">
826826 {link.type === "docs" ? <BookOpen size={12} className="shrink-0 text-faint" /> : <Globe size={12} className="shrink-0 text-faint" />}
827827 <span className="truncate">{link.type === "docs" ? `Docs · ${bare(link.url)}` : link.label}</span>
828− </a>
828+ </DeployLink>
829829 ))}
830830 </div>
831831 )}
865865 </p>
866866 {production ? (
867867 <>
868− <a
868+ <DeployLink
869869 href={productionUrl ?? production.url}
870870 className="mt-2 flex items-center gap-1.5 truncate font-mono text-lg font-medium hover:text-accent"
871871 >
872872 {host(productionUrl ?? production.url)}
873873 <ArrowUpRight size={16} className="shrink-0 text-faint" />
874− </a>
874+ </DeployLink>
875875 <p className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted">
876876 {latestProduction && <StatusDot status={latestProduction.status} label={latestProduction.status === "ready" ? "Live" : undefined} />}
877877 <span className="inline-flex items-center gap-1 font-mono">
908908 {member && (
909909 <div className="flex shrink-0 items-center gap-2">
910910 {production && (
911− <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument>
911+ <ButtonLink to={productionUrl ?? production.url} variant="accent" reloadDocument target="_blank" rel="noopener noreferrer">
912912 Visit
913913 <ArrowUpRight size={14} />
914914 </ButtonLink>
12641264 {previews.map((app) => (
12651265 <li key={app.url} className="flex items-center gap-3 px-4 py-2.5 text-sm">
12661266 <span className="min-w-0 grow">
1267− <a href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
1267+ <DeployLink href={app.url} className="block truncate font-mono text-[0.8125rem] hover:text-accent">
12681268 {host(app.url)}
1269− </a>
1269+ </DeployLink>
12701270 <span className="text-xs text-muted">
12711271 {app.branch}
12721272 {app.number != null && (
+5−4
9494 import { actions, agents, deployments, identity, inbox, repos, work } from "../../lib/services.server";
9595 import { commitChecksFor } from "../../lib/commit-checks.server";
9696 import { CommitChecksBadge } from "../../components/commit-checks";
97+import { DeployLink } from "../../components/deploy";
9798 import { assertSameOrigin, getViewer, requireUser } from "../../lib/session.server";
9899 import { accessTo, refusal, repoFor } from "../../lib/access.server";
99100 import { SubscriptionBox } from "../../components/notifications";
16831684 : "This branch is live"}
16841685 </p>
16851686 {preview ? (
1686− <a href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
1687+ <DeployLink href={preview.url} className="mt-0.5 block truncate font-mono text-xs text-muted hover:text-accent">
16871688 {preview.url.replace(/^https?:[/][/]/, "")}
1688− </a>
1689+ </DeployLink>
16891690 ) : (
16901691 build?.error && <p className="mt-0.5 truncate text-xs text-muted">{build.error}</p>
16911692 )}
16971698 </Link>
16981699 )}
16991700 {preview && (
1700− <a
1701+ <DeployLink
17011702 href={preview.url}
17021703 className="inline-flex items-center gap-1.5 rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-bg hover:opacity-90"
17031704 >
17041705 Visit preview
17051706 <ArrowUpRight size={13} />
1706− </a>
1707+ </DeployLink>
17071708 )}
17081709 </div>
17091710 </div>
+3−3
44 import type { PackageSummary, Project, ProjectDeploys, User } from "@g1t/contracts";
55
66 import type { Route } from "./+types/overview";
7−import { host, StatusDot } from "../../components/deploy";
7+import { DeployLink, host, StatusDot } from "../../components/deploy";
88 import { Avatar, ButtonLink, CopyLine, Pill, TimeAgo } from "../../components/ui";
99 import { UsageCard } from "../../components/usage-card";
1010 import { PullIcon } from "../../components/work-icons";
176176 {project.name}
177177 </Link>
178178 {link && !(library && pkg && !production) ? (
179− <a
179+ <DeployLink
180180 href={link}
181181 className="relative z-10 mt-0.5 flex items-center gap-1 truncate font-mono text-xs text-muted hover:text-accent"
182182 >
183183 {host(link)}
184184 <ArrowUpRight size={11} className="shrink-0" />
185− </a>
185+ </DeployLink>
186186 ) : library && pkg ? (
187187 <Link
188188 to={packagePath(pkg)}