การค้นหาโมดูล (Module Resolution)
เมื่อคุณเขียน import 'express' หรือ require('express') Node จะทำตามอัลกอริทึมการค้นหาที่กำหนดตายตัวเพื่อหาไฟล์จริงบนดิสก์ การเข้าใจอัลกอริทึมนี้ช่วยให้คุณ debug ข้อผิดพลาด missing-module เข้าใจว่าทำไม subpath imports ถึงใช้งานได้ และให้เหตุผลเกี่ยวกับวิธีที่ ESM กับ CJS package ทำงานร่วมกัน
Bare specifiers
หัวข้อที่มีชื่อว่า “Bare specifiers”bare specifier คือชื่อโมดูลที่ไม่ได้ขึ้นต้นด้วย ./, ../, หรือ / — เช่น 'express' หรือ 'lodash/fp' Node ค้นหา bare specifiers โดยการเดินขึ้นไปตามโครงสร้าง directory จากไฟล์ที่ import โดยตรวจสอบแต่ละโฟลเดอร์ node_modules/ ไปเรื่อยๆ จนกว่าจะพบหรือถึง root ของ filesystem
# Given a file at /project/src/app.js importing 'express',# Node checks these locations in order:/project/src/node_modules/express/project/node_modules/express ← found here/node_modules/expressการค้นหาแบบนี้ทำให้ package ที่ซ้อนกันสามารถพกพาเวอร์ชัน dependency ของตัวเองได้โดยไม่ขัดแย้งกับ sibling
นามสกุลไฟล์
หัวข้อที่มีชื่อว่า “นามสกุลไฟล์”Node ค้นหา specifier ไปยังไฟล์จริงโดยใช้กฎเหล่านี้:
.js— ถือเป็น CJS โดยค่าเริ่มต้น หรือ ESM เมื่อpackage.jsonที่ใกล้ที่สุดมี"type": "module".mjs— เป็น ESM เสมอ โดยไม่คำนึงถึงpackage.json.cjs— เป็น CJS เสมอ โดยไม่คำนึงถึงpackage.json
ESM บังคับให้ใช้ นามสกุลที่ชัดเจน ใน specifier ต่างจาก CJS คุณไม่สามารถละเว้นนามสกุลและให้ Node เดาได้:
// CJS — extension optional (Node tries .js, .json, index.js …)const utils = require('./utils');
// ESM — extension requiredimport utils from './utils.js';การทำงานร่วมกันของ ESM กับ CJS
หัวข้อที่มีชื่อว่า “การทำงานร่วมกันของ ESM กับ CJS”ระบบโมดูลทั้งสองสามารถอยู่ร่วมกันได้ แต่มีข้อจำกัด:
// ── CJS file (utils.cjs) ─────────────────────────────────────// CJS can require other CJS modules normally.const fs = require('fs');module.exports = { readConfig: () => fs.readFileSync('.env', 'utf8') };
// Since Node 22.12 (and 20.19 LTS), CJS CAN require() an ESM module:// const esm = require('./modern.mjs'); // ✅ (throws ERR_REQUIRE_ASYNC_MODULE only if it uses top-level await)
// ── ESM file (app.mjs) ──────────────────────────────────────// ESM CAN import a CJS module; it receives module.exports as the default.import cjsUtils from './utils.cjs'; // ✅ default = module.exports object
// ESM can also import another ESM module normally.import { helper } from './helpers.mjs'; // ✅เดิมที CJS ไม่สามารถ require() โมดูล ESM ได้ — จะ throw ERR_REQUIRE_ESM — เพราะ ESM loading เป็นแบบ static และ asynchronous ส่วน CJS loading เป็นแบบ synchronous แต่ตั้งแต่ Node 22.12 (และ 20.19 LTS) เป็นต้นมา require() โมดูล ESM ทำงานได้เป็นค่าเริ่มต้น ตราบใดที่โมดูลนั้นไม่มี top-level await ส่วนโมดูลที่ใช้ top-level await ยังคง throw ERR_REQUIRE_ASYNC_MODULE เพราะการเรียก require() แบบ synchronous ไม่สามารถรอการประมวลผลแบบ async ได้
exports map
หัวข้อที่มีชื่อว่า “exports map”ฟิลด์ "exports" ใน package.json เป็นวิธีที่ authoritative สำหรับ package ในการประกาศ public API surface path ใดก็ตามที่ไม่ได้ระบุใน "exports" เป็น private — ผู้ใช้ไม่สามารถ import โดยตรงได้
{ "name": "my-lib", "version": "1.0.0", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, "./utils": { "import": "./dist/utils.mjs", "require": "./dist/utils.cjs" } }}ด้วย map นี้:
import 'my-lib'จะค้นหาไปที่dist/index.mjsrequire('my-lib')จะค้นหาไปที่dist/index.cjsimport 'my-lib/utils'จะค้นหาไปที่dist/utils.mjsimport 'my-lib/internal/secret'จะ throw —"./internal/secret"ไม่ได้ถูก export
การรัน ESM แบบ inline ด้วย --input-type
หัวข้อที่มีชื่อว่า “การรัน ESM แบบ inline ด้วย --input-type”คุณสามารถ pipe ESM source โดยตรงไปยัง Node โดยไม่ต้องมีไฟล์บนดิสก์:
echo "import os from 'node:os'; console.log(os.platform());" | node --input-type=moduleวิธีนี้มีประโยชน์สำหรับการทดลองอย่างรวดเร็วหรือ CI one-liner ที่ต้องการ ESM semantics โดยไม่ต้องแตะต้อง package.json ของโปรเจกต์
การ import built-in module
หัวข้อที่มีชื่อว่า “การ import built-in module”Needs the Node.js runtime — open in StackBlitz to run.