وب مدرن، رابط کاربری و تجربهی وب · فصل ۳ — Type System، Runtime، Async و Build · 42
Node.js ESM و Module Resolution
میتواند «Node.js ESM و Module Resolution» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «Node.js ESM و Module Resolution» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «Node.js ESM و Module Resolution» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
پیشنیازها
وقتی «import درست به نظر میرسد» هنوز مدرک نیست
ممکن است پروژه در editor بدون هشدار باز شود، autocomplete dependency را پیدا کند و dev tool هم اجرا شود؛ اما همان فایل وقتی مستقیم با Node اجرا میشود روی import شکست بخورد. سؤال دقیق این نیست که «چرا Node سختگیری میکند؟» سؤال این است: Node برای همین specifier، در همین package scope و با همین module format چه قرارداد runtimeای را اعمال کرده است؟
مالکیت این درس دقیقاً همین مرز است. در پایان باید یک Node ESM Resolution Trace v1 بسازید که چهار لایه را از هم جدا کند: module format، specifier class، package interface و نتیجه واقعی runtime. قبل از اجرا failure را پیشبینی میکنید، آن را در Node پینشده مشاهده میکنید، فقط همان مرز خراب را repair میکنید و سپس همین روش را روی یک changed-context مستقل تکرار میکنید.
هدف حفظکردن چند ترفند ESM نیست. هدف ساختن یک روش تشخیص تکرارپذیر است. موفقیت در editor، TypeScript یا bundler میتواند برای ساخت hypothesis مفید باشد، اما proof این درس نیست. سطح مشاهده اینجا خود processِ Node است.
محدوده، پیشنیاز و مسیر یادگیری
WEB-001 پیشنیاز مفهومی سخت این درس است. هرجا لازم شد تفاوت signal زمان توسعه/TypeScript را با signal زمان اجرای Node تشخیص دهید، evidence محدود WEB-001 را reuse کنید؛ اما TypeScript را دوباره در این درس آموزش ندهید. WEB-003 مالک async behavior و WEB-014 مالک Vite bundling و production asset graph است. ممکن است سناریو به این ابزارها اشاره کند، ولی completion target این درس نیستند.
artifact پایان درس Node ESM Resolution Trace v1 است. یک trace قابلقبول باید این موارد را ثبت کند: importing file و package scope؛ marker مربوط به module format؛ specifier دقیق؛ کلاس specifier؛ exports یا imports مرتبط؛ target یا failure boundary پیشبینیشده؛ نتیجه واقعی Node؛ repair؛ و حداقل یک residual limitation.
در route پایه، classification checklist را جلوی چشم نگه دارید. در repair route فقط یک row خراب را جدا کنید و دوباره اجرا کنید. در experienced route میتوانید توضیح syntax را فشرده کنید، اما prediction، mutation، runtime evidence و transfer حذف نمیشوند.
مدل ذهنی ششمرحلهای resolution
قبل از تغییر کد، این زنجیره را اجرا کنید:
Import specifier → package scope / format marker → specifier class → exports/imports rules → resolved URL/format → load result
- Import specifier: رشته را دقیقاً همانطور که نوشته شده ثبت کنید.
./local.js،demo-pkg/publicو#configشکلهای یکسانی نیستند. - Package scope و module format: مشخص کنید Node فایل را در چه contextی تفسیر میکند. markerهای صریح مثل
.mjs،.cjsو مقدار"type"درpackage.jsonنزدیک و مرتبط، evidence قوی هستند. رنگ syntax در editor evidence runtime نیست. - Specifier class: specifier نسبی یا URL-like مسیر resolution خودش را دارد؛ bare specifier وارد package resolution میشود؛ و specifier شروعشونده با
#وارد package-import resolution میشود. اول classify کنید، بعد debug. - Package interface: برای bare import، مرز package را بررسی کنید. اگر
exportsوجود دارد، فقط subpathهای اعلامشده بخشی از interface resolution آن package هستند. وجود فیزیکی یک فایل درnode_modulesبهتنهایی کافی نیست. - Resolved URL و format: ESM در Node بر مبنای URL resolve میشود. برای relative/absolute file specifier روی extension search یا directory index fallback حساب نکنید. target واقعی یا failure boundary را ثبت کنید.
- Load result: resolution و loading یک چیز نیستند. ممکن است URL resolve شود ولی load بهخاطر format یا protocol شکست بخورد. هر خطا را با برچسب کلی «module not found» جمع نکنید.
نمودار WEB-002-A1 همین زنجیره را نشان میدهد و text fallback آن بهتنهایی برای انتقال مفهوم کافی است.
پایه: چهار قرارداد مستقل
۱) قرارداد module format
Node هم CommonJS و هم ECMAScript Modules را پشتیبانی میکند. در این درس format را یک مسئله classification در runtime در نظر بگیرید. ابتدا سراغ evidence صریح بروید: .mjs یعنی ES module، .cjs یعنی CommonJS، و "type": "module" یا "type": "commonjs" در package scope مربوط روی فایلهای .js اثر میگذارد. Node فعلی در برخی حالتهای بدون marker میتواند syntax را نیز تشخیص دهد، اما این موضوع دلیل خوبی برای مبهمکردن package boundary نیست. fixture درس marker را صریح نگه میدارد تا diagnosis قابل بازرسی باشد. [S-NODE-ESM]
خطای رایج این است که import را دستکاری کنیم در حالی که فایل تحت module system دیگری تفسیر میشود. قبل از هر repair، بنویسید Node importing file را چگونه classify کرده است.
۲) قرارداد relative specifier
در Node ESM، relative و absolute file specifier باید extension صریح داشته باشند. ./local به معنی «هر فایلی که Node حدس بزند local است» نیست. اگر مقصد local.js است، نام فایل کامل بخشی از contract است. directory index نیز باید کامل مشخص شود. [S-NODE-ESM]
به همین دلیل pathی که در ابزار توسعه با extension inference کار میکند ممکن است در اجرای مستقیم Node شکست بخورد. proof این درس command و result واقعی runtime است، نه success قبلی dev tool.
۳) قرارداد bare package
demo-pkg و demo-pkg/public bare package specifier هستند. رفتار آنها از package resolution و interface اعلامشده package میآید. اگر exports تعریف شده باشد، subpathای که در آن export نشده صرفاً به دلیل وجود فایل متناظر روی disk، entry point پشتیبانیشده محسوب نمیشود. [S-NODE-PACKAGES]
exports را package interface constraint بدانید، نه security boundary مطلق. این field مسیرهای resolution پشتیبانیشده را مشخص میکند، اما بهتنهایی ثابت نمیکند فایل داخلی از همه مسیرها یا در برابر همه threat modelها غیرقابل دسترسی است.
۴) قرارداد runtime evidence
سبز بودن import در editor، resolution در TypeScript language service یا موفقیت bundler میتواند hypothesis بسازد. هیچکدام observation موردنیاز این درس را جایگزین نمیکند: Node پینشده را اجرا کنید و output یا error class را ثبت کنید. در artifact، observation را از inference جدا کنید؛ مثلاً «process با exit code غیرصفر و package-path export failure تمام شد» observation است و «requested subpath بیرون interface تعریفشده package است» diagnosis/inference است.
توضیح عمیق: چرا دو import ظاهراً مشابه میتوانند متفاوت شکست بخورند
فرض کنید main.js این دو خط را دارد:
import { localValue } from './local';
import { publicValue } from 'demo-pkg/public.js';
دو contract مستقل میتوانند خراب باشند.
اولی با ./ شروع میشود، پس relative specifier است. در ESM، Node URL-style resolution انجام میدهد و .js را برای شما حدس نمیزند. اگر مقصد واقعی local.js است، نبودن extension defect همان لایه است.
دومی bare specifier است و وارد package boundary میشود. dependency را اینگونه تصور کنید:
{
"name": "demo-pkg",
"type": "module",
"exports": {
".": "./index.js",
"./public": "./public.js"
}
}
package، demo-pkg/public را expose میکند؛ نه لزوماً demo-pkg/public.js. فایل public.js روی disk وجود دارد، اما requested subpath باید با interface تعریفشده توسط exports سازگار باشد. repair import اول، import دوم را repair نمیکند.
قاعده تشخیصی مهم این است: متن بعد از from فقط ورودی اول resolution است. package scope، specifier class و package interface هم در نتیجه نقش دارند.
الگوریتم رسمی Node را میتوان بهصورت decision tree خواند: URL معتبر بهعنوان URL؛ شروع با ./، ../ یا / به relative/absolute URL resolution؛ شروع با # به package-import resolution؛ و بقیه به bare package resolution میروند. برای file URL سپس وجود فایل، directory بودن و module format در مسیر resolution/loading بررسی میشود. [S-NODE-ESM-RESOLVE]
این الگوریتم را به checklist مکانیکی تبدیل نکنید. هدف trace این است که حدس را حذف کند: هر row باید بگوید «این specifier وارد کدام branch شد، evidence آن چیست و Node واقعاً چه چیزی گزارش کرد؟»
مثال کامل: از دو failure تا runtime trace سالم
starter عمداً همان دو import بالا را دارد. قبل از اجرا دو prediction بنویسید:
./localباید در relative-specifier boundary شکست بخورد چون extension صریح ندارد.demo-pkg/public.jsدر صورتی که package فقط./publicرا expose کند باید در package-interface boundary شکست بخورد.
fixture را طبق lab://WEB-002/WEB002-LAB1 اجرا کنید. Node version، working directory، command، exit code و اولین error classification مفید را ثبت کنید. stack trace طولانی را بهجای diagnosis تحویل ندهید.
ابتدا فقط relative boundary را repair کنید؛ specifier باید فایل موردنظر را صریح نام ببرد. دوباره اجرا کنید. اگر failure بعدی حالا روی dependency ظاهر شد، همین جابهجایی error evidence ارزشمند است: contract اول repair شده و defect مستقل دوم نمایان شده است.
بعد package.json dependency را باز کنید و requested subpath را با keyهای exports مقایسه کنید. بهجای حذف exports برای ساکتکردن خطا، specifier را با public subpath موجود همراستا کنید. دوباره اجرا و success output را ثبت کنید.
جدول WEB-002-TAB1 حداقل rowهای module-format context، relative specifier، bare package subpath، runtime result، mutation و residual limitation را نگه میدارد. row قوی شامل input، prediction، observation، diagnosis، bounded repair و post-repair observation است.
Failure modeها: mutationهای عمدی برای شکستن اعتماد کاذب
Mutation A — حذف extension نسبی
از baseline سالم، ./local.js را دوباره به ./local تبدیل کنید. قبل از اجرا failure را پیشبینی کنید. diagnosis صحیح «ESM خراب است» نیست؛ diagnosis این است که relative ESM specifier دیگر فایل را بهصورت صریح نامگذاری نمیکند. بعد از observation فقط همان specifier را restore و دوباره اجرا کنید.
Mutation B — بستن package subpath
از baseline سالم، exports dependency را طوری تغییر دهید که public subpath استفادهشده دیگر expose نشود، در حالی که فایل فیزیکی سر جایش باقی بماند. failure را پیشبینی کنید، Node را اجرا کنید و package-interface failure را ثبت کنید. سپس interface را restore و recovery را verify کنید.
Mutation C — تغییر module-format marker
در app package مقدار "type" را از "module" به "commonjs" تغییر دهید ولی ESM syntax را در entry .js نگه دارید. قبل از اجرا اثر آن بر runtime classification را پیشبینی کنید. این بار diagnosis باید روی format mismatch متمرکز بماند، نه روی pathهای package.
این mutationها عمداً مستقل هستند. برچسب کلی «import fail شد» مهارت موردنظر را پنهان میکند. trace باید critical dimension هر mutation را مشخص کند: specifier_class، package_interface یا module_format.
چهار shortcut ممنوع این درس نیز باید در review رد شوند: یکساندانستن Node/TypeScript/bundler resolution؛ runtime proof دانستن editor success؛ security boundary مطلق دانستن exports؛ و تصور اینکه افزودن extension همه failureهای ESM را حل میکند.
مسیر باتجربه: از boundary تشخیص بده، نه از عادت toolchain
اگر با TypeScript، monorepo، package manager و bundler کار کردهاید، syntax را فشرده کنید و این sequence سختگیرانه را اجرا کنید:
- runtime command و importing file را ثبت کنید.
- nearest package scope و format markerهای صریح را پیدا کنید.
- هر specifier خراب را قبل از edit classify کنید.
- برای bare یا package-import specifier، قبل از گشتن در file tree interface package را ببینید.
- هر بار فقط یک hypothesis را با runtime evidence آزمایش کنید.
- repair را آنقدر کوچک نگه دارید که تغییر محل failure قابل تفسیر باشد.
- بعد از baseline success حتماً mutation را اجرا کنید؛ وگرنه نشان ندادهاید evidence شما failure را detect میکند.
این روش در monorepo اهمیت بیشتری دارد؛ دو فایل با source مشابه ممکن است زیر package scopeهای متفاوت باشند. build tool میتواند specifier را rewrite یا bundle کند در حالی که Node source اولیه را همانطور resolve نمیکند. package هم ممکن است public entry پایدار بدهد و layout داخلی خود را آزادانه تغییر دهد. تجربه بالا یعنی حفظ این لایهها، نه حفظکردن تعداد بیشتری exception.
گزینهها و trade-offها
.mjs یا "type": "module"
هر دو میتوانند ESM intent را صریح کنند. .mjs signal را در سطح فایل واضح میکند؛ "type": "module" default سطح package برای .js میسازد. انتخاب به organization و compatibility package بستگی دارد. این درس winner همیشگی معرفی نمیکند؛ فقط میخواهد marker انتخابشده در trace صریح و قابل بررسی باشد.
Public subpath یا internal file path
وقتی package، demo-pkg/public را expose میکند، استفاده از entry تعریفشده نسبت به reach کردن به نام فایل داخلی coupling کمتری به layout package دارد. این استدلال درباره API/resolution است، نه ادعای security sandbox بودن exports.
Repair source runtime یا تکیه بر bundler rewrite
bundler ممکن است در محصولی که همیشه bundle میشود، module graph را عمداً transform کند. اما اگر artifact موردنظر یک direct Node script است، success در bundler completion این درس را ثابت نمیکند. mechanism باید با execution context واقعی هماهنگ باشد.
تمرین هدایتشده و transfer مستقل
Baseline هدایتشده
مسیر WEB002-LAB1/learner/starter را باز کنید و قبل از edit، rowهای اولیه evidence template را پر کنید:
- importing file و package scope؛
- format marker؛
- relative specifier دقیق و class آن؛
- package specifier دقیق و class آن؛
exportsمرتبط؛- predicted first failure.
starter را اجرا و observation را با prediction مقایسه کنید. هر بار فقط یک boundary را repair کنید تا baseline، مقدار local و dependency را با موفقیت چاپ کند. سپس mutation probe را روی starter repairشده اجرا کنید و حداقل یک mutation را با prediction و observation ثبت کنید.
Transfer مستقل
به WEB002-LAB1/learner/transfer بروید. fixture دوم helper و dependency interface متفاوتی دارد. literal fixهای baseline را copy نکنید. دوباره format context را تعیین کنید، specifierها را classify کنید، package interface را بررسی کنید، prediction بنویسید، اجرا کنید و evidence بسازید. completion فقط وقتی معتبر است که transfer بدون ارائه decisive fix توسط متن درس به success برسد.
جواب فقط code نهایی نیست. transfer evidence باید توضیح دهد چرا specifier repairشده با contract جدید سازگار است.
Verification: چه چیزی evidence محسوب میشود
Node ESM Resolution Trace v1 باید آنقدر inspectable باشد که reviewer بتواند conclusion را reconstruct کند. حداقل این fieldها را نگه دارید:
- environment: Node version و command؛
- package scope: path
package.jsonمرتبط و مقدارtype؛ - specifier: متن دقیق و class؛
- package interface: entry مرتبط
exports/importsیا «not applicable»؛ - prediction: target یا failure boundary مورد انتظار قبل از اجرا؛
- observation: exit code و runtime result خلاصه؛
- diagnosis: یک critical dimension دقیق؛
- repair: کوچکترین boundary تغییرکرده؛
- re-observation: evidence بازگشت behavior؛
- mutation: deliberate failure پیشبینیشده و مشاهدهشده؛
- transfer: runtime evidence changed-context؛
- residual limitation: چیزی که fixture ثابت نمیکند.
residual limitation میتواند این باشد که fixture رفتار bundler را ثابت نمیکند، همه conditional exportها را پوشش نمیدهد، security boundary package را اثبات نمیکند یا برای Node versionهایی که اجرا نشدهاند ادعا ندارد.
Fact و inference را قاطی نکنید. «process با package-path-not-exported error تمام شد» observation است؛ «requested bare subpath خارج interface اعلامشده dependency است» diagnosis است که با metadata و runtime result پشتیبانی میشود.
Assessment و معیار completion
پنج assessment بهترتیب retrieval، application، diagnosis، decision و transfer را میسنجند. application، diagnosis و transfer blocking هستند. درس وقتی complete است که baseline در Node پینشده اجرا شود، حداقل یک deliberate mutation detect و repair شود و changed-context transfer هم با evidence مستقل و یک residual limitation بسته شود.
hidden answer key نباید learner-visible باشد. evaluator باید artifact و runtime evidence را بررسی کند. file tree ظاهراً درست یا screenshot editor کافی نیست.
مرز human accountability نیز ثابت است: AI بعد از اینکه شما contract و evidence need را مشخص کردید میتواند check پیشنهاد دهد یا observationهای واقعی را خلاصه کند؛ اما اجازه ندارد Node run ساختگی، error مشاهدهنشده یا judgment نهایی package interface را جایگزین کند.
منابع و freshness boundary
این درس دقیقاً از frozen source set خود WEB-002 استفاده میکند: مستند رسمی ESM در Node، مستند رسمی Packages و بخش resolution/loading algorithm. تاریخ retrieval فریزشده 2026-09-02 و review due برابر 2026-10-17 است. spot-check نویسندگی در 2026-09-10 نشان داد قواعد bounded مورد استفاده این درس همچنان در مستند رسمی وجود دارند و هیچ source جدیدی به authority اضافه نشد.
برای module markerها، specifier classها، extension اجباری relative/absolute specifier و URL-based ESM از [S-NODE-ESM] استفاده کنید؛ برای exports/imports و package boundary از [S-NODE-PACKAGES]؛ و برای مدل تصمیم resolution/loading از [S-NODE-ESM-RESOLVE]. در review due یا زمانی که lab دیگر با pinned runtime همخوان نبود، volatile claimها باید دوباره بررسی شوند.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
WEB-002-S1 retrieval
قرارداد چهارلایه WEB-002 شامل module format، specifier class، package interface و runtime trace را بیان کنید و یک مورد را بگویید که موفقیت editor یا bundler ثابت نمیکند.
WEB-002-S2 application
baseline آزمایش WEB002-LAB1 را طوری repair کنید که اجرای مستقیم Node موفق شود. format context، specifier classها، exports مرتبط، prediction، observation، repair محدود و نتیجه نهایی runtime را ثبت کنید.
WEB-002-S3 diagnosis
از baseline سالم یک mutation عمدی اجرا کنید: حذف extension لازم relative import، بستن subpath استفادهشده در exports یا تغییر module-format marker. ابتدا prediction، سپس اجرا، diagnosis یک critical dimension، repair محدود و re-run را ثبت کنید.
WEB-002-S4 decision
برای یک Node package کوچک، strategy صریح ESM marker را انتخاب و دفاع کنید. سپس بین public subpath اعلامشده dependency و نام فایل داخلی تصمیم بگیرید و evidence تغییردهنده هر تصمیم را مشخص کنید.
WEB-002-S5 transfer
fixture مستقل changed-context در WEB002-LAB1/learner/transfer را repair کنید. local و package specifierهای آن را از metadata جدید دوباره classify کنید، direct Node evidence بگیرید و یک residual limitation ثبت کنید.
ملاکهای ارزیابی (rubric)
Critical dimensions: module_format, specifier_class, package_interface, runtime_trace.
- Contract correctness — critical: implementation and explanation preserve Node runtime ESM classification/resolution boundaries.
- Evidence quality — critical: prediction, direct Node observation, mutation, repair and transfer are traceable; editor/bundler success is not substituted for runtime evidence.
- Scope discipline — critical: TypeScript compilation, async behavior and bundler graph concerns are deferred to their owning lessons.
- Transfer independence — critical: the changed-context fixture is solved without a decisive scaffold.
Fail if a critical mutation is not detected, runtime evidence is fabricated/omitted, or the hard prerequisite/chapter boundary is violated.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
هدف: تشخیص direct Node ESM resolution در یک fixture محلی و غیرمخرب. شبکه لازم نیست.
runtime فریزشده: Node v22.16.0. قبل از ثبت evidence، node --version را اجرا کنید. اگر خروجی دقیقاً v22.16.0 نیست، توقف کنید؛ آن اجرا برای این lab فریزشده evidence معتبر محسوب نمیشود.
- در evidence هم
required_node_version=v22.16.0و هم نسخه مشاهدهشده ازnode --versionرا ثبت کنید. - در
starterفایلهایpackage.json،main.jsوnode_modules/demo-pkg/package.jsonرا بررسی و قبل از اجرا first failure را پیشبینی کنید. node main.jsرا اجرا کنید و هر بار فقط یک boundary متعلق به درس را repair کنید تا baseline موفق شود.- از root آزمایش،
python mutation_probe.py --submission-dir learner/starterرا اجرا کنید. probe فقط temporary copy را mutate میکند. حداقل یک prediction/observation ثبت کنید و در صورت ارتباط،stderr_classificationرا بهعنوان runtime evidence نگه دارید. - fixture مستقل
transferرا بدون copy کردن literal fixهای baseline repair و مستقیم با Node اجرا کنید. evidence/learner_evidence.jsonرا طبق schema عمومی trace کامل کنید.
برای ساکتکردن خطا package-interface metadata را حذف نکنید و موفقیت editor/TypeScript/bundler را جای direct Node evidence نگذارید.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)