Skip to content

Commit fba4511

Browse files
feat(packages): get package descriptions
1 parent 1058317 commit fba4511

File tree

7 files changed

+149
-33
lines changed

7 files changed

+149
-33
lines changed

src/lib/server/github-cache.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type GitHubRelease = Awaited<
88
ReturnType<InstanceType<typeof Octokit>["rest"]["repos"]["listReleases"]>
99
>["data"][number];
1010

11+
type KeyType = "releases" | "descriptions";
12+
1113
/**
1214
* The maximum items amount to get per-page
1315
* when fetching from GitHub API.
@@ -57,11 +59,12 @@ export class GitHubCache {
5759
*
5860
* @param owner the GitHub repository owner
5961
* @param repo the GitHub repository name
62+
* @param type the kind of cache to use
6063
* @returns the pure computed key
6164
* @private
6265
*/
63-
#getRepoKey(owner: string, repo: string) {
64-
return `repo:${owner}/${repo}:releases`;
66+
#getRepoKey(owner: string, repo: string, type: KeyType) {
67+
return `repo:${owner}/${repo}:${type}`;
6568
}
6669

6770
/**
@@ -71,15 +74,15 @@ export class GitHubCache {
7174
* @returns the releases, either cached or fetched
7275
*/
7376
async getReleases(repository: Repository) {
74-
const cacheKey = this.#getRepoKey(repository.owner, repository.repoName);
77+
const cacheKey = this.#getRepoKey(repository.owner, repository.repoName, "releases");
7578

7679
const cachedReleases = await this.#redis.json.get<GitHubRelease[]>(cacheKey);
7780
if (cachedReleases) {
78-
console.log(`Cache hit for ${cacheKey}`);
81+
console.log(`Cache hit for releases for ${cacheKey}`);
7982
return cachedReleases;
8083
}
8184

82-
console.log(`Cache miss for ${cacheKey}, fetching from GitHub API`);
85+
console.log(`Cache miss for releases for ${cacheKey}, fetching from GitHub API`);
8386

8487
const releases = await this.#fetchReleases(repository);
8588

@@ -213,6 +216,72 @@ export class GitHubCache {
213216
);
214217
}
215218

219+
/**
220+
* Get a map that contains the descriptions
221+
* of all the packages in the given repository.
222+
* Irrelevant paths (e.g., tests) or empty descriptions
223+
* are excluded.
224+
*
225+
* @param repository the repository to fetch the
226+
* descriptions in
227+
* @returns a map of paths to descriptions.
228+
* @private
229+
*/
230+
async getDescriptions(repository: Repository) {
231+
const cacheKey = this.#getRepoKey(repository.owner, repository.repoName, "descriptions");
232+
233+
const cachedDescriptions = await this.#redis.json.get<{ [key: string]: string }>(cacheKey);
234+
if (cachedDescriptions) {
235+
console.log(`Cache hit for descriptions for ${cacheKey}`);
236+
return cachedDescriptions;
237+
}
238+
239+
console.log(`Cache miss for releases for ${cacheKey}, fetching from GitHub API`);
240+
241+
const { owner, repoName: repo } = repository;
242+
243+
const { data: allFiles } = await this.#octokit.rest.git.getTree({
244+
owner,
245+
repo,
246+
tree_sha: "HEAD",
247+
recursive: "true"
248+
});
249+
250+
const allPackageJson = allFiles.tree
251+
.map(({ path }) => path)
252+
.filter(path => path !== undefined)
253+
.filter(
254+
path =>
255+
!path.includes("/test/") && (path === "package.json" || path.endsWith("/package.json"))
256+
);
257+
258+
const descriptions = new Map<string, string>();
259+
for (const path of allPackageJson) {
260+
const { data: packageJson } = await this.#octokit.rest.repos.getContent({
261+
owner,
262+
repo,
263+
path
264+
});
265+
266+
if (!("content" in packageJson)) continue; // filter out empty or multiple results
267+
const { content, encoding, type } = packageJson;
268+
if (type !== "file" || !content) continue; // filter out directories and empty files
269+
const packageFile =
270+
encoding === "base64" ? Buffer.from(content, "base64").toString() : content;
271+
272+
try {
273+
const { description } = JSON.parse(packageFile) as { description: string };
274+
if (description) descriptions.set(path, description);
275+
} catch {
276+
// ignore
277+
}
278+
}
279+
280+
await this.#redis.json.set(cacheKey, "$", Object.fromEntries(descriptions));
281+
282+
return Object.fromEntries(descriptions);
283+
}
284+
216285
/**
217286
* Checks if releases are present in the cache for the
218287
* given GitHub info
@@ -221,10 +290,11 @@ export class GitHubCache {
221290
* existence in the cache for
222291
* @param repo the name of the GitHub repository to check the
223292
* existence in the cache for
293+
* @param type the kind of cache to target
224294
* @returns whether the repository is cached or not
225295
*/
226-
async exists(owner: string, repo: string) {
227-
const cacheKey = this.#getRepoKey(owner, repo);
296+
async exists(owner: string, repo: string, type: KeyType) {
297+
const cacheKey = this.#getRepoKey(owner, repo, type);
228298
const result = await this.#redis.exists(cacheKey);
229299
return result === 1;
230300
}
@@ -236,9 +306,10 @@ export class GitHubCache {
236306
* from the cache
237307
* @param repo the name of the GitHub repository to remove
238308
* from the cache
309+
* @param type the kind of cache to target
239310
*/
240-
async deleteEntry(owner: string, repo: string) {
241-
const cacheKey = this.#getRepoKey(owner, repo);
311+
async deleteEntry(owner: string, repo: string, type: KeyType) {
312+
const cacheKey = this.#getRepoKey(owner, repo, type);
242313
await this.#redis.del(cacheKey);
243314
}
244315
}

src/lib/server/package-discoverer.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@ import type { Prettify } from "$lib/types";
22
import { GitHubCache, gitHubCache } from "./github-cache";
33
import { publicRepos, type Repository } from "$lib/repositories";
44

5+
type Package = {
6+
name: string;
7+
description: string;
8+
};
9+
510
export type DiscoveredPackage = Prettify<
611
Repository & {
7-
packages: string[];
12+
packages: Package[];
813
}
914
>;
1015

1116
export type CategorizedPackage = Prettify<
1217
Pick<Repository, "category"> & {
13-
packages: (Omit<Repository, "category"> & { packageName: string })[];
18+
packages: (Omit<Repository, "category"> & { pkg: Package })[];
1419
}
1520
>;
1621

@@ -33,6 +38,7 @@ export class PackageDiscoverer {
3338
this.#packages = await Promise.all(
3439
this.#repos.map(async repo => {
3540
const releases = await this.#cache.getReleases(repo);
41+
const descriptions = await this.#cache.getDescriptions(repo);
3642
const packages = [
3743
...new Set(
3844
releases
@@ -46,11 +52,48 @@ export class PackageDiscoverer {
4652
console.log(
4753
`Discovered ${packages.length} packages for ${repo.owner}/${repo.repoName}: ${packages.join(", ")}`
4854
);
49-
return { ...repo, packages };
55+
return {
56+
...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+
})
70+
};
5071
})
5172
);
5273
}
5374

