Files
xenia-pos-local/waiter_pwa/scripts/pack-bundle.mjs
T
bonaminandClaude Opus 5.5 a1e415ac05 feat(waiter): venue servers publish the native UI bundle (manifest + zip)
- scripts/pack-bundle.mjs zips dist-native deterministically (sorted paths,
  fixed mtimes) into dist/downloads/waiter-bundle-<hash>.zip and writes
  waiter-bundle.json {format, version "<APP_VERSION>-<hash12>", file,
  sha256 (hex), size, min_shell_build, app_version, built_at}
- src/native/shell.js: MIN_SHELL_BUILD - lowest APK versionCode a bundle
  runs in (bump with native changes)
- Dockerfile: node 22; builds web + native bundle + pack (APP_VERSION build
  arg, from ${VERSION} in docker-compose.dev.yml)
- vite native mode strips public/downloads from dist-native - the 10.8 MB
  APK was being copied into the native build (and would have ended up inside
  the next APK); bundle 15 MB → 5 MB
- nginx /downloads/: json/zip types, CORS * (the app reads the manifest from
  origin http://localhost), no-cache
- .dockerignore: android/, releases/, dist-native/ out of the build context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 18:24:48 +03:00

71 lines
2.8 KiB
JavaScript

// 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-<hash>.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}`)