Skip to content

Commit 99bf14c

Browse files
feat!: badges in sidebar (#56)
1 parent 6e3677c commit 99bf14c

File tree

6 files changed

+474
-263
lines changed

6 files changed

+474
-263
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import semver from "semver";
2+
import { gitHubCache, type GitHubRelease } from "$lib/server/github-cache";
3+
import { discoverer } from "$lib/server/package-discoverer";
4+
import type { Repository } from "$lib/repositories";
5+
import type { Prettify } from "$lib/types";
6+
7+
/**
8+
* Get all the releases for a single package.
9+
*
10+
* @param packageName the package to get the releases for
11+
* @param allPackages all the known packages
12+
* @returns the package's repository alongside its releases, or
13+
* undefined if not found
14+
*/
15+
async function getPackageReleases(
16+
packageName: string,
17+
allPackages: Awaited<ReturnType<typeof discoverer.getOrDiscoverCategorized>>
18+
) {
19+
let currentPackage:
20+
| Prettify<
21+
Omit<Repository, "dataFilter" | "metadataFromTag" | "changelogContentsReplacer"> &
22+
Pick<(typeof allPackages)[number]["packages"][number], "pkg">
23+
>
24+
| undefined = undefined;
25+
const foundVersions = new Set<string>();
26+
const releases: ({ cleanName: string; cleanVersion: string } & GitHubRelease)[] = [];
27+
28+
// Discover releases
29+
console.log("Starting loading releases...");
30+
for (const { category, packages } of allPackages) {
31+
for (const { pkg, ...repo } of packages) {
32+
if (pkg.name.localeCompare(packageName, undefined, { sensitivity: "base" }) !== 0) continue;
33+
34+
// 1. Get releases
35+
const cachedReleases = await gitHubCache.getReleases({ ...repo, category });
36+
console.log(
37+
`${cachedReleases.length} releases found for repo ${repo.owner}/${repo.repoName}`
38+
);
39+
40+
// 2. Filter out invalid ones
41+
const validReleases = cachedReleases
42+
.filter(release => {
43+
const [name] = repo.metadataFromTag(release.tag_name);
44+
return (
45+
(repo.dataFilter?.(release) ?? true) &&
46+
pkg.name.localeCompare(name, undefined, { sensitivity: "base" }) === 0
47+
);
48+
})
49+
.sort((a, b) => {
50+
const [, firstVersion] = repo.metadataFromTag(a.tag_name);
51+
const [, secondVersion] = repo.metadataFromTag(b.tag_name);
52+
return semver.rcompare(firstVersion, secondVersion);
53+
});
54+
console.log("Length after filtering:", validReleases.length);
55+
// Get the releases matching the slug, or all of them if none
56+
// (the latter case for repos with no package in names)
57+
console.log("Final filtered count:", validReleases.length);
58+
59+
// 3. For each release, check if it is already found, searching by versions
60+
const { dataFilter, metadataFromTag, changelogContentsReplacer, ...rest } = repo;
61+
for (const release of validReleases) {
62+
const [cleanName, cleanVersion] = repo.metadataFromTag(release.tag_name);
63+
console.log(`Release ${release.tag_name}, extracted version: ${cleanVersion}`);
64+
if (foundVersions.has(cleanVersion)) continue;
65+
66+
// If not, add its version to the set and itself to the final version
67+
const currentNewestVersion = [...foundVersions].sort(semver.rcompare)[0];
68+
console.log("Current newest version", currentNewestVersion);
69+
foundVersions.add(cleanVersion);
70+
releases.push({ cleanName, cleanVersion, ...release });
71+
72+
// If it is newer than the newest we got, set this repo as the "final repo"
73+
if (!currentNewestVersion || semver.gt(cleanVersion, currentNewestVersion)) {
74+
console.log(
75+
`Current newest version "${currentNewestVersion}" doesn't exist or is lesser than ${cleanVersion}, setting ${rest.owner}/${rest.repoName} as final repo`
76+
);
77+
currentPackage = {
78+
category,
79+
pkg,
80+
...rest
81+
};
82+
}
83+
}
84+
console.log("Done");
85+
}
86+
}
87+
88+
return currentPackage
89+
? {
90+
releasesRepo: currentPackage,
91+
releases: releases.toSorted(
92+
(a, b) =>
93+
new Date(b.published_at ?? b.created_at).getTime() -
94+
new Date(a.published_at ?? a.created_at).getTime()
95+
)
96+
}
97+
: undefined;
98+
}
99+
100+
/**
101+
* Get all the repositories and releases for all the
102+
* known packages.
103+
*
104+
* @param allPackages all the known packages
105+
* @returns a map of package names to their awaitable result
106+
*/
107+
function getAllPackagesReleases(
108+
allPackages: Awaited<ReturnType<typeof discoverer.getOrDiscoverCategorized>>
109+
) {
110+
const packages = allPackages.flatMap(({ packages }) => packages);
111+
112+
return packages.reduce<Record<string, ReturnType<typeof getPackageReleases>>>(
113+
(acc, { pkg: { name } }) => {
114+
if (acc[name])
115+
console.warn(
116+
`Duplicate package "${name}" while aggregating packages releases; this should not happen!`
117+
);
118+
acc[name] = getPackageReleases(name, allPackages);
119+
return acc;
120+
},
121+
{}
122+
);
123+
}
124+
125+
/**
126+
* The goal of this load function is to serve any `[...package]`
127+
* page by handing it a bunch of promises, so it can await the one
128+
* it needs. The other ones are for the sidebar badges, so the page
129+
* doesn't have to re-run the data loading every time we switch from
130+
* a package to another.
131+
*/
132+
export async function load() {
133+
// 1. Get all the packages
134+
const categorizedPackages = await discoverer.getOrDiscoverCategorized();
135+
136+
// 2. Use them to get a map of packages to promises of releases
137+
const allReleases = getAllPackagesReleases(categorizedPackages);
138+
139+
// 3. Send all that down to the page's load function
140+
return { allReleases };
141+
}

src/routes/package/+layout.svelte

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script lang="ts">
2+
import { page } from "$app/state";
3+
import { Menu } from "@lucide/svelte";
4+
import { Button } from "$lib/components/ui/button";
5+
import * as Sheet from "$lib/components/ui/sheet";
6+
import SidePanel from "./SidePanel.svelte";
7+
8+
let { data, children } = $props();
9+
10+
let showPrereleases = $state(true);
11+
</script>
12+
13+
<div class="relative mt-8 flex gap-8 lg:mt-0">
14+
<div class="flex-1">
15+
{@render children()}
16+
</div>
17+
18+
<Sheet.Root>
19+
<Sheet.Trigger>
20+
{#snippet child({ props })}
21+
<Button
22+
{...props}
23+
variant="secondary"
24+
class={[
25+
"absolute right-0 mt-12 ml-auto lg:hidden",
26+
page.data.currentPackage.pkg.description?.length && "mt-16"
27+
]}
28+
>
29+
<Menu />
30+
<span class="sr-only">Menu</span>
31+
</Button>
32+
{/snippet}
33+
</Sheet.Trigger>
34+
<Sheet.Content class="overflow-y-auto">
35+
<Sheet.Header>
36+
<Sheet.Title>Packages</Sheet.Title>
37+
</Sheet.Header>
38+
<SidePanel
39+
headless
40+
packageName={page.data.currentPackage.pkg.name}
41+
allPackages={data.displayablePackages}
42+
otherReleases={data.allReleases}
43+
bind:showPrereleases
44+
class="my-8"
45+
/>
46+
</Sheet.Content>
47+
</Sheet.Root>
48+
49+
<SidePanel
50+
packageName={page.data.currentPackage.pkg.name}
51+
allPackages={data.displayablePackages}
52+
otherReleases={data.allReleases}
53+
class={[
54+
"mt-35 hidden h-fit w-100 shrink-0 lg:flex",
55+
page.data.currentPackage.pkg.description?.length && "mt-45"
56+
]}
57+
bind:showPrereleases
58+
/>
59+
</div>

0 commit comments

Comments
 (0)