返回博客

同一文件,两条路径:打包后 A 模块成功 B 模块 not-found

某个桌面 App 打包后,运行一条自动化工作流,日志里出现了让人困惑的一幕:

✅ 登录态校验      DONE  → 已登录(新登录)
❌ 读取用户数据    ERR   → storageState not found · 请先跑登录工作流

两个步骤在同一进程里顺序执行,第一步明确写着"已登录",第二步立刻说"找不到登录文件"。这不是偶现——每次打包版都必现,dev 模式下从未出现。

现象

  • check_login 模块:成功读到 storage-state.json,返回"已登录"。
  • scraper 模块:下一行就报 storageState not found,完全找不到同一个文件。
  • 本地 bun src/index.ts 跑:两步都成功,从未报错。
  • 打包为 Tauri sidecar 后:必现。

根因

grep 一下两个模块各自怎么算路径:

// auth.ts(后来更新过,支持打包部署)
const STATE_FILE = process.env.APP_USER_DATA
  ? resolve(process.env.APP_USER_DATA, 'xhs-state/storage-state.json')
  : resolve(homedir(), 'Library/Application Support/com.example.app/xhs-state/storage-state.json')
 
// scraper.ts(最早写的,从未碰过)
const STATE_FILE = resolve(process.cwd(), 'var', 'xhs-state', 'storage-state.json')

本地 dev 时process.cwd() 指向 repo 根目录,刚好 var/xhs-state/ 目录是开发期手动建的,或者某次调试时复制过去了。两个路径"碰巧"指向不同位置,但文件存在于两处(其中一处是你不知道的副本),测试通过。

Tauri sidecar 启动时:sidecar 进程的 cwd/(或者系统临时目录)。scraper.ts 解析出 /var/xhs-state/storage-state.json,根本不存在。auth.ts 走的是 APP_USER_DATA 环境变量,正确解析到 ~/Library/Application Support/...,文件在那里。

两个模块名义上操作"同一份"登录态,实际上在打包环境下操作的是完全不同的两个文件系统路径。

坑:模块各自硬算文件路径,dev 下靠 cwd 巧合对齐

process.cwd() 在 dev 环境等于 repo 根目录,var/xhs-state/ 这种相对路径"刚好"能找到。但只要换一个启动上下文——打包二进制、Docker WORKDIR /app、systemd unit、sidecar 子进程——cwd 就不再是 repo 根,路径立刻分叉。

更危险的是:分叉不报错,只是两个模块在操作不同的文件。A 说成功,B 说找不到,你的第一反应是"登录流程有 bug",而不是"路径根本不同"。

正解

一个资源,一个路径解析函数,所有消费者统一导入。

第一步:提取成导出函数

把"谁更新得更新、逻辑更完整"的那个模块的路径计算逻辑提取出来,改为导出函数(注意:用函数而不是常量,原因见后文):

// auth.ts
function getStateDir(): string {
  if (process.env.APP_USER_DATA) {
    return resolve(process.env.APP_USER_DATA, 'xhs-state')
  }
  if (process.platform === 'darwin') {
    return resolve(homedir(), 'Library/Application Support/com.example.app/xhs-state')
  }
  // Linux / Windows fallback...
  return resolve(homedir(), '.config/com.example.app/xhs-state')
}
 
export function getStatePath(): string {
  return resolve(getStateDir(), 'storage-state.json')
}

第二步:消费者全部改为导入

// scraper.ts
- const STATE_FILE = resolve(process.cwd(), 'var', 'xhs-state', 'storage-state.json')
+ import { getStatePath } from './auth'
+ const STATE_FILE = getStatePath()

用 grep 找出所有还在自己算路径的地方,统一替换:

rg -n "resolve\(\s*process\.cwd\(\)" src/
rg -n "const\s+(STATE_FILE|.*_PATH|.*_FILE)\s*=" src/

第三步:清理遗留的 legacy 目录

repo 里 var/xhs-state/ 如果有提交进去的文件,删掉——它是 dev 下掩盖 bug 的凶手之一。更新 .gitignore,把旧路径的 ignore 规则替换为新路径(或直接删除,新路径在 repo 之外)。

正解:一个 getXxxPath() 函数,全局唯一导入

路径解析逻辑只存在于一处。优先级判断(env var > 平台默认 > 兜底)全在这个函数里,调用者只管 import { getStatePath } from './auth',无需关心任何部署上下文。

函数而不是模块顶层常量,是因为常量在模块加载时就求值,如果 env var 在 import 之后才被设置(Tauri sidecar 经常这样),常量会拿到 undefined;函数则在调用时读环境变量,永远是最新值。

一句话外卖

process.cwd() 只在 dev 环境等于 repo 根——只要有打包、Docker、sidecar,相对路径就会漂移;持久化文件的路径必须集中在一个导出函数里,所有模块统一导入,不能各算各的。