From adbee23e12eda9509736330caa7f19000f5db449 Mon Sep 17 00:00:00 2001 From: Pranav Date: Sat, 10 Jan 2026 16:35:22 -0800 Subject: [PATCH] refactor: Colocate service worker build script with source files Move build script from scripts/build-service-worker.js to app/javascript/service-worker/build.js to keep all service worker related code in one place. Co-Authored-By: Claude Opus 4.5 --- .../javascript/service-worker/build.js | 35 +++++++---------- .../service-worker/push-handlers-only.js | 39 ------------------- .../service-worker/push-handlers.js | 16 +++----- app/javascript/service-worker/sw-runtime.js | 4 +- package.json | 2 +- 5 files changed, 22 insertions(+), 74 deletions(-) rename scripts/build-service-worker.js => app/javascript/service-worker/build.js (80%) delete mode 100644 app/javascript/service-worker/push-handlers-only.js diff --git a/scripts/build-service-worker.js b/app/javascript/service-worker/build.js similarity index 80% rename from scripts/build-service-worker.js rename to app/javascript/service-worker/build.js index 8d2f5a51b..e4e087572 100644 --- a/scripts/build-service-worker.js +++ b/app/javascript/service-worker/build.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +/* eslint-disable no-console, no-restricted-syntax, no-continue */ const fs = require('fs'); const path = require('path'); @@ -9,9 +10,10 @@ const { build } = require('vite'); * 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/packs/.vite/manifest.json'), - path.resolve(__dirname, '../public/vite-dev/.vite/manifest.json'), + path.resolve(__dirname, '../../../public/vite/.vite/manifest.json'), + path.resolve(__dirname, '../../../public/vite-dev/.vite/manifest.json'), ]; let manifestPath = manifestPaths.find(p => fs.existsSync(p)); @@ -56,29 +58,26 @@ async function buildServiceWorker() { const assetOrigin = process.env.ASSET_CDN_HOST || ''; const isProduction = process.env.NODE_ENV === 'production'; - // In production assets are in /packs/, in development they're in /vite-dev/ - const assetPath = isProduction ? '/packs/' : '/vite-dev/'; + // vite-plugin-ruby serves from /vite/ in production, /vite-dev/ in development + const assetPath = isProduction ? '/vite/' : '/vite-dev/'; console.log(`🌐 Asset origin: ${assetOrigin || '(local)'}`); console.log(`📁 Asset path: ${assetPath}`); console.log(`🏭 Environment: ${isProduction ? 'production' : 'development'}`); - // In development mode, just copy the push-handlers-only version + // 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, - '../app/javascript/service-worker/push-handlers-only.js' - ), + path.resolve(__dirname, 'push-handlers.js'), 'utf8' ); fs.writeFileSync( - path.resolve(__dirname, '../public/sw.js'), + path.resolve(__dirname, '../../../public/sw.js'), devServiceWorker ); @@ -103,15 +102,12 @@ async function buildServiceWorker() { }, build: { lib: { - entry: path.resolve( - __dirname, - '../app/javascript/service-worker/sw-runtime.js' - ), + entry: path.resolve(__dirname, 'sw-runtime.js'), formats: ['iife'], name: 'ServiceWorkerRuntime', fileName: () => 'sw-runtime.js', }, - outDir: path.resolve(__dirname, '../tmp/sw-build'), + outDir: path.resolve(__dirname, '../../../tmp/sw-build'), emptyOutDir: true, minify: true, rollupOptions: { @@ -133,16 +129,13 @@ async function buildServiceWorker() { // Read the built runtime const runtimeBundle = fs.readFileSync( - path.resolve(__dirname, '../tmp/sw-build/sw-runtime.js'), + path.resolve(__dirname, '../../../tmp/sw-build/sw-runtime.js'), 'utf8' ); // Read the push handlers const pushHandlers = fs.readFileSync( - path.resolve( - __dirname, - '../app/javascript/service-worker/push-handlers.js' - ), + path.resolve(__dirname, 'push-handlers.js'), 'utf8' ); @@ -158,7 +151,7 @@ ${pushHandlers} // Write to public/sw.js fs.writeFileSync( - path.resolve(__dirname, '../public/sw.js'), + path.resolve(__dirname, '../../../public/sw.js'), finalServiceWorker ); diff --git a/app/javascript/service-worker/push-handlers-only.js b/app/javascript/service-worker/push-handlers-only.js deleted file mode 100644 index 807203d25..000000000 --- a/app/javascript/service-worker/push-handlers-only.js +++ /dev/null @@ -1,39 +0,0 @@ -/* Development-only service worker - Push notifications only, no caching */ -/* eslint-disable no-restricted-globals, no-console */ -/* globals clients */ - -self.addEventListener('push', event => { - let notification = event.data && event.data.json(); - - event.waitUntil( - self.registration.showNotification(notification.title, { - tag: notification.tag, - data: { - url: notification.url, - }, - }) - ); -}); - -self.addEventListener('notificationclick', event => { - let notification = event.notification; - - event.waitUntil( - clients.matchAll({ type: 'window' }).then(windowClients => { - let matchingWindowClients = windowClients.filter( - client => client.url === notification.data.url - ); - - if (matchingWindowClients.length) { - let firstWindow = matchingWindowClients[0]; - if (firstWindow && 'focus' in firstWindow) { - firstWindow.focus(); - return; - } - } - if (clients.openWindow) { - clients.openWindow(notification.data.url); - } - }) - ); -}); diff --git a/app/javascript/service-worker/push-handlers.js b/app/javascript/service-worker/push-handlers.js index 5aad04536..2ad6cafb0 100644 --- a/app/javascript/service-worker/push-handlers.js +++ b/app/javascript/service-worker/push-handlers.js @@ -1,11 +1,7 @@ -/* eslint-disable no-restricted-globals */ +/* eslint-disable no-restricted-globals, no-console */ /* globals clients */ - -// Push notification handler self.addEventListener('push', event => { - const notification = event.data && event.data.json(); - - if (!notification) return; + let notification = event.data && event.data.json(); event.waitUntil( self.registration.showNotification(notification.title, { @@ -17,19 +13,17 @@ self.addEventListener('push', event => { ); }); -// Notification click handler self.addEventListener('notificationclick', event => { - const notification = event.notification; - notification.close(); + let notification = event.notification; event.waitUntil( clients.matchAll({ type: 'window' }).then(windowClients => { - const matchingWindowClients = windowClients.filter( + let matchingWindowClients = windowClients.filter( client => client.url === notification.data.url ); if (matchingWindowClients.length) { - const firstWindow = matchingWindowClients[0]; + let firstWindow = matchingWindowClients[0]; if (firstWindow && 'focus' in firstWindow) { firstWindow.focus(); return; diff --git a/app/javascript/service-worker/sw-runtime.js b/app/javascript/service-worker/sw-runtime.js index 4829b32a7..695bbde55 100644 --- a/app/javascript/service-worker/sw-runtime.js +++ b/app/javascript/service-worker/sw-runtime.js @@ -46,7 +46,7 @@ self.addEventListener('fetch', event => { // Assets (JS/CSS/fonts) → cache-first const isAsset = url.pathname.startsWith('/vite-dev/') || - url.pathname.startsWith('/packs/') || + url.pathname.startsWith('/vite/') || url.origin === ASSET_ORIGIN || /\.(js|css|woff2?|ttf|otf|eot)$/i.test(url.pathname); @@ -183,7 +183,7 @@ async function cleanupStaleAssets() { .filter(request => { const url = new URL(request.url); const isAssetPath = - url.pathname.startsWith('/packs/assets/') || + url.pathname.startsWith('/vite/assets/') || url.pathname.startsWith('/vite-dev/assets/'); // Only clean up asset files, keep other cached items (like /app shell) return isAssetPath && !validUrls.has(request.url); diff --git a/package.json b/package.json index c1ca8cce1..22dab5e28 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dev": "overmind start -f ./Procfile.dev", "ruby:prettier": "bundle exec rubocop -a", "build:sdk": "BUILD_MODE=library vite build", - "build:sw": "node scripts/build-service-worker.js", + "build:sw": "node app/javascript/service-worker/build.js", "prepare": "husky install", "size": "size-limit", "story:dev": "histoire dev",