From c9ef85743c2a5395b6f6e4824f69e45cc41e7c45 Mon Sep 17 00:00:00 2001 From: Darren Baldwin <68653294+DarrenBaldwin07@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:56:53 -0400 Subject: [PATCH] Isr - revalidation for stale data (#478) Co-authored-by: Darren B <68653294+Devd0@users.noreply.github.com> --- .github/workflows/cli.yml | 16 +- ui/src/Components/Header/header.module.scss | 2 +- ui/src/pages/api/revalidate/[ext].ts | 12 - ui/src/pages/extensions/[ext]/index.tsx | 324 +++++++++++++------- ui/src/pages/index.tsx | 118 ++++--- ui/src/stringHelpers.ts | 3 + ui/src/stringHelpers.tsx | 3 - 7 files changed, 290 insertions(+), 188 deletions(-) delete mode 100644 ui/src/pages/api/revalidate/[ext].ts create mode 100644 ui/src/stringHelpers.ts delete mode 100644 ui/src/stringHelpers.tsx diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 6111456e..c3731554 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -10,16 +10,20 @@ on: branches: - main paths: - - '.github/workflows/cli.yml' - - 'cli/**' - - '!cli/README.md' + - ".github/workflows/cli.yml" + - "cli/**" + - "!cli/README.md" + paths-ignore: + - "cli/.gitignore" push: branches: - main paths: - - '.github/workflows/cli.yml' - - 'cli/**' - - '!cli/README.md' + - ".github/workflows/cli.yml" + - "cli/**" + - "!cli/README.md" + paths-ignore: + - "cli/.gitignore" jobs: lint: diff --git a/ui/src/Components/Header/header.module.scss b/ui/src/Components/Header/header.module.scss index 00d05dec..3e7278a8 100644 --- a/ui/src/Components/Header/header.module.scss +++ b/ui/src/Components/Header/header.module.scss @@ -3,7 +3,7 @@ position: fixed; top: 0; background-color: transparent; - transition: all 0.5s ease-in-out; + transition: all 0.3s ease-in-out; z-index: 5; padding: 0 $spacing-m; display: flex; diff --git a/ui/src/pages/api/revalidate/[ext].ts b/ui/src/pages/api/revalidate/[ext].ts deleted file mode 100644 index 088f34e9..00000000 --- a/ui/src/pages/api/revalidate/[ext].ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const { ext } = req.query; - await res.revalidate(`/extensions/${ext}`); - await res.revalidate("/"); - return res.json({ revalidated: true }); - } catch (err) { - return res.status(500).send("Error revalidating"); - } -} diff --git a/ui/src/pages/extensions/[ext]/index.tsx b/ui/src/pages/extensions/[ext]/index.tsx index 93dd0692..b695f0ec 100644 --- a/ui/src/pages/extensions/[ext]/index.tsx +++ b/ui/src/pages/extensions/[ext]/index.tsx @@ -1,45 +1,50 @@ -import Head from "next/head"; -import { useState, useEffect } from "react"; -import type { InferGetStaticPropsType, GetStaticProps } from "next"; -import { Extension } from "@/types"; -import ReactMarkdown from "react-markdown"; -import styles from "./extension.module.scss"; -import cx from "classnames"; -import remarkGfm from "remark-gfm"; -import rehypeRaw from 'rehype-raw' -import { truncate } from "@/stringHelpers"; -import { formatDateString } from "@/formatDate"; -import Image from "next/image"; -import Header from "@/Components/Header"; -import { useRouter } from "next/router"; -import InfoIcon from "@/Components/InfoIcon"; +import Head from "next/head" +import { useState, useEffect } from "react" +import type { InferGetStaticPropsType } from "next" +import { Extension } from "@/types" +import ReactMarkdown from "react-markdown" +import styles from "./extension.module.scss" +import cx from "classnames" +import remarkGfm from "remark-gfm" +import rehypeRaw from "rehype-raw" +import { truncate } from "@/stringHelpers" +import { formatDateString } from "@/formatDate" +import Image from "next/image" +import Header from "@/Components/Header" +import { useRouter } from "next/router" +import InfoIcon from "@/Components/InfoIcon" -const Octocat = "/OctocatIcon.png"; -const LinkIcon = "/LinkIcon.png"; -const CopyIcon = "/copy.png"; -const REGISTRY_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://registry.pgtrunk.io"; +const Octocat = "/OctocatIcon.png" +const LinkIcon = "/LinkIcon.png" +const CopyIcon = "/copy.png" +const REGISTRY_URL = + process.env.NEXT_PUBLIC_API_BASE_URL || "https://registry.pgtrunk.io" -export default function Page({ extension, readme, repoDescription }: InferGetStaticPropsType) { - const [showFeedback, setShowFeedback] = useState(false); - const router = useRouter(); +export default function Page({ + extension, + readme, + repoDescription, +}: InferGetStaticPropsType) { + const [showFeedback, setShowFeedback] = useState(false) + const router = useRouter() useEffect(() => { if (showFeedback) { const timer = setTimeout(() => { - setShowFeedback(false); - }, 2000); + setShowFeedback(false) + }, 2000) - return () => clearTimeout(timer); + return () => clearTimeout(timer) } - }, [showFeedback]); + }, [showFeedback]) if (!extension && !router.isFallback) { - console.log("EXT MISSING DATA"); + console.log("EXT MISSING DATA") return (

Error

- ); + ) } if (router.isFallback) { @@ -47,27 +52,31 @@ export default function Page({ extension, readme, repoDescription }: InferGetSta

Loading...

- ); + ) } - const latestVersion: Extension = extension!; - const installText = `trunk install ${latestVersion.name}` ?? ""; + const latestVersion: Extension = extension! + const installText = `trunk install ${latestVersion.name}` ?? "" const handleCopy = async () => { try { - navigator.clipboard.writeText(installText); - setShowFeedback(true); + navigator.clipboard.writeText(installText) + setShowFeedback(true) } catch (error) { - console.log(error); + console.log(error) } - }; + } return (
{`Trunk - ${latestVersion.name ?? ""}`}
@@ -79,7 +88,14 @@ export default function Page({ extension, readme, repoDescription }: InferGetSta

Install

-
Copied to clipboard!
+
+ Copied to clipboard! +

{installText}

@@ -153,64 +220,67 @@ export default function Page({ extension, readme, repoDescription }: InferGetSta
- ); + ) } export async function getStaticPaths() { try { - const extRes = await fetch(`${REGISTRY_URL}/extensions/all`); - const extensions = await extRes.json(); + const extRes = await fetch(`${REGISTRY_URL}/extensions/all`) + const extensions = await extRes.json() const paths = extensions.map((ext: Extension) => ({ params: { ext: ext.name }, - })); + })) - console.log("********** BUILT PATHS **********"); - return { paths, fallback: true }; + console.log("********** BUILT PATHS **********") + return { paths, fallback: true } // return { paths: [], fallback: true }; } catch (error) { - console.log("ERROR BUILDING PATHS", error); - return { paths: [] }; + console.log("ERROR BUILDING PATHS", error) + return { paths: [] } } } async function getReadme(repositoryUrl: string): Promise { - const GITHUB_TOKEN = process.env.GITHUB_TOKEN; - const markdownRegex = /.*\.md/; - let readme; - let githubReadmeUrl; - let readmeFileName; - let readmeBase64Contents; - let isContrib = false; - - const noGh = repositoryUrl.split("https://github.com/")[1]; - const split = noGh.split("/"); - + const GITHUB_TOKEN = process.env.GITHUB_TOKEN + const markdownRegex = /.*\.md/ + let readme + let githubReadmeUrl + let readmeFileName + let readmeBase64Contents + let isContrib = false + + const noGh = repositoryUrl.split("https://github.com/")[1] + const split = noGh.split("/") + if (split.length === 2) { - githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme`; + githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme` } else if (split[2] === "tree") { - isContrib = true; - githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme`; + isContrib = true + githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme` } else { - githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme/${split[2]}`; + githubReadmeUrl = `https://api.github.com/repos/${split[0]}/${split[1]}/readme/${split[2]}` } - const readmeProm: Promise<{ name: string, content: string }> = fetch(githubReadmeUrl, { - headers: { - Authorization: `token ${GITHUB_TOKEN}`, - }, - }).then((resp) => resp.json()); + const readmeProm: Promise<{ name: string; content: string }> = fetch( + githubReadmeUrl, + { + headers: { + Authorization: `token ${GITHUB_TOKEN}`, + }, + } + ).then((resp) => resp.json()) try { - const readmeJson = await readmeProm; - readmeFileName = readmeJson.name; - readmeBase64Contents = readmeJson.content; + const readmeJson = await readmeProm + readmeFileName = readmeJson.name + readmeBase64Contents = readmeJson.content // If this README is Markdown.. - if(isContrib || markdownRegex.test(readmeFileName)) { + if (isContrib || markdownRegex.test(readmeFileName)) { // Decode its base64 contents - readme = Buffer.from(readmeBase64Contents, "base64").toString("utf-8"); + readme = Buffer.from(readmeBase64Contents, "base64").toString("utf-8") } else { // Get the HTML-converted contents. // With the `application/vnd.github.html` header, @@ -219,70 +289,88 @@ async function getReadme(repositoryUrl: string): Promise { const readmeRes = await fetch(githubReadmeUrl, { headers: { Authorization: `token ${GITHUB_TOKEN}`, - Accept: "application/vnd.github.html" + Accept: "application/vnd.github.html", }, - }); - readme = await readmeRes.text(); + }) + readme = await readmeRes.text() } } catch (err) { - return Promise.reject(Error(`Fetching GitHub API failed: ${err}`)); + return Promise.reject(Error(`Fetching GitHub API failed: ${err}`)) } - return readme; + return readme } // Lexicographically compare semantic version tags const compareBySemver = (a: string, b: string) => { - const a1 = a.split('.'); - const b1 = b.split('.'); - - const len = Math.min(a1.length, b1.length); - + const a1 = a.split(".") + const b1 = b.split(".") + + const len = Math.min(a1.length, b1.length) + for (let i = 0; i < len; i++) { - const a2 = +a1[ i ] || 0; - const b2 = +b1[ i ] || 0; - - if (a2 !== b2) { - return a2 > b2 ? 1 : -1; - } + const a2 = +a1[i] || 0 + const b2 = +b1[i] || 0 + + if (a2 !== b2) { + return a2 > b2 ? 1 : -1 + } } - return b1.length - a1.length; -}; + return b1.length - a1.length +} export async function getStaticProps({ params }: { params: { ext: string } }) { - let readme = ""; - let extensions = null; - let repoDescription = ""; + let readme = "" + let extensions = null + let repoDescription = "" function sortExtensions(extensions: Extension[]) { - return extensions.sort((a, b) => compareBySemver(a.version, b.version)); + return extensions.sort((a, b) => compareBySemver(a.version, b.version)) } try { try { - const extRes = await fetch(`${REGISTRY_URL}/extensions/detail/${params.ext}`); - extensions = await extRes.json()!; - sortExtensions(extensions); + const extRes = await fetch( + `${REGISTRY_URL}/extensions/detail/${params.ext}` + ) + extensions = await extRes.json()! + sortExtensions(extensions) } catch (error) { - return Promise.reject(Error(`Failed to fetch '${params.ext}' from Trunk: ${error}`)); + return Promise.reject( + Error(`Failed to fetch '${params.ext}' from Trunk: ${error}`) + ) } - const latestVersion: Extension = extensions[extensions.length - 1]; - if (extensions && latestVersion?.repository && latestVersion.repository.includes("github.com")) { - const repo = latestVersion.repository; + const latestVersion: Extension = extensions[extensions.length - 1] + if ( + extensions && + latestVersion?.repository && + latestVersion.repository.includes("github.com") + ) { + const repo = latestVersion.repository try { - readme = await getReadme(repo); - repoDescription = latestVersion.description; + readme = await getReadme(repo) + repoDescription = latestVersion.description } catch (err) { - console.log(`getReadme failed: ${err}`); - return Promise.reject(Error(`getReadmeAndDescription failed: ${err}`)); + console.log(`getReadme failed: ${err}`) + return Promise.reject(Error(`getReadmeAndDescription failed: ${err}`)) } } - return { props: { extension: latestVersion, readme, repoDescription } }; + return { + props: { extension: latestVersion, readme, repoDescription }, + revalidate: 10, + } } catch (error: any) { - console.log("********** STATIC PROPS ERROR **********", error.message, params, extensions); - return { props: { extension: null, readme: "", repoDescription: "" } }; + console.log( + "********** STATIC PROPS ERROR **********", + error.message, + params, + extensions + ) + return { + props: { extension: null, readme: "", repoDescription: "" }, + } } } diff --git a/ui/src/pages/index.tsx b/ui/src/pages/index.tsx index c8a0bbb1..508ea85a 100644 --- a/ui/src/pages/index.tsx +++ b/ui/src/pages/index.tsx @@ -1,68 +1,83 @@ -import { useState } from "react"; -import type { InferGetStaticPropsType, GetStaticProps } from "next"; -import Head from "next/head"; -import styles from "./index.module.scss"; -import Hero from "../Components/Hero"; -import Categories from "../Components/Categories"; -import ExtGrid from "../Components/ExtGrid"; -import { Category, CategoriesForGrid, Extension } from "@/types"; -import Header from "@/Components/Header"; - -const REGISTRY_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "https://registry.pgtrunk.io"; +import { useState } from "react" +import type { GetStaticProps } from "next" +import Head from "next/head" +import styles from "./index.module.scss" +import Hero from "../Components/Hero" +import Categories from "../Components/Categories" +import ExtGrid from "../Components/ExtGrid" +import { Category, CategoriesForGrid, Extension } from "@/types" +import Header from "@/Components/Header" + +const REGISTRY_URL = + process.env.NEXT_PUBLIC_API_BASE_URL || "https://registry.pgtrunk.io" + export const getStaticProps: GetStaticProps<{ - categories: Category[]; + categories: Category[] }> = async () => { try { - const catRes = await fetch(`${REGISTRY_URL}/categories/all`); - const extRes = await fetch(`${REGISTRY_URL}/extensions/all`); + const catRes = await fetch(`${REGISTRY_URL}/categories/all`) + const extRes = await fetch(`${REGISTRY_URL}/extensions/all`) // 🐈‍ - const cats: Category[] = await catRes.json(); - const exts: Extension[] = await extRes.json(); + const cats: Category[] = await catRes.json() + const exts: Extension[] = await extRes.json() - console.log(`info: Got ${exts.length} extensions in index.tsx`); + console.info(`Got ${exts.length} extensions in index.tsx`) - const sortedCategories = moveFeaturedCategoryToStart(cats.sort((a, b) => (a.name < b.name ? -1 : 1))); - const sortedExtensions = sortExtensionsByFeatured(exts); + const sortedCategories = moveFeaturedCategoryToStart( + cats.sort((a, b) => (a.name < b.name ? -1 : 1)) + ) + const sortedExtensions = sortExtensionsByFeatured(exts) - const categoriesForGrid: CategoriesForGrid = {}; + const categoriesForGrid: CategoriesForGrid = {} cats.forEach((cat: Category) => { - categoriesForGrid[cat.slug] = { displayName: cat.name }; - }); - - return { props: { categories: sortedCategories, extensions: sortedExtensions, categoriesForGrid } }; + categoriesForGrid[cat.slug] = { displayName: cat.name } + }) + + return { + props: { + categories: sortedCategories, + extensions: sortedExtensions, + categoriesForGrid, + }, + revalidate: 10, + } } catch (error) { - console.log("ERROR LOADING DATA: ", error); + console.log("ERROR LOADING DATA: ", error) - return { props: { categories: [], extensions: [], categoriesForGrid: {} } }; + return { + props: { categories: [], extensions: [], categoriesForGrid: {} }, + } } -}; +} // TODO(vrmiguel): find a way to do this in-place? function sortExtensionsByFeatured(extensions: Extension[]): Extension[] { - const featuredExtensions = extensions.filter(extension => - extension.categories.includes('Featured') - ); + const featuredExtensions = extensions.filter((extension) => + extension.categories.includes("Featured") + ) - console.log(`Featured: ${featuredExtensions}`); + console.log(`Featured: ${featuredExtensions}`) const nonFeaturedExtensions = extensions.filter( - extension => !extension.categories.includes('Featured') - ); + (extension) => !extension.categories.includes("Featured") + ) - return [...featuredExtensions, ...nonFeaturedExtensions]; + return [...featuredExtensions, ...nonFeaturedExtensions] } function moveFeaturedCategoryToStart(categories: Category[]): Category[] { - const featuredCategoryIndex = categories.findIndex(category => category.slug === 'featured'); + const featuredCategoryIndex = categories.findIndex( + (category) => category.slug === "featured" + ) if (featuredCategoryIndex !== -1) { // Move it to the start of the array - const featuredCategory = categories.splice(featuredCategoryIndex, 1)[0]; - categories.unshift(featuredCategory); + const featuredCategory = categories.splice(featuredCategoryIndex, 1)[0] + categories.unshift(featuredCategory) } - return categories; + return categories } export default function Home({ @@ -70,27 +85,34 @@ export default function Home({ extensions, categoriesForGrid, }: { - categories: Category[]; - extensions: Extension[]; - categoriesForGrid: {}; + categories: Category[] + extensions: Extension[] + categoriesForGrid: {} }) { - const [showMobileCategories, setShowMobileCategories] = useState(false); + const [showMobileCategories, setShowMobileCategories] = useState(false) const showMobileCategoriesHandler = () => { - window.scrollTo({ top: 0 }); - setShowMobileCategories(true); - }; + window.scrollTo({ top: 0 }) + setShowMobileCategories(true) + } return (
Trunk - +
- +
- ); + ) } diff --git a/ui/src/stringHelpers.ts b/ui/src/stringHelpers.ts new file mode 100644 index 00000000..cb3b146b --- /dev/null +++ b/ui/src/stringHelpers.ts @@ -0,0 +1,3 @@ +export const truncate = (str: String, n = 100) => { + return str.length > n ? str.slice(0, n - 1) + "..." : str +} diff --git a/ui/src/stringHelpers.tsx b/ui/src/stringHelpers.tsx deleted file mode 100644 index d37bb356..00000000 --- a/ui/src/stringHelpers.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const truncate = (str: String, n = 100) => { - return str.length > n ? str.slice(0, n - 1) + "..." : str; -};