2025-07-25
记一次 esbuild-register + npx 的踩坑

前言

之前常用 ts-node 或者 sucrase 作为 Typescript 运行库。某一次在配置 Rsbuild 时在无意之间在 Rsbuild 官网文档发现了 esbuild-register 这个工具之后简直如获至宝。

首先它不像 ts-node 那样非常的慢且需要配置正确的 TSConfig 才能正常工作。而 sucrase 是另一个极端,它支持的语法太少了,尤其是不支持装饰器是一大问题。

esbuild-register 在开箱即用、运行效率、可配置三个方面达到了完美的平衡:

#!/usr/bin/env node
require('esbuild-register');
require('./index.ts');

坑点

在为 SDK 编写脚手架时,引入 esbuild-register 可以去掉需要将 TS 构建为 JS 的步骤,化简之后的设计如下:

# 目录结构
├ src/
│ └─ cli.ts
├ cli.js
├ package.json
// cli.js
#!/usr/bin/env node
require('esbuild-register');
require('./src/cli.ts');
// package.json
{
  "bin": {
    "create-app": "cli.js"
  }
}

在脚手架项目中运行 node ./cli.js 或者 npx create-app都没有任何问题,直到我将它发布到 npm 上然后尝试下载安装:

npx @coze-workflow/create-sdk@latest

然后就报错了!

解决的方法是需要关闭 hookIgnoreNodeModules 配置:

require('esbuild-register/dist/node').register({
  hookIgnoreNodeModules: false,
});

这并非是 esbuild 内部的配置而是描述 esbuild-register 行为的配置。目前还没有任何文档有详细的说明,从源码的注释解释是自动忽略对 node_modules 下文件的翻译

然后再回过头看报错信息就恍然大悟了,cli.ts 文件被过滤掉了。

不过这属于马后炮了,实际上我先入为主认为是需要额外的 tsconfig 配置(因为 ts-node 会因为 tsconfig 原因报类似的错误),这花费了我大量的时间,究其原因还是我因为不熟悉 npx 的原理。

npx 原理

npx 也是一个 npm 包,从 node16 开始整合进 npm 中。它按照如下优先级执行:

package.json 中的 bin 字段
node_modules/.bin 文件夹中的脚本
远程的 npm 包 package.json 的 bin 字段

其中 node_modules/.bin中的脚本是在安装依赖的时候,npm 会逐个为依赖的 package.json 的 bin 字段指向的文件创建软连接(symbolic link)。

而执行远程的包是基于上述原理进一步封装,npx 会在创建一个缓存文件夹,路径如 ~/.npm/_npx/abcde

这个缓存文件夹同时是一个“虚拟包”,它包含 package.json、package-lock.json 和 node_modules。这个“虚拟包”有且只有一个 dependencies 就是需要被下载安装的包。

创建“虚拟包”这种形式可以最大程度保证 npm 的一致性,譬如被下载的包的依赖树也会遵循规则被下载。

最后 npx 使用 child_process.spawn 执行文件,并设置 cwd 指向当前的工作目录。使得远程包和本地包使用起来没有太多区别。

所以 npx 并不是直接执行脚手架包的 bin 文件,而是让脚手架作为依赖被执。esbuild-register 的 hookIgnoreNodeModules 字段默认为 true,导致自己把自己给过滤掉了。

还是让我们期待一下 Running TypeScript code with Node.js 的普及吧!