Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
513bf70
feat: add CNPM registry browser
thonatos Aug 5, 2026
97fd3e9
docs: archive completed changes and sync specs
thonatos Aug 5, 2026
2360e93
fix: address copilot review on CNPM registry browser
thonatos Aug 5, 2026
05eaa9e
fix: address suppressed copilot suggestions
thonatos Aug 5, 2026
95ff0d5
fix: apply second-round copilot review suggestions
thonatos Aug 5, 2026
47abfd9
fix: apply third-round copilot review suggestions
thonatos Aug 5, 2026
d552885
fix: apply fourth-round copilot review suggestions
thonatos Aug 5, 2026
a3624b3
fix: skip search fetch when query is blank, keep dir error in own row
thonatos Aug 5, 2026
be99570
fix: full self-review of CNPM registry browser
thonatos Aug 5, 2026
96144bd
fix: address suppressed copilot suggestions on download card and sear…
thonatos Aug 5, 2026
dc16c6d
fix: deterministic version fallback, sanitize chart id, support git+h…
thonatos Aug 5, 2026
6d11236
fix: strip userinfo from git repos, render explicit error for empty m…
thonatos Aug 5, 2026
509096e
fix: encode scoped package names as a single path segment
thonatos Aug 5, 2026
fd85328
fix: typography and avatar polish from copilot review
thonatos Aug 5, 2026
629bbec
fix: clamp download range and use trailing-slash files root
thonatos Aug 5, 2026
d26993c
fix: normalize trailing slashes in getDir paths
thonatos Aug 6, 2026
a1608a5
fix: address copilot suggestions on stats, autofocus, dates, keys
thonatos Aug 6, 2026
644b2cd
fix: generate download range dates in UTC
thonatos Aug 6, 2026
5e4b4f9
fix: normalize invalid version query params to the resolved version
thonatos Aug 6, 2026
d95f289
docs: cap Copilot review re-requests at 5 rounds per PR
thonatos Aug 6, 2026
f40d0bd
fix: validate homepage/tarball URL schemes to prevent javascript: inj…
Copilot Aug 6, 2026
da9b9a8
chore: regenerate lockfile after rebase on main
thonatos Aug 6, 2026
d1a2c14
fix: satisfy type-aware lint rules introduced on main
thonatos Aug 6, 2026
27439bb
docs: drop Copilot review loop cap from AGENTS.md
thonatos Aug 6, 2026
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
11 changes: 11 additions & 0 deletions apps/web/app/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
User,
Settings,
LogOut,
Boxes,
} from "lucide-react";
import { ThemeToggle } from "./ThemeToggle";
import { useNavTransition } from "./NavProgress";
Expand Down Expand Up @@ -102,6 +103,10 @@ export function Header() {
{z.name}
</NavLink>
))}
<NavLink to="/cnpm">
<Boxes className="h-4 w-4" />
CNPM
</NavLink>
<NavLink to="/api">
<Code className="h-4 w-4" />
API
Expand Down Expand Up @@ -317,6 +322,12 @@ function MobileNavTrigger({ visibleZones = [] }: { visibleZones?: any[] }) {
>
<Code className="h-5 w-5 text-primary" /> API
</Link>
<Link
to="/cnpm"
className="flex min-h-12 items-center gap-3 rounded-xl bg-muted px-3 text-sm text-foreground transition-colors hover:bg-accent"
>
<Boxes className="h-5 w-5 text-primary" /> CNPM
</Link>
<Link
to="/about"
className="flex min-h-12 items-center gap-3 rounded-xl bg-muted px-3 text-sm text-foreground transition-colors hover:bg-accent"
Expand Down
78 changes: 78 additions & 0 deletions apps/web/app/components/cnpm/DepsView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Link } from "react-router";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import type { RegistryManifest } from "~/lib/registry/types";

type DepGroup = {
key: "dependencies" | "devDependencies" | "optionalDependencies" | "peerDependencies";
label: string;
};

const GROUPS: DepGroup[] = [
{ key: "dependencies", label: "dependencies" },
{ key: "devDependencies", label: "devDependencies" },
{ key: "optionalDependencies", label: "optionalDependencies" },
{ key: "peerDependencies", label: "peerDependencies" },
];

