Skip to content

Commit a8ed0cd

Browse files
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>
1 parent 1c3b280 commit a8ed0cd

5 files changed

Lines changed: 70 additions & 20 deletions

File tree

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ bun run docs:dev
7676
### Runtime & Build
7777

7878
- **Runtime**: Bun (not Node.js). All imports, builds, and execution use Bun APIs.
79-
- **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 都可运行)。
79+
- **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/` 下。
80+
- **Build (Vite)**: `vite.config.ts` + `scripts/post-build.ts`,chunk 输出到 `dist/chunks/`。post-build 同样复制 vendor 文件到 `dist/vendor/`
81+
- **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/` 子路径,确保不同构建产物层级下路径一致。
8082
- **Dev mode**: `scripts/dev.ts` 通过 Bun `-d` flag 注入 `MACRO.*` defines,运行 `src/entrypoints/cli.tsx`。默认启用全部 feature。
8183
- **Module system**: ESM (`"type": "module"`), TSX with `react-jsx` transform.
8284
- **Monorepo**: Bun workspaces — 15 个 workspace packages + 若干辅助目录 in `packages/` resolved via `workspace:*`

build.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,14 @@ console.log(
7575
`Bundled ${result.outputs.length} files to ${outdir}/ (patched ${patched} for import.meta.require, ${bunPatched} for Bun destructure)`,
7676
)
7777

