الأسبوع ١ · فصل ١

المفاهيم و pnpm workspaces

Concepts · pnpm workspaces · workspace protocol · symlinks
🎯 الهدف: تفهم إيه اللي بيحصل فعلًا في node_modules لما مشروعين في نفس الريبو يستخدموا بعض.

يعني إيه Monorepo بجد؟

Monorepo = ريبو Git واحد فيه أكتر من project، وكل project ليه package.json بتاعه. الفكرة مش “نحط كل الكود في فولدر واحد وخلاص” — الفكرة إن الـ projects دي تعرف بعض وتستخدم بعض كأنهم packages متنشرة، من غير ما تنشرهم فعلًا.

😩 Polyrepo (ريبو لكل حاجة)
  • غيّرت Button في الـ design system؟ ← publish ← bump في 3 ريبوهات ← 3 PRs.
  • الـ types بين الـ API والـ Front بتختلف من غير ما حد ياخد باله.
  • كل ريبو بـ ESLint و tsconfig مختلفين.
😎 Monorepo
  • تغيير واحد + commit واحد (atomic commit) يعدّل الـ API والـ UI مع بعض.
  • الـ types مصدرها واحد.
  • Config موحّد، والـ build بيتعمل للي اتغيّر بس (مع Turbo/Nx).

الـ Workspace: أول طوبة

الـ workspace هو اللي بيقول لـ pnpm: “الفولدرات دي كلها packages تبعي، اربطهم ببعض”. بيتعرّف في ملف واحد في الـ root:

pnpm-workspace.yaml
packages:  - "apps/*"       # كل فولدر جوه apps هو package  - "packages/*"   # وكل فولدر جوه packages برضه
package.json (root)
{  "name": "my-monorepo",  "private": true,  "packageManager": "pnpm@9.12.0",  "scripts": {    "dev": "pnpm -r --parallel dev",    "build": "pnpm -r build"  }}
my-monorepo/├── apps/│   └── web/              # package.json → "name": "web"├── packages/│   └── ui/               # package.json → "name": "@repo/ui"├── package.json├── pnpm-lock.yaml        # lockfile واحد للريبو كله!└── pnpm-workspace.yaml

لاحظ: lockfile واحد بس في الـ root. ده من أهم مميزات الـ monorepo — كل الـ apps بتشوف نفس الـ dependency graph.

workspace:* ضد الـ versions العادية

لما web عاوز يستخدم @repo/ui، بتكتب في apps/web/package.json:

apps/web/package.json
{  "name": "web",  "dependencies": {    "@repo/ui": "workspace:*",    "react": "^18.3.1"  }}

الـ workspace: ده اسمه workspace protocol، ومعناه: “هات النسخة المحلية من الريبو، وإياك تروح للـ npm registry”.

اللي بتكتبهpnpm بيعمل إيه وقت installبيتحوّل لإيه وقت publish
workspace:*يربط النسخة المحلية مهما كان رقمهاالرقم بالظبط: 1.4.2
workspace:^يربط المحلية^1.4.2
workspace:~يربط المحلية~1.4.2
^1.4.0 (عادي)حسب إعدادات pnpm؛ ممكن ينزّلها من الـ registry بدل المحليةزي ما هي

وعشان تضيف dependency داخلية من الـ CLI:

pnpm add @repo/ui --filter web --workspace# --workspace بيجبر pnpm يدوّر جوه الريبو بس

ده أهم جزء في الأسبوع الأول. pnpm مختلف عن npm و yarn classic في إنه مش بيعمل node_modules مسطّحة (flat). بيمشي على 3 طبقات:

  1. الـ Store العام: كل نسخة من كل package بتتخزن مرة واحدة بس على الجهاز كله (content-addressable). عندك 20 مشروع فيهم react 18.3.1؟ نسخة واحدة على الديسك.
  2. الـ Virtual Store node_modules/.pnpm: فيه فولدر لكل package@version، والملفات جواه hard links للـ store (يعني مش بتاخد مساحة زيادة).
  3. الـ Symlinks: node_modules/react مجرد سهم بيشاور على .pnpm/react@18.3.1/node_modules/react.