export function DepsView({ manifest, version }: { manifest: RegistryManifest; version: string }) {
const versionData = manifest.versions?.[version];
if (!versionData) {
return <p className="text-sm text-muted-foreground">该版本没有依赖信息</p>;
}

const groups = GROUPS.filter((group) => {
const deps = versionData[group.key];
return deps && Object.keys(deps).length > 0;
});

if (groups.length === 0) {
return <p className="text-sm text-muted-foreground">该版本没有任何依赖</p>;
}

return (
<div className="flex flex-col gap-6">
{groups.map((group) => {
const deps = versionData[group.key]!;
const entries = Object.entries(deps).sort(([a], [b]) => a.localeCompare(b));
return (
<div key={group.key} className="flex flex-col gap-2">
<h3 className="font-mono text-sm font-medium text-muted-foreground">
{group.label}
<span className="ml-2 text-muted-foreground/70">{entries.length}</span>
</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>名称</TableHead>
<TableHead>版本范围</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{entries.map(([pkg, spec]) => (
<TableRow key={pkg}>
<TableCell>
<Link
to={`/cnpm/pkg/${pkg}`}
className="text-foreground hover:text-primary"
>
{pkg}
</Link>
Comment on lines +61 to +66
</TableCell>
<TableCell className="font-mono text-muted-foreground">{spec}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
})}
</div>
);
}
132 changes: 132 additions & 0 deletions apps/web/app/components/cnpm/DownloadCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { getDownloads, useRegistryQuery } from "~/lib/registry/client";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "~/components/ui/chart";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import { Skeleton } from "~/components/ui/skeleton";
import { Empty, EmptyDescription, EmptyTitle } from "~/components/ui/empty";
import { Alert, AlertDescription } from "~/components/ui/alert";
import { Button } from "~/components/ui/button";

const chartConfig = {
downloads: {
label: "下载量",
color: "var(--primary)",
},
} satisfies ChartConfig;

export function DownloadCard({
pkgName,
range = 7,
}: {
pkgName: string;
range?: number;
}) {
const { data, error, loading, retry } = useRegistryQuery(
() => getDownloads(pkgName, range),
[pkgName, range],
);

if (loading) {
return (
<Card>
<CardHeader>
<CardTitle>下载量</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-3">
<Skeleton className="h-6 w-24" />
<Skeleton className="h-24 w-full" />
</CardContent>
</Card>
);
}

if (error) {
return (
<Card>
<CardHeader>
<CardTitle>下载量</CardTitle>
</CardHeader>
<CardContent>
<Alert variant="destructive">
<AlertDescription className="flex items-center justify-between gap-3">
下载数据加载失败
<Button type="button" variant="outline" size="sm" onClick={retry}>
重试
</Button>
</AlertDescription>
</Alert>
</CardContent>
</Card>
);
}

const points = data?.downloads || [];
const total = points.reduce((sum, point) => sum + point.downloads, 0);

if (points.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>下载量</CardTitle>
</CardHeader>
<CardContent>
<Empty>
<EmptyTitle>暂无数据</EmptyTitle>
<EmptyDescription>该包暂无下载数据</EmptyDescription>
</Empty>
</CardContent>
</Card>
);
}

return (
<Card>
<CardHeader>
<CardTitle className="flex items-baseline gap-2">
近 {range} 天下载
<span className="font-mono text-xl font-semibold tabular-nums text-foreground">
{total.toLocaleString("en-US")}
</span>
</CardTitle>
</CardHeader>
<CardContent>
<ChartContainer
config={chartConfig}
className="h-32 aspect-auto [&_.recharts-text]:fill-muted-foreground"
>
<AreaChart data={points} margin={{ left: 0, right: 0, top: 4, bottom: 0 }}>
<defs>
<linearGradient id="cnpm-download-fill" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="var(--color-downloads)" stopOpacity={0.35} />
<stop offset="95%" stopColor="var(--color-downloads)" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid vertical={false} strokeDasharray="4 4" />
<XAxis
dataKey="day"
tickLine={false}
axisLine={false}
tickMargin={8}
minTickGap={24}
tickFormatter={(value: string) => value.slice(5)}
/>
<ChartTooltip content={<ChartTooltipContent />} />
<Area
dataKey="downloads"
type="monotone"
fill="url(#cnpm-download-fill)"
stroke="var(--color-downloads)"
strokeWidth={2}
dot={false}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
Loading