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>
94 lines
3.7 KiB
JavaScript
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');
|