使用bundler resolution构建正常但运行报错ERR_MODULE_NOT_FOUND
将项目切换为bundler模块解析策略后,tsc构建可正常完成,但启动Node.js应用时出现Error [ERR_MODULE_NOT_FOUND],提示无法找到模块/home/node/app/dist/websocket/config/setup。已配置TypeScript采用bundler resolution,按规则无需添加文件扩展名,现分析问题原因及解决方法。
报错日志
node:internal/errors:496 │ ErrorCaptureStackTrace(err); │ ^ │ │ Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/node/app/dist/websocket/config/setup' imported from /home/node/app/dist/app.js │ at new NodeError (node:internal/errors:405:5) │ at finalizeResolution (node:internal/modules/esm/resolve:327:11) │ at moduleResolve (node:internal/modules/esm/resolve:980:10) │ at defaultResolve (node:internal/modules/esm/resolve:1206:11) │ at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:404:12) │ at ModuleLoader.resolve (node:internal/modules/esm/loader:373:25) │ at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:250:38) │ at ModuleWrap.<anonymous> (node:internal/modules/esm/module_job:76:39) │ at link (node:internal/modules/esm/module_job:75:36) { │ url: 'file:///home/node/app/dist/websocket/config/setup', │ code: 'ERR_MODULE_NOT_FOUND' │ } │ │ Node.js v18.20.6
tsconfig.json
{ "compilerOptions": { "target": "ES2020", "module": "esnext", "moduleResolution": "bundler", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, }, "include": ["src/**/*"], "files": ["src/app.ts"] }
package.json
{ "name": "texhub-broadcast", "version": "1.0.19", "description": "", "main": "./dist/app.js", "module": "./dist/app.js", "types": "./dist/app.d.ts", "type":"module", "exports": { "./dist/websocket/conn/socket_io_client_provider": "./dist/websocket/conn/socket_io_client_provider.js", ".": { "import": "./dist/app.js" } }, "files": [ "dist/*" ], "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "lint": "eslint --fix", "dev": "vite-node src/app.ts", "build": "npx tsc", "dist": "npx tsc" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "dotenv": "^16.4.7", "express": "^4.21.2", "flatted": "^3.3.2", "globals": "^15.14.0", "lib0": "^0.2.99", "lodash": "^4.17.21", "log4js": "^6.9.1", "meilisearch": "^0.35.1", "prom-client": "^14.2.0", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", "ws": "^8.18.0", "y-leveldb": "^0.1.2", "y-protocols": "^1.0.6", "y-websocket": "^1.5.0", "yjs": "^13.6.23" }, "devDependencies": { "@types/express": "^5.0.0", "@types/lodash": "^4.17.15", "@types/node": "^22.12.0", "@types/ws": "^8.5.14", "@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/parser": "^8.22.0", "eslint": "^9.19.0", "typescript": "^5.7.3", "vite": "^6.0.11", "vite-node": "^3.0.4", "vitest": "^3.0.4" } }
导入代码
import { setupWSConnection } from "./websocket/config/setup";
目录结构
├── dist │ ├── websocket | |__ config | |_ setup.js │ └── app.js ├── package-lock.json ├── package.json ├── src │ ├── websoccket | |_ config | |_ setup.ts │ └── app.ts └── tsconfig.json
问题原因
1. Node.js ESM 不兼容 bundler resolution 规则
TypeScript 的moduleResolution: "bundler"是为 Vite、Webpack 等打包工具设计的,这类工具会自动处理无扩展名的导入逻辑。但 Node.js 原生 ESM 要求导入必须明确指定文件扩展名(如.js),tsc 编译后生成的导入语句仍然保留无扩展名的写法,导致 Node.js 无法匹配到实际的.js文件。
2. 目录拼写错误(潜在问题)
从目录结构看,src下的目录名为websoccket(多了一个c),但导入路径写的是./websocket/config/setup。虽然当前构建未报错,但这属于路径不匹配的隐患,可能导致后续维护或构建异常。
解决方法
方法一:切换到 Node.js 兼容的模块解析策略
修改tsconfig.json中的module和moduleResolution配置,让 TypeScript 生成符合 Node.js ESM 规则的代码(自动添加扩展名):
{ "compilerOptions": { "module": "NodeNext", // 或 Node16 "moduleResolution": "NodeNext", // 与module配置保持一致 // 其他原有配置不变 } }
重新执行npm run build后,生成的dist/app.js中的导入语句会自动追加.js扩展名,Node.js 可正常解析文件。
方法二:使用 Vite 打包替代纯 tsc 编译
利用项目已依赖的 Vite 处理 bundler 规则的导入,生成可直接运行的产物:
- 在项目根目录创建
vite.config.ts:
import { defineConfig } from 'vite'; export default defineConfig({ build: { outDir: 'dist', lib: { entry: 'src/app.ts', formats: ['es'], fileName: 'app' }, rollupOptions: { // 排除依赖包,避免打包进产物 external: Object.keys(require('./package.json').dependencies) } } });
- 修改
package.json的build脚本:
"scripts": { "build": "vite build", // 其他脚本保持不变 }
执行npm run build后,Vite 会自动处理无扩展名导入的问题,生成符合 Node.js 运行要求的产物。
方法三:手动添加导入扩展名
如果坚持使用bundler解析策略,可手动给所有导入语句添加.js扩展名(注意是编译后的.js,而非源文件的.ts):
import { setupWSConnection } from "./websocket/config/setup.js";
这种方法虽繁琐,但可让 Node.js 直接运行 tsc 编译后的代码。
额外修复:目录拼写错误
将src下的websoccket目录重命名为websocket,保持导入路径与实际目录结构一致,消除路径匹配隐患。
内容的提问来源于stack exchange,提问作者Dolphin

