import {defineConfig} from 'vite'; import {readdirSync, statSync} from 'node:fs'; import {join, relative} from 'node:path'; import {createHash} from 'node:crypto'; const listFiles = dir => readdirSync(dir).flatMap(name => { const path = join(dir, name); return statSync(path).isDirectory() ? listFiles(path) : [path]; }); // Emits sw.js listing every built file, so the installed game works offline. The cache name is a // hash of that list: a new build gets a new cache and the page offers to reload into it. // Satellite tiles (16 MB) are not precached; they are kept as the player passes them. function offlineServiceWorker() { return { name: 'offline-service-worker', apply: 'build', generateBundle(_, bundle) { const files = [...Object.keys(bundle), ...listFiles('public').map(f => relative('public', f).replaceAll('\\', '/'))] .filter(f => f !== 'sw.js' && !f.endsWith('.map') && !(f.startsWith('imagery/') && f.endsWith('.jpg'))); const version = createHash('sha1').update(files.join('\n')).digest('hex').slice(0, 12); this.emitFile({ type: 'asset', fileName: 'sw.js', source: `// Generated by vite.config.js — do not edit. const CACHE = 'robot-market-${version}'; const TILES = 'robot-market-imagery'; const FILES = ${JSON.stringify(['./', ...files.map(f => `./${f}`)])}; self.addEventListener('install', event => event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(FILES)))); self.addEventListener('activate', event => event.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k.startsWith('robot-market-') && k !== CACHE && k !== TILES).map(k => caches.delete(k)))).then(() => self.clients.claim()))); self.addEventListener('message', event => event.data === 'skip-waiting' && self.skipWaiting()); self.addEventListener('fetch', event => { const request = event.request; if (request.method !== 'GET' || new URL(request.url).origin !== location.origin) return; // Pages: network first so updates arrive, cached page when offline. Everything else: cache first. if (request.mode === 'navigate') { event.respondWith(fetch(request).catch(() => caches.match('./'))); return; } // Satellite tiles: cache first, and keep each one fetched so visited areas work offline. if (new URL(request.url).pathname.includes('/imagery/')) { event.respondWith(caches.open(TILES).then(cache => cache.match(request).then(hit => hit ?? fetch(request).then(response => { if (response.ok) cache.put(request, response.clone()); return response; })))); return; } event.respondWith(caches.match(request, {ignoreSearch: true}).then(hit => hit ?? fetch(request))); }); ` }); } }; } export default defineConfig({ // Relative assets also load under a GitHub Pages repository path. base: './', plugins: [offlineServiceWorker()], // Top-level await loads the building and imagery files before the world is built. build: {target: 'es2022'}, server: { host: true, allowedHosts: [ 'robotmarkets.ir', 'www.robotmarkets.ir', ], }, });