139 lines
5.4 KiB
JavaScript
139 lines
5.4 KiB
JavaScript
// 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";
|
|
|
|
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 is bundled in bin/app already; node:sqlite is built-in.
|
|
// This is purely a *speed optimization* — app works without it via fallbacks.
|
|
function ensureSqliteRuntime({ silent = false } = {}) {
|
|
ensureRuntimeDir();
|
|
|
|
const needBetterSqlite = !hasModule("better-sqlite3") || !isBetterSqliteBinaryValid();
|
|
if (!needBetterSqlite) {
|
|
if (!silent) console.log("✅ SQLite engine ready");
|
|
return { betterSqlite: true };
|
|
}
|
|
|
|
const ok = npmInstall([`better-sqlite3@${BETTER_SQLITE3_VERSION}`], { optional: true, silent });
|
|
return {
|
|
betterSqlite: ok && hasModule("better-sqlite3") && isBetterSqliteBinaryValid(),
|
|
};
|
|
}
|
|
|
|
// 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,
|
|
};
|