// Pack the native UI bundle that venue servers hand to phones (plan step 8). // // node scripts/pack-bundle.mjs (after `vite build --mode native`) // // Zips dist-native/ into dist/downloads/waiter-bundle-.zip and writes // dist/downloads/waiter-bundle.json: // { format, version, file, sha256, size, min_shell_build, app_version, built_at } // The app downloads the zip only if its SHA-256 (hex, lowercase) matches the // manifest, which it fetches over the pinned TLS link. The version is derived // from the content hash, so identical code always yields the same version and // any change yields a new one — no manual version bumping. import { createHash } from 'node:crypto' import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs' import { dirname, join, relative } from 'node:path' import { fileURLToPath } from 'node:url' import { zipSync } from 'fflate' const root = join(dirname(fileURLToPath(import.meta.url)), '..') const src = join(root, 'dist-native') const outDir = join(root, 'dist', 'downloads') function walk(dir) { return readdirSync(dir).flatMap(name => { const p = join(dir, name) return statSync(p).isDirectory() ? walk(p) : [p] }) } const files = walk(src).filter(f => !relative(src, f).startsWith('downloads')).sort() if (!files.some(f => relative(src, f) === 'index.html')) { console.error('dist-native/index.html missing — run `vite build --mode native` first') process.exit(1) } // Deterministic zip: sorted paths, fixed timestamps → same code, same bytes, same hash const entries = {} for (const f of files) { entries[relative(src, f).split('\\').join('/')] = [readFileSync(f), { mtime: new Date('2000-01-01T00:00:00Z') }] } const zip = zipSync(entries, { level: 9 }) const sha256 = createHash('sha256').update(zip).digest('hex') const minShell = Number(readFileSync(join(root, 'src', 'native', 'shell.js'), 'utf8') .match(/MIN_SHELL_BUILD\s*=\s*(\d+)/)?.[1]) if (!minShell) { console.error('MIN_SHELL_BUILD not found in src/native/shell.js') process.exit(1) } const appVersion = process.env.APP_VERSION || JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version const version = `${appVersion}-${sha256.slice(0, 12)}` const file = `waiter-bundle-${sha256.slice(0, 12)}.zip` mkdirSync(outDir, { recursive: true }) for (const old of readdirSync(outDir)) { if (/^waiter-bundle-.*\.zip$/.test(old)) unlinkSync(join(outDir, old)) } writeFileSync(join(outDir, file), zip) const manifest = { format: 1, version, file, sha256, size: zip.length, min_shell_build: minShell, app_version: appVersion, built_at: new Date().toISOString(), } writeFileSync(join(outDir, 'waiter-bundle.json'), JSON.stringify(manifest, null, 2) + '\n') console.log(`bundle ${version}: ${files.length} files, ${(zip.length / 1024).toFixed(0)} KiB → dist/downloads/${file}`)