Files
Literature-Management/scripts/run-tests.cjs
2026-10-21 14:42:59 +08:00

110 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
// 自适应 vitest 启动器 —— 解决 better-sqlite3 的「双 ABI 互相覆盖」问题。
//
// 背景(2026-10-19 实地复现):
// better-sqlite3 不是 N-API,一个 .node 只对应一个 ABI:
// - Electron 30.5(NODE_MODULE_VERSION 123)要 Electron 版 —— app 运行时用
// - 系统 Node 24(NODE_MODULE_VERSION 137)要 Node 版 —— vitest 用
// 两者共用同一个路径:
// node_modules/better-sqlite3/build/Release/better_sqlite3.node
//
// 旧做法(pretest / pretest:run / pretest:coverage 都是这一行):
// node scripts/verify-abi-node.cjs || npm rebuild better-sqlite3
// → 为了让测试能跑,**破坏性地**把 app 那份覆盖成 Node 版。
// Windows 上 .node 被运行中的 electron 进程锁住 → unlink 报
// EPERM/EBUSY → 重建失败 → pretest 非 0 退出 → npm 把整条
// `npm run test:run` 一起中止(哪怕测试本身全绿)。
// 反向也成立:测试跑完 .node 变成 Node 版,下一次 `npm run dev` 又要
// 重建回 Electron 版 —— 每次切换都可能被运行中的 app 锁死。
//
// 策略:不再为了跑测试而重建。改成二选一启动器,按「当前 .node 到底是谁的」
// 决定用谁来跑 vitest:
// 1. 系统 Node 能直接 require('better-sqlite3') → 用系统 Node 跑 vitest。
// (Linux/macOS 的 napi prebuilt 天然两用;Windows 上若已是 Node 版 .node
// 也走这条)
// 2. 否则(Windows + Electron 版 .node,也就是常态)→ 用 Electron 自带的
// Node 跑 vitest(ELECTRON_RUN_AS_NODE=1)。此时测试与 app 共用同一份
// 原生模块:零重建、零 EBUSY、也不会把 app 那份覆盖掉。
//
// 附带收益:Electron 30.5 内嵌 Node 是 v20.16.0,仍在 package.json 的
// engines(>=20.16.0 <25)范围内,所以这条路不引入新的版本约束。
//
// 退出码透传 vitest 的退出码;CI 语义不变。
'use strict';
const { spawnSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const ROOT = path.resolve(__dirname, '..');
const VITEST_ENTRY = path.join(ROOT, 'node_modules', 'vitest', 'vitest.mjs');
const argv = process.argv.slice(2);
function log(msg) {
process.stderr.write(`[run-tests] ${msg}\n`);
}
/** 系统 Node 当前能否加载 better-sqlite3(= .node 是否是 Node ABI 版本)。 */
function nativeLoadable() {
try {
const Database = require('better-sqlite3');
const db = new Database(':memory:');
db.close();
return true;
} catch {
return false;
}
}
/** electron npm 包在 Node 下导出的是二进制路径字符串;拿不到就退回 dist 猜路径。 */
function electronBinary() {
try {
const p = require('electron');
if (typeof p === 'string' && fs.existsSync(p)) return p;
} catch {
/* 落到下面的猜测 */
}
const exe = process.platform === 'win32' ? 'electron.exe' : 'electron';
const guess = path.join(ROOT, 'node_modules', 'electron', 'dist', exe);
return fs.existsSync(guess) ? guess : null;
}
function run(cmd, cmdArgs, env) {
const res = spawnSync(cmd, cmdArgs, { stdio: 'inherit', cwd: ROOT, env });
if (res.error) {
log(`启动失败: ${res.error.message}`);
return 1;
}
// 被信号杀死时 status 为 null
return typeof res.status === 'number' ? res.status : 1;
}
if (!fs.existsSync(VITEST_ENTRY)) {
log(`找不到 vitest 入口 ${VITEST_ENTRY} —— 先执行 npm install`);
process.exit(1);
}
let code;
if (nativeLoadable()) {
// 路线 1:系统 Node 直跑(app 那份 .node 已经是 Node 版,或 napi 通用版)
code = run(process.execPath, [VITEST_ENTRY, ...argv], process.env);
} else {
// 路线 2:用 Electron 自带 Node 跑 —— 与 app 共用同一份原生模块
const electron = electronBinary();
if (!electron) {
log('better-sqlite3 在系统 Node 下不可加载,且找不到 electron 可执行文件。');
log(' 手动修复(选一条):');
log(' npx electron-rebuild -f -w better-sqlite3 # 给 app 用');
log(' npm rebuild better-sqlite3 # 给系统 Node 用');
process.exit(1);
}
log('better-sqlite3 当前为 Electron ABI —— 改用 Electron 自带 Node 跑 vitest');
log(' (与 app 共用同一份原生模块:不重建、不覆盖,规避 EBUSY)');
code = run(electron, [VITEST_ENTRY, ...argv], {
...process.env,
ELECTRON_RUN_AS_NODE: '1',
});
}
process.exit(code);