وبالنسبة للـ workspace packages، الموضوع أبسط: الـ symlink بيشاور على الفولدر الأصلي بتاع الـ package في الريبو.

cd apps/webls -la node_modules/@repo#   ui -> ../../../packages/ui        ← سهم مباشر للكود بتاعك!ls -la node_modules/react#   react -> ../../../node_modules/.pnpm/react@18.3.1/node_modules/react

الـ Strictness و Phantom Dependencies

في npm التقليدي، لو react-dom بيعتمد على scheduler، الـ scheduler بيتحط في الـ root بتاع node_modules، فتقدر تعمل import 'scheduler' في كودك رغم إنك ماكتبتهوش في package.json. ده اسمه phantom dependency — بيشتغل عندك وبيضرب فجأة لما حد يحدّث حاجة.

pnpm بسبب الهيكل اللي فوق، مش هيخليك تـ import حاجة مش معرّفة في package.json بتاعك. في الأول هتتضايق، بعد كده هتحبه. في الـ monorepo ده بيمنع إن web يستخدم بالغلط dependency تبع api.

أوامر pnpm اللي هتعيش بيها

الأمربيعمل إيه
pnpm installمن الـ root: ينزّل لكل الـ workspace ويربطهم
pnpm add -Dw typescript-w = ضيف في الـ root (أدوات مشتركة)
pnpm add zod --filter @repo/typesضيف لـ package معينة
pnpm -r buildشغّل build في كل الـ packages بترتيب الـ dependencies
pnpm --filter web devشغّل script في package واحدة
pnpm --filter "web..." buildweb + كل اللي بيعتمد عليه
pnpm why reactمين جايب react ده ومنين؟
pnpm ls -r --depth -1اعرض كل الـ workspace packages

🧪 Lab الأسبوع الأول (اعمله بإيدك)

mkdir mono-lab && cd mono-lab && git initcorepack enablepnpm initmkdir -p apps/web packages/greet/srcprintf 'packages:\n  - "apps/*"\n  - "packages/*"\n' > pnpm-workspace.yaml
packages/greet/package.json
{  "name": "@repo/greet",  "version": "1.0.0",  "private": true,  "type": "module",  "main": "./src/index.js"}
packages/greet/src/index.js
export const greet = (name) => `أهلًا يا ${name} 👋`;
cd apps/web && pnpm init && cd ../..pnpm add @repo/greet --filter web --workspaceecho 'import { greet } from "@repo/greet"; console.log(greet("سعيد"));' > apps/web/index.mjsnode apps/web/index.mjs                 # أهلًا يا سعيد 👋ls -la apps/web/node_modules/@repo      # شوف الـ symlink بعينك

بعد كده: غيّر الرسالة في greet وشغّل تاني من غير أي install. ثم جرّب import 'something-not-installed' وشوف pnpm بيعترض إزاي.

Checklist الأسبوع الأول

🧠 اختبر نفسك
١) عندك 5 مشاريع على جهازك كلهم بيستخدموا lodash بنفس النسخة. pnpm هيخزنها كام مرة على الديسك؟
مرة واحدة في الـ store؛ والباقي hard links.
٢) نشرت @repo/ui وفي dependencies عندها "@repo/tokens": "workspace:^" والـ tokens نسختها 2.1.0. الـ package المنشورة هتشوف إيه؟
"@repo/tokens": "^2.1.0".
٣) ليه تعديل في packages/ui بيظهر فورًا في apps/web؟
لأن node_modules/@repo/ui symlink للفولدر الأصلي، مفيش نسخة.
٤) زميلك عمل import dayjs في web وشغال عنده بس بيضرب في الـ CI. إيه أول حاجة تشك فيها؟
phantom dependency: dayjs مش في package.json بتاع web، كان واصلها بالصدفة. ضيفها صراحةً.