#!/usr/bin/env node /** * Update Cache Version - Unified Cache Busting * * CRITICAL: Run this script EVERY TIME JavaScript files are modified! * * Updates: * 1. All HTML files with ?v= cache-busting parameters * 2. public/service-worker.js CACHE_VERSION constant * 3. public/version.json with new version and changelog * * Format: v={package.version}.{timestamp} * Example: v=0.1.0.1760201234 * * This ensures: * - Browser cache is invalidated * - Service worker forces refresh * - Version tracking is updated */ const fs = require('fs'); const path = require('path'); const packageJson = require('../package.json'); // Parse semantic version from package.json const [major, minor, patch] = packageJson.version.split('.').map(Number); // Generate cache version: package version + timestamp const timestamp = Date.now(); const CACHE_VERSION = `${packageJson.version}.${timestamp}`; // Bump patch version for version.json const NEW_SEMVER = `${major}.${minor}.${patch + 1}`; const VERSION_FILE = path.join(__dirname, '../public/version.json'); const SERVICE_WORKER_FILE = path.join(__dirname, '../public/service-worker.js'); // HTML files to update (relative to project root) const HTML_FILES = [ 'public/index.html', 'public/docs.html', 'public/faq.html', 'public/researcher.html', 'public/implementer.html', 'public/leader.html', 'public/about.html', 'public/privacy.html', 'public/blog.html', 'public/blog-post.html', 'public/docs-viewer.html', 'public/api-reference.html', 'public/media-inquiry.html', 'public/case-submission.html', 'public/koha.html', 'public/check-version.html', 'public/village-ai.html', 'public/architecture.html', 'public/village-case-study.html', 'public/architectural-alignment.html', 'public/architectural-alignment-community.html', 'public/architectural-alignment-policymakers.html', 'public/korero-counter-arguments.html', 'public/admin/blog-curation.html', 'public/admin/dashboard.html' ]; /** * Update cache version in a file * Replaces all instances of ?v=X with ?v={CACHE_VERSION} */ function updateCacheVersion(filePath) { try { const fullPath = path.join(__dirname, '..', filePath); if (!fs.existsSync(fullPath)) { console.warn(`⚠️ File not found: ${filePath}`); return false; } let content = fs.readFileSync(fullPath, 'utf8'); const originalContent = content; // Pattern: ?v=ANYTHING → ?v={CACHE_VERSION} // Matches: ?v=1.0.4, ?v=1759833751, ?v=1.0.5.1760123456 content = content.replace(/\?v=[0-9a-zA-Z._-]+/g, `?v=${CACHE_VERSION}`); // Also catch bare CSS/JS references that are missing ?v= entirely // Adds ?v= to .css and .js hrefs/srcs that don't have one content = content.replace(/(href|src)="([^"]+\.(?:css|js))(?!.*\?v=)"/g, `$1="$2?v=${CACHE_VERSION}"`); // Only write if changed if (content !== originalContent) { fs.writeFileSync(fullPath, content, 'utf8'); // Count replacements const matches = originalContent.match(/\?v=[0-9a-zA-Z._-]+/g) || []; console.log(`✅ ${filePath}: Updated ${matches.length} cache version(s)`); return true; } else { console.log(`ℹ️ ${filePath}: No changes needed`); return false; } } catch (error) { console.error(`❌ Error updating ${filePath}:`, error.message); return false; } } /** * Update service worker CACHE_VERSION */ function updateServiceWorker() { try { let content = fs.readFileSync(SERVICE_WORKER_FILE, 'utf8'); const original = content; // Update CACHE_VERSION constant content = content.replace( /const CACHE_VERSION = '[^']+';/, `const CACHE_VERSION = '${NEW_SEMVER}';` ); if (content !== original) { fs.writeFileSync(SERVICE_WORKER_FILE, content); console.log(`✅ service-worker.js: Updated CACHE_VERSION to ${NEW_SEMVER}`); return true; } return false; } catch (error) { console.error(`❌ Error updating service-worker.js:`, error.message); return false; } } /** * Update version.json */ function updateVersionJson() { try { const versionData = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8')); versionData.version = NEW_SEMVER; versionData.buildDate = new Date().toISOString(); versionData.forceUpdate = false; // Preserve existing changelog if (!versionData.changelog) { versionData.changelog = ['Cache version update - JavaScript files modified']; } fs.writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n'); console.log(`✅ version.json: Updated to ${NEW_SEMVER}`); return true; } catch (error) { console.error(`❌ Error updating version.json:`, error.message); return false; } } /** * Main execution */ function main() { console.log(''); console.log('═'.repeat(70)); console.log(' Tractatus - Cache Version Update (CRITICAL FOR .JS CHANGES)'); console.log('═'.repeat(70)); console.log(''); console.log(`📦 Package version: ${packageJson.version}`); console.log(`🔄 New semantic version: ${NEW_SEMVER}`); console.log(`🔄 New cache-bust version: ${CACHE_VERSION}`); console.log(''); // Step 1: Update service worker console.log('Step 1: Updating service worker...'); updateServiceWorker(); console.log(''); // Step 2: Update version.json console.log('Step 2: Updating version.json...'); updateVersionJson(); console.log(''); // Step 3: Update HTML cache parameters console.log('Step 3: Updating HTML cache parameters...'); let updatedCount = 0; let totalFiles = 0; HTML_FILES.forEach(file => { totalFiles++; if (updateCacheVersion(file)) { updatedCount++; } }); console.log(''); console.log('═'.repeat(70)); console.log(` Summary: ${updatedCount}/${totalFiles} HTML files updated`); console.log('═'.repeat(70)); console.log(''); console.log('✅ Cache version update complete!'); console.log(''); console.log('📝 Files modified:'); console.log(' - public/service-worker.js (CACHE_VERSION)'); console.log(' - public/version.json (version + buildDate)'); console.log(` - ${updatedCount} HTML files (?v= parameters)`); console.log(''); console.log('⚠️ NEXT STEPS:'); console.log(' 1. Review changes: git diff'); console.log(' 2. Commit: git add -A && git commit -m "chore: bump cache version"'); console.log(' 3. Deploy to production'); console.log(''); } // Run if called directly if (require.main === module) { main(); } module.exports = { updateCacheVersion, CACHE_VERSION };