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

+3
View File
@@ -13,6 +13,9 @@ Electron + electron-vite + React 终端工具(本地终端 / SSH / SFTP)。
- 打包:`npm run dist`(**生命周期先自动跑 `predist` → `npm test`,即类型检查 + 18 个离线测试全部通过后才 build/package**,typecheck 全程只跑一次;predist 末尾的 `npm install --package-lock-only` 会把 `package-lock.json` 根版本号对齐 `package.json`,**发布提交必须带上 package-lock.json**),产物在 `release/`(msi + exe + latest.yml + blockmap)
- GitHub Actions:`.github/workflows/ci.yml` 在 windows-latest + Node 22 上跑 `npm ci` / `npm test`(含 pretest typecheck)/ `npm run build`,只做验证,不打包安装器、不发布
- 国内网络需镜像:`ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ npm run dist`
- 依赖一致性闸门:`predist` 开头跑 `node scripts/verify-deps.cjs`(逐条比对 `node_modules` 与 `package-lock.json` 的版本,跨平台 optional 依赖缺失跳过;不一致就 exit 1)——`npm install --package-lock-only` **只重算 lockfile、不碰 node_modules**(本版 npm 还会把 `node_modules/.package-lock.json` 一并重写成理想树,制造「已经装好了」的假象),所以**改动依赖版本后必须真实 `npm install` 或 `npm ci` 再构建**,别指望 lockfile 对齐就等于树里换了包;v1.0.21 的中文输入法回归正是这个坑(`@xterm/xterm` 锁 6.1.0-beta.304,树里还是 6.0.0,打进去的是旧代码)
- **换机/重装环境后第一次打包前必须先跑 `npm ci`**(verify-deps 会兜底拦截,但别浪费一次构建);在任何机器上都先 `node scripts/verify-deps.cjs` 确认再 `npm run dist`
- electron-builder `rename win-unpacked.tmp` 撞 EPERM = 安全软件实时扫描新解压文件短暂持锁(实测约 4s,进程退出后数秒即可手动重命名)。应对:`NODE_OPTIONS="--require $(pwd -W)/scripts/rename-retry-shim.cjs" npm run dist`(注入重试垫片,stderr 会打印每次重试);根治是把仓库目录加进安全软件信任区
## 仓库与远程
+8
View File
@@ -1,5 +1,13 @@
# OpenTerminal Changelog
## v1.0.22 - 2026-10-08
### IME
- **Fixed: Chinese IME candidate window not following the caret** (regression in v1.0.21): the v1.0.21 build environment had a terminal component dependency that did not match the locked version, so the installer shipped an older build without the upstream IME fix; this release restores the terminal component version used by v1.0.20, and the candidate window follows the caret again.
### Internal
- The packaging pipeline now verifies dependency consistency before building: installed dependencies are compared against the lockfile and the build aborts on any mismatch, preventing this class of issue from recurring.
## v1.0.21 - 2026-10-07
### Background image experience
+8
View File
@@ -1,5 +1,13 @@
# OpenTerminal 更新履歴
## v1.0.22 - 2026-10-08
### IME
- **中国語 IME の候補ウィンドウがキャレットに追従しない問題を修正**(v1.0.21 でのリグレッション):v1.0.21 のビルド環境では端末コンポーネントの依存がロックされたバージョンと一致しておらず、インストーラーには上流の IME 修正を含まない古いビルドが同梱されていました。本バージョンでは v1.0.20 と同じ端末コンポーネントに戻し、候補ウィンドウが再びキャレットに追従します。
### 内部
- パッケージング前に依存関係の整合性チェックを追加:インストール済み依存とロックファイルを照合し、不一致があればビルドを中止します。同種の問題の再発を防ぎます。
## v1.0.21 - 2026-10-07
### 背景画像の体験
+8
View File
@@ -1,5 +1,13 @@
# OpenTerminal 更新日志
## v1.0.22 - 2026-10-08
### 输入法
- **修复中文输入法候选窗不跟随光标**(v1.0.21 回归):v1.0.21 的构建环境中终端组件依赖未按锁定版本安装,安装包实际打进的是不含上游输入法修复的旧版本;本版本恢复 v1.0.20 所用的终端组件版本,候选窗重新紧贴光标。
### 内部
- 打包流程新增依赖一致性校验:构建前自动比对实际安装的依赖与锁定清单,不一致即中止,防止同类问题再次发生。
## v1.0.21 - 2026-10-07
### 背景图体验
+8
View File
@@ -1,5 +1,13 @@
# OpenTerminal 更新日誌
## v1.0.22 - 2026-10-08
### 輸入法
- **修復中文輸入法候選視窗不跟隨游標**(v1.0.21 回歸):v1.0.21 的建置環境中終端機元件相依未按鎖定版本安裝,安裝包實際打包的是不含上游輸入法修復的舊版本;本版本恢復 v1.0.20 所用的終端機元件版本,候選視窗重新緊貼游標。
### 內部
- 打包流程新增相依一致性校驗:建置前自動比對實際安裝的相依與鎖定清單,不一致即中止,防止同類問題再次發生。
## v1.0.21 - 2026-10-07
### 背景圖體驗
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "open-terminal",
"version": "1.0.21",
"version": "1.0.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-terminal",
"version": "1.0.21",
"version": "1.0.22",
"license": "MIT",
"dependencies": {
"@lydell/node-pty": "^1.2.0-beta.15",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "open-terminal",
"productName": "OpenTerminal",
"version": "1.0.21",
"version": "1.0.22",
"description": "Open-source terminal with SSH, split panes, themes and fonts",
"main": "out/main/index.js",
"author": "CodingPlan.Site",
@@ -14,7 +14,7 @@
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
"pretest": "npm run typecheck",
"test": "node tests/build-bundles.cjs && node tests/ssh-loopback.mjs && node tests/commands-store.mjs && node tests/connections-store.mjs && node tests/settings-store.mjs && node tests/local-path-grants.mjs && node tests/lock-store.mjs && node tests/lock-controller.mjs && node tests/lock-shortcuts.mjs && node tests/reserved-accelerators.mjs && node tests/.terminal-title.cjs && node tests/ipc-guard.mjs && node tests/updater-fallback.mjs && node tests/log-sanitizer.mjs && node tests/sftp-timeout.mjs && node tests/.hl-split-smoke.cjs && node tests/.hl-rules.cjs && node tests/zmodem-e2e.mjs && node tests/ssh-session-e2e.mjs && node tests/sysinfo-e2e.mjs",
"predist": "npm test && npm install --package-lock-only",
"predist": "node scripts/verify-deps.cjs && npm test && npm install --package-lock-only",
"dist": "electron-vite build && electron-builder --win msi nsis",
"dist:dir": "electron-vite build && electron-builder --win --dir"
},
+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`)