সরাসরি পর্বে যান
পর্ব ৪৫ · ৫৯-এর মধ্যে · মডিউল ৭

module — import/export, ES module বনাম CommonJS

JavaScript Modules — Named & Default Exports, Live Bindings, the Module Graph, Dynamic import(), Import Maps, CommonJS and Node.js Interop

Taught in Bangla, this lesson explains how JavaScript code is split into files: the history from globals and IIFEs to CommonJS and AMD, named and default exports, how ES modules are parsed, linked and evaluated as a graph, live bindings, dynamic import, import.meta and top-level await, modules in the browser with import maps, and CommonJS interop in Node.js.

এই পর্বে যা শিখবেন

  • বড় প্রোগ্রামে কেন এক ফাইল আর global ভ্যারিয়েবল ভেঙে পড়ে, আর module-এর আগে মানুষ কী করত (IIFE, CommonJS, AMD, UMD)
  • named আর default export, নাম বদল, re-export আর barrel file, import-এর চার রূপ — আর অনেক দল কেন named export পছন্দ করে
  • ব্রাউজার বা Node কীভাবে module graph বানায়, link করে আর পাতা থেকে শুরু করে চালায়; live binding, একবার চলা আর circular dependency
  • import() দিয়ে lazy loading, import.meta, top-level await, ব্রাউজারের type="module", import map আর modulepreload
  • CommonJS-এর require, module.exports বনাম exports-এর ফাঁদ, Node-এর .mjs/.cjs, ESM-CJS interop, আর AI-এর লেখা module কোড যাচাই

১ · এক ফাইল থেকে অনেক ফাইল — module কেন দরকার

শুরুতে প্রোগ্রাম ছোট থাকে। একটি app.js, তাতে পঞ্চাশ লাইন — দাম হিসাব, কার্টে যোগ, পাতায় দেখানো। ছয় মাস পর সেই অনলাইন দোকানের ফাইলটি দুই হাজার লাইন। তিনজন ডেভেলপার একই ফাইলে কাজ করেন, প্রতিদিন merge conflict. কেউ একটি total ভ্যারিয়েবল বানালেন, অন্য কারও লেখা আরেকটি total চুপচাপ সেটিকে মুছে দিল। কোন function কোনটির ওপর নির্ভর করে, কেউ নিশ্চিত নন — তাই কিছু মুছতে সবাই ভয় পান।

সমাধান হলো কোড ভাগ করা — কিন্তু শুধু আলাদা ফাইলে রাখলেই হয় না। ফাইলগুলোর মধ্যে তিনটি নিয়ম চাই: প্রতিটি ফাইলের নিজের গোপন জায়গা থাকবে; কোন জিনিস বাইরে দেওয়া হবে তা স্পষ্ট করে লেখা থাকবে; আর কোন ফাইল কার কাছ থেকে কী নেয়, তাও লেখা থাকবে। এই তিন নিয়ম মেনে চলা ফাইলকেই বলে module.

একটি garments কারখানা ভাবুন। Cutting, sewing আর finishing — তিনটি আলাদা সেকশন। প্রতিটির ভেতরে নিজের কাঁচি, মেশিন আর হিসাবের খাতা; অন্য সেকশনের কেউ সেগুলো ছোঁয় না। সেকশনগুলো জিনিস আদান-প্রদান করে শুধু একটি নির্দিষ্ট কাউন্টার দিয়ে, চালানসহ: “cutting দিচ্ছে ৫০০ পিস কাটা কাপড়।” Sewing জানে সে কী পাবে আর কার কাছ থেকে। কাউন্টারে যা দেওয়া হয় তা export, যা নেওয়া হয় তা import, আর চালানের খাতাটিই নির্ভরতার মানচিত্র। নতুন সুপারভাইজার এসে চালান দেখেই বুঝে নেন কারখানা কীভাবে চলে।

Module ব্যবস্থা থেকে চারটি বড় সুবিধা আসে:

🔒 নিজের scope

প্রতিটি ফাইলের top level-এর নাম শুধু সেই ফাইলের (module scope). দুই ফাইলে দুটি total থাকলেও কোনো সংঘর্ষ নেই।

🧭 স্পষ্ট নির্ভরতা

ফাইলের মাথায় import-এর তালিকা দেখেই বোঝা যায় সে কার ওপর নির্ভর করে — মানুষও বোঝে, টুলও বোঝে।

✂️ অব্যবহৃত কোড বাদ

কোন export কেউ ব্যবহার করে না, টুল তা হিসাব করে চূড়ান্ত ফাইল থেকে ফেলে দিতে পারে — ব্যবহারকারী কম ডেটা নামান।

♻️ পুনর্ব্যবহার

টাকা ফরম্যাট করার একটি module একবার লিখে দোকানের সাইট, অ্যাডমিন প্যানেল আর সার্ভার — তিন জায়গায় ব্যবহার করা যায়।

JavaScript-এ আজ দুটি module ব্যবস্থা পাশাপাশি চলে, আর পেশাদার কাজে দুটোই চিনতে হয়। একটি ভাষার নিজের, ES2015 (ES6) থেকে মানদণ্ডে: ES module, সংক্ষেপে ESM — import আর export. ব্রাউজার আর Node দুই জায়গাতেই চলে, আর নতুন কোডের জন্য এটিই পথ। অন্যটি Node.js-এর পুরোনো ব্যবস্থা CommonJS (CJS) — require() আর module.exports. npm-এর লাখো package আর বহু পুরোনো সার্ভার কোড এখনো এতে লেখা। এই পর্বের বড় অংশ এই দুইয়ের পার্থক্য আর মিলন নিয়ে।

২ · module আসার আগে — global, script-এর ক্রম, IIFE, CommonJS, AMD

১৯৯৫ সালে JavaScript এসেছিল পাতায় ছোট ছোট কাজ করতে — একটি বোতাম চাপলে বার্তা দেখানো। Module-এর কথা কেউ ভাবেনি। একটি পাতায় কয়েকটি <script> থাকলে সবার top-level var আর function একটিই global জায়গায় পড়ত (পর্ব ০৭-এর global scope). এতে তিনটি সমস্যা:

  • নামের সংঘর্ষ: আপনার function init() আর একটি slider library-র function init() — যেটি পরে লোড হলো, সেটিই টিকল।
  • ক্রমের দাসত্ব: cart.js যদি price.js-এর function ব্যবহার করে, তবে HTML-এ price.js-এর <script> আগে বসাতে হবে। বিশটি ফাইলে এই ক্রম হাতে ঠিক রাখা ছিল দুঃস্বপ্ন — আর কোথাও লেখা থাকত না কে কার ওপর নির্ভর করে।
  • সবকিছু সবার: যেকোনো ফাইল যেকোনো global বদলাতে পারত; গোপন রাখার কোনো উপায় ছিল না।

এরপর প্রায় দুই দশক মানুষ নানা কৌশলে এই অভাব পূরণ করেছে। পর্ব ২২-এ দেখেছেন প্রথম কৌশলটি — পুরো ফাইলকে একটি function-এ মুড়ে সাথে সাথে ডাকা (IIFE), আর শুধু একটি object ফেরত দেওয়া (module pattern). এতে গোপনীয়তা এল, কিন্তু নির্ভরতা আর ক্রমের সমস্যা রয়ে গেল। পরের ধাপগুলো:

ব্যবস্থাকীভাবে দেখতেকোথায়, কেনদুর্বলতা
Global + script-এর ক্রমvar Shop = {}১৯৯৫ থেকে, সব জায়গায়সংঘর্ষ, ক্রম হাতে রাখা
IIFE / module patternvar Cart = (function(){ … return {add}; })();২০০০-এর দশক, jQuery-র যুগনির্ভরতা তবু অদৃশ্য
CommonJSconst p = require("./price");
module.exports = {…}
২০০৯, সার্ভারের জন্য; Node.js গ্রহণ করেsynchronous — ব্রাউজারে সরাসরি চলে না
AMD (RequireJS)define(["./price"], function (p) { … })২০১০-এর আশেপাশে, ব্রাউজারের জন্য asynchronousলেখা জটিল, অনেক বন্ধনী
UMDএকটি মোড়ক যা দেখে define আছে নাকি module আছেlibrary যাতে সব পরিবেশে চলেভাষার সমাধান নয়, জোড়াতালি
ES moduleimport { addVat } from "./price.js";ES2015-এ মানদণ্ড, ২০১৭–১৮ থেকে ব্রাউজারেশুরুর দিকে Node-এ আসতে দেরি, CJS-এর সাথে মেলানোর ঝামেলা

CommonJS-এর জন্ম ২০০৯ সালে, ব্রাউজারের বাইরে JavaScript চালানোর আন্দোলন থেকে। এর নিয়ম সহজ: require("./price") ডাকলে ফাইলটি পড়া হয়, চালানো হয়, আর তার module.exports ফেরত আসে — সাথে সাথে, synchronous ভাবে। সার্ভারে ফাইল লোকাল ডিস্কে থাকে, তাই এটি দ্রুত। কিন্তু ব্রাউজারে প্রতিটি ফাইল নেটওয়ার্ক থেকে আসে; অপেক্ষার সময় পুরো পাতা আটকে রাখা চলে না। তাই ব্রাউজারের জন্য এল asynchronous AMD, আর পরে Browserify (২০১১) ও webpack-এর মতো bundler — যারা আগেই সব CommonJS ফাইল জুড়ে একটি বড় ফাইল বানিয়ে দিত। দুই শিবিরের এই টানাপোড়েন ভাবনার প্রশ্ন ২-এ।

একটি মজার সত্য: Node-এর CommonJS আসলে module pattern-কেই যন্ত্রের হাতে তুলে দিয়েছে। Node প্রতিটি ফাইলকে গোপনে এই function-এ মুড়ে চালায় — (function (exports, require, module, __filename, __dirname) { …আপনার কোড… }). সেজন্যই CommonJS ফাইলের top-level var global-এ যায় না, আর require, module, __dirname কোথাও ঘোষণা না করেও ব্যবহার করা যায় — এগুলো আসলে ওই function-এর parameter.

৩ · ES module-এর ভাষা — export আর import

চলুন একটি অনলাইন দোকানকে চারটি module-এ ভাগ করি: price.js (ভ্যাট আর ছাড়ের হিসাব), format.js (টাকা দেখানো), cart.js (কার্ট) আর main.js (শুরুর ফাইল)।

Named export — নাম ধরে রপ্তানি

export const VAT_RATE = 0.15;                 // ১৫% ভ্যাট

export function addVat(taka) {
  return Math.round(taka * (1 + VAT_RATE));
}

function roundDown(n) { return Math.floor(n / 10) * 10; }   // export নেই — গোপন

