From b68d98f7e93860baacd34aa67cfbdc0d9896a259 Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Mon, 11 Dec 2023 19:42:50 +0000 Subject: [PATCH] docs: add versioned docs --- .../docs/components/ReleaseSwitcher/index.tsx | 74 +++++++++++++++++++ .../ReleaseSwitcher/styles.module.css | 15 ++++ apps/docs/package.json | 3 +- apps/docs/pages/_app.tsx | 35 ++++++++- apps/docs/pages/api/releases.ts | 41 ++++++++++ apps/docs/pages/v/[[...fullPath]].tsx | 56 ++++++++++++++ apps/docs/theme.config.tsx | 6 ++ lerna.json | 1 + turbo.json | 6 +- yarn.lock | 5 ++ 10 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 apps/docs/components/ReleaseSwitcher/index.tsx create mode 100644 apps/docs/components/ReleaseSwitcher/styles.module.css create mode 100644 apps/docs/pages/api/releases.ts create mode 100644 apps/docs/pages/v/[[...fullPath]].tsx diff --git a/apps/docs/components/ReleaseSwitcher/index.tsx b/apps/docs/components/ReleaseSwitcher/index.tsx new file mode 100644 index 0000000000..bb7a531757 --- /dev/null +++ b/apps/docs/components/ReleaseSwitcher/index.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from "react"; + +import packageJson from "../../package.json"; +import { getClassNameFactory } from "@/core/lib"; + +import styles from "./styles.module.css"; + +const { version } = packageJson; + +const getClassName = getClassNameFactory("ReleaseSwitcher", styles); + +export const ReleaseSwitcher = () => { + const isCanary = process.env.NEXT_PUBLIC_IS_CANARY === "true" || false; + const isLatest = process.env.NEXT_PUBLIC_IS_LATEST === "true" || false; + + const currentValue = isCanary ? "canary" : isLatest ? "" : version; + + const [options, setOptions] = useState<{ value: string; label: string }[]>([ + { + label: "canary", + value: "canary", + }, + ...(isCanary + ? [] + : [ + { + label: isLatest ? `v${version} (latest)` : `v${version}`, + value: "", + }, + ]), + ]); + + useEffect(() => { + fetch("/api/releases").then(async (res) => { + const { releases } = await res.json(); + + if (releases.length === 0) return; + + const releaseOptions = releases.map((release) => ({ + label: release.name.split("releases/")[1], + value: release.name.split("releases/v")[1], // remove the leading `v` + })); + + releaseOptions[0].label = `${releaseOptions[0].label} (latest)`; + releaseOptions[0].value = ""; + + setOptions([{ label: "canary", value: "canary" }, ...releaseOptions]); + }); + }, []); + + return ( + + ); +}; diff --git a/apps/docs/components/ReleaseSwitcher/styles.module.css b/apps/docs/components/ReleaseSwitcher/styles.module.css new file mode 100644 index 0000000000..4ade671bf8 --- /dev/null +++ b/apps/docs/components/ReleaseSwitcher/styles.module.css @@ -0,0 +1,15 @@ +.ReleaseSwitcher { + appearance: none; /* Safari */ + background: url("data:image/svg+xml;utf8,") + no-repeat; + background-size: 12px; + background-position: calc(100% - 12px) calc(50% + 3px); + background-repeat: no-repeat; + background-color: var(--puck-color-grey-10); + border-radius: 100px; + color: black; + padding-left: 16px; + padding-right: 16px; + height: 33px; /* Magic number to align with Nextra search */ + width: 156px; +} diff --git a/apps/docs/package.json b/apps/docs/package.json index b8b7001df9..55651b1b1d 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -1,6 +1,6 @@ { "name": "docs", - "version": "1.0.0", + "version": "0.12.0", "private": true, "scripts": { "dev": "next dev", @@ -16,6 +16,7 @@ "typescript": "^4.5.3" }, "dependencies": { + "lru-cache": "^10.1.0", "next": "^13.5.4", "nextra": "^2.13.2", "nextra-theme-docs": "^2.13.2", diff --git a/apps/docs/pages/_app.tsx b/apps/docs/pages/_app.tsx index cc1b15240b..0e40678cce 100644 --- a/apps/docs/pages/_app.tsx +++ b/apps/docs/pages/_app.tsx @@ -1,8 +1,39 @@ -import React from "react"; +import React, { useEffect } from "react"; import type { AppProps } from "next/app"; import "../styles.css"; +import { useRouter } from "next/router"; +import { Message } from "./v/[[...fullPath]]"; + +export default function DocsApp({ Component, pageProps }: AppProps) { + const router = useRouter(); + + useEffect(() => { + if (!window.parent) return; + + const message: Message = { + type: "routeChange", + title: window.document.title, + }; + + window.parent.postMessage(message); + + const handleRouteChange = (url) => { + const message: Message = { + type: "routeChange", + url, + title: window.document.title, + }; + + window.parent.postMessage(message); + }; + + router.events.on("routeChangeComplete", handleRouteChange); + + return () => { + router.events.off("routeChangeComplete", handleRouteChange); + }; + }, []); -export default function MyApp({ Component, pageProps }: AppProps) { return ; } diff --git a/apps/docs/pages/api/releases.ts b/apps/docs/pages/api/releases.ts new file mode 100644 index 0000000000..809316ded5 --- /dev/null +++ b/apps/docs/pages/api/releases.ts @@ -0,0 +1,41 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { LRUCache } from "lru-cache"; + +type ResponseData = { + releases: object; +}; + +const cache = new LRUCache({ + ttl: 1000 * 60 * 2, // 2 minutes + ttlAutopurge: true, +}); + +/** + * Proxy GitHub and rely on Next.js cache to prevent rate limiting + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const cached = cache.get("releases"); + + if (cached) { + res.status(200).json({ releases: cached }); + + return; + } + + const data = [{ name: "releases/v0.12.0", protected: false }]; + + const releases: { name: string; protected: boolean }[] = data + .filter( + (item) => + item.name.indexOf("releases") === 0 && + item.name.indexOf(`v0.11.`) === -1 // Filter out any release branches before v0.12.0 + ) + .reverse(); + + res.status(200).json({ releases }); + + cache.set("releases", releases); +} diff --git a/apps/docs/pages/v/[[...fullPath]].tsx b/apps/docs/pages/v/[[...fullPath]].tsx new file mode 100644 index 0000000000..875887f516 --- /dev/null +++ b/apps/docs/pages/v/[[...fullPath]].tsx @@ -0,0 +1,56 @@ +import { useEffect, useRef } from "react"; + +export type Message = { + type: "routeChange"; + url?: string; + title: string; +}; + +export default function Version({ path, version }) { + useEffect(() => { + const handleMessageReceived = (event: MessageEvent) => { + if (event.data.type === "routeChange") { + const routeChange = event.data as Message; + + if (routeChange.url) { + window.history.pushState({}, "", `/v/${version}${routeChange.url}`); + } + + window.document.title = `${routeChange.title} [${ + version !== "canary" ? `v${version}` : version + }]`; + } + }; + + window.addEventListener("message", handleMessageReceived); + + return () => window.removeEventListener("message", handleMessageReceived); + }, []); + + const versionSlug = version.replace(/\./g, ""); + + const src = + version === "canary" + ? `https://puck-docs-git-canary-measured.vercel.app` + : `https://puck-docs-git-releases-v${versionSlug}-measured.vercel.app`; + + return ( + + ); +} + +export function getServerSideProps(ctx) { + const [version, ...path] = ctx.query.fullPath; + + return { props: { path: path.join("/"), version } }; +} diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index 41a87c0ee6..3959d16e0c 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -1,6 +1,9 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { useRouter } from "next/router"; import { DocsThemeConfig, useConfig } from "nextra-theme-docs"; +import { ReleaseSwitcher } from "./components/ReleaseSwitcher"; + const Head = () => { const { asPath, defaultLocale, locale } = useRouter(); const config = useConfig(); @@ -111,6 +114,9 @@ const theme: DocsThemeConfig = { }, docsRepositoryBase: "https://github.com/measuredco/puck/tree/main/apps/docs", primarySaturation: 0, + navbar: { + extraContent: ReleaseSwitcher, + }, }; export default theme; diff --git a/lerna.json b/lerna.json index 8f8bf757ea..b0a0433854 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,6 @@ { "packages": [ + "apps/docs", "packages/core", "packages/create-puck-app", "packages/plugin-heading-analyzer" diff --git a/turbo.json b/turbo.json index a60462b940..d175714e4b 100644 --- a/turbo.json +++ b/turbo.json @@ -1,7 +1,11 @@ { "$schema": "https://turbo.build/schema.json", "globalDependencies": ["**/.env.*local"], - "globalEnv": ["NEXT_PUBLIC_PLAUSIBLE_DATA_DOMAIN"], + "globalEnv": [ + "NEXT_PUBLIC_PLAUSIBLE_DATA_DOMAIN", + "NEXT_PUBLIC_IS_LATEST", + "NEXT_PUBLIC_IS_CANARY" + ], "pipeline": { "build": { "dependsOn": ["^build"], diff --git a/yarn.lock b/yarn.lock index 89b5e7ed6f..336f784098 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8484,6 +8484,11 @@ lower-case@^1.1.0, lower-case@^1.1.1, lower-case@^1.1.2: resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== +lru-cache@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"