diff --git a/.gitignore b/.gitignore index ea190fb..f5012f7 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ .DS_Store *.pem +# generated version info (regenerated by predev/prebuild/postversion) +/src/generated/ + # debug npm-debug.log* yarn-debug.log* diff --git a/package.json b/package.json index 8a47b0b..7c680bd 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "license": "MIT", "type": "module", "scripts": { - "build": "cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build --webpack", - "dev": "cross-env NODE_OPTIONS=--no-deprecation next dev --webpack", - "devsafe": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev --webpack", - "devturbo": "rm -rf .next && cross-env NODE_OPTIONS=--no-deprecation next dev --turbo", + "build": "bun run generate:version-info && cross-env NODE_OPTIONS=\"--no-deprecation --max-old-space-size=8000\" next build --webpack", + "dev": "bun run generate:version-info && cross-env NODE_OPTIONS=--no-deprecation next dev --webpack", + "devsafe": "rm -rf .next && bun run generate:version-info && cross-env NODE_OPTIONS=--no-deprecation next dev --webpack", + "devturbo": "rm -rf .next && bun run generate:version-info && cross-env NODE_OPTIONS=--no-deprecation next dev --turbo", "generate:importmap": "cross-env NODE_OPTIONS=--no-deprecation payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=--no-deprecation payload generate:types", + "generate:version-info": "bun run src/scripts/generateVersionInfo.ts", "lint": "cross-env NODE_OPTIONS=--no-deprecation next lint", "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", "db": "cross-env NODE_OPTIONS=--no-deprecation pnpm drizzle-kit", @@ -21,7 +22,7 @@ "test:int": "cross-env NODE_OPTIONS=--no-deprecation vitest run --config ./vitest.config.mts", "deploy": "bun pm version patch --message \"chore: update to %s\"", "preversion": "bun run build", - "postversion": "bash ./build/deploy.sh" + "postversion": "bun run generate:version-info && bash ./build/deploy.sh" }, "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts new file mode 100644 index 0000000..29749ae --- /dev/null +++ b/src/app/api/version/route.ts @@ -0,0 +1,41 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import type { VersionInfo } from "@/lib/versionInfo"; + +export const dynamic = "force-dynamic"; +export const runtime = "nodejs"; + +const VERSION_INFO_PATH = path.join( + process.cwd(), + "src", + "generated", + "versionInfo.json", +); + +const EMPTY: VersionInfo = { + version: null, + tag: null, + gitDescribe: null, + commitHash: null, + commitDate: null, + commitSubject: null, + buildTime: null, +}; + +const JSON_HEADERS = { + "Content-Type": "application/json", + "Cache-Control": "no-store", +} as const; + +export async function GET(): Promise { + try { + if (!existsSync(VERSION_INFO_PATH)) { + return new Response(JSON.stringify(EMPTY), { headers: JSON_HEADERS, status: 200 }); + } + const raw = readFileSync(VERSION_INFO_PATH, "utf8"); + const data = JSON.parse(raw) as VersionInfo; + return new Response(JSON.stringify(data), { headers: JSON_HEADERS, status: 200 }); + } catch { + return new Response(JSON.stringify(EMPTY), { headers: JSON_HEADERS, status: 200 }); + } +} \ No newline at end of file diff --git a/src/components/static/VersionOverlay.tsx b/src/components/static/VersionOverlay.tsx index 49ebd96..619bfcc 100644 --- a/src/components/static/VersionOverlay.tsx +++ b/src/components/static/VersionOverlay.tsx @@ -1,16 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { VersionInfo } from "@/lib/versionInfo"; + +const EMPTY: VersionInfo = { + version: null, + tag: null, + gitDescribe: null, + commitHash: null, + commitDate: null, + commitSubject: null, + buildTime: null, +}; + +function resolveVersionLabel(info: VersionInfo): string { + return info.gitDescribe ?? info.tag ?? info.version ?? "unknown version"; +} + +function formatCommitDate(iso: string | null): string | null { + if (!iso) return null; + const date = new Date(iso); + if (Number.isNaN(date.getTime())) return null; + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + const VersionOverlay = () => { - const appVersion = process.env.version; - const appLastUpdated = process.env.LAST_UPDATED; + const [info, setInfo] = useState(null); + + useEffect(() => { + let cancelled = false; + void fetch("/api/version", { cache: "no-store" }) + .then((res) => res.json() as Promise) + .then((data) => { + if (!cancelled) setInfo(data); + }) + .catch(() => { + if (!cancelled) setInfo(EMPTY); + }); + return () => { + cancelled = true; + }; + }, []); + + const resolved = info ?? EMPTY; + const versionLabel = resolveVersionLabel(resolved); + const lastUpdated = formatCommitDate(resolved.commitDate); + const commitInfo = + resolved.commitHash && resolved.commitSubject + ? `${resolved.commitHash} ยท ${resolved.commitSubject}` + : (resolved.commitHash ?? resolved.commitSubject ?? null); + const showSecondary = lastUpdated !== null || commitInfo !== null; return (
-
+
- {appVersion ? `version ${appVersion}` : "unknown version"} + version {versionLabel} - {appLastUpdated && ( + {showSecondary && ( - last updated {new Date(Number(appLastUpdated) * 1000).toLocaleString()} + {lastUpdated && <>last updated {lastUpdated}} + {lastUpdated && commitInfo &&
} + {commitInfo && <>{commitInfo}}
)}
diff --git a/src/lib/versionInfo.ts b/src/lib/versionInfo.ts new file mode 100644 index 0000000..5434255 --- /dev/null +++ b/src/lib/versionInfo.ts @@ -0,0 +1,9 @@ +export interface VersionInfo { + version: string | null; + tag: string | null; + gitDescribe: string | null; + commitHash: string | null; + commitDate: string | null; + commitSubject: string | null; + buildTime: string | null; +} \ No newline at end of file diff --git a/src/scripts/generateVersionInfo.ts b/src/scripts/generateVersionInfo.ts new file mode 100644 index 0000000..7fb47be --- /dev/null +++ b/src/scripts/generateVersionInfo.ts @@ -0,0 +1,70 @@ +/** + * Generates src/generated/versionInfo.json with the project's current version, + * git tag/describe info, and the last commit's metadata. Consumed by the admin + * VersionOverlay via GET /api/version (the file is read at runtime so a fresh + * post-bump regeneration rsync'd to the server shows up without a rebuild). + * + * Gracefully no-ops on missing git metadata (no .git, no tags) so the JSON + * always exists with whatever fields are resolvable. + */ +import { execSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(here, "..", ".."); + +function git(args: string): string | null { + try { + const out = execSync(`git ${args}`, { cwd: projectRoot, encoding: "utf8" }); + const trimmed = out.trim(); + return trimmed.length > 0 ? trimmed : null; + } catch { + return null; + } +} + +function readPackageVersion(): string | null { + try { + const pkg = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")); + return typeof pkg?.version === "string" && pkg.version.length > 0 ? pkg.version : null; + } catch { + return null; + } +} + +function main(): void { + const version = readPackageVersion(); + const tag = git("describe --tags --always --abbrev=0"); + const gitDescribe = git("describe --tags --always --abbrev=7"); + const commitHash = git("rev-parse --short HEAD"); + const commitDate = git("log -1 --format=%cI"); + const commitSubject = git("log -1 --format=%s"); + const buildTime = new Date().toISOString(); + + const info = { + version, + tag, + gitDescribe, + commitHash, + commitDate, + commitSubject, + buildTime, + }; + + const outDir = path.join(projectRoot, "src", "generated"); + if (!existsSync(outDir)) { + mkdirSync(outDir, { recursive: true }); + } + writeFileSync( + path.join(outDir, "versionInfo.json"), + `${JSON.stringify(info, null, 2)}\n`, + "utf8", + ); + + const label = gitDescribe ?? tag ?? version ?? "unknown"; + console.log(`[version-info] wrote src/generated/versionInfo.json (${label})`); +} + +main(); \ No newline at end of file