TypeScript Watch API 已 close 进程仍不退出:检查待执行更新
文件变化后立刻关闭 createWatchProgram,更新计时器可能在关闭后重建监听器。限定旧 JavaScript API,并由宿主清理自己登记的计时器。
一次性构建已经写出产物,也调用了 createWatchProgram() 返回对象的 close(),Node 进程却不退出。如果构建结束前刚发生过源码变更,应检查关闭后是否还有待执行的 program update,而不是先强制 process.exit()。
TypeScript 6.0.3 的 JavaScript Watch API 会把文件变化合并到约 250 毫秒后的更新。正式源码中的 close() 关闭了多类监听器,却未取消 timerToUpdateProgram。若关闭落在这段窗口内,稍后的回调仍可能更新程序、重新创建监听器,使进程保持存活。
只有关闭前存在待执行更新,才吻合这条路径
记录文件写入、构建结束和调用 close 的时间顺序,核对实际加载的 TypeScript 版本及创建 Watch Program 的插件。构建后的资源列表若仍出现文件监听资源,可以沿该宿主的 watch 生命周期检查;同一进程也可能因服务器、定时器或打开的流不退出,不能仅凭“挂住”就归因。
第二次调用 close 并不保证收拾到重新创建的监听器。旧实现已经清掉 builder 引用,关闭后的异步更新又破坏了原来的生命周期,重复清理不能替代取消待执行任务。
自己创建 Watch Host 时,跟踪它登记的计时器
以下为 JavaScript 集成片段,前提是 host 已由现有代码创建,且提供 setTimeout、clearTimeout。必须在调用 ts.createWatchProgram(host) 之前安装包装;ts 是现有的 TypeScript 模块导入。
const pending = new Set();
const schedule = host.setTimeout.bind(host);
const cancel = host.clearTimeout.bind(host);
host.setTimeout = (callback, ms, ...args) => {
const timer = schedule(() => {
pending.delete(timer);
callback(...args);
}, ms);
pending.add(timer);
return timer;
};
host.clearTimeout = (timer) => {
pending.delete(timer);
cancel(timer);
};
const watch = ts.createWatchProgram(host);
function closeWatch() {
watch.close();
for (const timer of pending) cancel(timer);
pending.clear();
}
应用的关闭入口应调用 closeWatch(),并停止向这份宿主投递新的文件变化。包装只处理这个 host 登记的计时器,不应清空进程中其他任务的 timer,也不要在已有插件包装之上再叠加一套互相争用的关闭逻辑。自定义宿主若改变计时器回调协议,需要按自己的协议保留参数和返回句柄。
通过 Rollup 等插件间接创建 Watch Program 时,应先检查插件是否已处理这项清理,并按其正式发行与兼容范围更新;应用侧通常拿不到内部 host,不应直接修改 node_modules 中编译器文件。当前 TypeScript 6.0.3 的源码仍有上述遗漏,不能把升级该包的小版本作为未经核对的保证。
TypeScript 7 不是同一个 Compiler API
原生 TypeScript 7 的包接口与旧 JavaScript Compiler API 不同,不能为了结束挂起就把依赖换到 7,再假定 createWatchProgram 仍可用。迁移需要调用工具明确支持,不是这条清理措施的等价替换。
验收要安排关闭前有变化与没有变化两条时序,确认关闭后不再触发编译、创建监听器,其他任务结束后进程能自然退出。还要覆盖正常持续 watch,确保包装没有提前取消应执行的更新。保留原退出码和错误输出;强制退出虽然能结束进程,却无法证明资源生命周期正确,也可能截断其他写入。


