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:
1 parent
ac43844f8a
commit
40336d014d
9 files changed
+128
-4
No files matched your search
@@ -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)
|
- 打包:`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`,只做验证,不打包安装器、不发布
|
- 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`
|
- 国内网络需镜像:`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 会打印每次重试);根治是把仓库目录加进安全软件信任区
|
||||||
|
|
||||||
## 仓库与远程
|
## 仓库与远程
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# OpenTerminal Changelog
|
# 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
|
## v1.0.21 - 2026-10-07
|
||||||
|
|
||||||
### Background image experience
|
### Background image experience
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# OpenTerminal 更新履歴
|
# 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
|
## v1.0.21 - 2026-10-07
|
||||||
|
|
||||||
### 背景画像の体験
|
### 背景画像の体験
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# OpenTerminal 更新日志
|
# OpenTerminal 更新日志
|
||||||
|
|
||||||
|
## v1.0.22 - 2026-10-08
|
||||||
|
|
||||||
|
### 输入法
|
||||||
|
- **修复中文输入法候选窗不跟随光标**(v1.0.21 回归):v1.0.21 的构建环境中终端组件依赖未按锁定版本安装,安装包实际打进的是不含上游输入法修复的旧版本;本版本恢复 v1.0.20 所用的终端组件版本,候选窗重新紧贴光标。
|
||||||
|
|
||||||
|
### 内部
|
||||||
|
- 打包流程新增依赖一致性校验:构建前自动比对实际安装的依赖与锁定清单,不一致即中止,防止同类问题再次发生。
|
||||||
|
|
||||||
## v1.0.21 - 2026-10-07
|
## v1.0.21 - 2026-10-07
|
||||||
|
|
||||||
### 背景图体验
|
### 背景图体验
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# OpenTerminal 更新日誌
|
# OpenTerminal 更新日誌
|
||||||
|
|
||||||
|
## v1.0.22 - 2026-10-08
|
||||||
|
|
||||||
|
### 輸入法
|
||||||
|
- **修復中文輸入法候選視窗不跟隨游標**(v1.0.21 回歸):v1.0.21 的建置環境中終端機元件相依未按鎖定版本安裝,安裝包實際打包的是不含上游輸入法修復的舊版本;本版本恢復 v1.0.20 所用的終端機元件版本,候選視窗重新緊貼游標。
|
||||||
|
|
||||||
|
### 內部
|
||||||
|
- 打包流程新增相依一致性校驗:建置前自動比對實際安裝的相依與鎖定清單,不一致即中止,防止同類問題再次發生。
|
||||||
|
|
||||||
## v1.0.21 - 2026-10-07
|
## v1.0.21 - 2026-10-07
|
||||||
|
|
||||||
### 背景圖體驗
|
### 背景圖體驗
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"version": "1.0.21",
|
"version": "1.0.22",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"version": "1.0.21",
|
"version": "1.0.22",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lydell/node-pty": "^1.2.0-beta.15",
|
"@lydell/node-pty": "^1.2.0-beta.15",
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "open-terminal",
|
"name": "open-terminal",
|
||||||
"productName": "OpenTerminal",
|
"productName": "OpenTerminal",
|
||||||
"version": "1.0.21",
|
"version": "1.0.22",
|
||||||
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
"description": "Open-source terminal with SSH, split panes, themes and fonts",
|
||||||
"main": "out/main/index.js",
|
"main": "out/main/index.js",
|
||||||
"author": "CodingPlan.Site",
|
"author": "CodingPlan.Site",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
"typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json",
|
||||||
"pretest": "npm run typecheck",
|
"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",
|
"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": "electron-vite build && electron-builder --win msi nsis",
|
||||||
"dist:dir": "electron-vite build && electron-builder --win --dir"
|
"dist:dir": "electron-vite build && electron-builder --win --dir"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -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`)
|
||||||
Reference in new issue
Block a user