我有一个基于 TypeScript 的项目,使用 Mocha 来运行测试用例,一直都正常工资,直到我将项目改成了 ESM 模块,然后就报错了。
在一番搜索之后,我发现了解决方案,记录一下。
TLDR:
使用 tsx 来替代 ts-node 作为 TypeScript 的运行时,就可以解决所有问题,而且性能更好。
现在让我们一步一步的来看看如何解决这个问题。
在开始之前,我们也许要声明一下,以下的所有问题和解决方案,都是基于这些相关库的特定版本的,在未来的版本中,可能会有所不同。
{
"mocha": "^10.1.0",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"tsx": "^4.7.1"
}
官方初始示例
Mocha 的官方仓库里面提供了一个项目支持 Typescript 的示例:https://github.com/mochajs/mocha-examples/tree/master/packages/typescript
这个示例实现了 CommonJs 下的 TypeScript 支持,示例里面的 .mocharc.json
配置如下:
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": "ts-node/register"
}
require
配置了 ts-node/register
,这个配置是用来在运行测试用例的时候,在 nodejs 调用时执行类似 node -r ts-node/register
的命令。从而让 ts-node 扩展了 nodejs 的模块加载器,使得 nodejs 可以直接加载 TypeScript 文件。
node -r <module>
是 nodejs 的一个命令行参数,用来在启动 nodejs 时加载一个模块。
通过这种方式,我们可以直接在 nodejs 中加载 TypeScript 文件,而不需要先编译成 JavaScript 文件。
这是我基于官方示例配置的一个样例项目:https://stackblitz.com/edit/node-qidqck?file=src%2Fbasic.ts&view=editor
但是上述有一个问题就是不支持我们在 tsconfig.json
里面配置的 paths
,会报类似 Error: Cannot find module ‘@utils/add’
的错误。
因为我们的 ts-node/register
只是扩展了 nodejs 的模块加载起,使得 TypeScript 文件在引用时,会被 ts-node 编译成 JavaScript 文件,然后再加载。但是并没有处理 import 的路径
这也是为什么错误是 Cannot find module
的原因了。
这个时候我们就需要 https://www.npmjs.com/package/tsconfig-paths 来做这一个功能。
tsconfig-paths
我们调整刚刚的 .mocharc.json
配置,改成如下:
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": ["ts-node/register", "tsconfig-paths/register"]
}
require
可以是一个 string[]
,这样我们就可以在启动 nodejs 时,先加载 ts-node/register
,然后再加载 tsconfig-paths/register
。
tsconfig-paths/register
会读取 tsconfig.json
里面的 paths
配置,然后在加载模块时,会根据 paths
配置来解析模块的路径。
同样,我也搭建了一个样例项目:https://stackblitz.com/edit/node-wnq7ku?file=.mocharc.json&view=editor
ESM
但是,当我们的项目启用了 ESM 模块时,又有新的错误发生了:TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for
为什么 .ts
后缀的文件会无法识别呢?
因为开启 esm 之后,ts-node/register
就无法正常工作了,这个时候需要使用 ts-node/esm
来替代 ts-node/register
。
如果我们的项目是 Typescript + ESM 的话,我们就可以用如下配置:
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"loader": "ts-node/esm"
}
这里面 loader
,是等同于在 nodejs 启动时,执行 node --loader ts-node/esm
或者 node --import ts-node/esm
的效果。
但是我们还是遇到最开始的问题,ts-node
无法处理 paths
的问题。
只是这次我们遇到另外一个问题,tsconfig-paths/register
也无法正常工作了。tsconfig-paths/register
目前只支持 CommonJs 模块。
事情到这一步似乎陷入僵局了。
tsx
这个时候,我们的 tsx 出场了。
tsx
是对 nodejs 的强化,使之可以支持 ESM 模块,同时也支持 TypeScript。
按照官方的说明,它有这些特性:
- Blazing fast on-demand TypeScript & ESM compilation
- Works in both CommonJS and ESM packages
- Supports next-gen TypeScript extensions (.cts & .mts)
- Hides experimental feature warnings
- TypeScript REPL
- Resolves tsconfig.json paths
这就很好的满足了我们在 ESM 模块下,同时支持 TypeScript 和 paths
的需求。
基于 tsx
,我们的 mocha
配置就可以改成如下:
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"import": "tsx"
}
同样的,这里提供了一个样例项目:https://stackblitz.com/edit/node-xexp1j?file=.mocharc.json&view=editor
但是上述配置适用于Node.js v20.6.0及以上版本,如果你的项目需要支持更低版本的 Node.js 那么配置需要调整为:
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"loader": "tsx"
}
原因是在 Node.js v20.6.0 之后,Node.js 新增了 --import
参数用来在 Node.js 启动前配置 register
。