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 } 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 loginFontFallbackPreloads = [ 'https://fonts.gstatic.com/s/roboto/v32/KFOmCnqEu92Fr1Me4GZLCzYlKw.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.110.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.113.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.114.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.115.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.116.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.117.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.118.woff2', 'https://fonts.gstatic.com/s/notosanskr/v36/PbyxFmXiEBPT4ITbgNA5Cgms3VYcOA-vvnIzzuoyeLGC5nwuDo-KBTUm6CryotyJROlrnQ.119.woff2', ]; const compressibleExtensions = new Set([ '.css', '.html', '.js', '.json', '.mjs', '.svg', '.toml', '.wasm', ]); 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)); } } bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*\{[\s\S]*?\}\s*\}\);/, '_flutter.loader.load({config:{canvasKitBaseUrl:"canvaskit/"}});', ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\);/, '_flutter.loader.load({config:{canvasKitBaseUrl:"canvaskit/"}});', ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{config:\{[^}]*\}\}\);/, '_flutter.loader.load({config:{canvasKitBaseUrl:"canvaskit/"}});', ); writeFileSync(bootstrapPath, bootstrap); if (existsSync(indexPath)) { let index = readFileSync(indexPath, 'utf8'); const preloadLinks = [ '', hashedEntrypoints.has('main.dart.mjs') ? `` : '', hashedEntrypoints.has('main.dart.wasm') ? `` : '', '', '', '', ...loginFontFallbackPreloads.map( (href) => ``, ), ] .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, '', ); index = index.replace('', ` ${preloadLinks}\n `); writeFileSync(indexPath, index); } 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; } console.log(`[userfront] optimized ${basename(buildDir)} with hashed entrypoints and brotli assets`);