Skip to content

Commit 9ee49b9

Browse files
feat: handle deprecated packages (#70)
1 parent 7d29fc8 commit 9ee49b9

File tree

4 files changed

+115
-18
lines changed

4 files changed

+115
-18
lines changed

src/lib/server/github-cache.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ const FULL_DETAILS_TTL = 60 * 60 * 2; // 2 hours
7070
* The TTL of the cached descriptions, in seconds.
7171
*/
7272
const DESCRIPTIONS_TTL = 60 * 60 * 24 * 10; // 10 days
73+
/**
74+
* The TTL for non-deprecated packages, in seconds
75+
*/
76+
const DEPRECATIONS_TTL = 60 * 60 * 24 * 2; // 2 days
7377

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

122+
/**
123+
* Generates a Redis key from the passed info.
124+
*
125+
* @param packageName the package name
126+
* @param args the optional additional values to append
127+
* at the end of the key; every element will be interpolated
128+
* in a string
129+
* @returns the pure computed key
130+
* @private
131+
*/
132+
#getPackageKey(packageName: string, ...args: unknown[]) {
133+
const strArgs = args.map(a => `:${a}`);
134+
return `package:${packageName}${strArgs}`;
135+
}
136+
118137
/**
119138
* An abstraction over general processing that:
120139
* 1. tries getting stuff from Redis cache
@@ -138,7 +157,7 @@ export class GitHubCache {
138157
cacheKey: string,
139158
promise: () => Promise<PromiseType>,
140159
transformer: (from: Awaited<PromiseType>) => RType | Promise<RType>,
141-
ttl: number | undefined = undefined
160+
ttl: number | ((value: RType) => number | undefined) | undefined = undefined
142161
): Promise<RType> => {
143162
const cachedValue = await this.#redis.json.get<RType>(cacheKey);
144163
if (cachedValue) {
@@ -152,7 +171,14 @@ export class GitHubCache {
152171

153172
await this.#redis.json.set(cacheKey, "$", newValue);
154173
if (ttl !== undefined) {
155-
await this.#redis.expire(cacheKey, ttl);
174+
if (typeof ttl === "function") {
175+
const ttlResult = ttl(newValue);
176+
if (ttlResult !== undefined) {
177+
await this.#redis.expire(cacheKey, ttlResult);
178+
}
179+
} else {
180+
await this.#redis.expire(cacheKey, ttl);
181+
}
156182
}
157183

158184
return newValue;
@@ -537,7 +563,6 @@ export class GitHubCache {
537563
* @param repo the GitHub repository name to fetch the
538564
* descriptions in
539565
* @returns a map of paths to descriptions.
540-
* @private
541566
*/
542567
async getDescriptions(owner: string, repo: string) {
543568
return await this.#processCached<{ [key: string]: string }>()(
@@ -586,6 +611,37 @@ export class GitHubCache {
586611
DESCRIPTIONS_TTL
587612
);
588613
}
614+
615+
/**
616+
* Get the deprecation state of a package from its name.
617+
*
618+
* @param packageName the name of the package to search
619+
* @returns the deprecation status message if any, `false` otherwise
620+
*/
621+
async getPackageDeprecation(packageName: string) {
622+
return await this.#processCached<{ value: string | false }>()(
623+
this.#getPackageKey(packageName, "deprecation"),
624+
async () => {
625+
try {
626+
// npmjs.org in a GitHub cache, I know, but hey, let's put that under the fact that
627+
// GitHub owns npmjs.org okay??
628+
const res = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
629+
if (res.status !== 200) return {};
630+
return (await res.json()) as { deprecated?: boolean | string };
631+
} catch (error) {
632+
console.error(`Error fetching npmjs.org for package ${packageName}:`, error);
633+
return {};
634+
}
635+
},
636+
({ deprecated }) => {
637+
if (deprecated === undefined) return { value: false };
638+
if (typeof deprecated === "boolean")
639+
return { value: deprecated && "This package is deprecated" };
640+
return { value: deprecated || "This package is deprecated" };
641+
},
642+
item => (item.value === false ? DEPRECATIONS_TTL : undefined)
643+
);
644+
}
589645
}
590646

591647
export const gitHubCache = new GitHubCache(KV_REST_API_URL, KV_REST_API_TOKEN, GITHUB_TOKEN);

src/lib/server/package-discoverer.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GitHubCache, gitHubCache } from "./github-cache";
55
type Package = {
66
name: string;
77
description: string;
8+
deprecated?: string;
89
};
910

