למה אי אפשר לייבא ES Module ממודול CommonJS ?

17/02/2023

אם תנסו לשלב קוד Node.JS שמשתמש ב ESM עם קוד שמשתמש ב require (מה שנקרא CommonJS), תגלו שהשילוב עובד ב 3 מתוך 4 אפשרויות:

1. מה עובד ומה לא

  1. מודול ES יכול לעשות import כדי לייבא קוד ממודול CommonJS

  2. מודול ES לעשות import כדי לייבא קוד ממודול ES אחר

  3. מודול CommonJS יכול לעשות import כדי לייבא קוד ממודול CommonJS אחר

  4. מודול CommonJS שינסה לעשות require למשהו שמיוצא ממודול ES יקבל שגיאה.

בשביל לראות את זה ניצור פרויקט חדש, ניצור קובץ בשם utils.mjs עם התוכן הבא:

export function twice(x) {
  return x * 2;
}

וקובץ בשם main.js עם התוכן הבא:

const { twice } = require('./utils.mjs');

console.log(twice(10));

נפעיל עם:

$ node main.js

ונקבל את השגיאה:

node:internal/modules/cjs/loader:1087
    throw new ERR_REQUIRE_ESM(filename, true);
    ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/ynonp/tmp/node/modules/utils.mjs not supported.
Instead change the require of /Users/ynonp/tmp/node/modules/utils.mjs to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/Users/ynonp/tmp/node/modules/main.js:1:19) {
  code: 'ERR_REQUIRE_ESM'
}

Node.js v18.14.0

2. למה טעינה כזו לא נתמכת?

אז הסיפור הוא כנראה לא שמישהו ב node.js התעצל, אלא שסט הפיצ'רים של ES Modules קצת שונה מזה של CommonJS. ההבדלים המרכזיים הם:

  1. ב ES Module אנחנו יודעים רק מלהסתכל על הקוד איזה שמות מיוצאים ממנו. ב CommonJS חייבים להריץ כדי לראות מה יהיו הערכים על אוביקט ה exports.

  2. ב ES Module אני יכול לכתוב await מחוץ לכל פונקציה, מה שיגרום לטעינה אסינכרונית של המודול. ב CommonJS הטעינה תמיד סינכרונית.

בקישור הזה יש דיון מאוד מעניין על האפשרות להוסיף await למודולי CommonJS ולמה היא כנראה תשבור הכל: https://github.com/nodejs/node/issues/21267

3. מה בכל זאת אפשר לעשות

אם הבעיה היא התמיכה המובלעת בקוד אסינכרוני, אז הפיתרון הפשוט הוא להפוך את המובלע למפורש. וזה מה שגם עובד ב Node.JS. הפונקציה import שמחזירה Promise למודול (נקראת גם Dynamic Import) מאפשרת לנו לייבא קובץ ES module מתוך קובץ CommonJS. אני מעדכן את הקוד ב main.js לקוד הבא:

async function main() {
  const { twice } = await import('./utils.mjs');
  console.log(twice(10));
}

main();

וה import מצליח לטעון את המודול בלי שום שינוי בקוד המודול utils.mjs.