export function discount(taka, percent) {
  return roundDown(taka * (1 - percent / 100));
}

যেকোনো ঘোষণার আগে export লিখলে সেটি named export — নিজের নামেই বাইরে যায়। roundDown-এর আগে export নেই, তাই অন্য কোনো ফাইল তাকে দেখতেই পায় না — IIFE ছাড়াই গোপনীয়তা। চাইলে সব ঘোষণা সাধারণভাবে লিখে ফাইলের শেষে একটি তালিকায়ও export করা যায়, আর as দিয়ে বাইরের নাম বদলানো যায়:

const bdt = new Intl.NumberFormat("bn-BD", { style: "currency", currency: "BDT" });

function formatTaka(n) { return bdt.format(n); }   // পর্ব ২০-এর Intl
function formatQty(n) { return `${n}টি`; }

export { formatTaka, formatQty as qty };         // export list, বাইরে নাম "qty"
export default formatTaka;                       // সাথে একটি default export

Default export — ফাইলের “প্রধান” জিনিস

প্রতিটি module-এ সর্বোচ্চ একটি default export থাকতে পারে। আমদানিকারী তাকে যেকোনো নামে ডাকতে পারে, বাঁকা বন্ধনী ছাড়াই। এবার ফাইলগুলো ব্যবহার করুন:

import { addVat, discount } from "./price.js";      // named import
import taka, { qty } from "./format.js";            // default + named একসাথে

const items = [];                                   // গোপন state

export function add(name, price) { items.push({ name, price }); }
export function summary(couponPercent = 0) {
  const sub = items.reduce((sum, i) => sum + i.price, 0);
  return `${qty(items.length)} পণ্য, মোট ${taka(addVat(discount(sub, couponPercent)))}`;
}
import * as cart from "./cart.js";                  // namespace import
import "./analytics.js";                            // শুধু চালানোর জন্য (side effect)

cart.add("চাল ৫ কেজি", 400);
cart.add("ডাল ১ কেজি", 140);
console.log(cart.summary(10));                      // ২টি পণ্য, মোট ৫৫২.০০৳

main.js-এ import-এর দুটি নতুন রূপ। import * as cart হলো namespace import — ফাইলের সব named export একটি object-এর মতো জিনিসে (module namespace object) একসাথে আসে, cart.add লিখে ব্যবহার করা যায়। import "./analytics.js" কিছু আনে না, শুধু ফাইলটিকে একবার চালায় — একে বলে side-effect import; polyfill বা global CSS লোডের মতো কাজে দেখবেন। হিসাবটি: ৪০০ + ১৪০ = ৫৪০, ১০% ছাড়ে ৪৮৬, দশকের ঘরে নামিয়ে ৪৮০, ১৫% ভ্যাটে ৫৫২. লক্ষ করুন, main.js জানেই না roundDown বলে কিছু আছে — হিসাবের খুঁটিনাটি price.js-এর ভেতরে লুকানো। format.js-এর Intl বাংলা অঙ্কে ৫৫২.০০৳ ধরনের লেখা দেয়; ঠিক রূপটি ব্রাউজারের ICU ডেটার ওপর নির্ভর করে।

Named না default — অনেক দল কেন named পছন্দ করে

Default export সুবিধাজনক মনে হয়, কিন্তু বড় প্রজেক্টে তিনটি সমস্যা আনে। এক, প্রতিটি আমদানিকারী নিজের ইচ্ছামতো নাম দেয় — একজন import taka, আরেকজন import formatMoney, তৃতীয়জন import fmt; পুরো কোডে একটি জিনিসের তিন নাম, খোঁজা কঠিন। দুই, editor-এর “সব জায়গায় নাম বদলাও” আর স্বয়ংক্রিয় import প্রস্তাব named export-এ বেশি নির্ভুল কাজ করে। তিন, ভুল নাম লিখলে named import-এ সাথে সাথে error, কিন্তু default-এ যেকোনো নামই “ঠিক”. তাই অনেক দলের নিয়ম: library আর utility-তে শুধু named export; default থাকবে কেবল যেখানে কোনো framework তা চায় (যেমন কিছু React বা Next.js রীতিতে পাতার component).

Re-export আর barrel file — সুবিধা ও দাম

অনেক ফাইলের export এক জায়গা থেকে দেওয়ার জন্য একটি ফাইল অন্যদের export আবার রপ্তানি করতে পারে — re-export:

export * from "./price.js";                        // সব named export আবার রপ্তানি
export { default as taka, qty } from "./format.js"; // বাছাই করে, নাম বদলে
export * as cart from "./cart.js";                 // namespace হিসেবে (ES2020)

এখন বাকি কোড শুধু লেখে import { addVat, taka } from "./shop/index.js". এমন সংগ্রাহক ফাইলকে বলে barrel file. ছোট, স্থির সংগ্রহে এটি পরিচ্ছন্ন। কিন্তু দামটি লুকানো: একটি জিনিস চাইলেও ব্রাউজার, Node বা test runner-কে barrel-এর পেছনের সব ফাইল খুঁজে, নামিয়ে, parse করতে হয়। শত শত ফাইলের barrel-এ development server ধীর হয়, test শুরু হতে দেরি হয়, circular dependency (অংশ ৫) সহজে তৈরি হয়, আর tree shaking-ও সবসময় সব বাদ দিতে পারে না। export *-এ দুটি ফাইলে একই নাম থাকলে সেই নামটি নিঃশব্দে বাদও পড়ে যায়। নিয়ম হিসেবে মনে রাখুন: একটি package-এর বাইরের দরজা হিসেবে ছোট barrel ঠিক আছে; পুরো প্রজেক্টের প্রতিটি ফোল্ডারে বিশাল index.js নয়।

৪ · module কীভাবে লোড হয় — static গঠন, graph, link, evaluate

ES module-এর সবচেয়ে গুরুত্বপূর্ণ বৈশিষ্ট্য: এটি static. import আর export শুধু ফাইলের top level-এ লেখা যায় — if-এর ভেতরে নয়, function-এর ভেতরে নয় — আর from-এর পরের ঠিকানা (module specifier) হতে হবে একটি সরাসরি লেখা string, ভ্যারিয়েবল নয়। এই কড়াকড়ির কারণ: কোনো কোড না চালিয়েই, শুধু পড়ে, ব্রাউজার বা টুল জেনে যায় কোন ফাইল কোন ফাইলের কোন নাম চায়। এই তথ্য দিয়ে তৈরি হয় module graph. নিচে main.js থেকে শুরু করে দোকানের চারটি ফাইল কীভাবে লোড হয়, ধাপে ধাপে দেখুন:

  1. main.jsimport cartশুরুর ফাইল
  2. cart.jsimport price, formatনামিয়ে parse
  3. price.js · format.jsকোনো import নেইএকসাথে নামানো, পাতা (leaf)
  4. Linkনাম ↔ bindingএখনো কোনো কোড চলেনি
  5. Evaluateprice → format → cart → mainপাতা থেকে শুরু
ধাপ ১ · main.js পড়া (parse)চালানো নয়, শুধু পড়া
import * as cart from "./cart.js";
import "./analytics.js";   // (সরলতার জন্য ছবিতে বাদ)
// ব্রাউজার: এই ফাইলের নির্ভরতা cart.js — এখনো একটি লাইনও চলেনি
// syntax error থাকলে এখানেই থামবে, কোনো ফাইলই চলবে না
ধাপ ২ · নির্ভরতা নামানোপাশাপাশি, একসাথে
GET /cart.js     → parse → import "./price.js", "./format.js"
// cart.js পড়েই জানা গেল আরও দুটি ফাইল লাগবে
// একটি URL = একটি module — অনেক ফাইল চাইলেও একবারই নামে
ধাপ ৩ · graph সম্পূর্ণ৪টি module, প্রতিটি একবার
GET /price.js  ┐ পাশাপাশি
GET /format.js ┘ → parse → কোনো import নেই
main.js ──► cart.js ──► price.js
               └──────► format.js
// price.js আর format.js — graph-এর "পাতা" (leaf)
ধাপ ৪ · link (instantiate)import আর export-এর তার জোড়া
cart.js-এর addVat  ──►  price.js-এর addVat (একই memory-ঘর)
cart.js-এর taka    ──►  format.js-এর default
// কেউ যদি লেখে import { addVAT } — বানান ভুল:
SyntaxError: The requested module './price.js' does not provide an export named 'addVAT'
ধাপ ৫ · evaluateপ্রতিটি ফাইলের top-level কোড একবার
১. price.js   VAT_RATE = 0.15, function-গুলো তৈরি
২. format.js  Intl.NumberFormat তৈরি
৩. cart.js    items = []
৪. main.js    cart.add(...) — এখন সব নির্ভরতা তৈরি
  1. Parse — শুধু পড়া। ব্রাউজার main.js নামিয়ে পুরোটা পড়ে, কিন্তু চালায় না। কারণ import-গুলো static, পড়েই সে জেনে যায় কোন কোন ফাইল লাগবে। কোনো ফাইলে syntax error থাকলে graph-এর একটি ফাইলও চলবে না।
  2. Fetch — নির্ভরতা নামানো। main.js-এর নির্ভরতা cart.js নামে, তারও parse — তাতে জানা যায় আরও দুটি ফাইল লাগবে: price.js আর format.js. একটি URL-এর module একবারই নামানো হয় — একাধিক ফাইল একই module চাইলেও অনুরোধ একটি।
  3. Graph সম্পূর্ণ। price.js আর format.js পাশাপাশি নামে; কোনোটিতে import নেই, তাই খোঁজা শেষ। এখন পুরো নির্ভরতার মানচিত্র জানা — কোনো কোড না চালিয়েই। এই তথ্যই bundler আর editor ব্যবহার করে।
  4. Link — নাম জোড়া। প্রতিটি import-কে রপ্তানিকারীর আসল binding-এর সাথে জোড়া হয় (কপি নয়, একই memory-ঘর — অংশ ৫)। যে নাম export-ই হয়নি, তার import এখানে ধরা পড়ে — SyntaxError, তখনো কোনো লাইন চলেনি। বার্তাটি Chrome/Edge-এর; Node-এ শুরুটা একই রকম।
  5. Evaluate — পাতা থেকে শিকড়ের দিকে। যার নির্ভরতা নেই সে আগে চলে: price.js, তারপর format.js, তারপর cart.js, সবশেষে main.js. ফলে কোনো ফাইল চলার সময় তার সব import আগেই তৈরি থাকে। প্রতিটি ফাইলের top-level কোড পুরো জীবনে একবারই চলে।

