From 80f6aba4a8874775aec3e923b82ac410aaa6eed4 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 10 Feb 2026 19:19:43 +0000
Subject: [PATCH] feat: display version hash and build date in footer
Refactored scripts/hash-dist.mjs to export buildHashState and support module import.
Modified scripts/build-dist.mjs to calculate the dist hash after asset rewriting and inject it along with the current date into dist/index.html below the tagline.
Verified with Playwright screenshot.
Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
---
scripts/build-dist.mjs | 35 +++++++++++++++++++++++++++++++++++
scripts/hash-dist.mjs | 7 +++++--
verification.png | Bin 0 -> 5944 bytes
verify_version.py | 29 +++++++++++++++++++++++++++++
4 files changed, 69 insertions(+), 2 deletions(-)
create mode 100644 verification.png
create mode 100644 verify_version.py
diff --git a/scripts/build-dist.mjs b/scripts/build-dist.mjs
index c19d684d..1aa36a6f 100644
--- a/scripts/build-dist.mjs
+++ b/scripts/build-dist.mjs
@@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import crypto from 'node:crypto';
+import { buildHashState } from './hash-dist.mjs';
const DIST = 'dist';
const FILES_TO_COPY = [
@@ -154,6 +155,38 @@ function rewriteEntryHtmlAssetPaths(manifest) {
}
}
+function injectVersionInfo() {
+ console.log('Injecting version hash and date...');
+ try {
+ const hashState = buildHashState(DIST);
+ const hash = hashState.combined;
+ const date = new Date().toISOString().split('T')[0];
+
+ const indexHtmlPath = path.join(DIST, 'index.html');
+ if (fs.existsSync(indexHtmlPath)) {
+ let content = fs.readFileSync(indexHtmlPath, 'utf8');
+
+ const versionHtml = `
+
+ v: ${hash.slice(0, 8)} • ${date}
+
`;
+
+ // Fallback search if whitespace differs
+ const taglineRegex = /(seed\. zap\. subscribe\.\s*<\/h2>)/;
+ if (taglineRegex.test(content)) {
+ content = content.replace(taglineRegex, '$1' + versionHtml);
+ fs.writeFileSync(indexHtmlPath, content);
+ console.log(`Injected version: ${hash.slice(0, 8)} • ${date}`);
+ } else {
+ console.warn('Could not find tagline to inject version info.');
+ }
+ }
+ } catch (error) {
+ console.error('Failed to inject version info:', error);
+ // Continue even if injection fails, as it's not critical for functionality
+ }
+}
+
function main() {
console.log('Validating service worker compatibility guard...');
try {
@@ -209,6 +242,8 @@ function main() {
console.log('Rewriting HTML entry points with hashed assets...');
rewriteEntryHtmlAssetPaths(manifest);
+ injectVersionInfo();
+
console.log('Build complete.');
}
diff --git a/scripts/hash-dist.mjs b/scripts/hash-dist.mjs
index 8637c776..dc41b53e 100644
--- a/scripts/hash-dist.mjs
+++ b/scripts/hash-dist.mjs
@@ -2,6 +2,7 @@
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
+import { fileURLToPath } from 'node:url';
function parseArgs(argv) {
const args = {
@@ -72,7 +73,7 @@ function hashBuffer(buffer) {
return crypto.createHash('sha256').update(buffer).digest('hex');
}
-function buildHashState(distDir) {
+export function buildHashState(distDir) {
const resolvedDistDir = path.resolve(distDir);
if (!fs.existsSync(resolvedDistDir)) {
throw new Error(`Dist directory does not exist: ${resolvedDistDir}`);
@@ -112,4 +113,6 @@ function main() {
console.log(serialized);
}
-main();
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ main();
+}
diff --git a/verification.png b/verification.png
new file mode 100644
index 0000000000000000000000000000000000000000..fda37f88bd5f6d2244fd8e13174f2292c9b85225
GIT binary patch
literal 5944
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
literal 0
HcmV?d00001
diff --git a/verify_version.py b/verify_version.py
new file mode 100644
index 00000000..0130aaff
--- /dev/null
+++ b/verify_version.py
@@ -0,0 +1,29 @@
+from playwright.sync_api import sync_playwright
+
+def run():
+ with sync_playwright() as p:
+ browser = p.chromium.launch()
+ page = browser.new_page()
+ page.goto("http://localhost:3000")
+
+ # Locate the tagline
+ tagline = page.get_by_text("seed. zap. subscribe.")
+
+ # Locate the version info which should be nearby
+ # The tagline is an h2 inside a div.
+ container = tagline.locator("..")
+
+ # Wait for fade-in to settle or just force visibility?
+ # The container has "fade-in" class. It might take time.
+ # I'll wait for the version text to be visible.
+ version_locator = container.locator("div.text-xs")
+ version_locator.wait_for()
+
+ print("Version text found:", version_locator.inner_text())
+
+ # Screenshot the container
+ container.screenshot(path="verification.png")
+ browser.close()
+
+if __name__ == "__main__":
+ run()