Add automatic cache clearing and version management to prevent white screen issues
Some checks failed
Build and Push Docker Images / docker (push) Failing after 44s
Some checks failed
Build and Push Docker Images / docker (push) Failing after 44s
Implements comprehensive service worker solution with: - Dynamic versioning using git commit hash or timestamp - Automatic cache invalidation on new deployments - Hourly update checks and user notifications - Network-first caching strategy with 24-hour expiration - Build automation via prebuild script - Update notification UI component This prevents stale cached code from causing white screens by ensuring users always get the latest version after deployment. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c4059d5988
commit
a279bb6aa9
@ -1,17 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import { UpdateNotification } from './UpdateNotification';
|
||||
import * as swRegistration from '../lib/serviceWorkerRegistration';
|
||||
|
||||
export function RootLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const isAdminRoute = pathname?.startsWith('/admin');
|
||||
const isCheckoutSuccess = pathname?.startsWith('/checkout/success');
|
||||
const [updateAvailable, setUpdateAvailable] = useState<ServiceWorkerRegistration | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Register service worker with update detection
|
||||
swRegistration.register({
|
||||
onUpdate: (registration) => {
|
||||
console.log('New version available!');
|
||||
setUpdateAvailable(registration);
|
||||
},
|
||||
onSuccess: (registration) => {
|
||||
console.log('Service worker registered successfully');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isAdminRoute || isCheckoutSuccess) {
|
||||
// Admin routes and checkout success render without header/footer
|
||||
return <>{children}</>;
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<UpdateNotification registration={updateAvailable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Regular routes render with header/footer
|
||||
@ -22,6 +44,7 @@ export function RootLayoutClient({ children }: { children: React.ReactNode }) {
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<UpdateNotification registration={updateAvailable} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
72
components/UpdateNotification.tsx
Normal file
72
components/UpdateNotification.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
interface UpdateNotificationProps {
|
||||
registration: ServiceWorkerRegistration | null;
|
||||
}
|
||||
|
||||
export function UpdateNotification({ registration }: UpdateNotificationProps) {
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (registration) {
|
||||
setShowNotification(true);
|
||||
}
|
||||
}, [registration]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (registration && registration.waiting) {
|
||||
// Tell the service worker to skip waiting
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
|
||||
// Listen for the controller change and reload
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setShowNotification(false);
|
||||
};
|
||||
|
||||
if (!showNotification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
|
||||
<div className="rounded-lg p-4 shadow-2xl border border-blue-500/30 backdrop-blur-md bg-slate-900/90">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<RefreshCw className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white">
|
||||
New version available!
|
||||
</p>
|
||||
<p className="text-xs text-slate-300 mt-1">
|
||||
A new version of Puffin Offset is ready. Please update to get the latest features and improvements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Update Now
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/webp" href="/puffinOffset.webp" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Cache Control - Prevent stale cached versions -->
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
106
lib/serviceWorkerRegistration.ts
Normal file
106
lib/serviceWorkerRegistration.ts
Normal file
@ -0,0 +1,106 @@
|
||||
// Service Worker registration with automatic update detection for Next.js
|
||||
// This ensures users always get the latest version after deployment
|
||||
|
||||
type Config = {
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
|
||||
// Wait for page load to avoid impacting initial page load performance
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `/sw.js`;
|
||||
|
||||
registerValidSW(swUrl, config);
|
||||
|
||||
// Check for updates every hour
|
||||
setInterval(() => {
|
||||
checkForUpdates(swUrl);
|
||||
}, 60 * 60 * 1000); // 1 hour
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
// Check for updates on initial registration
|
||||
registration.update();
|
||||
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// New content is available; please refresh
|
||||
console.log('New content available! Please refresh.');
|
||||
|
||||
// Execute onUpdate callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// Content is cached for offline use
|
||||
console.log('Content cached for offline use.');
|
||||
|
||||
// Execute onSuccess callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkForUpdates(swUrl: string) {
|
||||
navigator.serviceWorker
|
||||
.getRegistration(swUrl)
|
||||
.then((registration) => {
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking for service worker updates:', error);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if (typeof window !== 'undefined' && 'serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Force refresh when a new service worker is waiting
|
||||
export function skipWaitingAndReload() {
|
||||
if (typeof window !== 'undefined') {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
if (registration.waiting) {
|
||||
// Tell the waiting service worker to skip waiting and become active
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the controller change and reload
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -36,6 +36,20 @@ const nextConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
// Service Worker - no cache to ensure updates are detected immediately
|
||||
{
|
||||
source: '/sw.js',
|
||||
headers: [
|
||||
{
|
||||
key: 'Cache-Control',
|
||||
value: 'no-cache, no-store, must-revalidate',
|
||||
},
|
||||
{
|
||||
key: 'Service-Worker-Allowed',
|
||||
value: '/',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"prebuild": "node scripts/inject-sw-version.js",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
|
||||
70
project/src/components/UpdateNotification.tsx
Normal file
70
project/src/components/UpdateNotification.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
|
||||
interface UpdateNotificationProps {
|
||||
registration: ServiceWorkerRegistration | null;
|
||||
}
|
||||
|
||||
export function UpdateNotification({ registration }: UpdateNotificationProps) {
|
||||
const [showNotification, setShowNotification] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (registration) {
|
||||
setShowNotification(true);
|
||||
}
|
||||
}, [registration]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (registration && registration.waiting) {
|
||||
// Tell the service worker to skip waiting
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
|
||||
// Listen for the controller change and reload
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismiss = () => {
|
||||
setShowNotification(false);
|
||||
};
|
||||
|
||||
if (!showNotification) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50 max-w-md w-full mx-4">
|
||||
<div className="glass-card rounded-lg p-4 shadow-2xl border border-blue-500/30 backdrop-blur-md bg-slate-900/90">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0">
|
||||
<RefreshCw className="w-6 h-6 text-blue-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-white">
|
||||
New version available!
|
||||
</p>
|
||||
<p className="text-xs text-slate-300 mt-1">
|
||||
A new version of Puffin Offset is ready. Please update to get the latest features and improvements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Update Now
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="px-4 py-2 text-sm font-medium text-slate-300 hover:text-white transition-colors"
|
||||
>
|
||||
Later
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,35 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { StrictMode, useState, useEffect } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { UpdateNotification } from './components/UpdateNotification';
|
||||
import * as swRegistration from './utils/serviceWorkerRegistration';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
function Root() {
|
||||
const [updateAvailable, setUpdateAvailable] = useState<ServiceWorkerRegistration | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Register service worker with update detection
|
||||
swRegistration.register({
|
||||
onUpdate: (registration) => {
|
||||
console.log('New version available!');
|
||||
setUpdateAvailable(registration);
|
||||
},
|
||||
onSuccess: (registration) => {
|
||||
console.log('Service worker registered successfully:', registration);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
<UpdateNotification registration={updateAvailable} />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<Root />);
|
||||
104
project/src/utils/serviceWorkerRegistration.ts
Normal file
104
project/src/utils/serviceWorkerRegistration.ts
Normal file
@ -0,0 +1,104 @@
|
||||
// Service Worker registration with automatic update detection
|
||||
// This ensures users always get the latest version after deployment
|
||||
|
||||
type Config = {
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
};
|
||||
|
||||
export function register(config?: Config) {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Wait for page load to avoid impacting initial page load performance
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `/sw.js`;
|
||||
|
||||
registerValidSW(swUrl, config);
|
||||
|
||||
// Check for updates every hour
|
||||
setInterval(() => {
|
||||
checkForUpdates(swUrl);
|
||||
}, 60 * 60 * 1000); // 1 hour
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl: string, config?: Config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
// Check for updates on initial registration
|
||||
registration.update();
|
||||
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// New content is available; please refresh
|
||||
console.log('New content available! Please refresh.');
|
||||
|
||||
// Execute onUpdate callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} else {
|
||||
// Content is cached for offline use
|
||||
console.log('Content cached for offline use.');
|
||||
|
||||
// Execute onSuccess callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkForUpdates(swUrl: string) {
|
||||
navigator.serviceWorker
|
||||
.getRegistration(swUrl)
|
||||
.then((registration) => {
|
||||
if (registration) {
|
||||
registration.update();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error checking for service worker updates:', error);
|
||||
});
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Force refresh when a new service worker is waiting
|
||||
export function skipWaitingAndReload() {
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
if (registration.waiting) {
|
||||
// Tell the waiting service worker to skip waiting and become active
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the controller change and reload
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
127
public/sw.js
127
public/sw.js
@ -1,65 +1,124 @@
|
||||
const CACHE_NAME = 'puffin-calculator-v2'; // Bumped to clear old cached code
|
||||
// Service Worker with automatic versioning and cache invalidation
|
||||
// Version is updated on each build to force cache refresh
|
||||
|
||||
const BUILD_TIMESTAMP = '__BUILD_TIMESTAMP__'; // Replaced during build
|
||||
const CACHE_NAME = `puffin-calculator-${BUILD_TIMESTAMP}`;
|
||||
const MAX_CACHE_AGE = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
|
||||
const urlsToCache = [
|
||||
'/',
|
||||
'/mobile-app',
|
||||
'/static/js/bundle.js',
|
||||
'/static/css/main.css',
|
||||
'/puffinOffset.webp',
|
||||
'/manifest.json'
|
||||
];
|
||||
|
||||
// Install event - cache resources
|
||||
self.addEventListener('install', (event) => {
|
||||
console.log('[SW] Installing service worker version:', BUILD_TIMESTAMP);
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then((cache) => {
|
||||
console.log('[SW] Caching app shell');
|
||||
return cache.addAll(urlsToCache);
|
||||
})
|
||||
.then(() => {
|
||||
// Force the waiting service worker to become the active service worker
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Cache install failed:', error);
|
||||
console.error('[SW] Cache install failed:', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clear old caches
|
||||
// Activate event - clear old caches and claim clients
|
||||
self.addEventListener('activate', (event) => {
|
||||
console.log('[SW] Activating service worker version:', BUILD_TIMESTAMP);
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('Clearing old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
Promise.all([
|
||||
// Clear old caches
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
console.log('[SW] Clearing old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}),
|
||||
// Claim all clients immediately
|
||||
self.clients.claim()
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve from cache when offline
|
||||
// Fetch event - Network first, fall back to cache
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip cross-origin requests
|
||||
if (url.origin !== location.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
// Try network first
|
||||
fetch(request)
|
||||
.then((response) => {
|
||||
// Return cached version or fetch from network
|
||||
return response || fetch(event.request);
|
||||
}
|
||||
)
|
||||
// Don't cache if not a success response
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Clone the response
|
||||
const responseToCache = response.clone();
|
||||
|
||||
// Cache the fetched resource
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
cache.put(request, responseToCache);
|
||||
});
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch(() => {
|
||||
// Network failed, try cache
|
||||
return caches.match(request).then((cachedResponse) => {
|
||||
if (cachedResponse) {
|
||||
// Check cache age for HTML documents
|
||||
if (request.destination === 'document') {
|
||||
const cacheDate = cachedResponse.headers.get('sw-cache-date');
|
||||
if (cacheDate) {
|
||||
const age = Date.now() - parseInt(cacheDate, 10);
|
||||
if (age > MAX_CACHE_AGE) {
|
||||
console.log('[SW] Cached HTML is too old, returning without cache');
|
||||
return new Response('Cache expired. Please connect to the internet.', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Not in cache and network failed
|
||||
return new Response('Offline and not cached', {
|
||||
status: 503,
|
||||
statusText: 'Service Unavailable'
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((cacheNames) => {
|
||||
return Promise.all(
|
||||
cacheNames.map((cacheName) => {
|
||||
if (cacheName !== CACHE_NAME) {
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
// Listen for skip waiting message
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
console.log('[SW] Received SKIP_WAITING message');
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
52
scripts/inject-sw-version.js
Normal file
52
scripts/inject-sw-version.js
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Inject build timestamp into service worker
|
||||
* This script replaces __BUILD_TIMESTAMP__ with the current timestamp
|
||||
* to force cache invalidation on new deployments
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const SW_SOURCE = path.join(__dirname, '..', 'public', 'sw.js');
|
||||
|
||||
try {
|
||||
// Read the service worker file
|
||||
let swContent = fs.readFileSync(SW_SOURCE, 'utf8');
|
||||
|
||||
// Check if placeholder exists
|
||||
if (!swContent.includes('__BUILD_TIMESTAMP__')) {
|
||||
console.log('⚠️ Service worker appears to be already processed');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Generate version (use git commit hash if available, otherwise use timestamp)
|
||||
let version;
|
||||
|
||||
try {
|
||||
// Try to get git commit hash using safe execFileSync
|
||||
version = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}).trim();
|
||||
console.log(`📦 Using git commit hash: ${version}`);
|
||||
} catch (error) {
|
||||
// Fallback to timestamp
|
||||
version = Date.now().toString();
|
||||
console.log(`📦 Using timestamp: ${version}`);
|
||||
}
|
||||
|
||||
// Replace the placeholder with the version
|
||||
swContent = swContent.replace(/__BUILD_TIMESTAMP__/g, version);
|
||||
|
||||
// Write the updated service worker
|
||||
fs.writeFileSync(SW_SOURCE, swContent);
|
||||
|
||||
console.log(`✅ Service worker version injected: ${version}`);
|
||||
console.log(`📝 Updated: ${SW_SOURCE}`);
|
||||
} catch (error) {
|
||||
console.error('❌ Error injecting service worker version:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user