Module-এর ভেতরের চারটি নিয়ম

  • নিজের scope: top-level var, let, function — কোনোটিই window-এ বসে না। export না করলে বাইরে অদৃশ্য।
  • সবসময় strict mode: "use strict" লিখতে হয় না। ঘোষণা ছাড়া নামে মান বসালে ReferenceError (পর্ব ০৭-এর implicit global আর সম্ভব নয়)।
  • top-level this হলো undefined: classic script-এ এটি window, CommonJS-এ module.exports — ES module-এ কিছুই নয়।
  • Import hoisted: ফাইলের মাঝখানে import লিখলেও তা কোড চলার আগেই সম্পন্ন হয় (link ধাপে)। তাই ভালো অভ্যাস — সব import ফাইলের মাথায়।

একবারই চলে — তাই module নিজেই singleton

একটি module-কে দশটি ফাইল import করলেও তার top-level কোড একবারই চলে; সবাই একই binding আর একই object পায়। ব্রাউজার প্রতিটি পাতার জন্য URL ধরে একটি module map রাখে, Node রাখে নিজের cache. এর একটি চমৎকার ব্যবহার — অ্যাপের একটিমাত্র কনফিগারেশন object, যাকে বলে singleton:

console.log("config.js চলল");          // যতবারই import হোক, এক লাইন ছাপা হবে
export const config = {
  apiBase: "https://api.rahimstore.example",
  currency: "BDT",
  smsSender: "RAHIMSTORE",
};
// অন্য যেকোনো ফাইল: import { config } from "./config.js";  — সবাই একই object

সতর্কতা: “একবার” মানে একই URL-এর জন্য একবার। ./config.js আর ./config.js?v=2 ব্রাউজারের কাছে দুটি আলাদা module — দুটি আলাদা singleton. আর এই কারণে পরীক্ষার (test) সময় module-এর ভেতরের গোপন state এক test থেকে আরেক test-এ চলে যেতে পারে; পর্ব ৪৮-এ এর প্রতিকার দেখবেন।

Static মানে tree shaking — ESM বনাম CommonJS

Static গঠনের সবচেয়ে বড় পুরস্কার পান ব্যবহারকারী। bundler (পর্ব ৪৬-এ Vite) চূড়ান্ত ফাইল বানানোর সময় দেখে কোন export আসলে কেউ import করেছে; বাকিগুলো ফেলে দেয়। গাছ ঝাঁকিয়ে মরা পাতা ফেলার মতো — তাই নাম tree shaking. CommonJS-এ কেন এটি প্রায় অসম্ভব, দেখুন:

1// price.js — ৪টি export2export function addVat(t) { … }3export function discount(t, p) { … }4export function emi(t, months) { … }5export function bulkPrice(t, n) { … }6import { addVat } from "./price.js";   // ESM7const price = require("./price.cjs");  // CJS8price[userChoice](1000);               // কোনটি? চালানোর সময় জানা যাবে
bundler-এর হিসাব
জানা ব্যবহার
রাখা হবে
ফেলে দেওয়া
  1. শুরু। price.js-এ চারটি function, কিন্তু দোকানের পাতা আসলে শুধু addVat ব্যবহার করে। বাকিগুলো অন্য পাতার জন্য লেখা।
  2. ESM — পড়েই বোঝা যায়। import { addVat } একটি static ঘোষণা। bundler কোড না চালিয়েই জানে, এই ফাইল price.js থেকে কেবল একটি নাম চায়।
  3. বাকিগুলো বাদ। চূড়ান্ত bundle.js-এ থাকে শুধু addVat আর সে যা ব্যবহার করে। বাকি তিনটি “dead code” হিসেবে ফেলা হয়। শর্ত: ফাইলের top level-এ যেন এমন কোনো কাজ না থাকে যা বাদ দিলে আচরণ বদলায় (side effect); library-র লেখকেরা package.json-এ "sideEffects": false লিখে bundler-কে সেটি জানান।
  4. CJS — ফেরত আসে একটি সাধারণ object. require একটি সাধারণ function: একে if-এর ভেতরে ডাকা যায়, ঠিকানা ভ্যারিয়েবল হতে পারে, আর ফেরত object-এর যেকোনো property পরে price[userChoice] দিয়ে ছোঁয়া যায়। কোনটি লাগবে, তা জানা যাবে শুধু চালানোর সময়।
  5. সব রাখতে হয়। নিরাপদ থাকতে bundler পুরো module.exports রেখে দেয় — চারটি function-ই ব্যবহারকারীর ফোনে নামে। শিক্ষা: নতুন library আর ব্রাউজারের কোড ESM-এ লিখুন; কিছু bundler CJS-এর সহজ ক্ষেত্রে আংশিক বাদ দিতে পারে, কিন্তু ভরসা করার মতো নয়।

৫ · Live binding — import কপি নয়, জানালা

পর্ব ১৪-এ দেখেছেন, একটি ভ্যারিয়েবলে আরেকটির মান বসালে সংখ্যার মতো primitive-এর কপি হয়। অনেকে ধরে নেন import-ও তেমন — রপ্তানির মুহূর্তের মান কপি করে আনে। তা নয়। ES module-এর import হলো রপ্তানিকারীর আসল binding-এর দিকে খোলা একটি শুধু-পড়া জানালা। রপ্তানিকারী মান বদলালে জানালা দিয়ে নতুন মানই দেখা যায়; কিন্তু জানালা দিয়ে হাত ঢুকিয়ে মান বদলানো যায় না:

1// counter.js2export let count = 0;3export function increment() { count++; }4// main.js5import { count, increment } from "./counter.js";6console.log(count);7increment(); increment();8console.log(count);9count = 10;
memory
counter.js-এর count
main.js-এর count
  1. Link-এর পর। main.js-এর count কোনো নতুন ভ্যারিয়েবল নয় — counter.js-এর count-এর ঘরের দিকেই তাকিয়ে আছে। counter.js আগে চলেছে, তাই ঘরে 0.
  2. প্রথম log. জানালা দিয়ে দেখা মান 0.
  3. increment দুবার। function-টি counter.js-এর ভেতরে, তাই সে নিজের count বদলাতে পারে — এখন 2. main.js-এ কিছুই লেখা হয়নি।
  4. দ্বিতীয় log — 2, 0 নয়। কপি হলে 0 ছাপা হতো। এটিই live binding. CommonJS-এ const { count } = require(...) লিখলে কিন্তু কপি আসে — সেখানে 0 ছাপা হতো।
  5. বাইরে থেকে বদলানো নিষেধ। import করা নাম আমদানিকারীর কাছে const-এর মতো। Chrome/Edge আর Node-এর বার্তা: TypeError: Assignment to constant variable. মান বদলাতে হলে রপ্তানিকারী একটি function দেবে (যেমন increment) — বদলের নিয়ন্ত্রণ থাকে মালিকের হাতে।

খেয়াল করুন সূক্ষ্ম পার্থক্যটি: binding বদলানো নিষেধ, কিন্তু binding যে object-এর দিকে তাকিয়ে আছে তার ভেতরটা বদলানো যায়। import { config } from "./config.js"; config.currency = "USD"; — এটি চলবে, আর সব ফাইল নতুন মান দেখবে, কারণ সবাই একই object দেখছে। এটি সুবিধা আবার বিপদও; কেউ যেন ভুলে বদলাতে না পারে, সেজন্য Object.freeze(config) দিয়ে রপ্তানি করুন।

এবার সব কথা একসাথে, আসল Node.js 20-এ। নিচের প্রোগ্রাম নিজেই তিনটি ছোট module ফাইল লেখে — একটি ESM price.mjs, একটি ESM cart.mjs যে price আর একটি পুরোনো CommonJS SMS helper (sms.cjs) import করে — তারপর সেগুলো লোড করে:

"use strict";
const fs = require("node:fs");
fs.writeFileSync("price.mjs", `console.log("  (price.mjs চলল)");
export const VAT = 0.15;
export let sold = 0;
export function addVat(tk) { sold++; return Math.round(tk * (1 + VAT)); }
export default (n) => "৳" + n;`);
fs.writeFileSync("sms.cjs", `console.log("  (sms.cjs চলল)");
module.exports = { send: (to, text) => to + "-এ SMS: " + text };`);
fs.writeFileSync("cart.mjs", `import taka, { addVat, sold } from "./price.mjs";
import sms from "./sms.cjs";
export function checkout(items) {
  const total = items.reduce((sum, tk) => sum + addVat(tk), 0);
  console.log("  cart.mjs দেখছে sold =", sold);
  return sms.send("01711", "মোট " + taka(total));
}`);
(async () => {
  const cart = await import("./cart.mjs");
  const price = await import("./price.mjs");
  console.log(cart.checkout([1000, 250]));
  console.log("main দেখছে sold =", price.sold);
  try { price.sold = 99; } catch (e) { console.log(e.name + ":", e.message); }
  const viaRequire = require("./sms.cjs");
  console.log("require আর import একই object?", viaRequire === (await import("./sms.cjs")).default);
})();

আউটপুট লাইন ধরে: প্রথমে (price.mjs চলল) তারপর (sms.cjs চলল) — cart.mjs-এর নির্ভরতাগুলো তার আগে, import-এর ক্রমে চলেছে (পাতা থেকে শিকড়)। price.mjs-কে মূল ফাইল আবার import করলেও লাইনটি দ্বিতীয়বার ছাপা হয়নি — একবারই চলা। তারপর cart.mjs দেখছে sold = 2 আর 01711-এ SMS: মোট ৳1438 (১০০০-এ ভ্যাটসহ ১১৫০, ২৫০-এ ২৮৭.৫ থেকে ২৮৮). cart.mjs-এ sold import হয়েছিল যখন তা 0, তবু সে 2 দেখল — live binding. বাইরে থেকে namespace object-এ লেখার চেষ্টায় TypeError: Cannot assign to read only property 'sold' of object '[object Module]'. (প্রথম লাইনের "use strict" না থাকলে এই CommonJS ফাইলে লেখাটি চুপচাপ ব্যর্থ হতো, error ছাড়া।) শেষ লাইন true: ESM থেকে CommonJS import করলে default হিসেবে আসে ঠিক সেই module.exports object, যা require দেয় — দুই ব্যবস্থা একই cache ভাগ করে। লক্ষ করুন, এই ফাইলটি নিজে CommonJS, তাই top-level await লেখা যায়নি; একটি async IIFE লাগল।

Circular dependency — যখন দুই ফাইল একে অপরকে চায়

orders.js import করে customers.js, আর customers.js import করে orders.js — একে বলে circular dependency. ES module এটি অনুমতি দেয়, graph-এ চক্র থাকলে একটি ফাইল আরেকজনের আগে চলবেই। সমস্যা হয় যদি সেই আগের ফাইল top level-এ অন্যজনের এখনো-না-চলা const পড়ে:

