Cleanup the code to make more readable
This commit is contained in:
@@ -1,22 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/* eslint-disable no-console, no-restricted-syntax, no-continue */
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { build } = require('vite');
|
||||
|
||||
/**
|
||||
* Generate asset manifest from Vite build output
|
||||
* Returns array of { url, revision } for JS/CSS files
|
||||
*/
|
||||
function generateAssetManifest() {
|
||||
// vite-plugin-ruby outputs to 'vite' in production, 'vite-dev' in development
|
||||
const manifestPaths = [
|
||||
path.resolve(__dirname, '../../../public/vite/.vite/manifest.json'),
|
||||
path.resolve(__dirname, '../../../public/vite-dev/.vite/manifest.json'),
|
||||
];
|
||||
const ROOT_DIR = process.cwd();
|
||||
const PATHS = {
|
||||
manifest: path.join(ROOT_DIR, 'public/vite/.vite/manifest.json'),
|
||||
swRuntime: path.join(__dirname, 'sw-runtime.js'),
|
||||
swRuntimeBuilt: path.join(ROOT_DIR, 'tmp/sw-build/sw-runtime.js'),
|
||||
pushHandlers: path.join(__dirname, 'push-handlers.js'),
|
||||
buildDir: path.join(ROOT_DIR, 'tmp/sw-build'),
|
||||
output: path.join(ROOT_DIR, 'public/sw.js'),
|
||||
};
|
||||
|
||||
let manifestPath = manifestPaths.find(p => fs.existsSync(p));
|
||||
const buildSWContent = (runtimeBundle, pushHandlers, assetCount) => `
|
||||
/* Service Worker for Chatwoot */
|
||||
/* Generated: ${new Date().toISOString()} */
|
||||
/* Precached assets: ${assetCount} */
|
||||
|
||||
${runtimeBundle}
|
||||
|
||||
${pushHandlers}
|
||||
`;
|
||||
|
||||
function generateAssetManifest() {
|
||||
const manifestPath = fs.existsSync(PATHS.manifest) ? PATHS.manifest : null;
|
||||
|
||||
if (!manifestPath) {
|
||||
console.warn(
|
||||
@@ -31,8 +41,7 @@ function generateAssetManifest() {
|
||||
const assets = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const [, entry] of Object.entries(manifest)) {
|
||||
// Add the main file (JS or CSS)
|
||||
Object.entries(manifest).forEach(([, entry]) => {
|
||||
const file = entry.file;
|
||||
if (file && !seen.has(file)) {
|
||||
seen.add(file);
|
||||
@@ -41,78 +50,62 @@ function generateAssetManifest() {
|
||||
}
|
||||
}
|
||||
|
||||
// Add CSS files from the css array (CSS imported by JS modules)
|
||||
if (entry.css) {
|
||||
for (const cssFile of entry.css) {
|
||||
entry.css.forEach(cssFile => {
|
||||
if (!seen.has(cssFile)) {
|
||||
seen.add(cssFile);
|
||||
assets.push({ url: cssFile, revision: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add assets (fonts, images, etc.)
|
||||
if (entry.assets) {
|
||||
for (const assetFile of entry.assets) {
|
||||
entry.assets.forEach(assetFile => {
|
||||
if (!seen.has(assetFile)) {
|
||||
seen.add(assetFile);
|
||||
assets.push({ url: assetFile, revision: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`📦 Found ${assets.length} assets to precache`);
|
||||
return assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the service worker
|
||||
*/
|
||||
async function buildServiceWorker() {
|
||||
console.log('🔨 Building service worker...');
|
||||
|
||||
// Ensure CDN host has protocol prefix for absolute URLs
|
||||
let assetOrigin = process.env.ASSET_CDN_HOST || '';
|
||||
if (assetOrigin && !assetOrigin.startsWith('http')) {
|
||||
if (assetOrigin) {
|
||||
assetOrigin = `https://${assetOrigin}`;
|
||||
}
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
// vite-plugin-ruby serves from /vite/ in production, /vite-dev/ in development
|
||||
const assetPath = isProduction ? '/vite/' : '/vite-dev/';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const assetPath = '/vite/';
|
||||
|
||||
console.log(`📁 Root dir: ${ROOT_DIR}`);
|
||||
console.log(`🌐 Asset origin: ${assetOrigin || '(local)'}`);
|
||||
console.log(`📁 Asset path: ${assetPath}`);
|
||||
console.log(`🏭 Environment: ${isProduction ? 'production' : 'development'}`);
|
||||
|
||||
// In development mode, just use push handlers (no caching)
|
||||
if (!isProduction) {
|
||||
console.log(
|
||||
'⚠️ Development mode: Using push notifications only (no caching)'
|
||||
);
|
||||
|
||||
const devServiceWorker = fs.readFileSync(
|
||||
path.resolve(__dirname, 'push-handlers.js'),
|
||||
'utf8'
|
||||
);
|
||||
const devServiceWorker = fs.readFileSync(PATHS.pushHandlers, 'utf8');
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../../../public/sw.js'),
|
||||
devServiceWorker
|
||||
);
|
||||
fs.writeFileSync(PATHS.output, devServiceWorker);
|
||||
|
||||
console.log('✅ Development service worker created at public/sw.js');
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate asset manifest for precaching
|
||||
const assetManifest = generateAssetManifest();
|
||||
|
||||
// Production mode: Build with custom runtime
|
||||
try {
|
||||
console.log('📦 Building service worker runtime...');
|
||||
|
||||
// Build the runtime bundle
|
||||
await build({
|
||||
configFile: false,
|
||||
css: {
|
||||
@@ -122,12 +115,12 @@ async function buildServiceWorker() {
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'sw-runtime.js'),
|
||||
entry: PATHS.swRuntime,
|
||||
formats: ['iife'],
|
||||
name: 'ServiceWorkerRuntime',
|
||||
fileName: () => 'sw-runtime.js',
|
||||
},
|
||||
outDir: path.resolve(__dirname, '../../../tmp/sw-build'),
|
||||
outDir: PATHS.buildDir,
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
rollupOptions: {
|
||||
@@ -147,33 +140,17 @@ async function buildServiceWorker() {
|
||||
|
||||
console.log('✅ Service worker runtime built');
|
||||
|
||||
// Read the built runtime
|
||||
const runtimeBundle = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../../tmp/sw-build/sw-runtime.js'),
|
||||
'utf8'
|
||||
const runtimeBundle = fs.readFileSync(PATHS.swRuntimeBuilt, 'utf8');
|
||||
|
||||
const pushHandlers = fs.readFileSync(PATHS.pushHandlers, 'utf8');
|
||||
|
||||
const finalServiceWorker = buildSWContent(
|
||||
runtimeBundle,
|
||||
pushHandlers,
|
||||
assetManifest.length
|
||||
);
|
||||
|
||||
// Read the push handlers
|
||||
const pushHandlers = fs.readFileSync(
|
||||
path.resolve(__dirname, 'push-handlers.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Combine them
|
||||
const finalServiceWorker = `/* Service Worker for Chatwoot - Generated at build time */
|
||||
/* Generated: ${new Date().toISOString()} */
|
||||
/* Precached assets: ${assetManifest.length} */
|
||||
|
||||
${runtimeBundle}
|
||||
|
||||
${pushHandlers}
|
||||
`;
|
||||
|
||||
// Write to public/sw.js
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../../../public/sw.js'),
|
||||
finalServiceWorker
|
||||
);
|
||||
fs.writeFileSync(PATHS.output, finalServiceWorker);
|
||||
|
||||
console.log('✅ Service worker built successfully at public/sw.js');
|
||||
console.log(` - ${assetManifest.length} assets in precache manifest`);
|
||||
@@ -183,7 +160,6 @@ ${pushHandlers}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the build
|
||||
buildServiceWorker().catch(err => {
|
||||
console.error('❌ Unhandled error:', err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-restricted-globals, no-console */
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/* globals clients */
|
||||
|
||||
self.addEventListener('push', event => {
|
||||
let notification = event.data && event.data.json();
|
||||
|
||||
|
||||
@@ -5,11 +5,8 @@ const ASSET_ORIGIN = __ASSET_ORIGIN__;
|
||||
const ASSET_PATH = __ASSET_PATH__;
|
||||
const ASSET_MANIFEST = __ASSET_MANIFEST__;
|
||||
|
||||
// Stable cache name - Vite includes content hashes in filenames,
|
||||
// so unchanged files keep the same URL and don't need refetching
|
||||
const CACHE_NAME = 'chatwoot-assets-v1';
|
||||
|
||||
// Paths that should never be cached (API, auth, real-time, etc.)
|
||||
const EXCLUDED_PATHS = [
|
||||
'/api/',
|
||||
'/auth/',
|
||||
@@ -25,48 +22,40 @@ const EXCLUDED_PATHS = [
|
||||
'/sw.js',
|
||||
];
|
||||
|
||||
// =============================================================================
|
||||
// Fetch Handler
|
||||
// =============================================================================
|
||||
const BATCH_SIZE = 5;
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Only handle GET requests from same origin or CDN
|
||||
if (request.method !== 'GET') return;
|
||||
const isOurRequest =
|
||||
url.origin === self.location.origin ||
|
||||
(ASSET_ORIGIN && url.origin === ASSET_ORIGIN);
|
||||
if (!isOurRequest) return;
|
||||
|
||||
// Skip excluded paths
|
||||
if (EXCLUDED_PATHS.some(path => url.pathname.startsWith(path))) return;
|
||||
|
||||
// Assets (JS/CSS/fonts) → cache-first
|
||||
const isAsset =
|
||||
url.pathname.startsWith('/vite-dev/') ||
|
||||
url.pathname.startsWith('/vite/') ||
|
||||
url.origin === ASSET_ORIGIN ||
|
||||
/\.(js|css|woff2?|ttf|otf|eot)$/i.test(url.pathname);
|
||||
|
||||
if (isAsset) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
const buildAssetListChunks = (arr, size) => {
|
||||
const result = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
result.push(arr.slice(i, i + size));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Caching Strategies
|
||||
// =============================================================================
|
||||
const prefetchBatch = async (cache, baseUrl, assets) => {
|
||||
const fetchAndCacheAsset = async asset => {
|
||||
const url = `${baseUrl}${asset.url}`;
|
||||
if (await cache.match(url)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
cache.put(url, response);
|
||||
}
|
||||
} catch {
|
||||
// Ignore prefetch failures
|
||||
}
|
||||
};
|
||||
|
||||
async function cacheFirst(request) {
|
||||
await Promise.all(assets.map(fetchAndCacheAsset));
|
||||
};
|
||||
|
||||
const cacheFirst = async request => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
// Return cached version if available
|
||||
const cached = await cache.match(request);
|
||||
if (cached) return cached;
|
||||
|
||||
// Otherwise fetch and cache
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
@@ -74,49 +63,51 @@ async function cacheFirst(request) {
|
||||
}
|
||||
return response;
|
||||
} catch (err) {
|
||||
// Network failed - return stale cache if available
|
||||
const stale = await cache.match(request);
|
||||
if (stale) return stale;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// Background Prefetch
|
||||
// =============================================================================
|
||||
|
||||
async function prefetchAssets() {
|
||||
const prefetchAssets = async () => {
|
||||
if (!ASSET_MANIFEST?.length) return;
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Build base URL: CDN origin + asset path, or local origin + asset path
|
||||
const baseUrl = ASSET_ORIGIN
|
||||
? `${ASSET_ORIGIN}${ASSET_PATH}`
|
||||
: `${self.location.origin}${ASSET_PATH}`;
|
||||
|
||||
// Prefetch in batches of 5 (await in loop is intentional for throttling)
|
||||
for (let i = 0; i < ASSET_MANIFEST.length; i += 5) {
|
||||
const batch = ASSET_MANIFEST.slice(i, i + 5);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.all(
|
||||
batch.map(async asset => {
|
||||
const url = `${baseUrl}${asset.url}`;
|
||||
if (await cache.match(url)) return; // Already cached
|
||||
const batches = buildAssetListChunks(ASSET_MANIFEST, BATCH_SIZE);
|
||||
await batches.reduce(
|
||||
(promise, batch) =>
|
||||
promise.then(() => prefetchBatch(cache, baseUrl, batch)),
|
||||
Promise.resolve()
|
||||
);
|
||||
};
|
||||
const cleanupStaleAssets = async () => {
|
||||
if (!ASSET_MANIFEST?.length) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.ok) cache.put(url, response);
|
||||
} catch {
|
||||
// Ignore prefetch failures
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedRequests = await cache.keys();
|
||||
|
||||
// =============================================================================
|
||||
// Lifecycle Events
|
||||
// =============================================================================
|
||||
const baseUrl = ASSET_ORIGIN
|
||||
? `${ASSET_ORIGIN}${ASSET_PATH}`
|
||||
: `${self.location.origin}${ASSET_PATH}`;
|
||||
|
||||
const validUrls = new Set(
|
||||
ASSET_MANIFEST.map(asset => `${baseUrl}${asset.url}`)
|
||||
);
|
||||
|
||||
const deletions = cachedRequests
|
||||
.filter(request => {
|
||||
const url = new URL(request.url);
|
||||
const isAssetPath = url.pathname.startsWith('/vite/assets/');
|
||||
return isAssetPath && !validUrls.has(request.url);
|
||||
})
|
||||
.map(request => cache.delete(request));
|
||||
|
||||
await Promise.all(deletions);
|
||||
};
|
||||
|
||||
self.addEventListener('install', () => self.skipWaiting());
|
||||
|
||||
@@ -126,36 +117,26 @@ self.addEventListener('activate', event => {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Remove cached assets that aren't in the current manifest.
|
||||
* This keeps unchanged files (same content hash) while removing old versions.
|
||||
*/
|
||||
async function cleanupStaleAssets() {
|
||||
if (!ASSET_MANIFEST?.length) return;
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedRequests = await cache.keys();
|
||||
// Build base URL: CDN origin + asset path, or local origin + asset path
|
||||
const baseUrl = ASSET_ORIGIN
|
||||
? `${ASSET_ORIGIN}${ASSET_PATH}`
|
||||
: `${self.location.origin}${ASSET_PATH}`;
|
||||
if (EXCLUDED_PATHS.some(path => url.pathname.startsWith(path))) return;
|
||||
|
||||
// Build set of valid asset URLs from current manifest
|
||||
const validUrls = new Set(
|
||||
ASSET_MANIFEST.map(asset => `${baseUrl}${asset.url}`)
|
||||
);
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
// Delete cached entries that are assets but not in current manifest
|
||||
const deletions = cachedRequests
|
||||
.filter(request => {
|
||||
const url = new URL(request.url);
|
||||
const isAssetPath =
|
||||
url.pathname.startsWith('/vite/assets/') ||
|
||||
url.pathname.startsWith('/vite-dev/assets/');
|
||||
// Only clean up asset files
|
||||
return isAssetPath && !validUrls.has(request.url);
|
||||
})
|
||||
.map(request => cache.delete(request));
|
||||
const isAChatwootRequest =
|
||||
url.origin === self.location.origin ||
|
||||
(ASSET_ORIGIN && url.origin === ASSET_ORIGIN);
|
||||
|
||||
await Promise.all(deletions);
|
||||
}
|
||||
if (!isAChatwootRequest) return;
|
||||
|
||||
const isAnAsset =
|
||||
url.pathname.startsWith('/vite/') ||
|
||||
url.origin === ASSET_ORIGIN ||
|
||||
/\.(js|css|woff2?|ttf|otf|eot|svg|png)$/i.test(url.pathname);
|
||||
|
||||
if (isAnAsset) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user