Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d18be84b45 | ||
|
|
b023487c3e | ||
|
|
40d944b7d7 | ||
|
|
b7a3bb892d | ||
|
|
08c415ea0e | ||
|
|
6d662cf339 | ||
|
|
9d32e27c98 | ||
|
|
52625c8138 | ||
|
|
1b9f496832 | ||
|
|
3527c07e94 | ||
|
|
b345826566 | ||
|
|
6d3d76982a | ||
|
|
59427c4de3 | ||
|
|
7787ec1265 | ||
|
|
8daac4cdd1 | ||
|
|
adbee23e12 | ||
|
|
38232a73dc | ||
|
|
0d21a711e7 | ||
|
|
f5d538ba48 | ||
|
|
f713678328 | ||
|
|
d34df7c314 |
@@ -103,4 +103,5 @@ CLAUDE.local.md
|
|||||||
.histoire
|
.histoire
|
||||||
.pnpm-store/*
|
.pnpm-store/*
|
||||||
local/
|
local/
|
||||||
|
sw.js
|
||||||
Procfile.worktree
|
Procfile.worktree
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||||
import auth from '../api/auth';
|
import auth from '../api/auth';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
@@ -16,7 +15,26 @@ export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
|||||||
|
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register('/sw.js')
|
.register('/sw.js')
|
||||||
.then(registration => callback(registration))
|
.then(registration => {
|
||||||
|
// Check for updates on load
|
||||||
|
registration.update();
|
||||||
|
|
||||||
|
// Listen for updates
|
||||||
|
registration.addEventListener('updatefound', () => {
|
||||||
|
const newWorker = registration.installing;
|
||||||
|
newWorker.addEventListener('statechange', () => {
|
||||||
|
if (
|
||||||
|
newWorker.state === 'installed' &&
|
||||||
|
navigator.serviceWorker.controller
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('New service worker available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(registration);
|
||||||
|
})
|
||||||
.catch(registrationError => {
|
.catch(registrationError => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
console.log('SW registration failed: ', registrationError);
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { build } = require('vite');
|
||||||
|
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
'⚠️ No Vite manifest found, skipping asset manifest generation'
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📋 Reading manifest from: ${manifestPath}`);
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
const assets = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
Object.entries(manifest).forEach(([, entry]) => {
|
||||||
|
const file = entry.file;
|
||||||
|
if (file && !seen.has(file)) {
|
||||||
|
seen.add(file);
|
||||||
|
if (file.endsWith('.js') || file.endsWith('.css')) {
|
||||||
|
assets.push({ url: file, revision: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.css) {
|
||||||
|
entry.css.forEach(cssFile => {
|
||||||
|
if (!seen.has(cssFile)) {
|
||||||
|
seen.add(cssFile);
|
||||||
|
assets.push({ url: cssFile, revision: null });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildServiceWorker() {
|
||||||
|
console.log('🔨 Building service worker...');
|
||||||
|
|
||||||
|
let assetOrigin = process.env.ASSET_CDN_HOST || '';
|
||||||
|
if (assetOrigin && !assetOrigin.startsWith('http')) {
|
||||||
|
assetOrigin = `https://${assetOrigin}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
const assetPath = '/vite/';
|
||||||
|
|
||||||
|
console.log(`📁 Root dir: ${ROOT_DIR}`);
|
||||||
|
console.log(`🌐 Asset origin: ${assetOrigin || '(local)'}`);
|
||||||
|
console.log(`🏭 Environment: ${isProduction ? 'production' : 'development'}`);
|
||||||
|
|
||||||
|
if (!isProduction) {
|
||||||
|
console.log(
|
||||||
|
'⚠️ Development mode: Using push notifications only (no caching)'
|
||||||
|
);
|
||||||
|
|
||||||
|
const devServiceWorker = fs.readFileSync(PATHS.pushHandlers, 'utf8');
|
||||||
|
|
||||||
|
fs.writeFileSync(PATHS.output, devServiceWorker);
|
||||||
|
|
||||||
|
console.log('✅ Development service worker created at public/sw.js');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetManifest = generateAssetManifest();
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('📦 Building service worker runtime...');
|
||||||
|
|
||||||
|
await build({
|
||||||
|
configFile: false,
|
||||||
|
css: {
|
||||||
|
postcss: {
|
||||||
|
plugins: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: PATHS.swRuntime,
|
||||||
|
formats: ['iife'],
|
||||||
|
name: 'ServiceWorkerRuntime',
|
||||||
|
fileName: () => 'sw-runtime.js',
|
||||||
|
},
|
||||||
|
outDir: PATHS.buildDir,
|
||||||
|
emptyOutDir: true,
|
||||||
|
minify: true,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
inlineDynamicImports: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__ASSET_ORIGIN__: JSON.stringify(assetOrigin),
|
||||||
|
__ASSET_PATH__: JSON.stringify(assetPath),
|
||||||
|
__ASSET_MANIFEST__: JSON.stringify(assetManifest),
|
||||||
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
||||||
|
},
|
||||||
|
logLevel: 'warn',
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Service worker runtime built');
|
||||||
|
|
||||||
|
const runtimeBundle = fs.readFileSync(PATHS.swRuntimeBuilt, 'utf8');
|
||||||
|
|
||||||
|
const pushHandlers = fs.readFileSync(PATHS.pushHandlers, 'utf8');
|
||||||
|
|
||||||
|
const finalServiceWorker = buildSWContent(
|
||||||
|
runtimeBundle,
|
||||||
|
pushHandlers,
|
||||||
|
assetManifest.length
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(PATHS.output, finalServiceWorker);
|
||||||
|
|
||||||
|
console.log('✅ Service worker built successfully at public/sw.js');
|
||||||
|
console.log(` - ${assetManifest.length} assets in precache manifest`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to build service worker:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 */
|
/* globals clients */
|
||||||
|
|
||||||
self.addEventListener('push', event => {
|
self.addEventListener('push', event => {
|
||||||
let notification = event.data && event.data.json();
|
let notification = event.data && event.data.json();
|
||||||
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/* eslint-disable no-restricted-globals, no-use-before-define, no-undef */
|
||||||
|
|
||||||
|
// Build-time injected values (replaced by Vite define at build time)
|
||||||
|
const ASSET_ORIGIN = __ASSET_ORIGIN__;
|
||||||
|
const ASSET_PATH = __ASSET_PATH__;
|
||||||
|
const ASSET_MANIFEST = __ASSET_MANIFEST__;
|
||||||
|
|
||||||
|
const CACHE_NAME = 'chatwoot-assets-v1';
|
||||||
|
|
||||||
|
const EXCLUDED_PATHS = [
|
||||||
|
'/api/',
|
||||||
|
'/auth/',
|
||||||
|
'/rails/',
|
||||||
|
'/cable',
|
||||||
|
'/sidekiq',
|
||||||
|
'/super_admin',
|
||||||
|
'/swagger',
|
||||||
|
'/webhooks/',
|
||||||
|
'/widget',
|
||||||
|
'/survey/',
|
||||||
|
'/__vite',
|
||||||
|
'/sw.js',
|
||||||
|
];
|
||||||
|
|
||||||
|
const BATCH_SIZE = 5;
|
||||||
|
|
||||||
|
const buildAssetListChunks = (arr, size) => {
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < arr.length; i += size) {
|
||||||
|
result.push(arr.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all(assets.map(fetchAndCacheAsset));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheFirst = async request => {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const cached = await cache.match(request);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(request);
|
||||||
|
if (response.ok) {
|
||||||
|
cache.put(request, response.clone());
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
const stale = await cache.match(request);
|
||||||
|
if (stale) return stale;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefetchAssets = async () => {
|
||||||
|
if (!ASSET_MANIFEST?.length) return;
|
||||||
|
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const baseUrl = ASSET_ORIGIN
|
||||||
|
? `${ASSET_ORIGIN}${ASSET_PATH}`
|
||||||
|
: `${self.location.origin}${ASSET_PATH}`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const cachedRequests = await cache.keys();
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
event.waitUntil(
|
||||||
|
Promise.all([cleanupStaleAssets(), prefetchAssets(), self.clients.claim()])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
const { request } = event;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (EXCLUDED_PATHS.some(path => url.pathname.startsWith(path))) return;
|
||||||
|
|
||||||
|
if (request.method !== 'GET') return;
|
||||||
|
|
||||||
|
const isAChatwootRequest =
|
||||||
|
url.origin === self.location.origin ||
|
||||||
|
(ASSET_ORIGIN && url.origin === ASSET_ORIGIN);
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -8,6 +8,12 @@ task before_assets_precompile: :environment do
|
|||||||
system('echo "-------------- Bulding App for Production --------------"')
|
system('echo "-------------- Bulding App for Production --------------"')
|
||||||
end
|
end
|
||||||
|
|
||||||
# every time you execute 'rake assets:precompile'
|
task after_assets_precompile: :environment do
|
||||||
# run 'before_assets_precompile' first
|
# Build service worker after Vite has generated the asset manifest
|
||||||
Rake::Task['assets:precompile'].enhance %w[before_assets_precompile]
|
system('echo "-------------- Building Service Worker --------------"')
|
||||||
|
system('NODE_ENV=production pnpm run build:sw')
|
||||||
|
end
|
||||||
|
|
||||||
|
Rake::Task['assets:precompile'].enhance %w[before_assets_precompile] do
|
||||||
|
Rake::Task['after_assets_precompile'].invoke
|
||||||
|
end
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"dev": "overmind start -f ./Procfile.dev",
|
"dev": "overmind start -f ./Procfile.dev",
|
||||||
"ruby:prettier": "bundle exec rubocop -a",
|
"ruby:prettier": "bundle exec rubocop -a",
|
||||||
"build:sdk": "BUILD_MODE=library vite build",
|
"build:sdk": "BUILD_MODE=library vite build",
|
||||||
|
"build:sw": "node app/javascript/service-worker/build.js",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"size": "size-limit",
|
"size": "size-limit",
|
||||||
"story:dev": "histoire dev",
|
"story:dev": "histoire dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user