Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
137 changes: 137 additions & 0 deletions src/routes/package/+layout.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import semver from "semver";
import { gitHubCache, type GitHubRelease } from "$lib/server/github-cache";
import { discoverer } from "$lib/server/package-discoverer";
import type { Repository } from "$lib/repositories";
import type { Prettify } from "$lib/types";

/**
* Get all the releases for a single package.
*
* @param packageName the package to get the releases for
* @param allPackages all the known packages
* @returns the package's repository alongside its releases, or
* undefined if not found
*/
async function getPackageReleases(
packageName: string,
allPackages: Awaited<ReturnType<typeof discoverer.getOrDiscoverCategorized>>
) {
let currentPackage:
| Prettify<
Omit<Repository, "dataFilter" | "metadataFromTag" | "changelogContentsReplacer"> &
Pick<(typeof allPackages)[number]["packages"][number], "pkg">
>
| undefined = undefined;
const foundVersions = new Set<string>();
const releases: ({ cleanName: string; cleanVersion: string } & GitHubRelease)[] = [];

// Discover releases
console.log("Starting loading releases...");
for (const { category, packages } of allPackages) {
for (const { pkg, ...repo } of packages) {
if (pkg.name.localeCompare(packageName, undefined, { sensitivity: "base" }) !== 0) continue;

// 1. Get releases
const cachedReleases = await gitHubCache.getReleases({ ...repo, category });
console.log(
`${cachedReleases.length} releases found for repo ${repo.owner}/${repo.repoName}`
);

// 2. Filter out invalid ones
const validReleases = cachedReleases
.filter(release => {
const [name] = repo.metadataFromTag(release.tag_name);
return (
(repo.dataFilter?.(release) ?? true) &&
pkg.name.localeCompare(name, undefined, { sensitivity: "base" }) === 0
);
})
.sort((a, b) => {
const [, firstVersion] = repo.metadataFromTag(a.tag_name);
const [, secondVersion] = repo.metadataFromTag(b.tag_name);
return semver.rcompare(firstVersion, secondVersion);
});
console.log("Length after filtering:", validReleases.length);
// Get the releases matching the slug, or all of them if none
// (the latter case for repos with no package in names)
console.log("Final filtered count:", validReleases.length);

// 3. For each release, check if it is already found, searching by versions
const { dataFilter, metadataFromTag, changelogContentsReplacer, ...rest } = repo;
for (const release of validReleases) {
const [cleanName, cleanVersion] = repo.metadataFromTag(release.tag_name);
console.log(`Release ${release.tag_name}, extracted version: ${cleanVersion}`);
if (foundVersions.has(cleanVersion)) continue;

// If not, add its version to the set and itself to the final version
const currentNewestVersion = [...foundVersions].sort(semver.rcompare)[0];
console.log("Current newest version", currentNewestVersion);
foundVersions.add(cleanVersion);
releases.push({ cleanName, cleanVersion, ...release });

// If it is newer than the newest we got, set this repo as the "final repo"
if (!currentNewestVersion || semver.gt(cleanVersion, currentNewestVersion)) {
console.log(
`Current newest version "${currentNewestVersion}" doesn't exist or is lesser than ${cleanVersion}, setting ${rest.owner}/${rest.repoName} as final repo`
);
currentPackage = {
category,
pkg,
...rest
};
}
}
console.log("Done");
}
}

return currentPackage
? {
releasesRepo: currentPackage,
releases: releases.toSorted(
(a, b) =>
new Date(b.published_at ?? b.created_at).getTime() -
new Date(a.published_at ?? a.created_at).getTime()
)
}
: undefined;
}

/**
* Get all the repositories and releases for all the
* known packages.
*
* @param allPackages all the known packages
* @returns a map of package names to their awaitable result
*/
function getAllPackagesReleases(
allPackages: Awaited<ReturnType<typeof discoverer.getOrDiscoverCategorized>>
) {
const packages = allPackages.flatMap(({ packages }) => packages);

return packages.reduce<Record<string, ReturnType<typeof getPackageReleases>>>(
(acc, { pkg: { name } }) => {
acc[name] = getPackageReleases(name, allPackages);
return acc;
},
{}
);
}

/**
* The goal of this load function is to serve any `[...package]`
* page by handing it a bunch of promises, so it can await the one
* it needs. The other ones are for the sidebar badges, so the page
* doesn't have to re-run the data loading every time we switch from
* a package to another.
*/
export async function load() {
// 1. Get all the packages
const categorizedPackages = await discoverer.getOrDiscoverCategorized();

// 2. Use them to get a map of packages to promises of releases
const allReleases = getAllPackagesReleases(categorizedPackages);

// 3. Send all that down to the page's load function
return { allReleases };
}
59 changes: 59 additions & 0 deletions src/routes/package/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<script lang="ts">
import { page } from "$app/state";
import { Menu } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button";
import * as Sheet from "$lib/components/ui/sheet";
import SidePanel from "./SidePanel.svelte";

let { data, children } = $props();

let showPrereleases = $state(true);
</script>

<div class="relative mt-8 flex gap-8 lg:mt-0">
<div class="flex-1">
{@render children()}
</div>

<Sheet.Root>
<Sheet.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="secondary"
class={[
"absolute right-0 mt-12 ml-auto lg:hidden",
page.data.currentPackage.pkg.description?.length && "mt-16"
]}
>
<Menu />
<span class="sr-only">Menu</span>
</Button>
{/snippet}
</Sheet.Trigger>
<Sheet.Content class="overflow-y-auto">
<Sheet.Header>
<Sheet.Title>Packages</Sheet.Title>
</Sheet.Header>
<SidePanel
headless
packageName={page.data.currentPackage.pkg.name}
allPackages={data.displayablePackages}
otherReleases={data.allReleases}
bind:showPrereleases
class="my-8"
/>
</Sheet.Content>
</Sheet.Root>

<SidePanel
packageName={page.data.currentPackage.pkg.name}
allPackages={data.displayablePackages}
otherReleases={data.allReleases}
class={[
"mt-35 hidden h-fit w-100 shrink-0 lg:flex",
page.data.currentPackage.pkg.description?.length && "mt-45"
]}
bind:showPrereleases
/>
</div>
Loading
Loading