diff --git a/src/lib/server/github-cache.ts b/src/lib/server/github-cache.ts index d38ec580..b09f8c09 100644 --- a/src/lib/server/github-cache.ts +++ b/src/lib/server/github-cache.ts @@ -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 @@ -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 @@ -138,7 +157,7 @@ export class GitHubCache { cacheKey: string, promise: () => Promise, transformer: (from: Awaited) => RType | Promise, - ttl: number | undefined = undefined + ttl: number | ((value: RType) => number | undefined) | undefined = undefined ): Promise => { const cachedValue = await this.#redis.json.get(cacheKey); if (cachedValue) { @@ -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; @@ -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 }>()( @@ -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); diff --git a/src/lib/server/package-discoverer.ts b/src/lib/server/package-discoverer.ts index b6ee9e52..7ad92d0f 100644 --- a/src/lib/server/package-discoverer.ts +++ b/src/lib/server/package-discoverer.ts @@ -5,6 +5,7 @@ import { GitHubCache, gitHubCache } from "./github-cache"; type Package = { name: string; description: string; + deprecated?: string; }; export type DiscoveredPackage = Prettify< @@ -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 => { + 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 + }; + }) + ) }; }) ); diff --git a/src/routes/package/SidePanel.svelte b/src/routes/package/SidePanel.svelte index 546fe871..39ccc706 100644 --- a/src/routes/package/SidePanel.svelte +++ b/src/routes/package/SidePanel.svelte @@ -157,7 +157,16 @@ href="/package/{pkg.name}" class="group inline-flex w-full items-center gap-1" > - {pkg.name} + + {pkg.name} + {#if linkedBadgeData} {#await linkedBadgeData then data} diff --git a/src/routes/package/[...package]/+page.svelte b/src/routes/package/[...package]/+page.svelte index 9938b0d0..61b294c8 100644 --- a/src/routes/package/[...package]/+page.svelte +++ b/src/routes/package/[...package]/+page.svelte @@ -1,13 +1,15 @@