import { brotliCompressSync, constants } from 'node:zlib'; import { createHash } from 'node:crypto'; import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs'; import { basename, extname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const buildDir = process.argv[2] ?? join(__dirname, '..', 'build', 'web'); const bootstrapPath = join(buildDir, 'flutter_bootstrap.js'); const indexPath = join(buildDir, 'index.html'); const hashableEntrypoints = ['main.dart.js', 'main.dart.mjs', 'main.dart.wasm']; const compressibleExtensions = new Set([ '.css', '.html', '.js', '.json', '.mjs', '.svg', '.toml', '.wasm', ]); const serviceWorkerPath = join(buildDir, 'flutter_service_worker.js'); if (!existsSync(bootstrapPath)) { throw new Error(`Missing Flutter bootstrap file: ${bootstrapPath}`); } let bootstrap = readFileSync(bootstrapPath, 'utf8'); const hashedEntrypoints = new Map(); const activeEntrypointFiles = new Set(); for (const entrypoint of hashableEntrypoints) { const sourceName = findEntrypointSource(entrypoint, bootstrap); if (!sourceName) { continue; } const sourcePath = join(buildDir, sourceName); if (!existsSync(sourcePath)) { continue; } const content = readFileSync(sourcePath); const hash = createHash('sha256').update(content).digest('hex').slice(0, 12); const extension = extname(entrypoint); const stem = entrypoint.slice(0, -extension.length); const hashedName = `${stem}.${hash}${extension}`; const targetPath = join(buildDir, hashedName); if (sourceName !== hashedName) { renameSync(sourcePath, targetPath); } bootstrap = bootstrap.replaceAll(sourceName, hashedName); bootstrap = bootstrap.replaceAll(entrypoint, hashedName); hashedEntrypoints.set(entrypoint, hashedName); activeEntrypointFiles.add(hashedName); } for (const fileName of readdirSync(buildDir)) { if ( /^main\.dart\.[0-9a-f]{12}\.(?:js|mjs|wasm)(?:\.br)?$/.test(fileName) && !activeEntrypointFiles.has(fileName.replace(/\.br$/, '')) ) { unlinkSync(join(buildDir, fileName)); } } const canvasKitConfig = 'config:{canvasKitBaseUrl:"canvaskit/"}'; bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*,\s*config:\s*\{[\s\S]*?serviceWorkerUrl[\s\S]*?\}\s*,\s*config:\s*\{[^}]*\}\s*\}\);/g, (_match, settings) => `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*,\s*config:\s*\{[^}]*\}\s*\}\);/g, (_match, settings) => `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*\}\);/g, (_match, settings) => `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\);/g, `_flutter.loader.load({${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{config:\{[^}]*\}\}\);/g, `_flutter.loader.load({${canvasKitConfig}});`, ); writeFileSync(bootstrapPath, bootstrap); if (existsSync(indexPath)) { let index = readFileSync(indexPath, 'utf8'); const preloadLinks = [ '', hashedEntrypoints.has('main.dart.js') ? `` : '', hashedEntrypoints.has('main.dart.mjs') ? `` : '', hashedEntrypoints.has('main.dart.wasm') ? `` : '', '', '', ] .filter(Boolean) .join('\n '); index = index .replace(/\n\s*/g, '') .replace(/\n\s*/g, '') .replace(/\n\s*/g, '') .replace( /\n\s*/g, '', ) .replace( /\n\s*/g, '', ) .replace(/\n\s*/g, '') .replace( /\n\s*/g, '', ) .replace(/\n\s*/g, '') .replace( /\n\s*/g, '', ); index = index.replace('', ` ${preloadLinks}\n `); writeFileSync(indexPath, index); } writeFileSync(serviceWorkerPath, createServiceWorker()); for (const filePath of walk(buildDir)) { if (filePath.endsWith('.br')) { continue; } if (!compressibleExtensions.has(extname(filePath))) { continue; } const content = readFileSync(filePath); writeFileSync( `${filePath}.br`, brotliCompressSync(content, { params: { [constants.BROTLI_PARAM_QUALITY]: 11, }, }), ); } function* walk(directory) { for (const entry of readdirSync(directory, { withFileTypes: true })) { const entryPath = join(directory, entry.name); if (entry.isDirectory()) { yield* walk(entryPath); continue; } if (entry.isFile()) { yield entryPath; } } } function findEntrypointSource(entrypoint, bootstrap) { if (existsSync(join(buildDir, entrypoint))) { return entrypoint; } const extension = extname(entrypoint).replace('.', ''); const match = bootstrap.match( new RegExp(`main\\.dart\\.[0-9a-f]{12}\\.${extension}`), ); return match?.[0] ?? null; } function createServiceWorker() { const assets = []; const versionHash = createHash('sha256'); for (const filePath of walk(buildDir)) { if (filePath.endsWith('.br') || filePath === serviceWorkerPath) { continue; } const extension = extname(filePath); if ( !compressibleExtensions.has(extension) && !['.ico', '.png', '.webp', '.woff', '.woff2'].includes(extension) ) { continue; } const assetPath = `/${relative(buildDir, filePath).replaceAll('\\', '/')}`; assets.push(assetPath); versionHash.update(assetPath); versionHash.update(readFileSync(filePath)); } assets.sort(); const version = versionHash.digest('hex').slice(0, 16); const serializedAssets = JSON.stringify(assets, null, 2); return `'use strict'; const CACHE_NAME = 'baron-userfront-${version}'; const CORE_ASSETS = ${serializedAssets}; const NETWORK_FIRST_PATHS = new Set([ '/', '/index.html', '/flutter_bootstrap.js', '/version.json', '/manifest.json', ]); self.addEventListener('install', (event) => { event.waitUntil( caches .open(CACHE_NAME) .then((cache) => cache.addAll(CORE_ASSETS)) .then(() => self.skipWaiting()), ); }); self.addEventListener('activate', (event) => { event.waitUntil( caches .keys() .then((keys) => Promise.all( keys .filter((key) => key !== CACHE_NAME) .map((key) => caches.delete(key)), ), ) .then(() => self.clients.claim()), ); }); self.addEventListener('fetch', (event) => { const request = event.request; if (request.method !== 'GET') return; const url = new URL(request.url); if (url.origin !== self.location.origin || url.pathname.startsWith('/api/')) { return; } if (isAppShellPath(url) || NETWORK_FIRST_PATHS.has(url.pathname)) { event.respondWith(networkFirst(request, '/index.html')); return; } event.respondWith(cacheFirst(request)); }); function isAppShellPath(url) { return !url.pathname.split('/').pop().includes('.'); } async function networkFirst(request, fallbackPath) { const cache = await caches.open(CACHE_NAME); try { const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); } return response; } catch (error) { return ( (await cache.match(request)) ?? (fallbackPath ? await cache.match(fallbackPath) : undefined) ?? Response.error() ); } } async function cacheFirst(request) { const cache = await caches.open(CACHE_NAME); const cached = await cache.match(request); if (cached) return cached; const response = await fetch(request); if (response.ok) { await cache.put(request, response.clone()); } return response; } `; } function ensureServiceWorkerUrl(settings) { const serviceWorkerUrl = `"/flutter_service_worker.js?v=" + ${serviceWorkerVersionExpression(settings)}`; if (/serviceWorkerUrl\s*:/.test(settings)) { return settings.replace( /serviceWorkerUrl\s*:\s*[^,\n}]+,?/, `serviceWorkerUrl: ${serviceWorkerUrl},`, ); } const closingBraceIndex = settings.lastIndexOf('}'); if (closingBraceIndex < 0) { return settings; } const beforeClosing = settings.slice(0, closingBraceIndex).trimEnd(); const afterClosing = settings.slice(closingBraceIndex); const separator = beforeClosing.endsWith('{') || beforeClosing.endsWith(',') ? '' : ','; return `${beforeClosing}${separator} serviceWorkerUrl: ${serviceWorkerUrl}, ${afterClosing}`; } function serviceWorkerVersionExpression(settings) { const match = settings.match(/serviceWorkerVersion\s*:\s*([^,\n}]+)/); return ( match?.[1]?.replace(/\/\*[\s\S]*?\*\//g, '').trim() ?? 'serviceWorkerVersion' ); } console.log(`[userfront] optimized ${basename(buildDir)} with hashed entrypoints and brotli assets`);