DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

ts-node ESM 找不到相对模块:导入应写输出文件的 .js 扩展名

ts-node 已加载 ESM 入口却找不到相对模块时,核对显式扩展名、Node 模块解析与源文件映射;开发运行和编译产物分别验证。

给 package.json 加上 type: module,再用 ts-node --esm 启动 TypeScript,入口终于能加载,却接着报 Cannot find module 或 ERR_MODULE_NOT_FOUND,错误指向自己写的 ./module。如果目标是同目录的 module.ts,先核对相对导入是否缺少 ESM 要求的扩展名。

这里处理的是 ts-node 执行原生 ESM 时的相对路径规则。入口本身还报 ERR_UNKNOWN_FILE_EXTENSION 时,应先检查 ESM loader 与版本兼容性;已经进入导入解析后,继续添加 loader 不一定能修正缺少扩展名的路径。

源文件是 .ts,导入可以写 .js

Node 原生 ESM 的相对或绝对导入要求完整文件扩展名,不会按 CommonJS 的方式自动尝试多个后缀或目录 index 文件。package.json 的 type: module 不会取消这条规则。

TypeScript 编译到 JavaScript 时,应让导入路径指向预期输出。下面是两个局部源文件的示例,假设项目按普通 .ts 到 .js 的方式输出:

// src/module.ts
export function myLog() {
  console.log("module loaded");
}

// src/main.ts
import { myLog } from "./module.js";
myLog();

源目录里不需要为了这条 import 额外手写一份 module.js。适当的 TypeScript Node 模块解析会把这个输出路径对应到源 module.ts;ts-node 的相关解析钩子负责开发执行时的源文件映射。

目录导入也应写完整目标,例如 ./helpers/index.js,而不是仅写 ./helpers。包名称及包的 exports 子路径属于另一套入口契约,不能给所有依赖包名一律加上 .js。

让编译器规则与运行入口一致

以下配置片段用于 TypeScript 4.7 起的 Node16 模式及支持相应选项的 ts-node 项目,需合并到现有配置,保留其他字段:

{
  "compilerOptions": {
    "module": "Node16",
    "moduleResolution": "Node16"
  },
  "ts-node": {
    "esm": true,
    "experimentalResolver": true
  }
}

package.json 还需按该项目的 ESM 设计声明 type: module。experimentalResolver 用于映射 .js 到 .ts 等源文件扩展名;这是带版本边界的 ts-node 功能,不应在不支持该选项的旧版上盲目添加。项目采用其他 Node 模块模式时,应核对对应 TypeScript 与 ts-node 组合,不把 Node16 配置当成所有项目的统一设置。

在项目实际环境查看已安装的工具版本和有效配置:

ts-node -vv
ts-node --showConfig

使用项目已有的本地命令入口,避免全局 ts-node 与本地 TypeScript 混用。配置位置、--project 和环境变量覆盖都可能让运行时读取另一份 tsconfig;只改编辑器打开的文件不代表启动命令已经采用它。

确认实际版本兼容以后,再由原入口启动。例如现有项目脚本使用 ts-node CLI 时,可保持 --esm 或采用配置中的 esm: true。ts-node 原生 ESM 依赖 Node loader 接口,接口变化可能影响不同版本组合,不能保证仅增加 --esm 就适用于所有新 Node 版本;正式服务应按项目部署流程验证,不能据开发脚本恢复就直接替换生产入口。

不用忽略类型错误掩盖路径问题

在 TypeScript 4.7 的传统输出场景中,把导入改为 ./module.ts 可能触发 An import path cannot end with a '.ts' extension。用 @ts-ignore 隐藏错误,不能说明输出后的 JavaScript 可以在 Node 中加载该路径。

更新 TypeScript 存在允许或重写 TypeScript 扩展名的专门模式,这些模式有各自的输出条件。若项目没有明确采用它们,应保留与实际 .js 输出一致的导入,不为消除一个诊断随意混入另一种运行契约。

也不要仅为恢复无后缀导入切换 package.json 的 type。那会改变项目文件的模块解释方式,影响 require、import、配置文件与依赖行为;确需迁移 CommonJS 时,应作为完整模块方案调整。

同时检查开发执行和编译产物

保存包清单、锁文件、tsconfig 与原导入代码,在开发环境先执行原来失败的入口,确认同一相对模块加载成功,再按项目正常流程检查类型并构建。

核对输出目录中的 main.js 仍导入实际存在的 module.js,再检查编译产物的运行入口。ts-node 能执行源文件,不代表部署包包含对应产物或使用相同模块规则。未解决的路径别名也需另查,baseUrl、paths 不能仅靠这次扩展名修正就保证原生 Node 支持。

回退时一起恢复模块配置与导入写法,避免留下新配置配旧路径的组合;构建产物按正常构建流程重新生成,不以旧缓存中偶然存在的文件作为恢复依据。