157 lines
7.1 KiB
JavaScript
157 lines
7.1 KiB
JavaScript
#!/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 <script>: the web app has no module loader
|
||
|
|
'--target=es2019', // floor for older Android System WebView builds still in the wild
|
||
|
|
'--minify',
|
||
|
|
`--outfile=${path.join(WWW, 'native-shell.js')}`,
|
||
|
|
], { stdio: ['ignore', 'ignore', 'pipe'] }); // capture: esbuild's banner would break release.sh's indented output
|
||
|
|
} catch (err) {
|
||
|
|
console.error('✗ esbuild failed to bundle src/native-shell.js:');
|
||
|
|
console.error(String((err.stderr || err.message) || '').trim());
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4b. Bundle the LOCAL RUNTIME: the real server/server.js, compiled for the WebView behind
|
||
|
|
// Node shims (see src/local/). This is what makes "just on this device" work without
|
||
|
|
// reimplementing fifty API routes — local mode runs the same server code every other
|
||
|
|
// mode talks to. Aliases swap Node's modules for browser-capable stand-ins; __dirname is
|
||
|
|
// defined because server.js resolves its data and public directories from it.
|
||
|
|
const LOCAL_ALIASES = [
|
||
|
|
'express=./src/local/shims/express.js',
|
||
|
|
'fs=./src/local/shims/fs.js',
|
||
|
|
'fs/promises=./src/local/shims/fs-promises.js',
|
||
|
|
'path=./src/local/shims/path.js',
|
||
|
|
'crypto=./src/local/shims/crypto.js',
|
||
|
|
'nodemailer=./src/local/shims/nodemailer.js',
|
||
|
|
'node-cron=./src/local/shims/node-cron.js',
|
||
|
|
'dotenv=./src/local/shims/dotenv.js',
|
||
|
|
];
|
||
|
|
try {
|
||
|
|
execFileSync(esbuild, [
|
||
|
|
path.join(MOBILE, 'src', 'local', 'boot.js'),
|
||
|
|
'--bundle',
|
||
|
|
'--format=iife',
|
||
|
|
'--global-name=LocalServer',
|
||
|
|
'--target=es2019',
|
||
|
|
'--minify',
|
||
|
|
...LOCAL_ALIASES.map((a) => `--alias:${a}`),
|
||
|
|
'--define:__dirname="/"',
|
||
|
|
`--outfile=${path.join(WWW, 'local-server.js')}`,
|
||
|
|
], { cwd: MOBILE, stdio: ['ignore', 'ignore', 'pipe'] });
|
||
|
|
} catch (err) {
|
||
|
|
console.error('✗ esbuild failed to bundle the local runtime (src/local/boot.js):');
|
||
|
|
console.error(String((err.stderr || err.message) || '').trim());
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Inject into the bundle's index.html only. Order is load-bearing: native-config.js
|
||
|
|
// defines the API base, native-shell.js defines window.CapPlugins, and native-bridge.js
|
||
|
|
// reads both while it parses.
|
||
|
|
const indexPath = path.join(WWW, 'index.html');
|
||
|
|
let html = fs.readFileSync(indexPath, 'utf8');
|
||
|
|
const anchor = ' <script src="native-bridge.js"></script>';
|
||
|
|
if (!html.includes(anchor)) {
|
||
|
|
console.error('✗ could not find the native-bridge.js <script> tag in index.html — was it renamed?');
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
// local-server.js must load BEFORE native-bridge.js: the bridge wires local-mode routing
|
||
|
|
// while it parses and reads window.LocalServer to do it.
|
||
|
|
html = html.replace(anchor,
|
||
|
|
' <!-- GENERATED by mobile/scripts/sync-web.js (native bundles only). -->\n' +
|
||
|
|
' <script src="native-config.js"></script>\n' +
|
||
|
|
' <script src="local-server.js"></script>\n' +
|
||
|
|
' <script src="native-shell.js"></script>\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}`);
|