mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
Stage 7 of issue #10321 moves the optional ML and browser automation dependency closures out of the desktop bundle into checksummed, versioned packs installed on demand through the omniroute packs command. - scripts/build/optionalPackStaging.mjs stages pack members under .build/optional-packs, creates release tarballs, and emits optional-packs.index.json with per-member SHA-256 checksums. - scripts/packs provides manifest, install, remove, and verification helpers plus the packs CLI commands. - Runtime lookup includes installed pack node_modules directories, while LLMLingua and browser executors continue to degrade gracefully when packs are absent. The measured darwin-arm64 staging closure was about 534 MB of the 929 MB standalone node_modules tree (57%).
220 lines
9.0 KiB
JavaScript
220 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
import { existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
||
import { basename, dirname, join, relative } from "node:path";
|
||
import { fileURLToPath } from "node:url";
|
||
import { assembleStandalone } from "./assembleStandalone.mjs";
|
||
import { assertSqlitePrebuildExists } from "./electronRebuildPlan.mjs";
|
||
import { pruneElectronRuntimeDocs } from "./electronRuntimeDocs.mjs";
|
||
import { stageOptionalPacks } from "./optionalPackStaging.mjs";
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
const ROOT = join(__dirname, "..", "..");
|
||
|
||
const NEXT_DIST_DIR = process.env.NEXT_DIST_DIR || ".build/next";
|
||
const DIST_DIR = join(ROOT, NEXT_DIST_DIR);
|
||
const STANDALONE_DIR = join(DIST_DIR, "standalone");
|
||
const ELECTRON_STANDALONE_DIR = join(ROOT, ".build", "electron-standalone");
|
||
|
||
// --- Electron-UNIQUE: resolve the nested server.js location ----------------
|
||
|
||
function resolveStandaloneBundleDir() {
|
||
const directServer = join(STANDALONE_DIR, "server.js");
|
||
if (existsSync(directServer)) {
|
||
return STANDALONE_DIR;
|
||
}
|
||
|
||
const nestedCandidates = [
|
||
join(STANDALONE_DIR, "projects", "OmniRoute"),
|
||
join(STANDALONE_DIR, basename(ROOT)),
|
||
];
|
||
|
||
for (const candidate of nestedCandidates) {
|
||
if (existsSync(join(candidate, "server.js"))) {
|
||
return candidate;
|
||
}
|
||
}
|
||
|
||
throw new Error(
|
||
`Standalone server bundle not found in ${STANDALONE_DIR}. Run \`npm run build\` first.`
|
||
);
|
||
}
|
||
|
||
// --- Electron-UNIQUE: symlink guard (electron-builder fails on symlinked node_modules) ---
|
||
|
||
function assertBundleIsPackagable(bundleDir) {
|
||
const nodeModulesPath = join(bundleDir, "node_modules");
|
||
if (!existsSync(nodeModulesPath)) return;
|
||
|
||
if (lstatSync(nodeModulesPath).isSymbolicLink()) {
|
||
throw new Error(
|
||
[
|
||
"Next standalone emitted app/node_modules as a symlink.",
|
||
"electron-builder preserves extraResources symlinks, which would make the packaged app",
|
||
"depend on the original build machine path at runtime.",
|
||
"",
|
||
`Offending path: ${nodeModulesPath}`,
|
||
"Use a real node_modules directory in the build worktree before packaging Electron.",
|
||
].join("\n")
|
||
);
|
||
}
|
||
}
|
||
|
||
// --- Electron-UNIQUE: strip generated electron artifacts from staged dir ---
|
||
|
||
function removeGeneratedElectronArtifacts() {
|
||
const generatedDirs = [join(ELECTRON_STANDALONE_DIR, "electron", "dist-electron")];
|
||
|
||
for (const dir of generatedDirs) {
|
||
rmSync(dir, { recursive: true, force: true });
|
||
}
|
||
}
|
||
|
||
// --- Electron-UNIQUE: remove native modules for electron-builder ABI rebuild ---
|
||
|
||
function removeNativeModules(baseDir, prefixes = ["keytar"]) {
|
||
if (!existsSync(baseDir)) return;
|
||
const dirs = readdirSync(baseDir);
|
||
for (const dir of dirs) {
|
||
if (prefixes.some((p) => dir.startsWith(p))) {
|
||
const fullPath = join(baseDir, dir);
|
||
rmSync(fullPath, { recursive: true, force: true });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Fail the build if hashed native copies survived the cleanup above. Without this,
|
||
// a wrong baseDir makes removeNativeModules() a silent no-op (it early-returns when
|
||
// the directory does not exist) and the ABI mismatch only surfaces at runtime on a
|
||
// user machine as "Internal Server Error" on every route.
|
||
function assertNoStaleHashedNatives(baseDir, prefixes) {
|
||
if (!existsSync(baseDir)) return;
|
||
const leftovers = readdirSync(baseDir).filter((dir) => prefixes.some((p) => dir.startsWith(p)));
|
||
if (leftovers.length > 0) {
|
||
throw new Error(
|
||
`[electron] stale native module copies survived cleanup in ${baseDir}: ` +
|
||
`${leftovers.join(", ")}. These carry the plain-Node ABI and shadow the ` +
|
||
`Electron-rebuilt binaries at runtime (ERR_DLOPEN_FAILED -> sql.js fallback -> OOM).`
|
||
);
|
||
}
|
||
}
|
||
|
||
// --- Electron-UNIQUE: verify better-sqlite3 Node-API prebuilds ----------------
|
||
//
|
||
// better-sqlite3 >= 13 ships Node-API (NAPI_VERSION=10) prebuilds for every
|
||
// platform we package (darwin/linux/linuxmusl/win32 × x64/arm64) inside the
|
||
// npm tarball. Node-API addons are ABI-independent, so the same prebuild runs
|
||
// under plain Node (CI, CLI) and under the packaged app's ELECTRON_RUN_AS_NODE
|
||
// server (verified against electron 43 / NODE_MODULE_VERSION 148 — issue
|
||
// #10321 Stage 6). The historical source rebuild below existed because
|
||
// better-sqlite3@12 only shipped prebuilds up to electron-v146 and electron 43
|
||
// (v148) silently got no binary; v13 makes that obsolete.
|
||
//
|
||
// Instead of compiling from source on every build (tens of seconds to minutes
|
||
// per platform), we fail fast when the prebuild for the CURRENT build platform
|
||
// is missing — a missing prebuild must kill the build here, not the app on a
|
||
// user machine with "Nenhum driver SQLite disponível — better-sqlite3 (falhou)".
|
||
|
||
function verifyBetterSqlite3Prebuilds(standaloneNodeModules) {
|
||
const destMod = join(standaloneNodeModules, "better-sqlite3");
|
||
if (!existsSync(destMod)) {
|
||
console.warn("[electron] better-sqlite3 not found in standalone — skipping prebuild check.");
|
||
return;
|
||
}
|
||
|
||
// Fail fast when the loader would find no prebuild for THIS build platform.
|
||
// Mirrors better-sqlite3's own lib/binding.js selection logic.
|
||
const reportHeader = process.report?.getReport?.().header;
|
||
assertSqlitePrebuildExists(destMod, process.platform, process.arch, reportHeader);
|
||
|
||
// Drop compile inputs and stale Node-ABI build outputs to keep the packaged
|
||
// app lean and to guarantee the loader resolves the prebuild, not a leftover
|
||
// build/Release/better_sqlite3.node compiled for a different ABI.
|
||
for (const dir of ["build", "deps", "src"]) {
|
||
rmSync(join(destMod, dir), { recursive: true, force: true });
|
||
}
|
||
console.log(
|
||
`[electron] better-sqlite3 Node-API prebuilds verified for ${process.platform}-${process.arch}.`
|
||
);
|
||
}
|
||
|
||
function logContextualError(error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
console.error(`[electron] failed to prepare standalone bundle: ${message}`);
|
||
process.exitCode = 1;
|
||
}
|
||
|
||
process.on("uncaughtException", logContextualError);
|
||
|
||
// Resolve the bundle dir (handles nested project layout) and check for symlinks
|
||
const bundleDir = resolveStandaloneBundleDir();
|
||
assertBundleIsPackagable(bundleDir);
|
||
|
||
// Clean the stage dir before assembly
|
||
rmSync(ELECTRON_STANDALONE_DIR, { recursive: true, force: true });
|
||
|
||
// Shared assembly: standalone copy + .next/static + public + abs-path sanitization + natives/@swc/helpers
|
||
assembleStandalone({
|
||
distDir: DIST_DIR,
|
||
outDir: ELECTRON_STANDALONE_DIR,
|
||
projectRoot: ROOT,
|
||
sanitizePaths: true,
|
||
// Next can emit hashed external package names in instrumentation chunks.
|
||
// The standalone dependency tree contains the canonical package names, so
|
||
// normalize those imports before electron-builder copies the bundle.
|
||
patchTurbopackChunks: true,
|
||
copyNatives: true,
|
||
// #6724/#6594: dereference Turbopack hashed-module symlinks — inside the packaged
|
||
// app they would point at the build machine's absolute paths and break on install.
|
||
materializeSymlinks: true,
|
||
});
|
||
|
||
const docsPrune = pruneElectronRuntimeDocs(ELECTRON_STANDALONE_DIR);
|
||
if (docsPrune.removedFiles > 0) {
|
||
console.log(
|
||
`[electron] pruned ${docsPrune.removedFiles} authoring doc file(s) ` +
|
||
`(${docsPrune.removedBytes} bytes) from the staging bundle`
|
||
);
|
||
}
|
||
|
||
// Electron-UNIQUE post-assembly steps
|
||
removeGeneratedElectronArtifacts();
|
||
|
||
// Verify better-sqlite3 Node-API prebuilds in the primary node_modules (where
|
||
// the standalone server resolves it). keytar is still stripped so
|
||
// electron-builder's @electron/rebuild handles it (it has electron prebuilds);
|
||
// also drop any stray better-sqlite3 under .next/node_modules so it cannot
|
||
// shadow the prebuild-backed one.
|
||
verifyBetterSqlite3Prebuilds(join(ELECTRON_STANDALONE_DIR, "node_modules"));
|
||
removeNativeModules(join(ELECTRON_STANDALONE_DIR, "node_modules"), ["keytar"]);
|
||
removeNativeModules(join(ELECTRON_STANDALONE_DIR, NEXT_DIST_DIR, "node_modules"), [
|
||
"better-sqlite3",
|
||
"keytar",
|
||
]);
|
||
|
||
// Post-condition: the cleanup above must actually have removed the stale Node-ABI
|
||
// copies. It silently no-opped across releases because the path was hardcoded to
|
||
// ".next" while distDir is ".build/next", so an ABI-mismatched better_sqlite3.node
|
||
// shipped inside the installer and the app fell back to sql.js and OOM-ed.
|
||
assertNoStaleHashedNatives(join(ELECTRON_STANDALONE_DIR, NEXT_DIST_DIR, "node_modules"), [
|
||
"better-sqlite3",
|
||
"keytar",
|
||
]);
|
||
|
||
// Stage 7 (issue #10321): move the optional ML/browser dependency closure out of
|
||
// the desktop bundle into checksummed, versioned packs under
|
||
// `.build/optional-packs/` (+ tarballs) and emit `optional-packs.index.json` at
|
||
// the bundle root. Runs after the native-module steps so it only ever sees the
|
||
// final staging tree. Fail-open per member (see optionalPackStaging.mjs).
|
||
const OPTIONAL_PACKS_OUT_DIR = join(ROOT, ".build", "optional-packs");
|
||
await stageOptionalPacks({
|
||
stagingRoot: ELECTRON_STANDALONE_DIR,
|
||
packsOutDir: OPTIONAL_PACKS_OUT_DIR,
|
||
log: (msg) => console.log(msg.replace(/^\[optional-packs\]/, "[electron]")),
|
||
});
|
||
|
||
console.log(
|
||
`[electron] prepared standalone bundle: ${relative(ROOT, ELECTRON_STANDALONE_DIR) || "."}`
|
||
);
|