1// a.js  (main এটিকে import করে)2import { b } from "./b.js";3export const a = "A";4console.log("a.js দেখছে", b);5// b.js6import { a } from "./a.js";7export const b = "B";8console.log("b.js দেখছে", a);
binding-এর অবস্থা
a
b
কে চলছে
  1. Graph-এ চক্র। main → a.js → b.js → a.js. a.js আগেই তালিকায় আছে, তাই আবার নামানো হয় না; link স্বাভাবিকভাবে হয়ে যায়। দুটি const-ই তৈরি, কিন্তু মানহীন — পর্ব ০৭-এর TDZ-তে।
  2. b.js আগে চলে। নিয়ম “নির্ভরতা আগে” — a.js-এর নির্ভরতা b.js, তাই b.js আগে। b.js-এর নিজের নির্ভরতা a.js “চলছে” অবস্থায় আছে, তাই তার জন্য অপেক্ষা না করেই b.js চলে। b = "B".
  3. a এখনো TDZ-তে। b.js top level-এ a পড়তে গেল, কিন্তু a.js-এর তিন নম্বর লাইন এখনো চলেনি। বার্তাটি Chrome/Edge আর Node-এ একই: ReferenceError: Cannot access 'a' before initialization. পুরো graph-এর evaluation এখানেই থেমে যায়।
  4. সমাধান। (১) দুজনের ভাগ করা জিনিস তৃতীয় একটি ফাইলে সরান — চক্রই ভেঙে যায়; এটিই সেরা। (২) অন্যের মান top level-এ না পড়ে একটি function-এর ভেতরে পড়ুন, যা পরে ডাকা হবে — তখন সবাই চলা শেষ। (export function ঘোষণা link-এর সময়েই তৈরি হয়ে যায় বলে চক্রেও প্রায়ই কাজ করে।) চক্র খুঁজতে madge-এর মতো টুল বা ESLint-এর import/no-cycle নিয়ম আছে।

৬ · import(), import.meta আর top-level await

Dynamic import() — দরকার হলে তবেই লোড

Static import-এর একটি অসুবিধা: graph-এর সব ফাইল শুরুতেই নামে। দোকানের পাতায় একটি “PDF ইনভয়েস নামান” বোতাম আছে; PDF বানানোর library ৩০০ KB, আর শতকরা পাঁচজনও বোতামটি চাপেন না। বাকি পঁচানব্বই জন কেন সেই ৩০০ KB নামাবেন? এর উত্তর dynamic import — import(specifier), যা function-এর মতো দেখতে কিন্তু একটি বিশেষ syntax (ES2020). এটি যেকোনো জায়গায় লেখা যায় — if-এর ভেতরে, event handler-এ — ঠিকানা ভ্যারিয়েবলও হতে পারে, আর ফেরত দেয় একটি Promise, যা module namespace object দিয়ে পূর্ণ হয়:

const btn = document.querySelector("#invoice-btn");
btn.addEventListener("click", async () => {
  btn.disabled = true;
  try {
    const { makeInvoicePdf } = await import("./invoice-pdf.js");   // প্রথম চাপে নামে
    makeInvoicePdf(currentOrder);
  } catch (err) {
    showToast("ইনভয়েস লোড হলো না — নেটওয়ার্ক দেখে আবার চেষ্টা করুন");
  } finally {
    btn.disabled = false;
  }
});

bundler এই import() দেখে invoice-pdf.js আর তার নির্ভরতাগুলোকে একটি আলাদা ছোট ফাইলে (chunk) রাখে — এর নাম code splitting. Route ধরে পাতা ভাগ করা (React-এর lazy), ভারী chart library শুধু রিপোর্ট পাতায়, বাংলা-ইংরেজি অনুবাদের ফাইল ভাষা বাছাইয়ের পর — সবই একই কৌশল। দ্বিতীয়বার চাপলে module আর নামে না, cache থেকে সাথে সাথে আসে। আর নেটওয়ার্কের যেকোনো কাজের মতো এটিও ব্যর্থ হতে পারে, তাই try/catch জরুরি (পর্ব ৩৪)।

import.meta — module নিজের সম্পর্কে জানে

import.meta (ES2020) প্রতিটি module-এর নিজের তথ্যের একটি object. সব জায়গায় আছে import.meta.url — এই module ফাইলের পূর্ণ ঠিকানা। এর সাহায্যে ফাইলের পাশে রাখা জিনিসের ঠিকানা বানানো যায়: new URL("./logo.png", import.meta.url). Node-এ CommonJS-এর __dirname আর __filename ES module-এ নেই; তার বদলে Node 20.11 আর 21.2 থেকে import.meta.dirname আর import.meta.filename. পুরোনো Node-এও চলবে এমন কোড চাইলে fileURLToPath(new URL(".", import.meta.url)). ব্রাউজারে import.meta.dirname নেই — এটি শুধু Node-এর। আর Vite-এর মতো টুল import.meta.env যোগ করে (পর্ব ৪৬) — সেটিও মানদণ্ডের অংশ নয়, টুলের।

Top-level await — module-এর মাথায় অপেক্ষা

ES2022 থেকে module-এর top level-এ, কোনো async function ছাড়াই, await লেখা যায় (top-level await, পর্ব ৩৪)। যেমন const settings = await loadSettings();. তখন যে module একে import করে, তার evaluation এই অপেক্ষা শেষ হওয়া পর্যন্ত থেমে থাকে — তাই একটি ধীর top-level await পুরো অ্যাপের শুরু আটকে দিতে পারে। এটি শুধু ES module-এ চলে; classic script আর CommonJS-এ SyntaxError.

এখন এই তিনটি জিনিস নিজে চালিয়ে দেখুন। নিচের কোড-বক্সটি নিজেই একটি module হিসেবে চলে (<script type="module">). অন্য ফাইল এখানে রাখা যায় না, তাই “ইনভয়েস” module-টি কোডের ভেতরেই একটি data: URL হিসেবে বানানো হয়েছে, তারপর import() দিয়ে লোড:

var shopName = "রহিম স্টোর";
console.log("top-level this:", this);
console.log("var কি window-এ গেল?", "shopName" in window);
try { undeclaredTotal = 5; } catch (e) { console.log("strict mode:", e.name); }

const invoiceSource = `
  console.log("  (invoice module এইমাত্র লোড হলো)");
  export const meta = import.meta.url.slice(0, 5);
  export default function makeInvoice(items) {
    const total = items.reduce((s, i) => s + i.qty * i.price, 0);
    return items.length + "টি লাইন, মোট ৳" + total;
  }`;
const url = "data:text/javascript;charset=utf-8," + encodeURIComponent(invoiceSource);

console.log("বোতাম চাপার আগে: invoice এখনো নামেনি");
const mod = await import(url);                       // top-level await
console.log(mod.default([{ qty: 2, price: 60 }, { qty: 1, price: 450 }]));
console.log("module-এর ঠিকানা শুরু:", mod.meta);
const again = await import(url);
console.log("দ্বিতীয় import — একই module?", again === mod);

প্রথম তিন লাইনে module-এর নিয়ম: top-level this হলো undefined; var shopName window-এ যায়নি (false); আর ঘোষণা ছাড়া undeclaredTotal = 5 লেখায় ReferenceError — strict mode, কোনো "use strict" ছাড়াই। তারপর “এখনো নামেনি” লাইন আগে, আর (invoice module এইমাত্র লোড হলো) আসে শুধু import()-এর পর — lazy loading. হিসাব: ২×৬০ + ৪৫০ = ৫৭০, তাই 2টি লাইন, মোট ৳570. import.meta.url-এর প্রথম পাঁচ অক্ষর data: — module জানে সে কোথা থেকে এসেছে। শেষ লাইন true: একই URL দ্বিতীয়বার import করলে একই namespace object, আর “লোড হলো” লাইন আর ছাপা হয়নি। বাস্তবে data: URL-এর বদলে থাকবে "./invoice-pdf.js" — বাকি সব একই।

৭ · ব্রাউজারে module — type="module", CORS, import map

<!-- module: নিজে থেকেই defer, strict, নিজের scope -->
<script type="module" src="/js/main.js"></script>

<!-- খুব পুরোনো ব্রাউজারের জন্য বিকল্প (আজ প্রায় অপ্রয়োজনীয়) -->
<script nomodule src="/js/legacy-bundle.js"></script>

<!-- আগেভাগে নামিয়ে parse করে রাখো -->
<link rel="modulepreload" href="/js/cart.js">
  • নিজে থেকেই deferred: type="module"-এর script HTML পড়া আটকায় না, পাশাপাশি নামে, আর HTML পড়া শেষ হলে লেখার ক্রমে চলে — পর্ব ০২-এর defer-এর মতো। আলাদা করে defer লিখতে হয় না। async দিলে নামানো শেষ হলেই চলে।
  • একবারই চলে: একই src-এর module পাতায় দুবার থাকলেও একবার চলে; classic script দুবারই চলত।
  • CORS লাগে: অন্য origin থেকে module আনলে সেই সার্ভারকে CORS header দিতে হয় (পর্ব ৪৩)। আর সার্ভারকে ফাইলটি JavaScript-এর MIME type (যেমন text/javascript) দিয়ে পাঠাতে হয় — ভুল type হলে ব্রাউজার চালাবে না।
  • ঠিকানায় extension লাগে: ব্রাউজার "./price" থেকে নিজে .js যোগ করে না — "./price.js" লিখতে হয়। bundler এই ছাড় দেয় বলে অনেকে অবাক হন।
  • nomodule: module বোঝে এমন ব্রাউজার এই script উপেক্ষা করে, পুরোনোগুলো চালায়। আজকের প্রায় সব ব্রাউজার module বোঝে, তাই নতুন প্রজেক্টে সাধারণত লাগে না।
  • modulepreload: graph-এর গভীরের ফাইলগুলো ব্রাউজার একে একে আবিষ্কার করে — main পড়ে cart, cart পড়ে price. এই “জলপ্রপাত” এড়াতে আগেভাগে বলে দেওয়া যায়; ব্রাউজার নামিয়ে parse করে রাখে।

file:// দিয়ে খুললে module চলবে না। ফোল্ডারে দুই ক্লিকে index.html খুললে ঠিকানা হয় file:///C:/shop/index.html. Module-এর জন্য ব্রাউজার CORS নিয়ম মানে, আর file:// পাতার origin হলো null — তাই Chrome/Edge console-এ দেখায় “Access to script at 'file:///…/main.js' from origin 'null' has been blocked by CORS policy”. সমাধান একটি লোকাল সার্ভার: VS Code-এর Live Server extension, অথবা টার্মিনালে npx serve বা python -m http.server, তারপর http://localhost:… খুলুন (পর্ব ০৩)।

