From 024f9fdd46d73cae9b336c0aa466e56e68152f4f Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 10:59:38 -0700 Subject: [PATCH] feat(site-builder): stamp git sha + date into the editor bundle package.json's version is hand-maintained and never changes between builds, so a bug report can't identify which bundle produced it. Vite's `define` injects __EDITOR_BUILD__ (short git SHA + build date) at compile time; editorBuild() in build-stamp.ts is the only safe way to read it, falling back to 'dev' since vitest does not apply Vite's `define` and the identifier is otherwise undeclared. The execSync call falls back to 'nogit' when building outside a git checkout (release tarballs), verified by building from a directory with no git ancestry at all. Also wires editorBuild() into a startup console.log in main.tsx -- without any reference to it, Vite tree-shakes the unused module out of the bundle entirely and __EDITOR_BUILD__ never gets substituted, silently leaving every bug report saying 'dev'. Task 19 will add the real call site when it assembles the report payload. Co-Authored-By: Claude Opus 5 (1M context) --- craft/src/main.tsx | 7 +++++++ craft/src/utils/build-stamp.ts | 10 ++++++++++ craft/src/vite-env.d.ts | 5 +++++ craft/vite.config.ts | 17 +++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 craft/src/utils/build-stamp.ts create mode 100644 craft/src/vite-env.d.ts diff --git a/craft/src/main.tsx b/craft/src/main.tsx index 695fa33..e0da316 100644 --- a/craft/src/main.tsx +++ b/craft/src/main.tsx @@ -3,12 +3,19 @@ import ReactDOM from 'react-dom/client'; import { App } from './App'; import { WhpConfig } from './types'; import { installConsoleErrorBuffer } from './utils/console-buffer'; +import { editorBuild } from './utils/build-stamp'; import './styles/editor.css'; // Installed before React mounts so errors thrown during the first render are // captured too. installConsoleErrorBuffer(); +// Logged so the build that produced a given session is visible in the +// browser console; also the only current reference to `editorBuild()`, +// which keeps the __EDITOR_BUILD__-reading module from being tree-shaken +// out of the bundle before Task 19 wires it into the report payload. +console.log(`[site-builder] build ${editorBuild()}`); + // Read WHP_CONFIG injected by PHP wrapper (or null for standalone dev) const whpConfig: WhpConfig | null = (window as any).WHP_CONFIG || null; diff --git a/craft/src/utils/build-stamp.ts b/craft/src/utils/build-stamp.ts new file mode 100644 index 0000000..2fd273f --- /dev/null +++ b/craft/src/utils/build-stamp.ts @@ -0,0 +1,10 @@ +/** + * Reads the compile-time `__EDITOR_BUILD__` define. + * + * vitest does not apply Vite's `define`, and `npm run dev` in a non-git + * checkout may not either, so every read goes through here. `typeof` on an + * undeclared identifier is safe in JS -- it does not throw. + */ +export function editorBuild(): string { + return typeof __EDITOR_BUILD__ !== 'undefined' ? __EDITOR_BUILD__ : 'dev'; +} diff --git a/craft/src/vite-env.d.ts b/craft/src/vite-env.d.ts new file mode 100644 index 0000000..a3024a5 --- /dev/null +++ b/craft/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +/** Injected by vite.config.ts `define`. Absent under vitest -- always read + * it through `utils/build-stamp.ts`'s `editorBuild()`, never directly. */ +declare const __EDITOR_BUILD__: string; diff --git a/craft/vite.config.ts b/craft/vite.config.ts index 516589b..de42c20 100644 --- a/craft/vite.config.ts +++ b/craft/vite.config.ts @@ -1,6 +1,20 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import path from 'path' +import { execSync } from 'child_process' + +/** Short git SHA + build date, injected as __EDITOR_BUILD__ so an issue + * report identifies exactly which bundle produced it. package.json's + * version is hand-maintained and never changes between builds. */ +const editorBuild = (() => { + let sha = 'nogit' + try { + sha = execSync('git rev-parse --short HEAD', { cwd: __dirname }).toString().trim() + } catch { + // Building outside a git checkout (release tarball) -- keep 'nogit'. + } + return `${sha}-${new Date().toISOString().slice(0, 10)}` +})() export default defineConfig({ plugins: [react()], @@ -10,6 +24,9 @@ export default defineConfig({ '@': path.resolve(__dirname, './src'), }, }, + define: { + __EDITOR_BUILD__: JSON.stringify(editorBuild), + }, build: { outDir: 'dist', rollupOptions: {