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 <noreply@anthropic.com>
This commit is contained in:
Pranav
2026-01-10 19:30:15 -08:00
co-authored by Claude Opus 4.5
parent 38232a73dc
commit adbee23e12
5 changed files with 22 additions and 74 deletions
@@ -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
);
@@ -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);
}
})
);
});
+5 -11
View File
@@ -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;
+2 -2
View File
@@ -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);
+1 -1
View File
@@ -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",