Bare specifier আর import map

Node-এ আপনি লেখেন import confetti from "canvas-confetti" — শুধু package-এর নাম, কোনো ./ বা URL নেই। এমন ঠিকানাকে বলে bare specifier. Node জানে node_modules-এ খুঁজতে হবে, কিন্তু ব্রাউজার জানে না — Chrome/Edge বলে TypeError: Failed to resolve module specifier "canvas-confetti". Relative references must start with either "/", "./", or "../". সাধারণত bundler এই নামগুলোকে আসল ফাইলে বদলে দেয়। bundler ছাড়া পথ হলো import map — নাম থেকে URL-এর একটি তালিকা, পাতার মাথায়:

<script type="importmap">
{
  "imports": {
    "canvas-confetti": "https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.3/+esm",
    "@shop/": "/js/shop/"
  }
}
</script>
<script type="module">
  import confetti from "canvas-confetti";       // এখন ব্রাউজার জানে কোথায়
  import { addVat } from "@shop/price.js";      // → /js/shop/price.js
</script>

Import map প্রথম module script-এর আগে থাকতে হয়। এখন সব প্রধান ব্রাউজারে আছে (Safari-তে ২০২৩ সাল থেকে)। এর ওপর ভর করে “build ধাপ ছাড়াই ওয়েব অ্যাপ”-এর একটি আন্দোলন গড়ে উঠেছে — ভাবনার প্রশ্ন ৫. CDN থেকে কোড আনলে পর্ব ৪৩-এর supply-chain সতর্কতা আর নির্দিষ্ট সংস্করণ লেখার নিয়ম মনে রাখুন।

৮ · CommonJS আর Node-এর নিয়ম — require, .mjs/.cjs, interop

const https = require("node:https");          // যেকোনো জায়গায়, যেকোনো সময়
const path = require("node:path");

function send(to, text) { /* ... SMS gateway-তে অনুরোধ ... */ }
function isValidBdNumber(n) { return /^01[3-9]\d{8}$/.test(n); }

module.exports = { send, isValidBdNumber };   // যা ফেরত যাবে
console.log(__dirname, this === module.exports);   // ফোল্ডারের পথ, true
import https from "node:https";                // শুধু top level-এ
import path from "node:path";

export function send(to, text) { /* ... */ }
export function isValidBdNumber(n) { return /^01[3-9]\d{8}$/.test(n); }

console.log(import.meta.dirname, this);        // ফোল্ডারের পথ, undefined
বিষয়CommonJSES module
আমদানি / রপ্তানিrequire() / module.exportsimport / export
লোডsynchronous, চালানোর সময়, যেকোনো জায়গায়static, আগে graph, তারপর চালানো (asynchronous)
আমদানি করা মানফেরত object-এর কপি করা reference (destructure করলে মানের কপি)live binding, শুধু পড়া
Strict modeনিজে লিখতে হয়সবসময়
top-level thismodule.exportsundefined
ফাইলের পথ__dirname, __filenameimport.meta.url, import.meta.dirname
ঠিকানায় extensionবাদ দেওয়া যায় (require("./sms"))দিতে হয় ("./sms.js")
top-level awaitনেইআছে
Tree shakingকঠিনস্বাভাবিক
ব্রাউজারেbundler ছাড়া চলে নাসরাসরি চলে

module.exports বনাম exports — একটি পুরোনো ফাঁদ

Node প্রতিটি ফাইলের শুরুতে কার্যত করে let exports = module.exports = {}; — দুটি নাম, একই খালি object. আর require সবসময় ফেরত দেয় module.exports. তাই exports.send = send; চলে (একই object-এ property যোগ), কিন্তু exports = { send }; কাজ করে না — এটি শুধু স্থানীয় exports নামটিকে নতুন object-এর দিকে ঘুরিয়ে দেয়, module.exports সেই পুরোনো খালি {}-ই থাকে। ফলে require দেয় {}, আর sms.send হয় undefined. নিরাপদ নিয়ম: পুরো জিনিস দিতে চাইলে সবসময় module.exports = ….

CommonJS-এর cache-ও আছে: প্রথম require-এ ফাইল চলে, ফলটি require.cache-এ পূর্ণ পথ ধরে জমা থাকে, পরের সব require সেখান থেকে একই object পায়। Circular-এ CommonJS TDZ error দেয় না, বরং অসম্পূর্ণ module.exports ফেরত দেয় — যা পরে রহস্যময় undefined is not a function হয়ে দেখা দেয়।

Node কীভাবে ঠিক করে ফাইলটি ESM না CJS

ফাইলNode যেভাবে ধরে
.mjsসবসময় ES module
.cjsসবসময় CommonJS
.js, কাছের package.json-এ "type": "module"ES module
.js, "type" নেই বা "commonjs"CommonJS (ঐতিহাসিক ডিফল্ট)। Node 20-এ এমন ফাইলে import লিখলে: SyntaxError: Cannot use import statement outside a module. নতুন Node (22.7 থেকে) ফাইলে ESM syntax দেখলে নিজেই ESM হিসেবে চালানোর চেষ্টা করে, তবে এর ওপর ভরসা না করে "type" লিখে দিন।
{
  "name": "rahim-store",
  "type": "module",
  "exports": {
    ".": { "import": "./dist/index.js", "require": "./dist/index.cjs" },
    "./format": "./dist/format.js"
  }
}

"type": "module" দিলে প্রজেক্টের সব .js ESM; কোনো পুরোনো ফাইল CommonJS রাখতে চাইলে তার নাম .cjs. "exports" field বলে দেয় package-এর বাইরের দরজা কোনগুলো — কে import করলে কোন ফাইল, কে require করলে কোনটি, আর তালিকার বাইরের ভেতরের ফাইল বাইরে থেকে আমদানিই করা যায় না। package.json-এর বাকি ঘর আর npm-এর খুঁটিনাটি পর্ব ৪৬-এ।

ESM আর CJS-এর মিলন (interop)

  • ESM থেকে CJS আমদানি — সবসময় চলে। import sms from "./sms.cjs" দিলে module.exports আসে default হিসেবে (ওপরের Node উদাহরণে দেখেছেন)। Named import (import { send } from "./sms.cjs") Node চেষ্টা করে কোড বিশ্লেষণ করে দিতে; সরল module.exports = { … } বা exports.x =-এ সাধারণত চলে, জটিল ক্ষেত্রে নয় — তখন default নিয়ে destructure করুন।
  • CJS থেকে ESM — ঐতিহাসিকভাবে শুধু await import(). কারণ ESM-এ top-level await থাকতে পারে, আর synchronous require অপেক্ষা করতে পারে না। নতুন Node-এ require(esm) সম্ভব হয়েছে: Node 22.12 আর 23 থেকে flag ছাড়াই (শুরুতে experimental সতর্কবার্তাসহ), পরে 20.19-এও — শর্ত হলো সেই ESM graph-এ কোথাও top-level await থাকবে না; থাকলে ERR_REQUIRE_ASYNC_MODULE. পুরোনো Node 20-এ require করলে ERR_REQUIRE_ESM.
  • JSON module: ESM-এ import data from "./zones.json" with { type: "json" }; — import attributes আর JSON module ES2025-এ মানদণ্ডে এসেছে, Chrome/Edge আর নতুন Node-এ চলে। পুরোনো লেখায় assert { type: "json" } দেখবেন — সেটি পুরোনো খসড়া, এখন বাদ। CommonJS-এ সহজভাবে require("./zones.json").

প্রজেক্ট সাজানো — ধরন নয়, বৈশিষ্ট্য ধরে

নতুনরা প্রায়ই ফোল্ডার বানান ধরন ধরে: utils/, helpers/, services/ — কিছুদিন পর utils.js দুই হাজার লাইনের নতুন “এক ফাইল”. বড় প্রজেক্টে ভালো কাজ করে বৈশিষ্ট্য (feature) ধরে সাজানো: cart/ (cart.js, cart-view.js, cart.test.js), checkout/, products/, আর সত্যিই সবার কাজে লাগে এমন জিনিসের জন্য একটি ছোট shared/ (format.js, config.js). একটি feature-এর সব ফাইল পাশাপাশি, অন্য feature শুধু তার সামনের দরজা দিয়ে আসে, আর একটি feature মুছতে হলে একটি ফোল্ডার মুছলেই হয়।

C/C++/Python/Java জানলে

C-তে #include আসলে header ফাইলের লেখা হুবহু কপি করে বসায়, আর ফাইলগুলোর নাম জোড়ে linker; ফাইলের ভেতরে গোপন রাখতে static লাগে — JavaScript-এ উল্টো, export না করলেই গোপন (C-এর মডুলার প্রোগ্রাম)। C++20-এর export module/import ঠিক এই textual include-এর সমস্যা মেটাতে এসেছে (C++20/23). Python-এও প্রতিটি ফাইল module, প্রথম import-এ একবার চলে আর sys.modules-এ cache হয় — ESM-এর মতোই; কিন্তু Python-এর from price import sold নামটির তখনকার মান কপি করে, live binding নয় (Python-এর module ও package). Java-র package আর Java 9-এর JPMS-এর module-info.java-তে exports লেখার ধারণা ESM-এর “যা রপ্তানি নয় তা অদৃশ্য”-র কাছাকাছি (Java-র package ও module).

পরীক্ষা ও ইন্টারভিউতে
  • ES module আর CommonJS-এর মূল পার্থক্য? — ESM static (import/export, আগে graph, live binding, tree shaking, ব্রাউজারে চলে); CJS dynamic (require চালানোর সময়, synchronous, module.exports-এর object).
  • Named আর default export-এর পার্থক্য? — named অনেকগুলো থাকতে পারে, একই নামে বাঁকা বন্ধনীতে import; default একটিই, যেকোনো নামে বন্ধনী ছাড়া import.
  • Live binding কী? — import করা নাম রপ্তানিকারীর আসল binding দেখায়; সেখানে মান বদলালে আমদানিকারী নতুন মান দেখে, কিন্তু নিজে বদলাতে পারে না (TypeError).
  • exports = {…} কেন কাজ করে না? — require ফেরত দেয় module.exports; exports নামটি নতুন object-এ ঘোরালে module.exports বদলায় না।
  • Dynamic import কেন? — ভারী বা কম-ব্যবহৃত অংশ দরকারের সময় লোড (code splitting), Promise ফেরত দেয়, শর্ত বা ভ্যারিয়েবল ঠিকানাতেও চলে।

