Vue and svelte people can't relate to this one as their framework have their own extensions which makes them show up as languages in Github .
To list how many projects we've used react, Solid or Preact in , we'll need to check that package.json in our Github
first let's create a helper that will determine how we group the libraries
1export interface RequiredDecodedPackageJson {
2 name: string;
3 private?: boolean;
4 version: string;
5 type?: string;
6 scripts: KeyStringObject;
7 dependencies: KeyStringObject;
8 devDependencies: KeyStringObject;
9 [key: string]: any | undefined;
10}
11export type TPkgType =
12 | "React+Vite"
13 | "React+Relay"
14 | "Rakkasjs"
15 | "Nextjs"
16 | "Nodejs"
17 | "Deno"
18 | "React-native"
19 | "Bun"
20 | "Others";
21
22export function pkgTypeCondition(pkg: RequiredDecodedPackageJson): {
23 pkg_type: TPkgType;
24 condition: boolean;
25} {
26 const pkgs_string = JSON.stringify({
27 ...pkg.dependencies,
28 ...pkg.devDependencies,
29 });
30 if (pkgs_string.includes("rakkas")) {
31 return { pkg_type: "Rakkasjs", condition: true };
32 }
33 if (pkgs_string.includes("react-native")) {
34 return { pkg_type: "React-native", condition: true };
35 }
36
37 if (pkg.dependencies?.next) {
38 return { pkg_type: "Nextjs", condition: true };
39 }
40
41 if (pkg.dependencies?.react && pkg.dependencies?.["react-relay"]) {
42 return { pkg_type: "React+Relay", condition: true };
43 }
44
45 if (pkg.devDependencies?.vite && pkg.dependencies?.react) {
46 return { pkg_type: "React+Vite", condition: true };
47 }
48
49 if (
50 pkgs_string.includes("nodemon") ||
51 pkgs_string.includes("tsup") ||
52 pkgs_string.includes("fastify") ||
53 pkgs_string.includes("express") ||
54 pkgs_string.includes("nestjs") ||
55 pkgs_string.includes("mongoose")
56 ) {
57 return { pkg_type: "Nodejs", condition: true };
58 }
59 return { pkg_type: "Others", condition: false };
60}
now let's setup a function to fetch the package.json for a repository
```ts
// get repository package.json
// # TODO : check the repository language and early return if it's // not JS or TS
export async function getOneRepoPackageJson(
owner_repo: string,
viewer_token: string,
) {
try {
const headersList = {
Authorization: bearer ${viewer_token},
Accept: "application/vnd.github+json",
};
// is nodejs based
const response = await fetch(
https://api.github.com/repos/${owner_repo}/contents/package.json,
{
method: "GET",
headers: headersList,
},
);
const data = await response.json();
// console.log("package.json data ==== ", data);
if (data && data.encoding === "base64" && data.content) {
const stringBuffer = new TextDecoder().decode(
base64ToUint8Array(data.content),
);
const pkgjson = JSON.parse(stringBuffer) as DecodedPackageJson;
return await modifyPackageJson(pkgjson);
}
const deno_lock_response = await fetch(
https://api.github.com/repos/${owner_repo}/contents/deno.lock,
{
method: "GET",
headers: headersList,
},
);
const deno_lock_data = await deno_lock_response.json();
if (
!("documentation_url" in deno_lock_data && "message" in deno_lock_data)
) {
return {
name: owner_repo.split("/")[1],
favdeps: ["deno"],
dependencies: { deno: "latest" },
pkg_type: "Deno",
} as any as DecodedPackageJson;
}
const deno_json_response = await fetch(
https://api.github.com/repos/${owner_repo}/contents/deno.json,
{
method: "GET",
headers: headersList,
},
);
const deno_json_data = await deno_json_response.json();
if (
!("documentation_url" in deno_json_data && "message" in deno_json_data)
) {
return {
name: owner_repo.split("/")[1],
favdeps: ["deno"],
dependencies: { deno: "latest" },
pkg_type: "Deno",
} as any as DecodedPackageJson;
}
const bun_lock_response = await fetch(
https://api.github.com/repos/${owner_repo}/contents/bun.lock,
{
method: "GET",
headers: headersList,
},
);
const bun_lock_data = await bun_lock_response.json();
if (!("documentation_url" in bun_lock_data && "message" in bun_lock_data)) {
return {
name: owner_repo.split("/")[1],
favdeps: ["bun"],
dependencies: { bun: "latest" },
pkg_type: "Bun",
} as any as DecodedPackageJson;
}
return data as DecodedPackageJson;
} catch (error) {
logError("error getOneRepoPackageJson >>>>>>>>>>>> ", error);
return error as DecodedPackageJson;
}
}
1The function will also try to check if the repository is a Deno project by checking for a `deno.json` if it can't find a `package.json`
2
3The base64 encoder can vary between runtimes , the one below works on Deno
ts function base64ToUint8Array(base64: string): Uint8Array { const raw = atob(base64); const array = new Uint8Array(raw.length); for (let i = 0; i < raw.length; i++) { array[i] = raw.charCodeAt(i); } return array; }
1
2once we get our `package.json` , we can now modify it before saving it in a DB for future reference to avoid doing this step too often and grinding up against Github's rate limits.
3
ts // modify package.json to addthe pkg_type export function modifyPackageJson(pgkjson: DecodedPackageJson) { if ("name" in pgkjson) { const typeCondition = pkgTypeCondition(pgkjson); pgkjson["pkg_type"] = typeCondition.pkg_type;
const alldeps = Object.keys(pgkjson.dependencies) .map((key) => { return key.split("^")[0]; }) .concat( Object.keys(pgkjson.devDependencies).map((key) => { return key.split("^")[0]; }), );
const favdeps = mostFaveDepsList.filter((key) => { return alldeps.find((dep) => { return dep.includes(key); }); });
pgkjson["favdeps"] = favdeps; return pgkjson; } return pgkjson; }
1
2
3
4And with that all we need is to run our [recursive repo fetcher](https://gist.github.com/tigawanna/3ae4b14f2820c50f5b3d993c4f8773c6) and for every item execute this function
5
6
ts const viewer_token"ghp_0Bw5w5H5e8u7v9wjVBw5w5H5e8uw7W1e3z5n2vZr4P6qPq"; const repos = await fetchReposRecursivelyWithGQL({ viewer_token: gh_token, }); const reposPkgJson: DecodedPackageJson[] = [];
if (all_repos.data?.data) { const reposList = all_repos.data?.data.viewer.repositories.edges; for await (const repo of reposList) { const pkgjson = await getOneRepoPackageJson( repo.node.nameWithOwner, viewer_token, ); if ("message" in pkgjson && "documentation_url" in pkgjson) { continue; } if (pkgjson) { pkgjson.languages = repo.node.languages.edges; reposPkgJson.push(pkgjson); } } } return reposPkgJson;
1
2
3> ⚠ **Warning:** This method will hit the Github API limit very quickly and you might get timed out.
4Deno queues could help at this by staggering the requests [example with Deno queues](https://github.com/tigawanna/library-stats/blob/4569d944082d5f0bb8bfe74e19bba6f5b3a93d7e/routes/stats/helpers/computeLibStats.ts)
5 writing the response to a DB is probably a good idea so that we don't have to run this every time
6
7> ℹ️ **Info:** the Deno queues are not working on deploy only locally , where you'll have to write to a remote KV deployed on deploy .
8If anyone knows how to make that Deno queue work in deploy let me know I haven't been successful with that
9
10Now we can display them on sour site , but wait , mapping over the items and displaying a simple list looks a little not over engineered , so let's fix that
11
12
13
14we'll be copying from the TanStack website sponsors section where they had this cool visualization of circles that vary in size based on how big a contributor they are.
15
16
17
18
19
20
21We'll be using [VLSX](https://airbnb.io/visx/docs) by the people at Airbnb
22
sh npm i @visx/hierarchy @visx/responsive
1
2In my case , my data will be in this shape
3
ts export interface ViewerLibraries { highlighted_library_stats: Record<string, string>; library_stats: Record<string, string>; framework_stats: Record<string, string>; }
tsx const languages = Object.entries(libs?.highlighted_library_stats ?? {}) .map(([key, value]) => ({ key, value, }));
// initialize the pack const pack = React.useMemo( () => ({ children: languages, name: "root", radius: 0, distance: 0, }), [languages] );
// initialize the root const root = React.useMemo( () => hierarchy(pack) // d:{key:string;value:number} .sum((d: any) => { // // no("sum", d?.tier?.monthlyPriceInDollars) return 1 + d?.value; }) .sort((a, b) => (b.value ?? 0) - (a.value ?? 0)), [pack] );
1
2Then we'll map over the items to display them in our components.
3
tsx
<div className="w-full h-full flex flex-col items-center gap-2 pt-2">
<div className="w-full">
<ParentSize>
{({ width = 600 }) => { return width < 10 ? null : ( <div style={{ width, height: width, position: "relative", }}> <style dangerouslySetInnerHTML={{ __html: `
.spon-link { transition: all .2s ease; transform: translate(-50%, -50%); }
.spon-link:hover { z-index: 10; transform: translate(-50%, -50%) scale(1.1); }
.spon-link:hover .spon-tooltip { opacity: 1; } `, }} />
<Pack root={root} size={[width, width]} padding={width * 0.005}>
{(packData) => { // // no(" ===== PACK DATA ======= ", packData); const circles = packData.descendants().slice(1); // skip first layer // // no("========= CIRCLES DESCENDANT ======== ", circles); return (
<div>
{[...circles].reverse().map((circ, i) => { const circle = circ as any as Circle; const tooltipX = circle.x > width / 2 ? "left" : "right"; const tooltipY = circle.y > width / 2 ? "top" : "bottom";
return ( <a key={circle-${i}} href={https://github.com/${circle.data.key}} className={ spon-link ` + `absolute shadow-lg bg-white rounded-full z-0 } style={{ left: circle.x, top: circle.y, width: circle.r * 2, height: circle.r * 2, }}> <img key={circle-${i}} className={absolute bg-no-repeat bg-center bg-contain rounded-full w-[95%] h-[95%] dark:w-[100.5%] dark:h-[100.5%] left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 `} src={https://avatars0.githubusercontent.com/${ circle.data.key }?v=3&s=${Math.round(circle.r * 2)}`} /> <div className={twMerge( `spon-tooltip absolute text-sm bg-gray-800 text-white p-2 pointer-events-none transform opacity-0 shadow-xl rounded-lg flex flex-col items-center `,
tooltipX == "left" ? left-1/4 -translate-x-full : right-1/4 translate-x-full, tooltipY == "top" ? top-1/4 -translate-y-full : bottom-1/4 translate-y-full )}>
<p className={whitespace-nowrap font-bold}>{circle.data.key}</p>
</div>
</a>
); })}
</div>
); }}
</Pack>
</div>
); }}
</ParentSize>
</div>
</div>
1
2ℹ️ **Info:** the library types are a little off so we hade to typecast to the correct ones
3
4
5
6
7A little issue that might need some future over engineering is getting the correct libraries logos , the technique am currently using relies on the repository name matching the NPM package name which isn't always the case
8
ts src={https://avatars0.githubusercontent.com/${ circle.data.key }?v=3&s=${Math.round(circle.r * 2)}}
```
A more reliable solution might be to search the repository and save it's open-graph image in in the augment package JSON step.
That's enough over engineering for a day,
In the next part we'll adding a contact me form using NextJS server actions and try to make it as progressively enhancable as possible