From a8ed0cdce5042cdb7c804db508b35914eeb3b424 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sat, 25 Apr 2026 14:46:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=90=8E=20vendor=20=E4=BA=8C=E8=BF=9B=E5=88=B6=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF=EF=BC=88ripgrep/a?= =?UTF-8?q?udio-capture=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 构建后 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 --- CLAUDE.md | 4 ++- build.ts | 12 ++++--- packages/audio-capture-napi/src/index.ts | 42 +++++++++++++++++++----- scripts/post-build.ts | 10 ++++-- src/utils/ripgrep.ts | 22 ++++++++++--- 5 files changed, 70 insertions(+), 20 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 60437057f..5781152f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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:*`。 diff --git a/build.ts b/build.ts index 6a4c9321e..c54d09260 100644 --- a/build.ts +++ b/build.ts @@ -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') diff --git a/packages/audio-capture-napi/src/index.ts b/packages/audio-capture-napi/src/index.ts index 78b161604..b59199fec 100644 --- a/packages/audio-capture-napi/src/index.ts +++ b/packages/audio-capture-napi/src/index.ts @@ -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 = /vendor/ + * - Bun build: import.meta.url → dist/chunk-xxx.js + * → vendor root = /dist/vendor/ + * - Vite build: import.meta.url → dist/chunks/chunk-xxx.js + * → vendor root = /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 { diff --git a/scripts/post-build.ts b/scripts/post-build.ts index 132f8c2e6..73a3b466e 100644 --- a/scripts/post-build.ts +++ b/scripts/post-build.ts @@ -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"); diff --git a/src/utils/ripgrep.ts b/src/utils/ripgrep.ts index 683da0516..28b5af425 100644 --- a/src/utils/ripgrep.ts +++ b/src/utils/ripgrep.ts @@ -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 = /src/utils/ripgrep.ts → __dirname = /src/utils/ +// In built mode (bun): __filename = /dist/chunk-xxx.js → need /dist/ +// In built mode (vite): __filename = /dist/chunks/chunk-xxx.js → need /dist/ +// Both built modes: the dist root is at /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 /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'