78-
// Step 4: Copy native .node addon files (audio-capture)
79-
const vendorDir = join(outdir, 'vendor', 'audio-capture')
80-
await cp('vendor/audio-capture', vendorDir, { recursive: true })
81-
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`)
78+
// Step 4: Copy native .node addon files (audio-capture) and vendored binaries (ripgrep)
79+
const audioCaptureDir = join(outdir, 'vendor', 'audio-capture')
80+
await cp('vendor/audio-capture', audioCaptureDir, { recursive: true })
81+
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`)
82+
83+
const ripgrepDir = join(outdir, 'vendor', 'ripgrep')
84+
await cp('src/utils/vendor/ripgrep', ripgrepDir, { recursive: true })
85+
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`)
8286

8387
// Step 5: Generate cli-bun and cli-node executable entry points
8488
const cliBun = join(outdir, 'cli-bun.js')

packages/audio-capture-napi/src/index.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
11
import { createRequire } from 'node:module'
2-
2+
import { dirname, resolve, sep } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
34
// createRequire works in both Bun and Node.js ESM contexts.
45
// Needed because this package is "type": "module" but uses require() for
56
// loading native .node addons — bare require is not available in Node.js ESM.
67
const nodeRequire = createRequire(import.meta.url)
78

9+
/**
10+
* Resolve the "vendor root" directory where native .node binaries live.
11+
*
12+
* - Dev mode: import.meta.url → packages/audio-capture-napi/src/index.ts
13+
* → vendor root = <project>/vendor/
14+
* - Bun build: import.meta.url → dist/chunk-xxx.js
15+
* → vendor root = <project>/dist/vendor/
16+
* - Vite build: import.meta.url → dist/chunks/chunk-xxx.js
17+
* → vendor root = <project>/dist/vendor/
18+
*/
19+
function getVendorRoot(): string {
20+
const filePath = fileURLToPath(import.meta.url)
21+
const dir = dirname(filePath)
22+
const parts = dir.split(sep)
23+
const distIdx = parts.lastIndexOf('dist')
24+
if (distIdx !== -1) {
25+
return parts.slice(0, distIdx + 1).join(sep) + sep + 'vendor'
26+
}
27+
// Dev mode — go up from packages/audio-capture-napi/src/ to project root
28+
return resolve(dir, '..', '..', '..', 'vendor')
29+
}
30+
831
type AudioCaptureNapi = {
932
startRecording(
1033
onData: (data: Buffer) => void,
@@ -56,15 +79,18 @@ function loadModule(): AudioCaptureNapi | null {
5679
}
5780
}
5881

59-
// Candidates 2-4: npm-install, dev/source, and workspace layouts.
60-
// In bundled output, require() resolves relative to cli.js at the package root.
61-
// In dev, it resolves relative to this file. When loaded from a workspace
62-
// package (packages/audio-capture-napi/src/), we need an absolute path fallback.
82+
// Candidates 2-5: resolved vendor path + relative fallbacks.
83+
// The primary candidate uses getVendorRoot() to find the correct dist root
84+
// regardless of chunk nesting depth. Relative fallbacks cover edge cases.
6385
const platformDir = `${process.arch}-${platform}`
86+
const binaryRel = `audio-capture/${platformDir}/audio-capture.node`
87+
const vendorRoot = getVendorRoot()
6488
const fallbacks = [
65-
`./vendor/audio-capture/${platformDir}/audio-capture.node`,
66-
`../audio-capture/${platformDir}/audio-capture.node`,
67-
`${process.cwd()}/vendor/audio-capture/${platformDir}/audio-capture.node`,
89+
resolve(vendorRoot, binaryRel),
90+
`./vendor/${binaryRel}`,
91+
`../vendor/${binaryRel}`,
92+
`../../vendor/${binaryRel}`,
93+
`${process.cwd()}/vendor/${binaryRel}`,
6894
]
6995
for (const p of fallbacks) {
7096
try {

scripts/post-build.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,13 @@ async function postBuild() {
3636
}
3737

3838
// Step 2: Copy native addon files
39-
const vendorDir = join(outdir, "vendor", "audio-capture");
40-
await cp("vendor/audio-capture", vendorDir, { recursive: true } as never);
41-
console.log(`Copied vendor/audio-capture/ → ${vendorDir}/`);
39+
const audioCaptureDir = join(outdir, "vendor", "audio-capture");
40+
await cp("vendor/audio-capture", audioCaptureDir, { recursive: true } as never);
41+
console.log(`Copied vendor/audio-capture/ → ${audioCaptureDir}/`);
42+
43+
const ripgrepDir = join(outdir, "vendor", "ripgrep");
44+
await cp("src/utils/vendor/ripgrep", ripgrepDir, { recursive: true } as never);
45+
console.log(`Copied src/utils/vendor/ripgrep/ → ${ripgrepDir}/`);
4246

4347
// Step 3: Generate dual entry points
4448
const cliBun = join(outdir, "cli-bun.js");

src/utils/ripgrep.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,24 @@ import { countCharInString } from './stringUtils.js'
1616

1717
const __filename = fileURLToPath(import.meta.url)
1818
// we use node:path.join instead of node:url.resolve because the former doesn't encode spaces
19-
const __dirname = path.join(
20-
__filename,
21-
process.env.NODE_ENV === 'test' ? '../../../' : '../',
22-
)
19+
// In dev mode: __filename = <root>/src/utils/ripgrep.ts → __dirname = <root>/src/utils/
20+
// In built mode (bun): __filename = <root>/dist/chunk-xxx.js → need <root>/dist/
21+
// In built mode (vite): __filename = <root>/dist/chunks/chunk-xxx.js → need <root>/dist/
22+
// Both built modes: the dist root is at <root>/dist/ where dist/vendor/ripgrep/ lives.
23+
const __dirname = (() => {
24+
const dir = path.dirname(__filename)
25+
// Test mode: from src/utils/ → project root
26+
if (process.env.NODE_ENV === 'test') return path.resolve(dir, '../../../')
27+
// Check if we're inside a dist directory at any depth
28+
// (dist/ or dist/chunks/) — vendor lives at <dist-root>/vendor/ripgrep/
29+
const parts = dir.split(path.sep)
30+
const distIdx = parts.lastIndexOf('dist')
31+
if (distIdx !== -1) {
32+
return parts.slice(0, distIdx + 1).join(path.sep)
33+
}
34+
// Dev mode: from src/utils/ → src/utils/
35+
return dir
36+
})()
2337

2438
type RipgrepConfig = {
2539
mode: 'system' | 'builtin' | 'embedded'

0 commit comments

Comments
 (0)