Skip to content

feat: handle deprecated packages #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 11, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
62 changes: 59 additions & 3 deletions src/lib/server/github-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ const FULL_DETAILS_TTL = 60 * 60 * 2; // 2 hours
* The TTL of the cached descriptions, in seconds.
*/
const DESCRIPTIONS_TTL = 60 * 60 * 24 * 10; // 10 days
/**
* The TTL for non-deprecated packages, in seconds
*/
const DEPRECATIONS_TTL = 60 * 60 * 24 * 2; // 2 days

/**
* A fetch layer to reach the GitHub API
Expand Down Expand Up @@ -115,6 +119,21 @@ export class GitHubCache {
return `repo:${owner}/${repo}:${type}${strArgs}`;
}

/**
* Generates a Redis key from the passed info.
*
* @param packageName the package name
* @param args the optional additional values to append
* at the end of the key; every element will be interpolated
* in a string
* @returns the pure computed key
* @private
*/
#getPackageKey(packageName: string, ...args: unknown[]) {
const strArgs = args.map(a => `:${a}`);
return `package:${packageName}${strArgs}`;
}

/**
* An abstraction over general processing that:
* 1. tries getting stuff from Redis cache
Expand All @@ -138,7 +157,7 @@ export class GitHubCache {
cacheKey: string,
promise: () => Promise<PromiseType>,
transformer: (from: Awaited<PromiseType>) => RType | Promise<RType>,
ttl: number | undefined = undefined
ttl: number | ((value: RType) => number | undefined) | undefined = undefined
): Promise<RType> => {
const cachedValue = await this.#redis.json.get<RType>(cacheKey);
if (cachedValue) {
Expand All @@ -152,7 +171,14 @@ export class GitHubCache {

await this.#redis.json.set(cacheKey, "$", newValue);
if (ttl !== undefined) {
await this.#redis.expire(cacheKey, ttl);
if (typeof ttl === "function") {
const ttlResult = ttl(newValue);
if (ttlResult !== undefined) {
await this.#redis.expire(cacheKey, ttlResult);
}
} else {
await this.#redis.expire(cacheKey, ttl);
}
}

return newValue;
Expand Down Expand Up @@ -537,7 +563,6 @@ export class GitHubCache {
* @param repo the GitHub repository name to fetch the
* descriptions in
* @returns a map of paths to descriptions.
* @private
*/
async getDescriptions(owner: string, repo: string) {
return await this.#processCached<{ [key: string]: string }>()(
Expand Down Expand Up @@ -586,6 +611,37 @@ export class GitHubCache {
DESCRIPTIONS_TTL
);
}

/**
* Get the deprecation state of a package from its name.
*
* @param packageName the name of the package to search
* @returns the deprecation status message if any, `false` otherwise
*/
async getPackageDeprecation(packageName: string) {
return await this.#processCached<{ value: string | false }>()(
this.#getPackageKey(packageName, "deprecation"),
async () => {
try {
// npmjs.org in a GitHub cache, I know, but hey, let's put that under the fact that
// GitHub owns npmjs.org okay??
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
if (res.status !== 200) return {};
return (await res.json()) as { deprecated?: boolean | string };
} catch (error) {
console.error(`Error fetching npmjs.org for package ${packageName}:`, error);
return {};
}
},
({ deprecated }) => {
if (deprecated === undefined) return { value: false };
if (typeof deprecated === "boolean")
return { value: deprecated && "This package is deprecated" };
return { value: deprecated || "This package is deprecated" };
},
item => (item.value === false ? DEPRECATIONS_TTL : undefined)
);
}
}

export const gitHubCache = new GitHubCache(KV_REST_API_URL, KV_REST_API_TOKEN, GITHUB_TOKEN);
33 changes: 20 additions & 13 deletions src/lib/server/package-discoverer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { GitHubCache, gitHubCache } from "./github-cache";
type Package = {
name: string;
description: string;
deprecated?: string;
};

export type DiscoveredPackage = Prettify<
Expand Down Expand Up @@ -54,19 +55,25 @@ export class PackageDiscoverer {
);
return {
...repo,
packages: packages.map(pkg => {
const ghName = this.#gitHubDirectoryFromName(pkg);
return {
name: pkg,
description:
descriptions[`packages/${ghName}/package.json`] ??
descriptions[
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
] ??
descriptions["package.json"] ??
""
};
})
packages: await Promise.all(
packages.map(async (pkg): Promise<Package> => {
const ghName = this.#gitHubDirectoryFromName(pkg);
const deprecated = (await this.#cache.getPackageDeprecation(pkg)).value || undefined;
return {
name: pkg,
description: deprecated
? "" // descriptions of deprecated packages are often wrong as their code might be deleted,
: // thus falling back to a higher hierarchy description, often a mismatch
(descriptions[`packages/${ghName}/package.json`] ??
descriptions[
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
] ??
descriptions["package.json"] ??
""),
deprecated
};
})
)
};
})
);
Expand Down
11 changes: 10 additions & 1 deletion src/routes/package/SidePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,16 @@
href="/package/{pkg.name}"
class="group inline-flex w-full items-center gap-1"
>
<span class="underline-offset-4 group-hover:underline">{pkg.name}</span>
<span
class={[
"underline-offset-4 group-hover:underline",
pkg.deprecated &&
"transition-opacity duration-300 line-through opacity-75 group-hover:opacity-100"
]}
title={pkg.deprecated ? "Deprecated: " + pkg.deprecated : undefined}
>
{pkg.name}
</span>
<span class="ml-auto flex items-center gap-1">
{#if linkedBadgeData}
{#await linkedBadgeData then data}
Expand Down
27 changes: 26 additions & 1 deletion src/routes/package/[...package]/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script lang="ts">
import { navigating, page } from "$app/state";
import { ChevronRight, LoaderCircle, Rss } from "@lucide/svelte";
import { ChevronRight, CircleAlert, LoaderCircle, Rss } from "@lucide/svelte";
import semver from "semver";
import * as Accordion from "$lib/components/ui/accordion";
import { Button } from "$lib/components/ui/button";
import * as Card from "$lib/components/ui/card";
import * as Collapsible from "$lib/components/ui/collapsible";
import { Separator } from "$lib/components/ui/separator";
import { Skeleton } from "$lib/components/ui/skeleton";
import AnimatedCollapsibleContent from "$lib/components/AnimatedCollapsibleContent.svelte";
import MarkdownRenderer from "$lib/components/MarkdownRenderer.svelte";
import ReleaseCard from "./ReleaseCard.svelte";

let { data } = $props();
Expand Down Expand Up @@ -128,6 +130,29 @@
.map(({ id }) => id.toString())}
class="w-full space-y-2"
>
{#if data.currentPackage.pkg.deprecated}
<Card.Root class="border-amber-500 bg-amber-400/10 rounded-xl">
<Card.Header class="pb-6">
<Card.Title class="text-xl inline-flex items-center gap-2">
<CircleAlert class="size-5" />
Deprecated
</Card.Title>
<Card.Description>
<MarkdownRenderer
markdown={data.currentPackage.pkg.deprecated}
inline
class="text-sm text-muted-foreground"
>
{#snippet a({ style, children, class: className, title, href, hidden, type })}
<a {style} class={className} {title} {href} {hidden} {type} target="_blank">
{@render children?.()}
</a>
{/snippet}
</MarkdownRenderer>
</Card.Description>
</Card.Header>
</Card.Root>
{/if}
{#each displayableReleases as release, index (release.id)}
{@const semVersion = semver.coerce(release.cleanVersion)}
{@const isMajorRelease =
Expand Down
Loading