diff --git a/.gitignore b/.gitignore index 10b1bbc7..11227c54 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ yarn-error.log* apps/frontpage/content/docs apps/frontpage/content/snippets apps/frontpage/generated-redirects.json +apps/frontpage/generated-versions.json apps/frontpage/public/docs-assets # ui diff --git a/README.md b/README.md index 53aa3249..4ebfdf21 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,10 @@ This project is structured around [Turborepo](https://turbo.build/repo). This do > Generate redirects file +#### `npx turbo generate-versions` + +> Generate Storybook version metadata from npm dist-tags + #### `npx turbo dev` > Run all apps locally diff --git a/apps/frontpage/app/versions/route.ts b/apps/frontpage/app/versions/route.ts index ea6951d2..48c87be9 100644 --- a/apps/frontpage/app/versions/route.ts +++ b/apps/frontpage/app/versions/route.ts @@ -1,11 +1,9 @@ import crypto from 'node:crypto'; -import { readFile } from 'node:fs/promises'; -import path from 'node:path'; import { headers } from 'next/headers'; import type { NextRequest } from 'next/server'; import { BigQuery } from '@google-cloud/bigquery'; import { parse as semverParse } from 'semver'; -import { latestVersion } from '@repo/utils'; +import { readGeneratedVersions } from '../../lib/generated-versions'; // eslint-disable-next-line no-useless-escape, prefer-named-capture-group -- Escape is absolutely necessary for regex? const SEP_REGEX = /([\.:])/; @@ -15,13 +13,6 @@ const logger = { log: (..._msgs: unknown[]) => {} }; const { GCP_CREDENTIALS, SKIP_IP_HASH } = process.env; -const versionFilesDir = path.join( - process.cwd(), - 'content/docs', - latestVersion.id, - 'versions', -); - const md5 = (host: string) => { const hash = crypto.createHash('md5'); hash.update(host); @@ -99,27 +90,10 @@ const log = async (searchParams: URLSearchParams) => { await table.insert([row]); }; -type DistTag = 'latest' | 'next'; - const versions = async () => { logger.log('fetching versions'); - async function getVersionData(distTag: DistTag) { - const data = JSON.parse( - await readFile( - new URL(path.join(versionFilesDir, `${distTag}.json`), import.meta.url), - 'utf8', - ), - ); - // Strip off no-longer-used `info` property - const { version } = data as { version: string }; - return { version }; - } - - const latest = await getVersionData('latest'); - const next = await getVersionData('next'); - - return JSON.stringify({ latest, next }); + return JSON.stringify(await readGeneratedVersions()); }; export async function GET(request: NextRequest) { diff --git a/apps/frontpage/lib/generated-versions.ts b/apps/frontpage/lib/generated-versions.ts new file mode 100644 index 00000000..61c3634a --- /dev/null +++ b/apps/frontpage/lib/generated-versions.ts @@ -0,0 +1,79 @@ +import path from 'node:path'; +import { readFile, writeFile } from 'node:fs/promises'; +import { valid as isValidSemver } from 'semver'; + +export interface GeneratedVersion { + version: string; +} + +export interface GeneratedVersions { + latest: GeneratedVersion; + next: GeneratedVersion; +} + +const DIST_TAGS_URL = + 'https://registry.npmjs.org/-/package/storybook/dist-tags'; + +export const generatedVersionsFilePath = path.join( + process.cwd(), + 'generated-versions.json', +); + +function assertDistTagVersion( + distTag: keyof GeneratedVersions, + version: unknown, +): GeneratedVersion { + if (typeof version !== 'string' || !isValidSemver(version)) { + throw new Error( + `Expected a valid semver string for the ${distTag} dist-tag, received ${JSON.stringify(version)}`, + ); + } + + return { version }; +} + +export async function fetchGeneratedVersions(): Promise { + const response = await fetch(DIST_TAGS_URL, { + headers: { + Accept: 'application/json', + 'Cache-Control': 'no-cache', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch Storybook dist-tags from npm: ${String(response.status)} ${response.statusText}`, + ); + } + + const distTags = (await response.json()) as Partial< + Record + >; + + return { + latest: assertDistTagVersion('latest', distTags.latest), + next: assertDistTagVersion('next', distTags.next), + }; +} + +export async function readGeneratedVersions(): Promise { + const fileContents = await readFile(generatedVersionsFilePath, 'utf8'); + const parsed = JSON.parse(fileContents) as Partial< + Record + >; + + return { + latest: assertDistTagVersion('latest', parsed.latest?.version), + next: assertDistTagVersion('next', parsed.next?.version), + }; +} + +export async function writeGeneratedVersions( + versions: GeneratedVersions, +): Promise { + await writeFile( + generatedVersionsFilePath, + `${JSON.stringify(versions, null, 2)}\n`, + 'utf8', + ); +} diff --git a/apps/frontpage/next.config.js b/apps/frontpage/next.config.js index 537e1bca..b6088ab7 100644 --- a/apps/frontpage/next.config.js +++ b/apps/frontpage/next.config.js @@ -100,6 +100,7 @@ module.exports = withBundleAnalyzer( */ outputFileTracingIncludes: { '/docs/**': ['./content/docs/**'], + '/versions': ['./generated-versions.json'], }, }, async redirects() { diff --git a/apps/frontpage/package.json b/apps/frontpage/package.json index 9d5b123c..ce8cc64f 100644 --- a/apps/frontpage/package.json +++ b/apps/frontpage/package.json @@ -9,6 +9,7 @@ "lint": "next lint", "typecheck": "tsc --noEmit", "fetch-docs": "tsx --tsconfig tsconfig.json scripts/get-local-docs.ts", + "generate-versions": "tsx --tsconfig tsconfig.json scripts/generate-versions.ts", "generate-redirects": "tsx --tsconfig tsconfig.json scripts/generate-redirects.ts", "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next", "storybook": "storybook dev -p 6006", diff --git a/apps/frontpage/scripts/generate-versions.ts b/apps/frontpage/scripts/generate-versions.ts new file mode 100644 index 00000000..22e46830 --- /dev/null +++ b/apps/frontpage/scripts/generate-versions.ts @@ -0,0 +1,11 @@ +import { + fetchGeneratedVersions, + writeGeneratedVersions, +} from '../lib/generated-versions'; + +async function generate(): Promise { + const versions = await fetchGeneratedVersions(); + await writeGeneratedVersions(versions); +} + +void generate(); \ No newline at end of file diff --git a/turbo.json b/turbo.json index 9170ff80..e0fc033d 100644 --- a/turbo.json +++ b/turbo.json @@ -29,6 +29,10 @@ "outputs": ["./apps/frontpage/generated-redirects.json"], "cache": false }, + "generate-versions": { + "cache": false, + "outputs": ["./apps/frontpage/generated-versions.json"] + }, "fetch-docs": { "cache": false }, @@ -41,7 +45,7 @@ "inputs": ["$TURBO_DEFAULT$", ".env"] }, "build": { - "dependsOn": ["fetch-docs", "generate-redirects", "^build"], + "dependsOn": ["fetch-docs", "generate-redirects", "generate-versions", "^build"], "outputs": [ "dist/**", ".next/**", @@ -51,6 +55,7 @@ "inputs": [ "$TURBO_DEFAULT$", ".env", + "./apps/frontpage/generated-versions.json", "./apps/frontpage/content/docs/**", "./apps/frontpage/content/snippets/**", "./apps/frontpage/public/docs-assets/**"