Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions src/app/[...parts]/_page/TrustComparison/TrustComparison.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { type FunctionComponent } from "react";
import ExternalLink from "^/components/ExternalLink";
import type { TrustEvidence } from "^/lib/api/npm/getTrustEvidence";
import { cx } from "^/lib/cva";
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
import Halfs from "../DiffIntro/Halfs";

const TRUST_RANK = {
trustedPublisher: 2,
provenance: 1,
undefined: 0,
} as const;

function getTrustRank(evidence: TrustEvidence): number {
return TRUST_RANK[evidence ?? "undefined"];
}

function getTrustLabel(evidence: TrustEvidence): string {
switch (evidence) {
case "trustedPublisher":
return "Trusted Publisher";
case "provenance":
return "Provenance";
default:
return "No trust evidence";
}
}

function getTrustColor(evidence: TrustEvidence, isDowngrade: boolean): string {
if (isDowngrade) {
return "text-red-600 dark:text-red-400";
}
switch (evidence) {
case "trustedPublisher":
return "text-green-600 dark:text-green-400";
case "provenance":
return "text-blue-600 dark:text-blue-400";
default:
return "text-gray-500 dark:text-gray-400";
}
}

interface TrustBoxProps {
evidence: TrustEvidence;
provenanceUrl?: string;
isDowngrade?: boolean;
isUpgrade?: boolean;
}

const TrustBox: FunctionComponent<TrustBoxProps> = ({
evidence,
provenanceUrl,
isDowngrade = false,
isUpgrade = false,
}) => {
const label = getTrustLabel(evidence);
const colorClass = getTrustColor(evidence, isDowngrade);

const content = (
<div className={cx("p-2 text-center", colorClass)}>
<p className="font-semibold">{label}</p>
{isDowngrade ? <p className="text-xs">⚠️ Trust downgrade</p> : null}
{isUpgrade ? <p className="text-xs">✓ Trust improvement</p> : null}
</div>
);

if (provenanceUrl) {
return (
<ExternalLink
href={provenanceUrl}
className="rounded-lg hover:bg-muted"
>
{content}
</ExternalLink>
);
}

return content;
};

export interface TrustComparisonProps {
a: SimplePackageSpec;
b: SimplePackageSpec;
aEvidence: TrustEvidence;
bEvidence: TrustEvidence;
aProvenanceUrl?: string;
bProvenanceUrl?: string;
}

/** The padding of the center column and the right/left half has to be the same to line up */
const COMMON_PADDING = "p-2";

const TrustComparison: FunctionComponent<TrustComparisonProps> = ({
aEvidence,
bEvidence,
aProvenanceUrl,
bProvenanceUrl,
}) => {
const aRank = getTrustRank(aEvidence);
const bRank = getTrustRank(bEvidence);
const isDowngrade = bRank < aRank;
const isUpgrade = bRank > aRank;

const compareUrl =
aProvenanceUrl && bProvenanceUrl
? getGitHubCompareUrl(aProvenanceUrl, bProvenanceUrl)
: undefined;

return (
<>
<div className={cx("mb-2 text-center text-sm font-semibold")}>
Trust Level
</div>
<Halfs
className="w-full"
left={
<TrustBox
evidence={aEvidence}
provenanceUrl={aProvenanceUrl}
/>
}
center={
<section className={cx(COMMON_PADDING, "text-center")}>
<p>Trust</p>
</section>
}
right={
<TrustBox
evidence={bEvidence}
provenanceUrl={bProvenanceUrl}
isDowngrade={isDowngrade}
isUpgrade={isUpgrade}
/>
}
/>
{compareUrl ? (
<div className="mt-2 text-center text-xs">
<ExternalLink
href={compareUrl}
className="text-blue-600 hover:underline dark:text-blue-400"
>
Compare source on GitHub
</ExternalLink>
</div>
) : null}
</>
);
};

