Displaying our most used libraries/framewokrs

tigawanna

Dennis kinuthia

Posted on January 15, 2024

Displaying our most used libraries/framewokrs

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

export interface RequiredDecodedPackageJson {
  name: string;
  private?: boolean;
  version: string;
  type?: string;
  scripts: KeyStringObject;
  dependencies: KeyStringObject;
  devDependencies: KeyStringObject;
  [key: string]: any | undefined;
}
export type TPkgType =
  | "React+Vite"
  | "React+Relay"
  | "Rakkasjs"
  | "Nextjs"
  | "Nodejs"
  | "Deno"
  | "React-native"
  | "Bun"
  | "Others";

export function pkgTypeCondition(pkg: RequiredDecodedPackageJson): {
  pkg_type: TPkgType;
  condition: boolean;
} {
  const pkgs_string = JSON.stringify({
    ...pkg.dependencies,
    ...pkg.devDependencies,
  });
  if (pkgs_string.includes("rakkas")) {
    return { pkg_type: "Rakkasjs", condition: true };
  }
  if (pkgs_string.includes("react-native")) {
    return { pkg_type: "React-native", condition: true };
  }

  if (pkg.dependencies?.next) {
    return { pkg_type: "Nextjs", condition: true };
  }

  if (pkg.dependencies?.react && pkg.dependencies?.["react-relay"]) {
    return { pkg_type: "React+Relay", condition: true };
  }

  if (pkg.devDependencies?.vite && pkg.dependencies?.react) {
    return { pkg_type: "React+Vite", condition: true };
  }

  if (
    pkgs_string.includes("nodemon") ||
    pkgs_string.includes("tsup") ||
    pkgs_string.includes("fastify") ||
    pkgs_string.includes("express") ||
    pkgs_string.includes("nestjs") ||
    pkgs_string.includes("mongoose")
  ) {
    return { pkg_type: "Nodejs", condition: true };
  }
  return { pkg_type: "Others", condition: false };
}
Enter fullscreen mode Exit fullscreen mode

now let's setup a function to fetch the package.json for a repository

// 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;
  }
}

Enter fullscreen mode Exit fullscreen mode

The 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

The base64 encoder can vary between runtimes , the one below works on Deno

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;
}
Enter fullscreen mode Exit fullscreen mode

once 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.

//  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;
}

Enter fullscreen mode Exit fullscreen mode

And with that all we need is to run our recursive repo fetcher and for every item execute this function

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;
Enter fullscreen mode Exit fullscreen mode

Warning: This method will hit the Github API limit very quickly and you might get timed out.
Deno queues could help at this by staggering the requests example with Deno queues
writing the response to a DB is probably a good idea so that we don't have to run this every time

ℹ️ Info: the Deno queues are not working on deploy only locally , where you'll have to write to a remote KV deployed on deploy .
If anyone knows how to make that Deno queue work in deploy let me know I haven't been successful with that

Now 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

we'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.

Tanstack website visualization

We'll be using VLSX by the people at Airbnb

npm i @visx/hierarchy @visx/responsive 
Enter fullscreen mode Exit fullscreen mode

In my case , my data will be in this shape

export interface ViewerLibraries {
  highlighted_library_stats: Record<string, string>;
  library_stats: Record<string, string>;
  framework_stats: Record<string, string>;
}
Enter fullscreen mode Exit fullscreen mode

and we'll

  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]
  );

Enter fullscreen mode Exit fullscreen mode

Then we'll map over the items to display them in our components.



    <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>

Enter fullscreen mode Exit fullscreen mode

ℹ️ Info: the library types are a little off so we hade to typecast to the correct ones

Visualizations with VLSX

A 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

               src={`https://avatars0.githubusercontent.com/${
                                  circle.data.key
                                }?v=3&s=${Math.round(circle.r * 2)}`}
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
tigawanna
Dennis kinuthia

Posted on January 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related