#!/usr/bin/env node 'use strict'; // sync-web.js — rebuild mobile/www from the one true copy of the front-end. // // The website in ../server/public IS the source of truth. The store apps never get their // own fork of the UI; they get a byte-for-byte copy of these files plus a small, explicit // set of native-only additions. That means a UI fix is written once and reaches all three // targets — but it also means the apps are only as current as the last time this ran, // which is why release.sh always runs it before a build. // // What it does: // 1. Wipes and re-copies server/public -> www (a wipe, so a file DELETED from the web // app can never linger inside a shipped app bundle). // 2. Drops sw.js — the app bundle is already local; a service worker would only add a // second invisible cache layer, and iOS's capacitor:// scheme can't host one. // 3. Writes native-config.js — the API base and bundle stamp. This is why no server URL // is hard-coded in server/public: the website reads its API from its own origin. // 4. Bundles src/native-shell.js -> www/native-shell.js with esbuild. // 5. Injects both scripts into www/index.html ahead of native-bridge.js, which needs // window.CapPlugins and window.CHECKLIST_NATIVE_CONFIG to already exist. const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const MOBILE = path.resolve(__dirname, '..'); const WEB_SRC = path.resolve(MOBILE, '..', 'server', 'public'); const WWW = path.join(MOBILE, 'www'); const release = JSON.parse(fs.readFileSync(path.join(MOBILE, 'release.config.json'), 'utf8')); const apiBase = String(release.apiBase || '').replace(/\/+$/, ''); if (!/^https:\/\//i.test(apiBase)) { // Not style policing: both platforms block cleartext HTTP by default (iOS App Transport // Security, Android's cleartextTrafficPermitted=false), so an http:// base produces an // app that builds, ships, installs and then fails every single request. console.error(`✗ apiBase must be an https:// URL — got ${JSON.stringify(release.apiBase)}`); process.exit(1); } // Files that exist for the website only and must not ship inside an app bundle. const WEB_ONLY = new Set(['sw.js', '.DS_Store']); function copyTree(src, dest) { fs.mkdirSync(dest, { recursive: true }); for (const entry of fs.readdirSync(src, { withFileTypes: true })) { if (WEB_ONLY.has(entry.name)) continue; const from = path.join(src, entry.name); const to = path.join(dest, entry.name); if (entry.isDirectory()) copyTree(from, to); else if (entry.isFile()) fs.copyFileSync(from, to); } } // 1 + 2. Full wipe, then copy. fs.rmSync(WWW, { recursive: true, force: true }); copyTree(WEB_SRC, WWW); // A missing native-bridge.js means the copy silently produced an app that would resolve // every /api call against its own bundle. Fail the build instead of shipping that. if (!fs.existsSync(path.join(WWW, 'native-bridge.js'))) { console.error('✗ native-bridge.js is missing from server/public — the app cannot reach the API without it'); process.exit(1); } // 3. Native-only config. Regenerated every sync, so bundledAt doubles as "when did this // binary's UI last get refreshed from the web app". const bundledAt = new Date().toISOString(); fs.writeFileSync(path.join(WWW, 'native-config.js'), `// GENERATED by mobile/scripts/sync-web.js — do not edit, do not commit as source.\n` + `// Present only inside the iOS/Android app bundles; native-bridge.js is inert without it.\n` + `window.CHECKLIST_NATIVE_CONFIG = ${JSON.stringify({ apiBase, bundleVersion: release.version, bundledAt, }, null, 2)};\n`); // 4. Bundle the native-only shell. const esbuild = path.join(MOBILE, 'node_modules', '.bin', 'esbuild'); try { execFileSync(esbuild, [ path.join(MOBILE, 'src', 'native-shell.js'), '--bundle', '--format=iife', // a classic '; if (!html.includes(anchor)) { console.error('✗ could not find the native-bridge.js \n' + ' \n' + ' \n' + anchor); fs.writeFileSync(indexPath, html); const count = (function walk(dir) { return fs.readdirSync(dir, { withFileTypes: true }).reduce((n, e) => n + (e.isDirectory() ? walk(path.join(dir, e.name)) : 1), 0); })(WWW); console.log(`✓ www/ rebuilt from server/public — ${count} files`); console.log(` api base ${apiBase}`); console.log(` version ${release.version} (ios build ${release.iosBuild}, android code ${release.androidVersionCode})`); console.log(` bundled at ${bundledAt}`);