مقدمة
حين يتوسع منتجك من تطبيق واحد إلى عدة مشاريع مترابطة — موقع للعملاء، ولوحة تحكم للإدارة، ومكتبة مكوّنات مشتركة — يصبح الحفاظ على تزامن الكود بين مستودعات منفصلة أمرًا مؤلمًا. تنتهي إلى نسخ المكوّنات ولصقها، وصيانة منطق مكرر، والتخوف من لحظة تحديث أداة مشتركة في خمسة أماكن في آنٍ واحد.
Nx هو نظام بناء ذكي صُمّم تحديدًا لهذه المشكلة. يحوّل مشاريع متعددة إلى مساحة عمل واحدة متماسكة حيث:
- تُستورد المكتبات المشتركة كأي حزمة npm عادية (
@myorg/ui) - يُعاد بناء واختبار الكود المتأثر بتغييراتك فقط
- توفر المولّدات (Generators) بنيةً موحدة للكود عبر الفرق
- التخزين المؤقت عن بُعد عبر Nx Cloud يجعل خط CI يعمل في ثوانٍ لا دقائق
يأخذك هذا الدليل خطوةً بخطوة لبناء مساحة Nx جاهزة للإنتاج، تضم تطبيقَي Next.js يشتركان في مكتبة مكوّنات UI ومكتبة أدوات مساعدة.
المتطلبات الأساسية
قبل البدء، تأكد من توفر ما يلي:
- Node.js 20 أو أحدث (
node --version) - pnpm 8 أو أحدث (
npm install -g pnpm) - معرفة أساسية بـ TypeScript وReact/Next.js
- Git مُعدَّل على جهازك
ما ستبنيه
في نهاية هذا الدرس، ستمتلك مساحة عمل تحتوي على:
apps/web— تطبيق Next.js 15 الرئيسيapps/admin— تطبيق Next.js 15 للإدارةlibs/ui— مكتبة مكوّنات React مشتركة (@myorg/ui)libs/utils— أدوات TypeScript مشتركة (@myorg/utils)- إعدادات CI تستخدم
nx affectedلتجاهل المشاريع غير المتغيرة
الخطوة 1: إنشاء مساحة Nx
استخدم أمر البدء الرسمي لإنشاء مساحة عمل فارغة:
npx create-nx-workspace@latest myorg --preset=empty --pm=pnpm
cd myorgالخيار --preset=empty ينشئ مساحة عمل بسيطة — بدون تطبيقات بعد، فقط إعدادات Nx. ستجد:
myorg/
├── nx.json # إعدادات Nx وقواعد التخزين المؤقت
├── package.json # التبعيات الجذرية
├── pnpm-workspace.yaml # إعداد pnpm للمساحة المشتركة
└── tsconfig.base.json # مسارات TypeScript المشتركة
ثبّت الإضافات اللازمة:
pnpm add -D @nx/next @nx/react @nx/jsالخطوة 2: إضافة تطبيقَي Next.js
توليد التطبيق الرئيسي:
npx nx generate @nx/next:app web \
--directory=apps/web \
--style=css \
--appRouter=trueتوليد تطبيق الإدارة:
npx nx generate @nx/next:app admin \
--directory=apps/admin \
--style=css \
--appRouter=trueتحقق من عمل كلٍّ منهما:
npx nx serve web # يعمل على http://localhost:3000
npx nx serve admin # يعمل على http://localhost:4200كل تطبيق مستقل تمامًا — له next.config.js خاص، وموجّه app/ مستقل، ومنفذ (port) مختلف.
الخطوة 3: إنشاء مكتبة UI مشتركة
ولّد مكتبة مكوّنات React يمكن لكلا التطبيقين استيرادها:
npx nx generate @nx/react:library ui \
--directory=libs/ui \
--unitTestRunner=vitest \
--bundler=vite \
--importPath=@myorg/uiالخيار --importPath يُحدد اسم الاستيراد. يمكن لأي تطبيق في المساحة كتابة import { Button } from '@myorg/ui' ويقوم TypeScript بتحليله تلقائيًا.
أنشئ مكوّن Button قابلًا لإعادة الاستخدام:
// libs/ui/src/lib/button/button.tsx
import React from 'react';
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
onClick?: () => void;
disabled?: boolean;
className?: string;
}
export function Button({
children,
variant = 'primary',
onClick,
disabled,
className = '',
}: ButtonProps) {
const base = 'px-4 py-2 rounded font-medium transition-colors focus:outline-none focus:ring-2';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200 focus:ring-gray-400',
danger: 'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500',
};
return (
<button
className={`${base} ${variants[variant]} ${className}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}أنشئ مكوّن Card:
// libs/ui/src/lib/card/card.tsx
import React from 'react';
export interface CardProps {
title: string;
description?: string;
children?: React.ReactNode;
className?: string;
}
export function Card({ title, description, children, className = '' }: CardProps) {
return (
<div className={`rounded-lg border border-gray-200 bg-white p-6 shadow-sm ${className}`}>
<h3 className="mb-1 text-lg font-semibold text-gray-900">{title}</h3>
{description && <p className="mb-4 text-sm text-gray-600">{description}</p>}
{children}
</div>
);
}صدّر كل شيء من نقطة الدخول للمكتبة:
// libs/ui/src/index.ts
export * from './lib/button/button';
export * from './lib/card/card';الخطوة 4: إنشاء مكتبة الأدوات المشتركة
ولّد مكتبة TypeScript خالصة (بدون تبعية React) للمنطق التجاري:
npx nx generate @nx/js:library utils \
--directory=libs/utils \
--unitTestRunner=vitest \
--bundler=tsc \
--importPath=@myorg/utilsأضف دوال التنسيق:
// libs/utils/src/lib/format.ts
export function formatDate(date: Date, locale = 'ar-TN'): string {
return new Intl.DateTimeFormat(locale, {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
}
export function formatCurrency(
amount: number,
currency = 'TND',
locale = 'ar-TN'
): string {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
}).format(amount);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
export function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength).trimEnd() + '…';
}أضف أنواع TypeScript المشتركة:
// libs/utils/src/lib/types.ts
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, string[]>;
}
export interface SelectOption<T = string> {
label: string;
value: T;
disabled?: boolean;
}صدّر من نقطة الدخول:
// libs/utils/src/index.ts
export * from './lib/format';
export * from './lib/types';الخطوة 5: استخدام المكتبات المشتركة في التطبيقات
استورد المكتبات المشتركة في تطبيقات Next.js تمامًا كأي حزمة npm:
// apps/web/app/page.tsx
import { Button, Card } from '@myorg/ui';
import { formatDate, formatCurrency } from '@myorg/utils';
export default function HomePage() {
const today = new Date();
const price = 149.99;
return (
<main className="mx-auto max-w-4xl p-8">
<h1 className="mb-6 text-4xl font-bold">متجري</h1>
<div className="mb-8 grid gap-4 sm:grid-cols-2">
<Card title="تاريخ اليوم" description={formatDate(today)}>
<Button variant="secondary">عرض التقويم</Button>
</Card>
<Card title="منتج مميز" description={`السعر: ${formatCurrency(price)}`}>
<Button variant="primary">أضف إلى السلة</Button>
</Card>
</div>
</main>
);
}يستخدم تطبيق الإدارة المكوّنات نفسها:
// apps/admin/app/page.tsx
import { Button, Card } from '@myorg/ui';
import { formatDate } from '@myorg/utils';
export default function AdminDashboard() {
return (
<main className="p-8">
<h1 className="mb-4 text-2xl font-bold">لوحة الإدارة</h1>
<p className="mb-6 text-gray-600">آخر مزامنة: {formatDate(new Date())}</p>
<div className="flex gap-3">
<Button variant="primary">تصدير البيانات</Button>
<Button variant="danger">مسح التخزين المؤقت</Button>
</div>
</main>
);
}يحلل TypeScript عمليات الاستيراد عبر مسارات موجودة في tsconfig.base.json أعدّها Nx تلقائيًا.
الخطوة 6: استكشاف رسم بيان المشروع
يبني Nx رسم بيانًا حيًا للتبعيات في مساحة عملك. تصوّره بـ:
npx nx graphيفتح هذا واجهة مرئية في المتصفح تُظهر:
webيعتمد علىuiوutilsadminيعتمد علىuiوutilsuiلا يعتمد على شيءutilsلا يعتمد على شيء
هذا الرسم هو ما يُمكّن Nx من معرفة ما يجب إعادة بنائه عند تغيير الملفات. إذا عدّلت libs/utils/src/lib/format.ts، يعرف Nx أن web وadmin متأثران ويجب اختبارهما.
الخطوة 7: تشغيل المهام عبر مساحة العمل
يوفر Nx واجهةً موحدة لتشغيل أي مهمة في أي مشروع:
# تشغيل مشروع واحد
npx nx build web
npx nx test ui
npx nx lint utils
# تشغيل مهمة عبر جميع المشاريع
npx nx run-many -t build
npx nx run-many -t test
npx nx run-many -t lint
# تشغيل مهام متعددة بالتوازي
npx nx run-many -t build,test,lintالتخزين المؤقت المحلي يجعل عمليات البناء المتكررة فورية:
> nx build web
✔ nx run utils:build (231ms)
✔ nx run ui:build (1.2s)
✔ nx run web:build (4.1s)
> nx build web [التشغيل الثاني]
✔ nx run utils:build [ذاكرة مؤقتة محلية] (0s)
✔ nx run ui:build [ذاكرة مؤقتة محلية] (0s)
✔ nx run web:build [ذاكرة مؤقتة محلية] (0s)
الخطوة 8: Nx Affected — ثورة في الـ CI
هذه أقوى ميزة في Nx. بدلًا من إعادة بناء واختبار كل شيء عند كل commit، تعالج nx affected فقط المشاريع المتأثرة بتغييراتك.
# ما الذي تغيّر مقارنةً بـ main؟
npx nx affected -t build --base=main --head=HEAD
# اختبار المشاريع المتأثرة فقط
npx nx affected -t test --base=main
# lint وtest وbuild للمتأثرة فقط
npx nx affected -t lint,test,build --base=mainمثال عملي: أصلحت خطأً في libs/utils. يحسب Nx:
utils— تغيّر مباشرةً ✓ أعد البناءweb— يعتمد علىutils✓ أعد البناءadmin— يعتمد علىutils✓ أعد البناءui— لا تبعية علىutils✗ تجاهل
لو كان لديك 10 تطبيقات، تُعالَج فقط تلك التي تعتمد على utils.
إعداد GitHub Actions للـ CI
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # السجل الكامل ضروري لـ nx affected
- uses: pnpm/action-setup@v4
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: تحديد NX_BASE للـ PR
if: github.event_name == 'pull_request'
run: echo "NX_BASE=origin/${{ github.base_ref }}" >> $GITHUB_ENV
- name: تحديد NX_BASE للـ push
if: github.event_name == 'push'
run: echo "NX_BASE=HEAD~1" >> $GITHUB_ENV
- run: npx nx affected -t lint,test,build --base=$NX_BASEالخيار fetch-depth: 0 إلزامي — يحتاج Nx إلى سجل git الكامل لمقارنة الفروع.
الخطوة 9: Nx Cloud للتخزين المؤقت عن بُعد
التخزين المحلي يفيدك، أما Nx Cloud فيشارك الذاكرة المؤقتة مع فريقك بأكمله والـ CI:
npx nx connectاتبع الخطوات لربط مساحة عملك. بعد الربط:
- زميل يشغّل
nx build web→ نتيجة مخزّنة في Nx Cloud - الـ CI يشغّل البناء ذاته → يصل فوريًا من الذاكرة، بدون حساب
- تسحب آخر تغييرات وتشغّل → وصول فوري من الذاكرة
الخطة المجانية تغطي المشاريع مفتوحة المصدر والفرق الصغيرة. تحقق من nx.json بعد الربط:
{
"nxCloudAccessToken": "YOUR_TOKEN_HERE",
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"cacheableOperations": ["build", "test", "lint", "e2e"]
}
}
}
}الخطوة 10: المولّدات لبنية كود متسقة
مولّدات Nx تشبه قوالب الكود التي يشغّلها فريقك بدلًا من إنشاء الملفات يدويًا.
توليد مكوّن جديد في مكتبة UI:
npx nx generate @nx/react:component badge \
--project=ui \
--directory=src/lib/badge \
--exportيُنشئ هذا badge.tsx وbadge.spec.tsx ويُحدّث index.ts تلقائيًا.
توليد صفحة جديدة في تطبيق:
npx nx generate @nx/next:page pricing \
--project=web \
--directory=app/pricingيمكنك أيضًا كتابة مولّدات مخصصة لأنماط فريقك الخاصة — مثل مولّد ينشئ صفحة CRUD كاملة مع أنواع TypeScript وmarshRoute API واختبارات.
اختبار تطبيقك
نفّذ هذه القائمة للتحقق من أن مساحة العمل تعمل بشكل صحيح:
# 1. تثبيت جميع التبعيات
pnpm install
# 2. بناء المكتبات المشتركة
npx nx build ui
npx nx build utils
# 3. تشغيل تطبيق الويب — يجب أن يعرض Button وCard المشتركين
npx nx serve web
# افتح http://localhost:3000
# 4. تشغيل تطبيق الإدارة
npx nx serve admin
# افتح http://localhost:4200
# 5. تشغيل جميع الاختبارات
npx nx run-many -t test
# 6. التحقق من سلامة TypeScript
npx nx run-many -t typecheck
# 7. عرض رسم بيان التبعيات
npx nx graphإذا ظهر تطبيق الويب مع مكوّنات Button وCard والتاريخ والسعر المُنسَّقَين، فكل شيء يعمل.
استكشاف الأخطاء وإصلاحها
TypeScript لا يجد @myorg/ui أو @myorg/utils
افتح tsconfig.base.json في الجذر وتحقق من وجود مسارات الأنواع:
{
"compilerOptions": {
"paths": {
"@myorg/ui": ["libs/ui/src/index.ts"],
"@myorg/utils": ["libs/utils/src/index.ts"]
}
}
}إذا كانت غائبة، أعد تشغيل مولّد المكتبة — يجب أن يضيفها تلقائيًا.
nx affected تعتبر كل شيء متأثرًا
يحدث هذا حين لا يستطيع Nx إيجاد commit القاعدة. تأكد من استخدام fetch-depth: 0 في CI. محليًا:
git fetch origin main
npx nx affected -t build --base=origin/mainالذاكرة المؤقتة قديمة أو تالفة
امسح الذاكرة المؤقتة المحلية وأعد المحاولة:
npx nx reset
npx nx build webفشل pnpm بسبب أخطاء peer dependency
أضف إلى package.json:
{
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "19"
}
}
}
}الخطوات التالية
الآن بعد أن أصبحت مساحة عملك جاهزة، فكّر في هذه التوسعات:
- أضف طبقة خادم: ولّد تطبيق Hono أو NestJS في
apps/apiوأنشئ مكتبة@myorg/shared-typesلأنواع الطلبات والاستجابات - دمج Storybook: شغّل
nx generate @nx/storybook:configuration uiلإضافة عرض تقديمي للمكوّنات - تحديث الوحدات (Module Federation): استخدم
@nx/module-federationلتقسيم التطبيقات الكبيرة إلى micro-frontends مستقلة - اختبارات Playwright E2E: أضف
nx generate @nx/playwright:configuration --project=web-e2e - استكشف إضافات Nx: قواعد البيانات، Docker، والنشر — كلها متكاملة عبر إضافات المجتمع
خلاصة
بنيت مساحة Nx monorepo جاهزة للإنتاج، حيث يشترك تطبيقا Next.js في مكوّنات UI وأدوات مساعدة دون تكرار. أبرز ما يجب تذكّره:
- المكتبات المشتركة تجعل التغييرات ذرية — أصلح مرة واحدة، استفد في كل مكان
- رسم بيان المشروع يجعل جميع التبعيات صريحة وقابلة للاستعلام
- Nx Affected يقضي على وقت CI الضائع مع نمو الكود — يُعالَج ما تغيّر فقط
- المولّدات تفرض أنماطًا متسقة حتى يتبع كل مطور في الفريق الاتفاقيات ذاتها
- Nx Cloud يوسّع التخزين المؤقت من المحلي إلى الفريق بأكمله
تتألق الـ monorepos بشكل خاص مع نمو الفرق: بدلًا من تنسيق التغييرات العاجلة عبر مستودعات متعددة ونشر npm، يحدّث commit واحد كل شيء بشكل ذري. يجعل Nx هذا التنسيق غير مرئي.