75+
/**
76+
* Returns the directory on GitHub from the name
77+
* of the package.
78+
* Useful to retrieve the correct `package.json` file.
79+
*
80+
* @param name the package name
81+
* @returns the directory name in GitHub for that package
82+
* @private
83+
*/
84+
#gitHubDirectoryFromName(name: string): string {
85+
switch (name) {
86+
case "extensions":
87+
return "svelte-vscode";
88+
case "sv":
89+
return "cli";
90+
case "svelte-migrate":
91+
return "migrate";
92+
default:
93+
return name;
94+
}
95+
}
96+
5497
/**
5598
* Returns the saved packages if they're not empty,
5699
* otherwise calls {@link discoverAll} then returns the
@@ -76,9 +119,9 @@ export class PackageDiscoverer {
76119
async getOrDiscoverCategorized() {
77120
return (await this.getOrDiscover()).reduce<CategorizedPackage[]>(
78121
(acc, { category, ...rest }) => {
79-
const formattedPackages = rest.packages.map(packageName => ({
122+
const formattedPackages = rest.packages.map(pkg => ({
80123
...rest,
81-
packageName
124+
pkg
82125
}));
83126

84127
for (const [i, item] of acc.entries()) {
@@ -91,9 +134,9 @@ export class PackageDiscoverer {
91134
// If the category doesn't exist in the accumulator, create it
92135
acc.push({
93136
category,
94-
packages: rest.packages.map(packageName => ({
137+
packages: rest.packages.map(pkg => ({
95138
...rest,
96-
packageName
139+
pkg
97140
}))
98141
});
99142

src/routes/+layout.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ export async function load() {
1111
packages: uniq(
1212
res.packages
1313
.map(({ dataFilter, metadataFromTag, changelogContentsReplacer, ...rest }) => rest)
14-
.toSorted((a, b) => a.packageName.localeCompare(b.packageName)),
15-
item => item.packageName
14+
.toSorted((a, b) => a.pkg.name.localeCompare(b.pkg.name)),
15+
item => item.pkg.name
1616
)
1717
}))
1818
};

src/routes/package/[...package]/+page.server.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@ export async function load({ params }) {
99
const categorizedPackages = await discoverer.getOrDiscoverCategorized();
1010

1111
let currentPackage:
12-
| (Omit<Repository, "dataFilter" | "metadataFromTag" | "changelogContentsReplacer"> & {
13-
packageName: string;
14-
})
12+
| (Omit<Repository, "dataFilter" | "metadataFromTag" | "changelogContentsReplacer"> &
13+
Pick<(typeof categorizedPackages)[number]["packages"][number], "pkg">)
1514
| undefined = undefined;
1615
const foundVersions = new Set<string>();
1716
const releases: (GitHubRelease & { cleanVersion: string })[] = [];
1817

1918
// Discover releases
2019
console.log("Starting loading releases...");
2120
for (const { category, packages } of categorizedPackages) {
22-
for (const { packageName, ...repo } of packages) {
23-
if (packageName.toLowerCase() === slugPackage.toLowerCase()) {
21+
for (const { pkg, ...repo } of packages) {
22+
if (pkg.name.toLowerCase() === slugPackage.toLowerCase()) {
2423
// 1. Get releases
2524
const cachedReleases = await gitHubCache.getReleases({ ...repo, category });
2625
console.log(
@@ -65,7 +64,7 @@ export async function load({ params }) {
6564
);
6665
currentPackage = {
6766
category,
68-
packageName,
67+
pkg,
6968
...rest
7069
};
7170
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@
2323
{@render loading()}
2424
{:then}
2525
<div class="my-8">
26-
<h1 class="text-5xl font-semibold text-primary">{data.currentPackage.packageName}</h1>
26+
<h1 class="text-5xl font-semibold text-primary">{data.currentPackage.pkg.name}</h1>
2727
<h2 class="text-xl text-muted-foreground">
2828
{data.currentPackage.owner}/{data.currentPackage.repoName}
2929
</h2>
30+
{#if data.currentPackage.pkg.description}
31+
<h3 class="mt-4 italic">{data.currentPackage.pkg.description}</h3>
32+
{/if}
3033
</div>
3134
<div class="flex gap-8">
3235
<Accordion.Root
@@ -41,14 +44,14 @@
4144
>
4245
{#each data.releases as release (release.id)}
4346
<ReleaseCard
44-
packageName={data.currentPackage.packageName}
47+
packageName={data.currentPackage.pkg.name}
4548
repo={{ owner: data.currentPackage.owner, name: data.currentPackage.repoName }}
4649
{release}
4750
/>
4851
{/each}
4952
</Accordion.Root>
5053
<SidePanel
51-
packageName={data.currentPackage.packageName}
54+
packageName={data.currentPackage.pkg.name}
5255
allPackages={data.displayablePackages}
5356
class="h-fit w-2/5"
5457
/>

src/routes/package/[...package]/SidePanel.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@
3535
<li class="space-y-2">
3636
<h3 class="text-xl font-bold text-primary">{category.name}</h3>
3737
<ul class="space-y-2">
38-
{#each packages as { packageName } (packageName)}
38+
{#each packages as { pkg } (pkg.name)}
3939
<li>
40-
<a href="/package/{packageName}" class="underline-offset-4 hover:underline">
41-
{packageName}
40+
<a href="/package/{pkg.name}" class="underline-offset-4 hover:underline">
41+
{pkg.name}
4242
</a>
4343
</li>
4444
{/each}

src/routes/packages/+page.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@
1010
<li>
1111
<h3 class="text-2xl font-bold text-primary">{category.name}</h3>
1212
<ul class="mt-2">
13-
{#each packages as { owner, repoName, packageName }, index (packageName)}
13+
{#each packages as { owner, repoName, pkg }, index (pkg.name)}
1414
{#if index > 0}
1515
<Separator class="mx-auto my-1 w-[95%]" />
1616
{/if}
1717
<li>
1818
<a
19-
href="/package/{packageName}"
19+
href="/package/{pkg.name}"
2020
class="group flex items-center rounded-lg px-4 py-3 transition-colors hover:bg-neutral-800"
2121
>
2222
<div class="flex flex-col">
23-
<h4 class="font-medium">{packageName}</h4>
23+
<h4 class="font-medium">{pkg.name}</h4>
2424
{#if category.slug === "others"}
2525
<span class="text-muted-foreground">
2626
{owner}/{repoName}

0 commit comments

Comments
 (0)