/**
* Extract GitHub compare URL from two provenance URLs
*/
function getGitHubCompareUrl(aUrl: string, bUrl: string): string | undefined {
const aMatch = aUrl.match(
/github\.com\/([^/]+\/[^/]+)\/commit\/([a-f0-9]+)/,
);
const bMatch = bUrl.match(
/github\.com\/([^/]+\/[^/]+)\/commit\/([a-f0-9]+)/,
);

if (aMatch && bMatch && aMatch[1] === bMatch[1]) {
const repo = aMatch[1];
const aCommit = aMatch[2];
const bCommit = bMatch[2];
return `https://github.com/${repo}/compare/${aCommit}...${bCommit}`;
}

return undefined;
}

export default TrustComparison;
36 changes: 36 additions & 0 deletions src/app/[...parts]/_page/TrustComparison/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { type FunctionComponent } from "react";
import Skeleton from "^/components/ui/Skeleton";
import { cx } from "^/lib/cva";
import Halfs from "../DiffIntro/Halfs";

const COMMON_PADDING = "p-2";

export const TrustComparisonSkeleton: FunctionComponent = () => {
return (
<>
<div className={cx("mb-2 text-center text-sm font-semibold")}>
Trust Level
</div>
<Halfs
className="w-full"
left={
<div className={cx(COMMON_PADDING, "text-center")}>
<Skeleton className="mx-auto h-6 w-32" />
</div>
}
center={
<section className={cx(COMMON_PADDING, "text-center")}>
<p>Trust</p>
</section>
}
right={
<div className={cx(COMMON_PADDING, "text-center")}>
<Skeleton className="mx-auto h-6 w-32" />
</div>
}
/>
</>
);
};

export { default } from "./TrustComparison";
41 changes: 41 additions & 0 deletions src/app/[...parts]/_page/TrustDiff.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { cacheLife } from "next/cache";
import trustComparison from "^/lib/api/npm/trustComparison";
import type SimplePackageSpec from "^/lib/SimplePackageSpec";
import suspense from "^/lib/suspense";
import measuredPromise from "^/lib/utils/measuredPromise";
import TrustComparison, { TrustComparisonSkeleton } from "./TrustComparison";

export interface TrustDiffProps {
a: SimplePackageSpec;
b: SimplePackageSpec;
specs: [string, string];
}

const TrustDiffInner = async ({ specs, a, b }: TrustDiffProps) => {
"use cache";

// Cache for hours since trust evidence doesn't change often
cacheLife("hours");

const { result, time } = await measuredPromise(trustComparison(specs));

console.log("Trust comparison", { specs, time });

// Always render - showing "no trust evidence" is also valuable information
return (
<TrustComparison
a={a}
b={b}
aEvidence={result.a.evidence}
bEvidence={result.b.evidence}
aProvenanceUrl={result.a.provenanceUrl}
bProvenanceUrl={result.b.provenanceUrl}
/>
);
};

const TrustDiffSkeleton = () => <TrustComparisonSkeleton />;

const TrustDiff = suspense(TrustDiffInner, TrustDiffSkeleton);

export default TrustDiff;
9 changes: 9 additions & 0 deletions src/app/[...parts]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import DiffIntro from "./_page/DiffIntro";
import NpmDiff from "./_page/NpmDiff";
import PackagephobiaDiff from "./_page/PackagephobiaDiff";
import { type DIFF_TYPE_PARAM_NAME } from "./_page/paramNames";
import TrustDiff from "./_page/TrustDiff";

export interface DiffPageProps {
params: Promise<{ parts: string | string[] }>;
Expand Down Expand Up @@ -75,6 +76,14 @@ const DiffPageInner = async ({
b={b}
services={
<>
<TrustDiff
a={a}
b={b}
specs={canonicalSpecs}
suspenseKey={
"trust-" + canonicalSpecs.join("...")
}
/>
<BundlephobiaDiff
a={a}
b={b}
Expand Down
Loading