使用ESM+nodemon启动Node.js应用遇ERR_MODULE_NOT_FOUND报错求助
报错信息
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'app.js' imported from /Users/oumarmauret/Documents/AkalanSchool/models/index.js
models/index.js代码内容
import fs from 'fs'; import path from 'path'; import { Sequelize, DataTypes } from 'sequelize'; const __filename = path.basename(import.meta.url); const __dirname = path.dirname(__filename); const basename = path.basename(__filename); const env = process.env.NODE_ENV || 'development'; import configJson from '../config/config.json' assert { type: 'json' }; const config = configJson[env]; const db = {}; let sequelize; if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable], config); } else { sequelize = new Sequelize( config.database, config.username, config.password, config ); } fs.readdirSync(__dirname) .filter((file) => { return ( file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js' && file.indexOf('.test.js') === -1 ); }) .forEach(async (file) => { const { default: model } = await import(path.join(__dirname, file)); db[model.name] = model(sequelize, DataTypes); }); Object.keys(db).forEach((modelName) => { if (db[modelName].associate) { db[modelName].associate(db); } }); db.sequelize = sequelize; db.Sequelize = Sequelize; export default db;
app.js代码内容
import express from 'express'; import bodyParser from 'body-parser'; import rateLimit from 'express-rate-limit'; import helmet from 'helmet'; import morgan from 'morgan'; import cors from 'cors'; import cookieParser from 'cookie-parser'; import AppError from './utils/appError.js'; import globalErrorHandler from './controllers/errorController.js'; import path from 'path'; import { getCurrentUser } from './controllers/googleAuth.js'; import { getFacebookUserData } from './controllers/facebookAuth.js'; import session from 'express-session'; import xss from 'xss-clean'; import { Sequelize } from 'sequelize'; import config from './config/config.json' assert { type: 'json' }; const app = express(); // Import des routes import adminRoutes from './routes/admin.routes.js'; import oauthRoutes from './routes/oauth.routes.js'; import categoriesRoutes from './routes/categories.routes.js'; // Ajout de cette ligne import downloadRoutes from './routes/courseDownload.routes.js'; import coursesRoutes from './routes/course.routes.js'; import moduleRoutes from './routes/module.routes.js'; import LessonRoutes from './routes/lesson.routes.js'; import QuizRoutes from './routes/quiz.routes.js'; import QuestionRoutes from './routes/question.routes.js'; import AnswerRoutes from './routes/answer.route.js'; import userRoutes from './routes/user.routes.js'; import reviewRoutes from './routes/review.routes.js'; import statsRoutes from './routes/enrolment.routes.js'; app.enable('trust proxy'); app.set('views', path.join(import.meta.url, './views')); // 1) GLOBAL MIDDLEWARES // Set security HTTP headers app.use(helmet()); app.use( cors({ credentials: true, }) ); app.use(express.static(path.join(import.meta.url, 'views'))); app.use(express.static(path.join(import.meta.url, 'public'))); app.get('/', (req, res) => { res.render('signin.ejs'); }); // Limit requests from same API const limiter = rateLimit({ max: 5, windowMs: 60 * 60 * 1000, //one hour message: 'Trop de demandes provenant de cette IP, veuillez réessayer dans une heure !', }); app.use('/api/', limiter); // Body parser, reading data from body into req.body app.use(express.json({ limit: '10kb' })); app.use(express.urlencoded({ extended: true, limit: '10kb' })); app.use(cookieParser()); app.use( session({ secret: 'Keep it secret', resave: false, saveUninitialized: false, cookie: { secure: false }, }) ); app.get('/myaccount', getCurrentUser, async (req, res) => { try { const fullname = res.locals.user.fullname; const message = `Hello ${fullname} (Connected via Google)`; res.send(message); } catch (error) { console.error('Error fetching user data:', error.message); res.status(500).send(error.message); } }); app.get('/myprofile', getFacebookUserData, async (req, res) => { try { const fullname = res.locals.user.fullname; const message = `Hello ${fullname} (Connected via Facebook)`; res.send(message); } catch (error) { console.error('Error fetching user data:', error.message); res.status(500).send(error.message); } }); //Data sanitization against SQL query injection //Data sanitization against XSS app.use(xss()); // Initialize Sequelize const sequelize = new Sequelize( config[process.env.NODE_ENV || 'development'].database, config[process.env.NODE_ENV || 'development'].username, config[process.env.NODE_ENV || 'development'].password, config[process.env.NODE_ENV || 'development'] ); // Check database connection async function sequelizeDatabaseConnect() { try { await sequelize.authenticate(); console.log('Connexion à la base de données réussie.'); } catch (error) { console.error('Erreur de connexion à la base de données :', error); } } sequelizeDatabaseConnect(); if (process.env.NODE_ENV === 'development') { app.use(morgan('dev')); } app.use('/uploads', express.static(path.join(import.meta.url, 'uploads'))); app.use(cors()); app.use((req, res, next) => { req.requestTime = new Date().toISOString(); next(); }); // Routes app.use('/api/v1/courses', categoriesRoutes); app.use('/api/v1/courses/downloads', downloadRoutes); app.use('/api/v1/courses', coursesRoutes); app.use('/api/v1/courses', LessonRoutes); app.use('/api/v1/courses', moduleRoutes); app.use('/api/v1/courses', QuizRoutes); app.use('/api/v1/courses', QuestionRoutes); app.use('/api/v1/courses', AnswerRoutes); app.use('/api/v1/admins', adminRoutes); app.use('/api/v1/auth', oauthRoutes); app.use('/api/v1/users', userRoutes); app.use('/api/v1/stats', statsRoutes); app.use('/api/v1/reviews', reviewRoutes); app.all('*', (req, res, next) => { next( new AppError(`Impossible de trouver ${req.originalUrl} sur ce serveur`, 404) ); }); app.use(globalErrorHandler); export default app;
package.json配置内容
{ "name": "akalanschool", "version": "1.0.0", "description": "", "main": "server.js", "type": "module", "scripts": { "start": "nodemon --experimental-modules server.js", "start:prod": "NODE_ENV=production nodemon server.js" }, "keywords": [], "author": "Oumar MAURET", "license": "ISC", "dependencies": { "axios": "^1.6.5", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "cookie-parser": "^1.4.6", "cors": "^2.8.5", "dotenv": "^16.3.1", "ejs": "^3.1.9", "express": "^4.18.2", "express-rate-limit": "^7.1.5", "express-session": "^1.17.3", "firebase": "^10.8.0", "helmet": "^7.1.0", "html-to-text": "^9.0.5", "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "mysql": "^2.18.1", "mysql2": "^3.6.5", "nanoid": "^5.0.5", "nodemailer": "^6.9.7", "nodemon": "^3.0.1", "querystring": "^0.2.1", "sequelize": "^6.35.0", "sharp": "^0.33.2", "xss-clean": "^0.1.4" } }
问题描述
使用ECMAScript Modules(ESM)结合nodemon运行Node.js应用时出现上述报错,已尝试重启服务器、修改启动脚本为nodemon --experimental-modules server.js,但问题仍未解决,怀疑与nodemon适配ESM的配置有关,请求分析报错原因并提供解决方案。
报错原因分析
__dirname计算错误:在
models/index.js中,原代码通过path.basename(import.meta.url)获取__filename,再用path.dirname(__filename)得到__dirname,但import.meta.url是带file://前缀的URL,path.basename处理后仅得到文件名(如index.js),path.dirname对文件名处理会返回.(当前工作目录,即项目根目录)。这导致fs.readdirSync(__dirname)读取了项目根目录下的所有文件(包括app.js),后续尝试导入app.js时,因为路径没有./前缀,ESM会将其当作npm包查找,从而抛出ERR_MODULE_NOT_FOUND错误。启动脚本冗余参数:Node.js 14.13.0及以上版本已默认支持ESM,
--experimental-modules参数已无需使用,冗余参数不会导致报错,但属于无效配置。
解决方案
修正__dirname的正确获取方式:
在models/index.js顶部替换原__filename和__dirname的计算代码,使用ESM标准的文件路径处理方式:import { fileURLToPath } from 'url'; import path from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename);此方式会将
import.meta.url转换为本地文件路径,确保__dirname指向models目录,fs.readdirSync只会读取该目录下的模型文件,不会误读根目录的app.js。优化nodemon启动脚本:
修改package.json中的scripts字段,移除冗余参数,生产环境无需使用nodemon:"scripts": { "start": "nodemon server.js", "start:prod": "NODE_ENV=production node server.js" }验证server.js的导入路径:
确保server.js中正确导入app.js,路径需带相对前缀:import app from './app.js'; const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server running on port ${port}`); });
内容的提问来源于stack exchange,提问作者Oumar MAURET

