同一文件,两条路径:打包后 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/...,文件在那里。
两个模块名义上操作"同一份"登录态,实际上在打包环境下操作的是完全不同的两个文件系统路径。
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 之外)。
路径解析逻辑只存在于一处。优先级判断(env var > 平台默认 > 兜底)全在这个函数里,调用者只管 import { getStatePath } from './auth',无需关心任何部署上下文。
用函数而不是模块顶层常量,是因为常量在模块加载时就求值,如果 env var 在 import 之后才被设置(Tauri sidecar 经常这样),常量会拿到 undefined;函数则在调用时读环境变量,永远是最新值。
一句话外卖
process.cwd()只在 dev 环境等于 repo 根——只要有打包、Docker、sidecar,相对路径就会漂移;持久化文件的路径必须集中在一个导出函数里,所有模块统一导入,不能各算各的。