Files
church-translate/app/tools/check-packaged-files.js
christianandClaude Opus 5 c9eef0672c Church Translate, as it runs at the campuses
Live sermon translation into 100+ languages, running entirely on one Mac per
campus: audio in, voice detection, Whisper on the GPU, NLLB-200 in its own
process, and a built-in web server the congregation reads from on their
phones. No audio leaves the building.

This is the source only. What used to sit beside it — 14 GB of speech and
translation models, 8 GB of voice bundles, 6 GB of natural voices, the Electron
caches, and the app data holding the operator PIN and every service transcript
— is machine state and downloaded assets, and stays in the NAS backup where it
belongs. The vendored node, cloudflared and Windows ffmpeg binaries are out too
(533 MB, re-fetched for packaging); the browser-side runtime beside them is
kept, so a checkout still serves the congregation page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-23 15:41:21 -04:00

94 lines
3.7 KiB
JavaScript

/*
* check-packaged-files.js — refuse to build an app that is missing a module.
*
* package.json's build.files is an explicit ALLOW-LIST. Adding a new module and
* forgetting to list it produces a DMG that installs cleanly, launches cleanly,
* and then throws at the first require() — which is exactly what shipped once:
* three new files were left out, `require('./sidecar-langs.js')` threw inside
* the voice process, and the natural voices silently never started. Nothing in
* the build said a word.
*
* So the build now reads what the code actually loads — every local require()
* in the main/child processes and every <script src="./…"> in the pages — and
* fails if any of it would not be packaged.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const APP = path.join(__dirname, '..');
const pkg = JSON.parse(fs.readFileSync(path.join(APP, 'package.json'), 'utf8'));
const patterns = (pkg.build && pkg.build.files) || [];
/* Does the allow-list cover this path? Only the two glob shapes this file
actually uses — "server/**\/*" and plain paths. */
/* electron-builder always includes these, whatever the allow-list says. */
const ALWAYS = new Set(['package.json']);
/* The allow-list in package.json is forward-slash; path.join() gives
backslashes when this runs on Windows, so every server/** module would read
as un-packaged and refuse the build. Compare in one notation. */
const toPosix = (p) => String(p).split(path.sep).join('/');
function covered(rel) {
rel = toPosix(rel);
if (ALWAYS.has(rel)) return true;
return patterns.some((p) => {
if (p === rel) return true;
const star = p.indexOf('**');
if (star !== -1) return rel.startsWith(p.slice(0, star));
return false;
});
}
/* Entry points whose dependency graph must be packaged. */
const ROOTS = ['main.js', 'preload.js', 'engine.js', 'tts-service.js', 'mt-service.js',
'model-worker.js', 'server/server.js', 'kokoro-sidecar.js'];
const HTML = ['index.html', 'server/public/index.html'];
const seen = new Set();
const missing = [];
function scanJs(rel) {
if (seen.has(rel)) return;
seen.add(rel);
const abs = path.join(APP, rel);
let src;
try { src = fs.readFileSync(abs, 'utf8'); } catch (_) { return; }
const dir = toPosix(path.dirname(rel));
const re = /require\(\s*['"](\.[^'"]+)['"]\s*\)/g;
let m;
while ((m = re.exec(src))) {
let dep = toPosix(path.normalize(path.join(dir, m[1])));
if (!/\.[a-z]+$/.test(dep)) dep += '.js';
if (dep.startsWith('..')) continue; // outside the app dir
if (!fs.existsSync(path.join(APP, dep))) continue; // optional/native, let node fail loudly
if (!covered(dep)) missing.push(dep + ' (required by ' + rel + ')');
scanJs(dep);
}
}
for (const r of ROOTS) scanJs(r);
for (const page of HTML) {
let src;
try { src = fs.readFileSync(path.join(APP, page), 'utf8'); } catch (_) { continue; }
const dir = toPosix(path.dirname(page));
const re = /<script\s+src=["'](\.\/[^"']+)["']/g;
let m;
while ((m = re.exec(src))) {
const dep = toPosix(path.normalize(path.join(dir, m[1])));
if (!fs.existsSync(path.join(APP, dep))) continue;
if (!covered(dep)) missing.push(dep + ' (script in ' + page + ')');
if (dep.endsWith('.js')) scanJs(dep);
}
}
if (missing.length) {
console.error('\nBUILD REFUSED — these files are loaded at runtime but are NOT in package.json build.files:\n');
for (const f of Array.from(new Set(missing))) console.error(' ' + f);
console.error('\nAdd them to build.files, or the installed app will throw on first use.\n');
process.exit(1);
}
console.log('[check] all ' + seen.size + ' runtime modules are packaged');