৯ · AI-এর যুগে — module কোডে AI-এর চেনা ভুল

AI-কে “একটি Node API বানিয়ে দাও” বললে সে প্রায়ই ইন্টারনেটের দশ বছরের লেখা মিশিয়ে ফেলে — কিছু ২০১৫-র CommonJS tutorial থেকে, কিছু ২০২৪-এর ESM docs থেকে। কোড দেখতে ঠিক, কিন্তু চালালেই প্রথম লাইনে error. সবচেয়ে বেশি দেখা ভুলগুলো:

AI যা প্রায়ই লেখেফলযা চাইবেন
এক ফাইলে import express from "express" আর const fs = require("fs")ESM হলে ReferenceError: require is not defined in ES module scope; CJS হলে import-এ SyntaxErrorপ্রজেক্টের জন্য একটি ব্যবস্থা — নতুন কাজে ESM
package.json-এ "type": "module" নেই, অথচ .js-এ importCannot use import statement outside a module"type": "module" বা .mjs
ESM ফাইলে __dirnameReferenceError: __dirname is not defined in ES module scopeimport.meta.dirname (Node 20.11+) বা fileURLToPath
সব ফাইলে export defaultএকই জিনিসের নানা নাম, ভুল import ধরা পড়ে নাnamed export; default শুধু framework চাইলে
প্রতিটি ফোল্ডারে export *-ভরা বিশাল index.jsধীর dev server ও test, circular import, নীরবে নাম বাদসরাসরি ফাইল থেকে import; barrel শুধু package-এর দরজায়
Node ESM-এ import "./db" (extension নেই)ERR_MODULE_NOT_FOUND"./db.js" — পুরো নাম
পুরোনো assert { type: "json" }, বা ধরে নেওয়া “require(esm) কোথাও চলে না/সব জায়গায় চলে”সংস্করণভেদে SyntaxError বা ভুল সিদ্ধান্তwith { type: "json" }; Node-এর সংস্করণ লিখে প্রশ্ন

AI-কে module-ভিত্তিক কোড লিখতে বললে পরিবেশ আর নিয়ম শুরুতেই বলে দিন:

আমি Node.js 22 LTS-এ "রহিম স্টোর" নামের একটি অনলাইন দোকানের backend লিখছি। package.json-এ "type": "module" আছে। নিচের শর্ত মেনে কোড দাও: ১. শুধু ES module (import/export) — কোথাও require বা module.exports নয়। একটি পুরোনো CommonJS ফাইল sms.cjs আছে; সেটি import sms from "./sms.cjs" দিয়ে ব্যবহার করবে। ২. শুধু named export; export default নয়। ৩. লোকাল import-এ পুরো extension (./price.js), আর Node-এর built-in module "node:" দিয়ে (node:fs, node:path). ৪. ফাইলের পথ লাগলে import.meta.dirname — __dirname নয়। ৫. ফোল্ডার feature ধরে: products/, cart/, orders/, আর shared/ (config.js singleton, format.js). কোনো বিশাল index.js barrel নয়। ৬. PDF ইনভয়েস বানানোর ভারী অংশটি await import() দিয়ে শুধু দরকারের সময় লোড করবে, try/catch সহ। ৭. JSON ফাইল পড়তে হলে import ... with { type: "json" } অথবা fs দিয়ে পড়া — assert নয়। শেষে লেখো: কোনো দুই ফাইলের মধ্যে circular import তৈরি হলো কি না, আর প্রতিটি ফাইল কোন ফাইল থেকে কী import করে তার একটি ছোট তালিকা।

AI-এর লেখা module কোড — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১JavaScript প্রায় বিশ বছর module ছাড়া চলল কীভাবে? ২০০৯ সালের CommonJS উদ্যোগ কোন শূন্যতা পূরণ করেছিল, আর Node.js তা গ্রহণ করায় কী বদলাল?

১৯৯৫ সালে Brendan Eich মাত্র কয়েক দিনে JavaScript-এর প্রথম রূপ বানান — উদ্দেশ্য ছিল ওয়েব পাতায় ছোট script, যা ডিজাইনার আর শখের প্রোগ্রামাররা লিখবেন। একটি পাতার script কয়েক ডজন লাইনের হবে, এটাই ছিল অনুমান। তাই ভাষায় module, namespace বা package-এর কোনো ব্যবস্থা রাখা হয়নি; সব কিছু একটি global জায়গায়। পরের দশ বছর এতে বড় অসুবিধা হয়নি, কারণ পাতাগুলো সত্যিই ছোট ছিল।

২০০৫ সালের পর Ajax-এর যুগে Gmail আর Google Maps-এর মতো অ্যাপ দেখাল যে ব্রাউজারে হাজার হাজার লাইনের প্রোগ্রাম চলতে পারে। তখন মানুষ ভাষার ভেতরের একমাত্র গোপন জায়গা — function scope — ব্যবহার করে module pattern বানাল, আর নামের সংঘর্ষ এড়াতে YAHOO.util.Dom-এর মতো লম্বা namespace object. কিন্তু এগুলো রীতি, ভাষার নিয়ম নয়; কে কার ওপর নির্ভর করে তা কেউ যন্ত্রকে বলতে পারত না।

২০০৯ সালের জানুয়ারিতে Mozilla-র প্রকৌশলী Kevin Dangoor একটি ব্লগ লেখায় যুক্তি দেন: ব্রাউজারের বাইরে JavaScript দিয়ে সার্ভার, কমান্ড-লাইন টুল আর ডেস্কটপ অ্যাপ বানাতে হলে একটি সাধারণ standard library আর module ব্যবস্থা লাগবে, যাতে এক পরিবেশের কোড আরেকটিতে চলে। তিনি একটি mailing list শুরু করেন, যার নাম প্রথমে ছিল ServerJS, সেই বছরই বদলে হয় CommonJS. কয়েক মাসের আলোচনায় “Modules” নির্দেশিকা দাঁড়াল: প্রতিটি ফাইল একটি module, require দিয়ে আমদানি, exports দিয়ে রপ্তানি।

একই বছর Ryan Dahl Node.js প্রকাশ করেন আর CommonJS-এর module ধারণা গ্রহণ করেন (সাথে নিজের module.exports সুবিধা)। ২০১০ সালে npm এল। এই তিনের মিলনেই বিস্ফোরণ: যে কেউ ছোট একটি module লিখে npm-এ দিতে পারেন, আর require("নাম") লিখলেই অন্য কেউ ব্যবহার করতে পারেন। CommonJS-এর বাকি উচ্চাকাঙ্ক্ষা — সব সার্ভার-JavaScript-এর এক standard library — অনেকটাই হারিয়ে যায়, কারণ Node নিজেই কার্যত মানদণ্ড হয়ে ওঠে। কিন্তু module ব্যবস্থাটি টিকে যায়, আর এক দশক ধরে npm-এর লাখো package এতেই লেখা হয়।

মূল উপলব্ধি: ভাষার অভাব সমাজ রীতি আর টুল দিয়ে পূরণ করে, আর যে সমাধান প্রথম একটি জনপ্রিয় প্ল্যাটফর্মে (Node) আর একটি package ভান্ডারে (npm) বসে যায়, সেটি মানদণ্ড না হয়েও বহু বছর টিকে থাকে — পরে আসা সঠিক মানদণ্ডকে তার সাথে মিলিয়েই চলতে হয়।

প্র ০২AMD/RequireJS আর CommonJS-এর মধ্যে কেন “যুদ্ধ” হয়েছিল? Browserify, UMD আর webpack এই বিরোধ কীভাবে মেটাল — আর এর ফলে কোন নতুন সমস্যা জন্মাল?

CommonJS-এর require synchronous: ডাকলে ফাইল পড়ে চালিয়ে সাথে সাথে ফল দেয়। সার্ভারে ফাইল ডিস্কে, মিলিসেকেন্ডের কাজ। কিন্তু ব্রাউজারে প্রতিটি require মানে নেটওয়ার্কে একটি অনুরোধ, আর JavaScript-এর main thread তখন আটকে থাকলে পুরো পাতা জমে যায়। ব্রাউজার-পক্ষের ডেভেলপাররা তাই বললেন, ব্রাউজারের জন্য asynchronous ব্যবস্থা লাগবে, যাতে আগে নির্ভরতার তালিকা ঘোষণা করা হয়, সেগুলো পাশাপাশি নামে, আর সব এলে তবেই কোড চলে।

CommonJS-এর আলোচনা থেকেই এই প্রস্তাব আলাদা হয়ে Asynchronous Module Definition, সংক্ষেপে AMD নামে দাঁড়ায়; James Burke-এর RequireJS ছিল এর সবচেয়ে জনপ্রিয় বাস্তবায়ন। define(["jquery", "./cart"], function ($, cart) { … }) — নির্ভরতা একটি array-তে, কোড একটি callback-এ। কাজ করত, কিন্তু লেখা ভারী, আর সার্ভারের কোডের সাথে মেলে না। CommonJS-পক্ষ বলত AMD কুৎসিত; AMD-পক্ষ বলত CommonJS ব্রাউজারের বাস্তবতা উপেক্ষা করে। ২০১০-এর দশকের শুরুতে library লেখকদের দুই ব্যবস্থাতেই চলার মতো কোড দিতে হতো।

সমাধান এল দুই দিক থেকে। James Halliday (substack) ২০১১ সালে Browserify বানান: CommonJS-এই লিখুন, আর একটি build ধাপ সব require অনুসরণ করে সব ফাইল একটি bundle-এ জুড়ে দেবে — ব্রাউজারে আর কোনো asynchronous লোড লাগবে না। Library-গুলোর জন্য এল UMD — একটি মোড়ক যা চালানোর সময় দেখে define আছে কি না, module.exports আছে কি না, নাকি শুধু global. তারপর Tobias Koppers-এর webpack (২০১২) আরও এগোয়: CommonJS, AMD, পরে ES module — সবই বোঝে, আর CSS ও ছবিকেও module হিসেবে ধরে; code splitting-ও দেয়।

যুদ্ধ মিটল, কিন্তু একটি নতুন বাস্তবতা জন্মাল — build ধাপ ছাড়া আধুনিক JavaScript প্রায় লেখাই যায় না। জটিল কনফিগারেশন ফাইল, ধীর build, আর “আমার মেশিনে চলে” সমস্যা ফ্রন্ট-এন্ড জগতের রোজকার অংশ হয়ে গেল; “JavaScript fatigue” কথাটি এই সময়েরই। ভাষার নিজের module না থাকায় সমাজ টুলের ওপর নির্ভরশীল হলো, আর সেই নির্ভরতা কমানোর চেষ্টাই পরে ES module আর import map-এর গল্প (প্রশ্ন ৩ আর ৫)।

