#!/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}`);