构建工具打包 Node 原生模块的踩坑记录

2026-03-22 20:00:00 构建工具 438 words 3 min read

起因

有个项目是使用 Babel 作为 TypeScript AST 解析器,但 Babel 依赖多、类型支持也不好,后来换成了 oxc-parser。虽说代码能跑,但构建打包后运行就报错找不到 native binding。

这个问题不限于 oxc-parser,任何带 .node 原生二进制的模块(swc等)在做 bundle 时都会遇到。

问题本质

这类原生模块安装后,会根据当前平台下载对应的 binding 包,比如 Windows x64 对应 @xxx/binding-win32-x64-msvc,里面有个 .node 二进制文件:

          node_modules/@oxc-parser/binding-win32-x64-msvc/
  ├── package.json
  ├── parser.win32-x64-msvc.node   ← 原生二进制
  └── README.md

        

bundler(esbuild、rollup、tsdown 等)没法把 .node 文件内联到 JS bundle 里。构建后 JS 代码打包到了 dist/,但 .node 文件还留在 node_modules 中,运行时自然找不到。

解法方案

解决方法很简单,把当前平台对应的 .node 文件拷到 dist 目录就行了。以 tsdown 为例:

tsdown.config.ts
          export default defineConfig({
  copy: [
    {
      from: ['./node_modules/@oxc-parser/binding-win32-x64-msvc/parser.win32-x64-msvc.node'],
      to: './dist'
    }
  ]
})

        

这里只针对了当前设备的平台,如果需要支持多平台分发,得把各平台的 .node 文件都拷进去,或者在 CI 里按目标平台分别构建。

为什么放到 dist 就能被找到

看一下这类模块内部的加载逻辑(以 oxc-parser 的 bindings.js 为例),简化后大概是这样:

bindings.js
          const require = createRequire(import.meta.url)

function requireNative() {
  if (process.platform === 'win32' && process.arch === 'x64') {
    // 第一步:尝试从同目录加载 .node 文件
    try {
      return require('./parser.win32-x64-msvc.node')
    } catch (e) {
      loadErrors.push(e)
    }
    // 第二步:fallback 到对应的 binding 包
    try {
      return require('@oxc-parser/binding-win32-x64-msvc')
    } catch (e) {
      loadErrors.push(e)
    }
  }
  // 其他平台同理...
}

        

关键在 createRequire(import.meta.url),它的 require 基于当前文件所在目录解析相对路径。bundle 后这段代码被内联到了 dist/index.cjs,所以 ./parser.win32-x64-msvc.node 会从 dist/ 目录去找。把 .node 文件拷到 dist,刚好能被第一步命中。


构建工具打包 Node 原生模块的踩坑记录
http://localhost/articles/bundling-native-modules-in-nodejs
作者
sunshj
发布于
2026-03-22 20:00
许可
使用 tsdown 构建 vue 组件库
tsdown 内置 Vue 支持,让您能够使用现代 TypeScript 工具链打包 Vue 组件并生成类型声明
© 2021-2026 sunshj's Blog