মূল উপলব্ধি: একই সমস্যা দুই পরিবেশে (ডিস্ক বনাম নেটওয়ার্ক) ভিন্ন সমাধান দাবি করে। যখন ভাষা সিদ্ধান্ত দেয় না, টুল মধ্যস্থতা করে — কিন্তু প্রতিটি মধ্যস্থতাকারী জটিলতার নিজের একটি স্তর যোগ করে।

প্র ০৩ES2015-এ module-এর syntax এল, অথচ ব্রাউজারে চলতে লাগল ২০১৭–১৮ পর্যন্ত, আর Node-এ আরও পরে। একটি মানদণ্ড লেখা আর তা বাস্তবে চালু হওয়ার মধ্যে এত দূরত্ব কেন?

২০১৫ সালের জুনে ECMAScript 2015 প্রকাশিত হলো import আর export-সহ। কিন্তু মানদণ্ড শুধু syntax আর অর্থ (semantics) ঠিক করেছিল — একটি module কোথা থেকে, কীভাবে আনা হবে, অর্থাৎ “loader”-এর অংশটি ইচ্ছা করে বাদ রাখা হয়, কারণ ব্রাউজার আর সার্ভারের প্রয়োজন আলাদা। ব্রাউজারের অংশটি পরে WHATWG-এর HTML মানদণ্ডে লেখা হয়: <script type="module">, URL দিয়ে ঠিকানা, CORS, defer-এর মতো আচরণ। তাই ২০১৫ সালে syntax থাকলেও কোনো ব্রাউজার তা চালাতে পারত না; সবাই Babel দিয়ে ES module-কে CommonJS-এ রূপান্তর করে bundler-এ দিত।

ব্রাউজারে প্রথম আসে Safari 10.1-এ (মার্চ ২০১৭), তারপর Chrome 61 (সেপ্টেম্বর ২০১৭), Edge 16 (২০১৭-এর শেষ দিকে) আর Firefox 60 (মে ২০১৮)। প্রায় তিন বছরের দূরত্বের কারণ ছিল বাস্তবায়নের জটিলতা: graph খোঁজা, পাশাপাশি নামানো, link করা, circular dependency, error-এর নিয়ম — সব নতুন করে বানাতে হয়েছিল।

Node-এর পথ আরও কঠিন, কারণ সেখানে ইতিমধ্যে লাখো CommonJS package. মূল প্রশ্ন: একটি .js ফাইল দেখে Node কীভাবে বুঝবে সেটি কোন ধরনের? বহু বছরের বিতর্কের পর .mjs extension প্রস্তাব হয় — যাকে মজা করে “Michael Jackson Script” বলা হতো। ২০১৭ সালে Node 8.5-এ একটি experimental flag-এর পেছনে ESM আসে; ২০১৯-এর শেষে Node 13.2-এ flag ছাড়া চলতে শুরু করে (পরে 12.x-এর শেষ দিকের সংস্করণেও), আর package.json-এর "type": "module" নিয়ম যোগ হয়। Node 14 (২০২০)-এর পর থেকে বাস্তব প্রজেক্টে ESM ব্যবহার স্বচ্ছন্দ হতে থাকে। তবু CommonJS-এর সাথে দুই দিকের মিলন অসম্পূর্ণ ছিল, যা পরের প্রশ্নের বিষয়।

এখান থেকে একটি শিক্ষা: ভাষার মানদণ্ড, প্ল্যাটফর্মের মানদণ্ড (HTML), বাস্তবায়ন (ব্রাউজার ও Node) আর বাস্তুতন্ত্র (npm, টুল) — চারটি আলাদা স্তর, প্রতিটির নিজের গতি। একটি feature “ES2015-এর” হলেও আপনার ব্যবহারকারীর ব্রাউজারে বা আপনার সার্ভারের Node-এ তা আছে কি না, আলাদা প্রশ্ন।

মূল উপলব্ধি: মানদণ্ড প্রকাশ হলো শুরু, শেষ নয়। লাখো বিদ্যমান কোডের সাথে সামঞ্জস্য রেখে নতুন ব্যবস্থা চালু করতে বছরের পর বছর লাগে — তাই “কোন সংস্করণ থেকে” প্রশ্নটি পেশাদার ডেভেলপারের রোজকার অভ্যাস।

প্র ০৪“Dual package hazard” কী? npm-এর জগতে ESM আর CommonJS-এর ভাগাভাগি কেন এত যন্ত্রণা দিয়েছে, আর require(esm) সেই যন্ত্রণা কতটা কমায়?

Node-এ ESM আসার পর package লেখকদের সামনে প্রশ্ন: আমার package ESM-এ দেব, CommonJS-এ দেব, নাকি দুটোতেই? ESM-only দিলে পুরোনো CommonJS প্রজেক্টগুলো require করতে পারবে না — কারণ বহু বছর CommonJS থেকে ESM আনার একমাত্র উপায় ছিল asynchronous import(). দুটোই দিলে (package.json-এর "exports"-এ import আর require দুই শর্ত) নতুন একটি বিপদ: একই অ্যাপের একটি অংশ package-টি import করল, আরেকটি অংশ require করল — তাহলে মেমরিতে package-এর দুটি আলাদা কপি লোড হয়।

এটিই dual package hazard. দুই কপি মানে দুই আলাদা state: package-এর ভেতরের singleton (যেমন একটি কনফিগারেশন বা cache) দুটি হয়ে যায়, একটিতে সেট করা মান অন্যটিতে নেই। দুই কপির class-ও আলাদা, তাই একটি কপির বানানো object অন্য কপির instanceof পরীক্ষায় false. বাগগুলো রহস্যময়, কারণ কোডে কোনো ভুল চোখে পড়ে না। Node-এর নিজের docs এই বিপদ নিয়ে সতর্ক করে আর একটি পথ সুপারিশ করে — আসল কোড এক ধরনে রেখে অন্যটিতে শুধু পাতলা মোড়ক দেওয়া।

২০২১ সালে জনপ্রিয় বহু ছোট package-এর লেখক Sindre Sorhus তাঁর package-গুলো “pure ESM” করার ঘোষণা দেন, আর node-fetch-এর মতো আরও কিছু package-এর নতুন বড় সংস্করণ ESM-only হয়। উদ্দেশ্য ছিল বাস্তুতন্ত্রকে মানদণ্ডের দিকে ঠেলে দেওয়া; ফলাফল ছিল হাজারো প্রজেক্টে হঠাৎ ERR_REQUIRE_ESM, পুরোনো সংস্করণে আটকে থাকা, আর TypeScript, Jest-এর মতো টুলের কনফিগারেশন নিয়ে দীর্ঘ লড়াই। অনেক বছর ধরে “ESM বনাম CJS” ছিল Node ডেভেলপারদের সবচেয়ে বিরক্তিকর বিষয়গুলোর একটি।

মোড় ঘোরে require(esm)-এ: Node 22-এ flag-এর পেছনে, তারপর Node 22.12 আর 23-এ flag ছাড়া, পরে 20.19-এও। শর্ত একটি — require করা ESM graph-এ top-level await থাকা চলবে না, কারণ synchronous require অপেক্ষা করতে পারে না। এর ফলে package লেখকরা ESM-only দিলেও CommonJS ব্যবহারকারীরা require করতে পারেন, আর dual package দেওয়ার চাপ — সাথে hazard-ও — কমে আসে। পুরোনো Node সংস্করণ যতদিন চালু থাকে, ততদিন সমস্যাটি পুরো যায় না; তাই নিজের প্রজেক্টের Node সংস্করণ জানা জরুরি।

মূল উপলব্ধি: একই জিনিসের দুটি কপি থাকা মানেই দুটি সত্য — singleton আর identity-নির্ভর কোড তখন ভেঙে পড়ে। বাস্তুতন্ত্রের রূপান্তর জোর করে নয়, পুরোনো আর নতুনের মধ্যে সেতু বানিয়ে সবচেয়ে কম যন্ত্রণায় হয়।

প্র ০৫Import map আর “no-build” ওয়েব ডেভেলপমেন্ট — bundler ছাড়াই আধুনিক অ্যাপ কি সত্যিই সম্ভব? Rails 7-এর মতো উদ্যোগ কী যুক্তি দেয়, আর কোথায় bundler এখনো জেতে?

২০১০-এর দশক জুড়ে bundler ছিল অনিবার্য, তিনটি কারণে: ব্রাউজারে module ছিল না; শত শত ছোট ফাইল আলাদা নামানো HTTP/1.1-এ ধীর ছিল; আর npm package-এর import "lodash"-এর মতো bare specifier ব্রাউজার বুঝত না। ২০২০-এর দশকে তিনটি কারণই দুর্বল হয়েছে। সব ব্রাউজারে ES module আছে; HTTP/2 আর HTTP/3 একটি সংযোগে অনেক ফাইল পাশাপাশি পাঠাতে পারে; আর import map bare specifier-এর সমস্যা মেটায় — Chrome-এ ২০২১ সালে, Firefox-এ ২০২২-এর শেষে আর Safari-তে ২০২৩ সালে এসে এটি সব প্রধান ব্রাউজারে পৌঁছায়।

Ruby on Rails-এর স্রষ্টা David Heinemeier Hansson (DHH) এই পরিবর্তনকে জোরালোভাবে সামনে আনেন। ২০২১ সালে তিনি লেখেন, আধুনিক ওয়েব অ্যাপ bundling বা transpiling ছাড়াই বানানো যায়; সেই বছরের শেষে প্রকাশিত Rails 7-এ ডিফল্ট হিসেবে আসে importmap-rails — JavaScript ফাইল সরাসরি ব্রাউজারে যায়, package আসে import map দিয়ে। যুক্তি: Node, npm আর webpack-এর পুরো যন্ত্রপাতি ছাড়া ছোট দলের কাজ সহজ; যে কোড লেখা হলো, ব্রাউজারে ঠিক সেটাই চলে, ডিবাগ সহজ; build ভাঙার ভয় নেই। ফাইলের নামে hash দিয়ে cache নিয়ন্ত্রণের কাজ সার্ভারের দিকেই করা যায়।

