fix: 修复构建后 vendor 二进制路径解析错误(ripgrep/audio-capture)

构建后 chunk 文件位于 dist/chunks/(Vite)或 dist/(Bun),vendor 二进制在
dist/vendor/,但 ripgrep 和 audio-capture 的路径解析未考虑 chunks/ 层级,
导致 ENOENT。改用 import.meta.url 路径中 lastIndexOf('dist') 定位 dist 根,
并同步在 build.ts 和 post-build.ts 中添加 ripgrep vendor 文件复制。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
claude-code-best
2026-04-25 14:46:02 +08:00
parent 1c3b280c6a
commit a8ed0cdce5
5 changed files with 70 additions and 20 deletions

View File

@@ -76,7 +76,9 @@ bun run docs:dev
### Runtime & Build
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。
- **Build**: `build.ts` 执行 `Bun.build()` with `splitting: true`,入口 `src/entrypoints/cli.tsx`,输出 `dist/cli.js` + chunk files。Build 默认启用 19 个 feature见下方 Feature Flag 段)。构建后自动替换 `import.meta.require` 为 Node.js 兼容版本(产物 bun/node 都可运行)。构建时会将 `vendor/audio-capture/``src/utils/vendor/ripgrep/` 复制到 `dist/vendor/` 下。
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`chunk 输出到 `dist/chunks/`。post-build 同样复制 vendor 文件到 `dist/vendor/`
- **Vendor 路径解析**: 构建后 chunk 文件位于 `dist/``dist/chunks/`vendor 二进制在 `dist/vendor/``src/utils/ripgrep.ts``packages/audio-capture-napi/src/index.ts` 均通过 `import.meta.url` 路径中 `lastIndexOf('dist')` 定位 dist 根目录,再拼接 `vendor/` 子路径,确保不同构建产物层级下路径一致。
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`

View File

@@ -75,10 +75,14 @@ console.log(
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
)
// Step 4: Copy native .node addon files (audio-capture)
const vendorDir = join(outdir, 'vendor', 'audio-capture')
await cp('vendor/audio-capture', vendorDir, { recursive: true })
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
// Step 4: Copy native .node addon files (audio-capture) and vendored binaries (ripgrep)
const audioCaptureDir = join(outdir, 'vendor', 'audio-capture')
await cp('vendor/audio-capture', audioCaptureDir, { recursive: true })
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`)
const ripgrepDir = join(outdir, 'vendor', 'ripgrep')
await cp('src/utils/vendor/ripgrep', ripgrepDir, { recursive: true })
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`)
// Step 5: Generate cli-bun and cli-node executable entry points
const cliBun = join(outdir, 'cli-bun.js')

View File

@@ -1,10 +1,33 @@
import { createRequire } from 'node:module'
import { dirname, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
// createRequire works in both Bun and Node.js ESM contexts.
// Needed because this package is "type": "module" but uses require() for
// loading native .node addons — bare require is not available in Node.js ESM.
const nodeRequire = createRequire(import.meta.url)
/**
* Resolve the "vendor root" directory where native .node binaries live.
*
* - Dev mode: import.meta.url → packages/audio-capture-napi/src/index.ts
* → vendor root = <project>/vendor/
* - Bun build: import.meta.url → dist/chunk-xxx.js
* → vendor root = <project>/dist/vendor/
* - Vite build: import.meta.url → dist/chunks/chunk-xxx.js
* → vendor root = <project>/dist/vendor/
*/
function getVendorRoot(): string {
const filePath = fileURLToPath(import.meta.url)
const dir = dirname(filePath)
const parts = dir.split(sep)
const distIdx = parts.lastIndexOf('dist')
if (distIdx !== -1) {
return parts.slice(0, distIdx + 1).join(sep) + sep + 'vendor'
}
// Dev mode — go up from packages/audio-capture-napi/src/ to project root
return resolve(dir, '..', '..', '..', 'vendor')
}
type AudioCaptureNapi = {
startRecording(
onData: (data: Buffer) => void,
@@ -56,15 +79,18 @@ function loadModule(): AudioCaptureNapi | null {
}
}
// Candidates 2-4: npm-install, dev/source, and workspace layouts.
// In bundled output, require() resolves relative to cli.js at the package root.
// In dev, it resolves relative to this file. When loaded from a workspace
// package (packages/audio-capture-napi/src/), we need an absolute path fallback.
// Candidates 2-5: resolved vendor path + relative fallbacks.
// The primary candidate uses getVendorRoot() to find the correct dist root
// regardless of chunk nesting depth. Relative fallbacks cover edge cases.
const platformDir = `${process.arch}-${platform}`
const binaryRel = `audio-capture/${platformDir}/audio-capture.node`
const vendorRoot = getVendorRoot()
const fallbacks = [
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
`../audio-capture/${platformDir}/audio-capture.node`,
`${process.cwd()}/vendor/audio-capture/${platformDir}/audio-capture.node`,
resolve(vendorRoot, binaryRel),
`./vendor/${binaryRel}`,
`../vendor/${binaryRel}`,
`../../vendor/${binaryRel}`,
`${process.cwd()}/vendor/${binaryRel}`,
]
for (const p of fallbacks) {
try {

View File

@@ -36,9 +36,13 @@ async function postBuild() {
}
// Step 2: Copy native addon files
const vendorDir = join(outdir, "vendor", "audio-capture");
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
const audioCaptureDir = join(outdir, "vendor", "audio-capture");
await cp("vendor/audio-capture", audioCaptureDir, { recursive: true } as never);
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`);
const ripgrepDir = join(outdir, "vendor", "ripgrep");
await cp("src/utils/vendor/ripgrep", ripgrepDir, { recursive: true } as never);
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`);
// Step 3: Generate dual entry points
const cliBun = join(outdir, "cli-bun.js");

View File

@@ -16,10 +16,24 @@ import { countCharInString } from './stringUtils.js'
const __filename = fileURLToPath(import.meta.url)
// we use node:path.join instead of node:url.resolve because the former doesn't encode spaces
const __dirname = path.join(
__filename,
process.env.NODE_ENV === 'test' ? '../../../' : '../',
)
// In dev mode: __filename = <root>/src/utils/ripgrep.ts → __dirname = <root>/src/utils/
// In built mode (bun): __filename = <root>/dist/chunk-xxx.js → need <root>/dist/
// In built mode (vite): __filename = <root>/dist/chunks/chunk-xxx.js → need <root>/dist/
// Both built modes: the dist root is at <root>/dist/ where dist/vendor/ripgrep/ lives.
const __dirname = (() => {
const dir = path.dirname(__filename)
// Test mode: from src/utils/ → project root
if (process.env.NODE_ENV === 'test') return path.resolve(dir, '../../../')
// Check if we're inside a dist directory at any depth
// (dist/ or dist/chunks/) — vendor lives at <dist-root>/vendor/ripgrep/
const parts = dir.split(path.sep)
const distIdx = parts.lastIndexOf('dist')
if (distIdx !== -1) {
return parts.slice(0, distIdx + 1).join(path.sep)
}
// Dev mode: from src/utils/ → src/utils/
return dir
})()
type RipgrepConfig = {
mode: 'system' | 'builtin' | 'embedded'