// Ensure better-sqlite3 is installed in USER_DATA_DIR/runtime/node_modules // (user-writable, avoids Windows EBUSY locks during npm i -g updates). // sql.js is bundled in bin/app already; node:sqlite / bun:sqlite are built-in. const { execSync, spawnSync } = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); const BETTER_SQLITE3_VERSION = "12.6.2"; const SQL_JS_VERSION = "1.14.1"; function getDataDir() { if (process.env.DATA_DIR) return process.env.DATA_DIR; return process.platform === "win32" ? path.join(process.env.APPDATA || os.homedir(), "9router") : path.join(os.homedir(), ".9router"); } function getRuntimeDir() { return path.join(getDataDir(), "runtime"); } function getRuntimeNodeModules() { return path.join(getRuntimeDir(), "node_modules"); } function ensureRuntimeDir() { const dir = getRuntimeDir(); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); // Minimal package.json so npm treats it as a project root const pkgPath = path.join(dir, "package.json"); if (!fs.existsSync(pkgPath)) { fs.writeFileSync(pkgPath, JSON.stringify({ name: "9router-runtime", version: "1.0.0", private: true, description: "User-writable runtime deps for 9router (better-sqlite3 native binary)", }, null, 2)); } return dir; } function hasModule(name) { return fs.existsSync(path.join(getRuntimeNodeModules(), name, "package.json")); } function isBetterSqliteBinaryValid() { const binary = path.join(getRuntimeNodeModules(), "better-sqlite3", "build", "Release", "better_sqlite3.node"); if (!fs.existsSync(binary)) return false; try { const fd = fs.openSync(binary, "r"); const buf = Buffer.alloc(4); fs.readSync(fd, buf, 0, 4, 0); fs.closeSync(fd); const magic = buf.toString("hex"); if (process.platform === "linux") return magic.startsWith("7f454c46"); if (process.platform === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); if (process.platform === "win32") return magic.startsWith("4d5a"); return true; } catch { return false; } } // Extract a short, user-friendly reason from npm stderr. function summarizeNpmError(stderr = "") { const text = String(stderr); if (/ENOTFOUND|ETIMEDOUT|EAI_AGAIN|network|getaddrinfo/i.test(text)) return "No internet connection or registry unreachable"; if (/EACCES|EPERM|permission denied/i.test(text)) return "Permission denied (check folder permissions)"; if (/ENOSPC|no space/i.test(text)) return "Not enough disk space"; if (/node-gyp|gyp ERR|python|MSBuild|Visual Studio|Xcode/i.test(text)) return "Missing build tools (Xcode CLT / Python / VS Build Tools)"; if (/ETARGET|version.*not found/i.test(text)) return "Package version not found on registry"; const m = text.match(/npm ERR! (.+)/); if (m) return m[1].slice(0, 200); const lastLine = text.trim().split(/\r?\n/).filter(Boolean).pop(); return lastLine ? lastLine.slice(0, 200) : "Unknown error"; } function runNpmInstall({ cwd, pkgs, extraArgs = [], timeout = 180000 }) { const args = ["install", ...pkgs, "--no-audit", "--no-fund", "--prefer-online", ...extraArgs]; const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"; const res = spawnSync(npmCmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout, shell: process.platform === "win32", encoding: "utf8", }); return { ok: res.status === 0, code: res.status, stderr: res.stderr || "", stdout: res.stdout || "" }; } function npmInstall(pkgs, opts = {}) { const cwd = ensureRuntimeDir(); const extra = opts.optional ? ["--no-save"] : []; if (!opts.silent) console.log("⏳ Installing SQLite engine (first run)..."); const res = runNpmInstall({ cwd, pkgs, extraArgs: extra, timeout: opts.timeout || 180000 }); if (!res.ok && !opts.silent) { const reason = summarizeNpmError(res.stderr); console.warn("⚠️ SQLite engine install failed — using fallback"); console.warn(` Reason: ${reason}`); console.warn(` Retry: cd "${cwd}" && npm install ${pkgs.join(" ")}`); } return res.ok; } // Public: ensure better-sqlite3 native module is installed in user-writable // runtime dir. sql.js may be bundled in bin/app, but npm publish strips .wasm // from nested node_modules — verify and reinstall if missing. node:sqlite is // built-in. This is purely a *speed optimization* — app works without // better-sqlite3 via fallbacks. function isSqlJsWasmValid() { const bundledWasm = path.join(__dirname, "..", "app", "node_modules", "sql.js", "dist", "sql-wasm.wasm"); if (fs.existsSync(bundledWasm)) return true; const runtimeWasm = path.join(getRuntimeNodeModules(), "sql.js", "dist", "sql-wasm.wasm"); return fs.existsSync(runtimeWasm); } function ensureSqliteRuntime({ silent = false } = {}) { ensureRuntimeDir(); let sqlJsOk = isSqlJsWasmValid(); if (!sqlJsOk) { sqlJsOk = npmInstall([`sql.js@${SQL_JS_VERSION}`], { silent }); if (sqlJsOk) sqlJsOk = isSqlJsWasmValid(); } const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid(); if (!needBetterSqlite) { if (!silent) console.log("✅ SQLite engine ready"); return { betterSqlite: true, sqlJs: sqlJsOk }; } const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent }); return { betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(), sqlJs: sqlJsOk, }; } // Inject runtime + bundled node_modules into NODE_PATH so child Node processes // resolve sql.js (bundled in bin/app/node_modules) and better-sqlite3 (runtime). function buildEnvWithRuntime(baseEnv = process.env) { const runtimeNm = getRuntimeNodeModules(); const bundledNm = path.join(__dirname, "..", "app", "node_modules"); const existing = baseEnv.NODE_PATH || ""; const NODE_PATH = [runtimeNm, bundledNm, existing].filter(Boolean).join(path.delimiter); return { ...baseEnv, NODE_PATH }; } module.exports = { ensureSqliteRuntime, buildEnvWithRuntime, getRuntimeDir, getRuntimeNodeModules, runNpmInstall, summarizeNpmError, };