কিন্তু bundler এখনো যেখানে জেতে, তাও স্পষ্ট। গভীর module graph-এ ব্রাউজার ফাইলগুলো ধাপে ধাপে আবিষ্কার করে — main পড়ে cart, cart পড়ে price — প্রতিটি ধাপে একটি network round trip; ধীর মোবাইল নেটওয়ার্কে এই জলপ্রপাত চোখে পড়ার মতো। modulepreload আংশিক সমাধান। Tree shaking আর minify না হলে ব্যবহারকারী অব্যবহৃত কোডও নামান। আর TypeScript, JSX বা Vue-র single-file component-এর মতো যেকোনো রূপান্তর লাগলে কোনো না কোনো build ধাপ লাগবেই। তাই বড় React অ্যাপে Vite-এর মতো টুল (পর্ব ৪৬) এখনো স্বাভাবিক পছন্দ, যদিও Vite নিজেও development-এর সময় ব্রাউজারের native ESM ব্যবহার করে — পুরোনো bundler-গুলোর চেয়ে দ্রুত হওয়ার এটাই কারণ।

বাংলাদেশের প্রেক্ষাপটে প্রশ্নটি ব্যবহারিক: ধীর আর দামি মোবাইল ডেটায় প্রতিটি অতিরিক্ত round trip আর প্রতিটি অব্যবহৃত KB-র দাম আছে। ছোট অ্যাডমিন প্যানেল বা অভ্যন্তরীণ টুলে no-build দারুণ; লাখো মোবাইল ব্যবহারকারীর দোকানের পাতায় মেপে দেখা জরুরি।

মূল উপলব্ধি: টুল ব্যবহার করা উচিত সমস্যার জন্য, অভ্যাসের জন্য নয়। প্ল্যাটফর্ম যখন সমস্যাটি নিজেই মিটিয়ে দেয়, তখন পুরোনো টুলের প্রয়োজন নতুন করে প্রশ্ন করুন — আর উত্তর দিন মেপে, বিশ্বাসে নয়।

অনুশীলন

বেশিরভাগ কাজ ভাবনার আর আউটপুট অনুমানের — আগে উত্তর ভাবুন, তারপর মিলিয়ে নিন।

  1. কতবার ছাপা হবে? logger.js-এর প্রথম লাইনে console.log("logger তৈরি") আর export const logs = [];. cart.js, orders.js আর main.js — তিনটিই logger.js import করে, আর main.js বাকি দুটিকেও import করে। cart.js logs.push("cart") করে, orders.js logs.push("order"). main.js-এ console.log(logs) কী দেখাবে, আর “logger তৈরি” কতবার?

    “logger তৈরি” একবারই ছাপা হবে — একটি URL-এর module একবারই evaluate হয়। logs-এ দুটি জিনিস: main.js-এ যদি cart.js-এর import আগে লেখা থাকে, তবে [ 'cart', 'order' ], কারণ import-এর ক্রমে নির্ভরতাগুলো চলে (logger → cart → orders → main). তিন ফাইলই একই array দেখে — module নিজেই একটি singleton. Binding (logs) বদলানো যায় না, কিন্তু array-এর ভেতরে push করা যায়।

  2. বাগ খুঁজুন: AI-এর লেখা server.mjs-এ চারটি সমস্যা আছে। কী কী? const express = require("express");
    import { readFile } from "fs/promises";
    import db from "./db";
    const html = await readFile(__dirname + "/index.html", "utf8");
    exports.app = express();

    (১) .mjs ফাইল ESM — require নেই: ReferenceError: require is not defined in ES module scope. লিখুন import express from "express". (২) "./db"-এ extension নেই — Node ESM নিজে .js যোগ করে না, ERR_MODULE_NOT_FOUND; লিখুন "./db.js". (৩) __dirname ESM-এ নেই; import.meta.dirname (Node 20.11+) বা path.join(import.meta.dirname, "index.html"). (৪) exports.app CommonJS-এর রপ্তানি — ESM-এ exports নেই; লিখুন export const app = express();. Top-level await এখানে ঠিক আছে, কারণ ফাইলটি ESM. বাড়তি পরামর্শ: built-in module-এ "node:fs/promises".

  3. ভাগ করার পরিকল্পনা: একটি ৯০০ লাইনের shop.js-এ আছে: পণ্যের তালিকা আনা ও দেখানো, কার্ট, ভ্যাট ও কুপনের হিসাব, টাকা ও তারিখ ফরম্যাট, চেকআউট ফর্ম যাচাই আর PDF ইনভয়েস। কোন ফোল্ডার আর ফাইলে ভাগ করবেন, কোনটি কী export করবে, আর কোনটি import()-এ লোড হবে? (কোড না লিখে শুধু নকশা।)

    একটি সম্ভাব্য নকশা: products/ (products-api.js → fetchProducts; product-list.js → renderProducts), cart/ (cart.js → add, remove, summary; cart-view.js), pricing/ (price.js → addVat, applyCoupon — কোনো DOM নয়, তাই সহজে test করা যায়), checkout/ (checkout-form.js, validate.js), invoice/invoice-pdf.js → makeInvoicePdf, যা শুধু “ইনভয়েস” বোতামে await import() দিয়ে আসবে, আর shared/ (format.js → formatTaka, formatDate; config.js singleton). main.js শুধু জোড়া লাগায়। সব named export; বিশাল barrel নয়। খেয়াল রাখুন cart যেন pricing চায়, কিন্তু pricing যেন cart-কে না চায় — নির্ভরতা এক দিকে রাখলে circular import হয় না।

  4. নিজের হাতে (কোড): একটি ফোল্ডারে index.html, main.js, price.js বানান; price.js থেকে addVat named export করে main.js-এ import করুন আর <script type="module" src="main.js"> দিয়ে লোড করুন। (ক) ফোল্ডারে দুই ক্লিকে খুলুন — console-এ কী দেখেন? (খ) লোকাল সার্ভারে খুলুন। (গ) import-এ নামের বানান ভুল করুন। (ঘ) "./price" লিখে extension বাদ দিন।

    (ক) file://-এ Chrome/Edge CORS error দেখায় — origin 'null' থেকে script আটকে দেওয়া হয়েছে; কিছুই চলে না। (খ) VS Code Live Server, npx serve বা python -m http.server দিয়ে http://localhost-এ খুললে ঠিকঠাক চলবে। Network tab-এ দেখবেন price.js আলাদা অনুরোধে এসেছে। (গ) SyntaxError: The requested module './price.js' does not provide an export named … — কোনো লাইন চলার আগেই, link ধাপে। (ঘ) ব্রাউজার /price চাইবে, সার্ভার 404 দেবে — ব্রাউজার extension অনুমান করে না। চাইলে একটি import map যোগ করে "@shop/" উপসর্গ দিয়ে import করে দেখুন।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমব্রাউজার একটি <script type="module" src="main.js"> পেল। ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. main.js নামিয়ে parse করা — import-গুলোর তালিকা পাওয়া
  2. নির্ভরতার ফাইলগুলো নামিয়ে parse করা, যতক্ষণ না পুরো graph জানা হয়
  3. link — প্রতিটি import-কে রপ্তানিকারীর binding-এর সাথে জোড়া
  4. যাদের কোনো নির্ভরতা নেই, সেই module-গুলোর কোড আগে চলা
  5. সবশেষে main.js-এর top-level কোড চলা

counter.js-এ আছে export let count = 0; export function inc() { count++; }. অন্য একটি module লেখে import { count, inc } from "./counter.js"; inc(); console.log(count);. কী ছাপা হবে?

  • 0
  • 1
  • TypeError
  • undefined

একটি CommonJS ফাইলে শুধু লেখা আছে function send() {} exports = { send };. অন্য ফাইলে const sms = require("./sms"); console.log(typeof sms.send); কী দেখাবে?

  • "function"
  • TypeError — exports বদলানো যায় না
  • "undefined"
  • Error: Cannot find module

HTML-এ <script type="module" src="app.js"></script> লেখা থাকলে, defer বা async ছাড়া, script কখন চলে?

  • HTML পড়া শেষ হওয়ার পর, defer-এর মতো
  • সাথে সাথে, HTML পড়া থামিয়ে
  • সব ছবি লোড হওয়ার পর, load event-এ
  • নামানো শেষ হওয়া মাত্র, async-এর মতো

একজন শিক্ষার্থী ফোল্ডারে দুই ক্লিকে index.html খুললেন (file://), যেখানে type="module" script অন্য একটি ফাইল import করে। কিছুই চলছে না। সবচেয়ে সম্ভাব্য কারণ আর সমাধান?

  • ব্রাউজার module সমর্থন করে না — নতুন ব্রাউজার লাগবে
  • module-এ strict mode — "use strict" মুছতে হবে
  • nomodule attribute যোগ করতে হবে
  • file:// পাতায় module CORS-এ আটকে যায় — লোকাল সার্ভার দিয়ে খুলতে হবে

Node 20-এ একটি প্রজেক্টের package.json-এ কোনো "type" নেই। app.js-এ import fs from "node:fs"; লিখে node app.js চালালে কী হবে?

  • স্বাভাবিকভাবে চলবে
  • SyntaxError: Cannot use import statement outside a module
  • Error: Cannot find module 'node:fs'
  • Node নিজে import-কে require-এ বদলে চালাবে

bundler অব্যবহৃত কোড বাদ দেওয়ার (tree shaking) কাজ সবচেয়ে নির্ভরযোগ্যভাবে কোনটিতে করতে পারে?

  • const lib = require("./lib")
  • await import(pathFromUser)
  • import { addVat } from "./price.js"
  • window.Shop = { addVat, emi }

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Module = নিজের scope-ওয়ালা ফাইল; export না করলে গোপন, import না করলে অদৃশ্য। নতুন কোডে ES module, পুরোনো Node কোডে CommonJS.
  2. Named export বেশি নিরাপদ ও খোঁজা সহজ; default একটিই। Barrel file ছোট রাখুন, প্রতিটি ফোল্ডারে বিশাল index.js নয়।
  3. ES module static: parse → fetch → link → evaluate (পাতা থেকে শুরু)। ভুল নাম শুরুতেই ধরা পড়ে, tree shaking সম্ভব; প্রতিটি module একবারই চলে।
  4. Import হলো live binding — নতুন মান দেখা যায়, বাইরে থেকে বদলানো যায় না। Circular import-এ TDZ error; ভাগ করা জিনিস তৃতীয় ফাইলে।
  5. ভারী অংশ import()-এ; ব্রাউজারে type="module" আর লোকাল সার্ভার; Node-এ "type": "module" বা .mjs, ESM-এ import.meta.dirname, আর এক ফাইলে require-import মেশানো নয়।

পরের পর্বে: module তো লিখলেন — এবার অন্যদের লেখা হাজারো module আনবেন কীভাবে, আর সব ফাইল জুড়ে ছোট, দ্রুত ব্রাউজার-কোড বানাবেন কীভাবে? npm, package.json, semver, Vite-এর bundling ও tree shaking, আর ESLint ও Prettier দিয়ে কোড পরিচ্ছন্ন রাখা — পর্ব ৪৬-এ।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান