Cleanup the code to make more readable

This commit is contained in:
Pranav
2026-01-11 11:18:17 -08:00
parent b345826566
commit 3527c07e94
3 changed files with 124 additions and 166 deletions
+46 -70
View File
@@ -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();
+76 -95
View File
@@ -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));
}
});