import type { BunFile } from 'bun'; import path from 'path'; import { minify } from 'html-minifier'; import UglifyJS from 'uglify-js'; import wasmPlugin from './wasmPlugin'; import imagePlugin from './imagePlugin'; import fontPlugin from './fontPlugin'; import audioPlugin from './audioPlugin'; import filePlugin from './filePlugin'; import { getGames } from './isGame'; const b64 = async (file: string | BunFile) => ( typeof file === 'string' ? Buffer.from(file) : Buffer.from(await file.arrayBuffer()) ).toString('base64'); interface Args { production?: boolean; mobile?: boolean; local?: boolean; } export async function buildHTML(game: string, { production = false, mobile = false, local = false }: Args = {}) { const assetsDir = path.resolve(import.meta.dir, 'assets'); const srcDir = path.resolve(import.meta.dir, '..', 'src'); const gameDir = path.resolve(srcDir, 'games', game); const gameAssetsDir = path.resolve(gameDir, 'assets'); const html = await Bun.file(path.resolve(assetsDir, local ? 'index-local.html' : 'index.html')).text(); const bundle = await Bun.build({ outdir: '/tmp', entrypoints: [path.resolve(srcDir, 'index.ts')], sourcemap: production ? 'none' : 'inline', define: { global: 'window', GAME: `"${game}"`, GAMES: JSON.stringify(await getGames()), }, plugins: [ imagePlugin, audioPlugin, fontPlugin, wasmPlugin({ production }), filePlugin, ] }); if (bundle.success) { const scriptFile = bundle.outputs.find(a => a.kind === 'entry-point' && a.path.endsWith('.js')); if (!scriptFile) { console.error('No entry point found:', bundle.outputs); return; } const iconFile = Bun.file(path.resolve(gameAssetsDir, 'favicon.ico')); let icon = ''; if (await iconFile.exists()) { icon = ``; } let style = ''; let styleFiles = bundle.outputs.filter(a => a.kind === 'asset' && a.path.endsWith('.css')); for (let styleFile of styleFiles) { style += await styleFile.text(); } const title = game[0].toUpperCase() + game.slice(1).toLowerCase(); let pwaIconFile = Bun.file(path.resolve(gameAssetsDir, 'pwa_icon.png')); if (!await pwaIconFile.exists()) { pwaIconFile = Bun.file(path.resolve(assetsDir, 'pwa_icon.png')); } let manifest = ''; if (production && !local) { const pwaIcon = `data:;base64,${await b64(pwaIconFile)}`; const publishURL = process.env.PUBLISH_URL ? `${process.env.PUBLISH_URL}${game}` : '.'; const manifestJSON = JSON.stringify({ name: title, short_name: title, start_url: publishURL, id: `/${game}`, display_override: ["window-controls-overlay"], display: "fullscreen", background_color: "#ffffff", theme_color: "#000000", icons: [{ src: pwaIcon, sizes: '192x192', type: 'image/png' }] }); manifest = ``; } let script = await scriptFile.text(); const inits = new Set(); script = script.replace(/var (init_[^ ]+) = __esm\(\(\)/g, (_, $1) => { inits.add($1); return `var ${$1} = __esm(async ()`; }); for (const init of inits) { script = script.replaceAll(`${init}()`, `await ${init}()`); } script = script.replaceAll('await Promise.resolve().then(() =>', '('); let scriptPrefix = ''; if (production) { const minifyResult = UglifyJS.minify(script, { module: true, toplevel: true, }); if (minifyResult.error) { console.warn(`Minify error: ${minifyResult.error}`); } else { script = minifyResult.code; } } else if (mobile) { const eruda = await Bun.file(path.resolve(import.meta.dir, '..', 'node_modules', 'eruda', 'eruda.js')).text(); scriptPrefix = ``; } // function to avoid $& being replaced // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement const resultHTML = html .replace('', () => `${scriptPrefix}`) .replace('', () => title) .replace('', () => icon) .replace('', () => manifest) .replace('/*$STYLE$*/', () => style); return minify(resultHTML, { collapseWhitespace: production, decodeEntities: true, minifyCSS: production, minifyJS: production, }); } else { console.error('Failed: ', !bundle.success, bundle); } }