whitesurge

Untitled

Jul 2nd, 2026
15,833
0
Never
2
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 3.53 KB | None | 0 0
  1. Implement a "build hash" opt-in refresh system for this Vite + React app, so users on a stale tab get notified when a new deploy has shipped and can choose to reload — without losing unsaved work.
  2.  
  3. 1. Vite plugin (vite.config.ts)
  4.  
  5. Add a build-time hash of all source files:
  6.  
  7. Write a generateBuildHash() function that recursively walks src/ and public/ (skip node_modules and dist), hashing (SHA-256) the file path + contents of every .ts/.tsx/.js/.jsx/.css/.html/.json/.md file, plus the contents of package.json (so dependency changes count too). Sort directory entries before hashing for determinism. Truncate the digest to the first 16 hex chars.
  8. Add a custom Vite plugin with a generateBundle() hook that calls generateBundle → this.emitFile({ type: "asset", fileName: "build-hash.txt", source: buildHash }), so the hash is emitted as a plain-text static asset at /build-hash.txt in the production build.
  9. Also add define: { __BUILD_HASH__: JSON.stringify(generateBuildHash()) } so the same hash is inlined as a global constant baked directly into the JS bundle at build time.
  10. Declare the global for TypeScript in vite-env.d.ts: declare const __BUILD_HASH__: string;
  11. Both values come from the same hash computed at build time, so they always match for a given deploy and diverge only when the deploy changes.
  12.  
  13. 2. React context (UpdateContext.tsx + types)
  14.  
  15. Create an UpdateProvider that:
  16.  
  17. Captures const currentBuildHash = __BUILD_HASH__ — the hash the currently-running bundle was built with.
  18. Exposes a checkForUpdates() async function that does fetch("/build-hash.txt?t=" + Date.now(), { cache: "no-cache", headers: { "Cache-Control": "no-cache, no-store, must-revalidate", Pragma: "no-cache", Expires: "0" } }) — the query-string cache-buster plus explicit no-cache headers are required so this always hits the live server, never a stale CDN/browser cache. Compare the fetched text (trimmed) against currentBuildHash; if different, set hasUpdate = true.
  19. Runs this check: once ~5s after mount, then on a repeating interval (config value checkIntervalMinutes, default 15), and again whenever the browser regains network connectivity.
  20. Exposes hasUnsavedData / setHasUnsavedData state (other parts of the app set this when a form/edit is in progress) and a confirmUnsavedOnRefresh flag.
  21. Exposes refreshApp(): if hasUnsavedData && confirmUnsavedOnRefresh, show a native window.confirm(...) warning before reloading; otherwise call window.location.reload() directly.
  22. Exposes dismissUpdate() to clear hasUpdate without reloading.
  23. Add a beforeunload handler that warns on tab close/navigation when there's unsaved data and the confirm flag is set.
  24. Keep the context object and the useUpdate() hook in separate files from the provider component, so Vite's React Fast Refresh doesn't break (a file that exports both a component and non-component values loses fast-refresh support).
  25.  
  26. 3. Notification UI (UpdateNotification.tsx)
  27.  
  28. A small dismissible toast, fixed-position bottom-right, that renders only when hasUpdate is true. Show "Update Available" with a short explanatory line (different wording if there's unsaved data), a primary "Refresh Now" (or "Refresh Anyway" if unsaved data exists) button calling refreshApp(), and secondary "Later" / dismiss (X) buttons calling dismissUpdate().
  29.  
  30. Key design point: detecting a new deploy never force-reloads the page. It only flips a flag; the actual reload is a deliberate user click, gated by an unsaved-changes confirmation. This avoids yanking a form out from under someone mid-edit while still keeping users off stale bundles for long.
Advertisement