1011
export type DiscoveredPackage = Prettify<
@@ -54,19 +55,25 @@ export class PackageDiscoverer {
5455
);
5556
return {
5657
...repo,
57-
packages: packages.map(pkg => {
58-
const ghName = this.#gitHubDirectoryFromName(pkg);
59-
return {
60-
name: pkg,
61-
description:
62-
descriptions[`packages/${ghName}/package.json`] ??
63-
descriptions[
64-
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
65-
] ??
66-
descriptions["package.json"] ??
67-
""
68-
};
69-
})
58+
packages: await Promise.all(
59+
packages.map(async (pkg): Promise<Package> => {
60+
const ghName = this.#gitHubDirectoryFromName(pkg);
61+
const deprecated = (await this.#cache.getPackageDeprecation(pkg)).value || undefined;
62+
return {
63+
name: pkg,
64+
description: deprecated
65+
? "" // descriptions of deprecated packages are often wrong as their code might be deleted,
66+
: // thus falling back to a higher hierarchy description, often a mismatch
67+
(descriptions[`packages/${ghName}/package.json`] ??
68+
descriptions[
69+
`packages/${ghName.substring(ghName.lastIndexOf("/") + 1)}/package.json`
70+
] ??
71+
descriptions["package.json"] ??
72+
""),
73+
deprecated
74+
};
75+
})
76+
)
7077
};
7178
})
7279
);

src/routes/package/SidePanel.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,16 @@
157157
href="/package/{pkg.name}"
158158
class="group inline-flex w-full items-center gap-1"
159159
>
160-
<span class="underline-offset-4 group-hover:underline">{pkg.name}</span>
160+
<span
161+
class={[
162+
"underline-offset-4 group-hover:underline",
163+
pkg.deprecated &&
164+
"transition-opacity duration-300 line-through opacity-75 group-hover:opacity-100"
165+
]}
166+
title={pkg.deprecated ? "Deprecated: " + pkg.deprecated : undefined}
167+
>
168+
{pkg.name}
169+
</span>
161170
<span class="ml-auto flex items-center gap-1">
162171
{#if linkedBadgeData}
163172
{#await linkedBadgeData then data}

src/routes/package/[...package]/+page.svelte

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
<script lang="ts">
22
import { navigating, page } from "$app/state";
3-
import { ChevronRight, LoaderCircle, Rss } from "@lucide/svelte";
3+
import { ChevronRight, CircleAlert, LoaderCircle, Rss } from "@lucide/svelte";
44
import semver from "semver";
55
import * as Accordion from "$lib/components/ui/accordion";
66
import { Button } from "$lib/components/ui/button";
7+
import * as Card from "$lib/components/ui/card";
78
import * as Collapsible from "$lib/components/ui/collapsible";
89
import { Separator } from "$lib/components/ui/separator";
910
import { Skeleton } from "$lib/components/ui/skeleton";
1011
import AnimatedCollapsibleContent from "$lib/components/AnimatedCollapsibleContent.svelte";
12+
import MarkdownRenderer from "$lib/components/MarkdownRenderer.svelte";
1113
import ReleaseCard from "./ReleaseCard.svelte";
1214
1315
let { data } = $props();
@@ -128,6 +130,29 @@
128130
.map(({ id }) => id.toString())}
129131
class="w-full space-y-2"
130132
>
133+
{#if data.currentPackage.pkg.deprecated}
134+
<Card.Root class="border-amber-500 bg-amber-400/10 rounded-xl">
135+
<Card.Header class="pb-6">
136+
<Card.Title class="text-xl inline-flex items-center gap-2">
137+
<CircleAlert class="size-5" />
138+
Deprecated
139+
</Card.Title>
140+
<Card.Description>
141+
<MarkdownRenderer
142+
markdown={data.currentPackage.pkg.deprecated}
143+
inline
144+
class="text-sm text-muted-foreground"
145+
>
146+
{#snippet a({ style, children, class: className, title, href, hidden, type })}
147+
<a {style} class={className} {title} {href} {hidden} {type} target="_blank">
148+
{@render children?.()}
149+
</a>
150+
{/snippet}
151+
</MarkdownRenderer>
152+
</Card.Description>
153+
</Card.Header>
154+
</Card.Root>
155+
{/if}
131156
{#each displayableReleases as release, index (release.id)}
132157
{@const semVersion = semver.coerce(release.cleanVersion)}
133158
{@const isMajorRelease =

0 commit comments

Comments
 (0)