release: v1.0.22
CI / typecheck + test + build (windows) (push) Waiting to run

fix(ime): actually ship @xterm/xterm 6.1.0-beta.304 (v1.0.21 built from stale
node_modules with 6.0.0); add predist dependency-consistency gate
(scripts/verify-deps.cjs) and rename-retry shim for the AV scan EPERM race
This commit is contained in:
Bill committed 2026-10-08 13:09:13 +08:00
1 parent ac43844f8a
commit 40336d014d
9 files changed
+128 -4

No files matched your search

+30
View File
@@ -0,0 +1,30 @@
// 构建垫片:Windows 上安全软件实时扫描新解压的文件时会短暂持有目录句柄(实测 ~4s),
// electron-builder 解包后立即 rename 会撞 EPERM。对 fs.promises 的 rename/rm/unlink/copyFile
// 加 EPERM/EBUSY/ENOTEMPTY 重试(30 次 × 1s),stderr 会打印每次重试。
// 用法(命中 EPERM 时):NODE_OPTIONS="--require $(pwd -W)/scripts/rename-retry-shim.cjs" npm run dist
const fs = require('node:fs');
const RETRY_CODES = new Set(['EPERM', 'EBUSY', 'ENOTEMPTY']);
const MAX_TRIES = 30;
const DELAY_MS = 1000;
function wrap(name) {
const orig = fs.promises[name];
if (typeof orig !== 'function') return;
fs.promises[name] = async function (...args) {
for (let i = 0; ; i++) {
try {
return await orig.apply(fs.promises, args);
} catch (err) {
if (!RETRY_CODES.has(err && err.code) || i >= MAX_TRIES - 1) throw err;
process.stderr.write(`[rename-retry-shim] ${name} ${args[0]} -> ${err.code}, retry ${i + 1}/${MAX_TRIES}\n`);
await new Promise(r => setTimeout(r, DELAY_MS));
}
}
};
}
wrap('rename');
wrap('rm');
wrap('unlink');
wrap('copyFile');
+59
View File
@@ -0,0 +1,59 @@
/* Dependency consistency gate: node_modules must match package-lock.json.
Usage: node scripts/verify-deps.cjs (runs first in `predist`)
Why it exists: v1.0.21 shipped an input-method fix that never reached the
build. @xterm/xterm had been pinned to 6.1.0-beta.304 in package.json, but
node_modules still held 6.0.0 from an older install — `npm install
--package-lock-only` (the last step of predist) rewrites only the lockfile,
so the lockfile agreed with package.json while the tree on disk stayed
stale, and the bundler took the stale tree. An install that never happened
is invisible to every other check, so it gets its own gate here.
Scope: version equality only, for every entry the lockfile lists under
node_modules. The root entry ("") is deliberately excluded — bumping
package.json makes the lockfile root version lag by design until the
`npm install --package-lock-only` at the end of predist rewrites it. */
const fs = require('node:fs')
const path = require('node:path')
const ROOT = path.join(__dirname, '..')
const lock = JSON.parse(fs.readFileSync(path.join(ROOT, 'package-lock.json'), 'utf8'))
const packages = lock.packages ?? {}
const mismatched = []
let checked = 0
// Optional entries are the cross-platform variants (darwin/linux/arm64 …) that
// npm records in the lockfile but never installs on this machine. Their absence
// is expected, not drift.
let skippedOptional = 0
for (const [key, entry] of Object.entries(packages)) {
if (key === '' || !key.startsWith('node_modules/')) continue
// The key is the path under node_modules, so it also resolves nested
// ("node_modules/a/node_modules/b") and scoped ("node_modules/@scope/pkg")
// entries without any name reconstruction.
let installed
try {
installed = JSON.parse(fs.readFileSync(path.join(ROOT, key, 'package.json'), 'utf8')).version
} catch {
installed = undefined
}
if (installed === undefined) {
if (entry.optional) skippedOptional++
else mismatched.push([key, entry.version, 'MISSING'])
continue
}
checked++
if (installed !== entry.version) mismatched.push([key, entry.version, installed])
}
if (mismatched.length) {
console.error('node_modules does not match package-lock.json:')
for (const [name, expected, actual] of mismatched) {
console.error(` ${name}: lockfile expects ${expected}, installed ${actual}`)
}
console.error('\nnode_modules 与 package-lock.json 不一致,请运行 npm ci 重装依赖')
process.exit(1)
}
console.log(`dependencies ok: ${checked} packages verified, ${skippedOptional} optional entries skipped`)