Skip to content

Commit 68840a0

Browse files
CopilotoBusk
andcommitted
Fix: Move catalog to /[name] route and fix prerelease version comparisons
Co-authored-by: oBusk <13413409+oBusk@users.noreply.github.com>
1 parent 2b7e2d4 commit 68840a0

7 files changed

Lines changed: 116 additions & 85 deletions

File tree

next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
File renamed without changes.
File renamed without changes.

src/app/[...parts]/page.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { type Metadata } from "next";
2+
import { cacheLife } from "next/cache";
23
import { redirect } from "next/navigation";
4+
import npa from "npm-package-arg";
35
import { type JSX, Suspense } from "react";
46
import { type ViewType } from "react-diff-view";
7+
import Skeleton from "^/components/ui/Skeleton";
8+
import getVersionData from "^/lib/api/npm/getVersionData";
9+
import packument from "^/lib/api/npm/packument";
510
import { createSimplePackageSpec } from "^/lib/createSimplePackageSpec";
611
import { DEFAULT_DIFF_FILES_GLOB } from "^/lib/default-diff-files";
712
import destination from "^/lib/destination";
813
import { parseQuery, type QueryParams } from "^/lib/query";
914
import { simplePackageSpecToString } from "^/lib/SimplePackageSpec";
1015
import decodeParts from "^/lib/utils/decodeParts";
16+
import { generateComparisons } from "^/lib/utils/generateComparisons";
1117
import specsToDiff from "^/lib/utils/specsToDiff";
1218
import splitParts from "^/lib/utils/splitParts";
1319
import BundlephobiaDiff from "./_page/BundlephobiaDiff";
20+
import ComparisonList from "./_page/catalog/ComparisonList";
21+
import PackageMeta from "./_page/catalog/PackageMeta";
1422
import DiffIntro from "./_page/DiffIntro";
1523
import NpmDiff from "./_page/NpmDiff";
1624
import PackagephobiaDiff from "./_page/PackagephobiaDiff";
@@ -28,6 +36,17 @@ export async function generateMetadata({
2836
const { parts } = await params;
2937
const specs = splitParts(decodeParts(parts));
3038

39+
// Check if this is a catalog page (single package name without version)
40+
if (specs.length === 1) {
41+
const parsed = npa(specs[0]);
42+
if (parsed.rawSpec === "*" && parsed.name) {
43+
return {
44+
title: `${parsed.name} - Package Catalog`,
45+
description: `Version catalog and comparison links for the npm package "${parsed.name}"`,
46+
};
47+
}
48+
}
49+
3150
const [a, b] = specs.map((spec) => createSimplePackageSpec(spec));
3251

3352
return {
@@ -36,6 +55,55 @@ export async function generateMetadata({
3655
};
3756
}
3857

58+
async function CatalogPageInner({ packageName }: { packageName: string }) {
59+
"use cache";
60+
61+
cacheLife("hours");
62+
63+
// Fetch package data
64+
const [pack, versionMap] = await Promise.all([
65+
packument(packageName),
66+
getVersionData(packageName),
67+
]);
68+
69+
// Get all versions
70+
const versions = Object.keys(versionMap);
71+
72+
// Generate comparisons
73+
const comparisons = generateComparisons(versions, versionMap);
74+
75+
return (
76+
<div className="mx-auto w-full max-w-7xl py-8">
77+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
78+
<div className="flex flex-col">
79+
<PackageMeta packument={pack} />
80+
</div>
81+
<div className="flex flex-col">
82+
<ComparisonList
83+
packageName={packageName}
84+
comparisons={comparisons}
85+
/>
86+
</div>
87+
</div>
88+
</div>
89+
);
90+
}
91+
92+
function CatalogPageFallback() {
93+
return (
94+
<div className="mx-auto w-full max-w-7xl py-8">
95+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
96+
<div className="flex flex-col">
97+
<Skeleton className="h-96 w-full rounded-md" />
98+
</div>
99+
<div className="flex flex-col">
100+
<Skeleton className="h-96 w-full rounded-md" />
101+
</div>
102+
</div>
103+
</div>
104+
);
105+
}
106+
39107
const DiffPageInner = async ({
40108
params,
41109
searchParams,
@@ -44,6 +112,20 @@ const DiffPageInner = async ({
44112
const { diffFiles, ...optionsQuery } = await searchParams;
45113

46114
const specsOrVersions = splitParts(decodeParts(parts));
115+
116+
// Check if this is a catalog page (single package name without version)
117+
if (specsOrVersions.length === 1) {
118+
const parsed = npa(specsOrVersions[0]);
119+
if (parsed.rawSpec === "*" && parsed.name) {
120+
// This is a catalog page
121+
return (
122+
<Suspense fallback={<CatalogPageFallback />}>
123+
<CatalogPageInner packageName={parsed.name} />
124+
</Suspense>
125+
);
126+
}
127+
}
128+
47129
const { redirect: redirectTarget, canonicalSpecs } =
48130
await destination(specsOrVersions);
49131

src/app/pkg/[name]/page.tsx

Lines changed: 0 additions & 83 deletions
This file was deleted.

src/lib/utils/generateComparisons.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,34 @@ describe("generateComparisons", () => {
195195
expect(comparison.to).toBeTruthy();
196196
});
197197
});
198+
199+
it("does not create patch comparisons for versions with same major.minor.patch but different prerelease", () => {
200+
const versions = [
201+
"19.3.0-canary-f93b9fd4-20251217",
202+
"19.3.0-canary-f6a48828-20251019",
203+
"19.3.0-canary-fb2177c1-20251114",
204+
"19.2.4",
205+
"19.2.3",
206+
];
207+
const versionMap: Record<string, { time: string }> = {};
208+
versions.forEach((v, i) => {
209+
versionMap[v] = {
210+
time: `2025-12-${String(i + 1).padStart(2, "0")}`,
211+
};
212+
});
213+
214+
const result = generateComparisons(versions, versionMap);
215+
216+
// Should not include any patch comparisons between 19.3.0-canary versions
217+
// since they all have the same major.minor.patch
218+
const patchBumps = result.filter((c) => c.type === "patch");
219+
const invalidPatches = patchBumps.filter(
220+
(c) =>
221+
(c.from.startsWith("19.3.0-canary") &&
222+
c.to.startsWith("19.3.0-canary")) ||
223+
(c.from.includes("-canary") && c.to.includes("-canary")),
224+
);
225+
226+
expect(invalidPatches).toHaveLength(0);
227+
});
198228
});

src/lib/utils/generateComparisons.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,9 +185,11 @@ function findPatchBumps(sortedVersions: string[]): Comparison[] {
185185
const from = sortedVersions[i + 1];
186186

187187
// Only consider it a patch bump if major and minor are the same
188+
// and the versions are actually different (not just different prerelease tags)
188189
if (
189190
semver.major(to) === semver.major(from) &&
190-
semver.minor(to) === semver.minor(from)
191+
semver.minor(to) === semver.minor(from) &&
192+
semver.patch(to) !== semver.patch(from)
191193
) {
192194
comparisons.push({
193195
from,

0 commit comments

Comments
 (0)