يعني إيه Monorepo بجد؟
Monorepo = ريبو Git واحد فيه أكتر من project، وكل project ليه package.json بتاعه. الفكرة مش “نحط كل الكود في فولدر واحد وخلاص” — الفكرة إن الـ projects دي تعرف بعض وتستخدم بعض كأنهم packages متنشرة، من غير ما تنشرهم فعلًا.
- غيّرت Button في الـ design system؟ ← publish ← bump في 3 ريبوهات ← 3 PRs.
- الـ types بين الـ API والـ Front بتختلف من غير ما حد ياخد باله.
- كل ريبو بـ ESLint و tsconfig مختلفين.
- تغيير واحد + commit واحد (atomic commit) يعدّل الـ API والـ UI مع بعض.
- الـ types مصدرها واحد.
- Config موحّد، والـ build بيتعمل للي اتغيّر بس (مع Turbo/Nx).
الـ Workspace: أول طوبة
الـ workspace هو اللي بيقول لـ pnpm: “الفولدرات دي كلها packages تبعي، اربطهم ببعض”. بيتعرّف في ملف واحد في الـ root:
packages: - "apps/*" # كل فولدر جوه apps هو package - "packages/*" # وكل فولدر جوه packages برضه{ "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:
{ "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 بيبني node_modules؟ (symlinks و hard links)
ده أهم جزء في الأسبوع الأول. pnpm مختلف عن npm و yarn classic في إنه مش بيعمل node_modules مسطّحة (flat). بيمشي على 3 طبقات:
- الـ Store العام: كل نسخة من كل package بتتخزن مرة واحدة بس على الجهاز كله (content-addressable). عندك 20 مشروع فيهم react 18.3.1؟ نسخة واحدة على الديسك.
- الـ Virtual Store
node_modules/.pnpm: فيه فولدر لكلpackage@version، والملفات جواه hard links للـ store (يعني مش بتاخد مساحة زيادة). - الـ 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..." build | web + كل اللي بيعتمد عليه |
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
{ "name": "@repo/greet", "version": "1.0.0", "private": true, "type": "module", "main": "./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 هيخزنها كام مرة على الديسك؟
٢) نشرت @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 للفولدر الأصلي، مفيش نسخة.