بناء مستودع أحادي جاهز للإنتاج باستخدام Turborepo و Next.js والحزم المشتركة

مستودع واحد يحكمهم جميعًا. يُعدّ Turborepo نظام البناء عالي الأداء لمستودعات JavaScript و TypeScript الأحادية. في هذا الدليل التعليمي، ستبني مستودعًا أحاديًا بمستوى إنتاجي يتضمّن عدة تطبيقات Next.js ومكوّنات واجهة مستخدم مشتركة وإعدادات TypeScript مشتركة وتخزينًا مؤقتًا عن بُعد لتسريع بناء CI بشكل كبير.
ما ستتعلّمه
بنهاية هذا الدليل التعليمي، ستكون قادرًا على:
- إعداد مستودع أحادي Turborepo باستخدام مساحات عمل pnpm
- إنشاء عدة تطبيقات Next.js تتشارك الشيفرة البرمجية
- بناء مكتبة مكوّنات واجهة مستخدم مشتركة باستخدام TypeScript
- تهيئة حزمة إعدادات TypeScript مشتركة
- إنشاء حزمة أدوات مساعدة مشتركة تُستخدم عبر التطبيقات
- إعداد خطوط أنابيب Turborepo لتنسيق المهام بكفاءة
- تفعيل التخزين المؤقت عن بُعد لبناء CI شبه فوري
- نشر التطبيقات الفردية مع سير عمل CI/CD مناسب
المتطلبات الأساسية
قبل البدء، تأكد من توفّر ما يلي:
- Node.js 20+ مُثبّت (
node --version) - pnpm 9+ مُثبّت (
pnpm --version) — إن لم يكن موجودًا:npm install -g pnpm - خبرة في TypeScript (الأنواع، الأنواع العامة، دقة الوحدات)
- إلمام بـ Next.js (App Router، Server Components)
- محرر أكواد — يُنصح بـ VS Code أو Cursor
لماذا Turborepo؟
مع نمو المشاريع، ينتهي بك الأمر غالبًا بعدة تطبيقات (موقع تسويقي، لوحة تحكم، لوحة إدارة) وشيفرة مشتركة (مكوّنات واجهة المستخدم، أدوات مساعدة، إعدادات). أمامك خياران:
- مستودعات متعددة: مستودع منفصل لكل حزمة. يصبح النشر وإدارة الإصدارات ومزامنة التبعيات أمرًا مرهقًا.
- مستودع أحادي: جميع الشيفرات في مستودع واحد مع أدوات مشتركة. أسهل لمشاركة الشيفرة والتغييرات الذرية عبر الحزم وتوحيد CI/CD.
يجعل Turborepo المستودعات الأحادية عملية من خلال حل مشكلتها الأكبر: أداء البناء. يوفّر:
| الميزة | الفائدة |
|---|---|
| تجزئة المهام | يعيد بناء ما تغيّر فقط |
| التنفيذ المتوازي | يشغّل المهام المستقلة بشكل متزامن |
| التخزين المؤقت عن بُعد | مشاركة مخرجات البناء عبر الفريق وCI |
| تنسيق خط الأنابيب | يحترم ترتيب التبعيات تلقائيًا |
| التبني التدريجي | يُضاف للمستودعات الحالية دون إعادة كتابة |
مقارنةً بالبدائل مثل Nx، يتميّز Turborepo بأنه أخف وزنًا، ولا يحتاج إعدادات افتراضية، ومُصمَّم خصيصًا لمنظومة JavaScript.
الخطوة 1: إنشاء هيكل المستودع الأحادي
أنشئ مشروع Turborepo جديدًا باستخدام القالب الرسمي:
pnpm dlx create-turbo@latest my-monorepoعند ظهور الخيارات:
- اختر pnpm كمدير الحزم
- اختر القالب الافتراضي
انتقل إلى المشروع:
cd my-monorepoسترى هذا الهيكل:
my-monorepo/
├── apps/
│ ├── web/ # Next.js app
│ └── docs/ # Next.js docs app
├── packages/
│ ├── ui/ # Shared UI components
│ ├── eslint-config/ # Shared ESLint config
│ └── typescript-config/ # Shared tsconfig
├── turbo.json # Turborepo pipeline config
├── pnpm-workspace.yaml
└── package.json
دعنا نفهم كل جزء قبل تخصيصه.
الخطوة 2: فهم هيكل مساحة العمل
ملف package.json الجذري
يُعرّف ملف package.json الجذري السكريبتات على مستوى مساحة العمل وتبعيات التطوير:
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"prettier": "^3.4.2",
"turbo": "^2.4.4",
"typescript": "^5.7.3"
},
"packageManager": "pnpm@9.15.4"
}لاحظ أن turbo run build لا يبني أي شيء بنفسه. بل ينسّق سكريبت build في كل حزمة مساحة عمل، مع احترام ترتيب التبعيات وتخزين النتائج مؤقتًا.
ملف pnpm-workspace.yaml
يُخبر هذا الملف pnpm بأماكن حزم مساحة العمل:
packages:
- "apps/*"
- "packages/*"ملف turbo.json
هذا هو قلب Turborepo — إعداد خط الأنابيب:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}المفاهيم الأساسية:
dependsOn: ["^build"]— الرمز^يعني "شغّل هذه المهمة في التبعيات أولًا." إذا كانwebيعتمد علىui، فسيعملui#buildقبلweb#build.outputs— الملفات التي يجب على Turborepo تخزينها مؤقتًا. في عمليات التشغيل اللاحقة، إذا لم تتغيّر المدخلات، يُعيد Turborepo تشغيل المخرجات المخزّنة فورًا.cache: false— خوادم التطوير يجب ألا تُخزَّن مؤقتًا أبدًا.persistent: true— يُعلّم المهام طويلة التشغيل (مثل خوادم التطوير) التي لا تنتهي.
الخطوة 3: إنشاء مكتبة مكوّنات واجهة مستخدم مشتركة
يتضمّن القالب حزمة ui أساسية. دعنا نُحسّنها لتصبح مكتبة مكوّنات متكاملة.
إعداد الحزمة
عدّل ملف packages/ui/package.json:
{
"name": "@repo/ui",
"version": "0.0.1",
"private": true,
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx",
"./input": "./src/input.tsx",
"./badge": "./src/badge.tsx",
"./dialog": "./src/dialog.tsx"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "^5.7.3"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}حقل exports بالغ الأهمية — فهو يُعرّف الواجهة العامة لحزمتك. بدلاً من تصدير واحد شامل، يحصل كل مكوّن على نقطة دخول خاصة به. هذا يُمكّن حذف الشيفرة غير المستخدمة (tree-shaking) بحيث لا تُجمَّع المكوّنات غير المُستخدمة.
إنشاء مكوّن Button
أنشئ ملف packages/ui/src/button.tsx:
import { type ButtonHTMLAttributes, forwardRef } from "react";
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "danger";
type ButtonSize = "sm" | "md" | "lg";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
}
const variantStyles: Record<ButtonVariant, string> = {
primary:
"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",
secondary:
"bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500",
outline:
"border-2 border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500",
ghost:
"text-gray-700 hover:bg-gray-100 focus:ring-gray-500",
danger:
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",
};
const sizeStyles: Record<ButtonSize, string> = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", size = "md", isLoading, className = "", children, disabled, ...props }, ref) => {
return (
<button
ref={ref}
className={`inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
disabled={disabled || isLoading}
{...props}
>
{isLoading && (
<svg
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = "Button";
export { Button, type ButtonProps };إنشاء مكوّن Card
أنشئ ملف packages/ui/src/card.tsx:
import type { HTMLAttributes, ReactNode } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
function Card({ children, className = "", ...props }: CardProps) {
return (
<div
className={`rounded-xl border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md ${className}`}
{...props}
>
{children}
</div>
);
}
function CardHeader({ children, className = "", ...props }: CardProps) {
return (
<div className={`mb-4 ${className}`} {...props}>
{children}
</div>
);
}
function CardTitle({ children, className = "", ...props }: CardProps) {
return (
<h3 className={`text-lg font-semibold text-gray-900 ${className}`} {...props}>
{children}
</h3>
);
}
function CardContent({ children, className = "", ...props }: CardProps) {
return (
<div className={`text-gray-600 ${className}`} {...props}>
{children}
</div>
);
}
export { Card, CardHeader, CardTitle, CardContent };إنشاء مكوّن Input
أنشئ ملف packages/ui/src/input.tsx:
import { type InputHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, className = "", id, ...props }, ref) => {
const inputId = id || label?.toLowerCase().replace(/\s+/g, "-");
return (
<div className="space-y-1">
{label && (
<label htmlFor={inputId} className="block text-sm font-medium text-gray-700">
{label}
</label>
)}
<input
ref={ref}
id={inputId}
className={`block w-full rounded-lg border px-3 py-2 text-gray-900 shadow-sm transition-colors focus:outline-none focus:ring-2 focus:ring-offset-1 ${
error
? "border-red-300 focus:border-red-500 focus:ring-red-500"
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
} ${className}`}
{...props}
/>
{error && <p className="text-sm text-red-600">{error}</p>}
</div>
);
}
);
Input.displayName = "Input";
export { Input, type InputProps };إنشاء مكوّن Badge
أنشئ ملف packages/ui/src/badge.tsx:
import type { HTMLAttributes } from "react";
type BadgeVariant = "default" | "success" | "warning" | "error" | "info";
interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
variant?: BadgeVariant;
}
const variantStyles: Record<BadgeVariant, string> = {
default: "bg-gray-100 text-gray-800",
success: "bg-green-100 text-green-800",
warning: "bg-yellow-100 text-yellow-800",
error: "bg-red-100 text-red-800",
info: "bg-blue-100 text-blue-800",
};
function Badge({ variant = "default", className = "", children, ...props }: BadgeProps) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${variantStyles[variant]} ${className}`}
{...props}
>
{children}
</span>
);
}
export { Badge, type BadgeProps };الخطوة 4: إنشاء حزمة أدوات مساعدة مشتركة
المستودعات الأحادية الحقيقية تتشارك أكثر من مجرد مكوّنات. أنشئ حزمة أدوات مساعدة للمنطق المشترك.
تهيئة الحزمة
mkdir -p packages/utils/srcأنشئ ملف packages/utils/package.json:
{
"name": "@repo/utils",
"version": "0.0.1",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./cn": "./src/cn.ts",
"./format": "./src/format.ts",
"./validators": "./src/validators.ts"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*",
"typescript": "^5.7.3"
}
}إنشاء دوال الأدوات المساعدة
أنشئ ملف packages/utils/src/cn.ts — أداة دمج أسماء الأصناف الكلاسيكية:
type ClassValue = string | number | boolean | undefined | null | ClassValue[];
export function cn(...inputs: ClassValue[]): string {
return inputs
.flat(Infinity)
.filter((x) => typeof x === "string" && x.length > 0)
.join(" ");
}أنشئ ملف packages/utils/src/format.ts:
export function formatCurrency(amount: number, currency = "USD", locale = "en-US"): string {
return new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(amount);
}
export function formatDate(date: Date | string, locale = "en-US"): string {
const d = typeof date === "string" ? new Date(date) : date;
return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
}).format(d);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_]+/g, "-")
.replace(/^-+|-+$/g, "");
}أنشئ ملف packages/utils/src/validators.ts:
export function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export function isNotEmpty(value: string): boolean {
return value.trim().length > 0;
}
export function isWithinLength(value: string, min: number, max: number): boolean {
const len = value.trim().length;
return len >= min && len <= max;
}أنشئ ملف packages/utils/src/index.ts:
export { cn } from "./cn";
export { formatCurrency, formatDate, slugify } from "./format";
export { isValidEmail, isNotEmpty, isWithinLength } from "./validators";أضف ملف tsconfig.json في packages/utils/tsconfig.json:
{
"extends": "@repo/typescript-config/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"]
}الخطوة 5: تهيئة تطبيق الويب لاستخدام الحزم المشتركة
الآن اربط الحزم المشتركة بتطبيق Next.js الخاص بك.
إضافة التبعيات
في ملف apps/web/package.json، أضف تبعيات مساحة العمل:
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}شغّل pnpm install من الجذر لربط كل شيء:
pnpm installتهيئة Next.js للمستودع الأحادي
يحتاج Next.js لمعرفة الحزم خارج مجلده. عدّل ملف apps/web/next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@repo/ui", "@repo/utils"],
};
export default nextConfig;خيار transpilePackages يُخبر Next.js بتجميع حزم مساحة العمل هذه عبر المُجمّع الخاص به، مما يتيح لك كتابة TypeScript مباشرةً دون خطوة بناء منفصلة.
استخدام المكوّنات المشتركة
الآن استخدم الحزم المشتركة في تطبيقك. عدّل ملف apps/web/app/page.tsx:
import { Button } from "@repo/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@repo/ui/card";
import { Input } from "@repo/ui/input";
import { Badge } from "@repo/ui/badge";
import { formatDate } from "@repo/utils/format";
export default function Home() {
return (
<main className="mx-auto max-w-4xl p-8">
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900">
Turborepo Monorepo
</h1>
<p className="mt-2 text-lg text-gray-600">
Built with shared packages — {formatDate(new Date())}
</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle>Shared UI Components</CardTitle>
</CardHeader>
<CardContent>
<p className="mb-4">
These components come from <code>@repo/ui</code>:
</p>
<div className="flex flex-wrap gap-2">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Badges & Inputs</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-4 flex gap-2">
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="error">Failed</Badge>
<Badge variant="info">Info</Badge>
</div>
<Input label="Email" placeholder="you@example.com" />
</CardContent>
</Card>
</div>
</main>
);
}شغّل خادم التطوير للتحقق:
pnpm dev --filter=webالعلم --filter=web يُخبر Turborepo بتشغيل مهمة dev لتطبيق web فقط (وتبعياته).
الخطوة 6: إضافة تطبيق ثانٍ (لوحة تحكم إدارية)
تظهر القوة الحقيقية للمستودعات الأحادية عندما يكون لديك عدة تطبيقات تتشارك الشيفرة.
إنشاء تطبيق الإدارة
cd apps
pnpm dlx create-next-app@latest admin --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"ربط التبعيات المشتركة
عدّل ملف apps/admin/package.json لإضافة تبعيات مساحة العمل:
{
"dependencies": {
"@repo/ui": "workspace:*",
"@repo/utils": "workspace:*",
"next": "^15.2.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}حدّث ملف apps/admin/next.config.ts:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@repo/ui", "@repo/utils"],
};
export default nextConfig;شغّل التثبيت من الجذر:
cd ..
pnpm installبناء صفحة لوحة التحكم الإدارية
أنشئ ملف apps/admin/app/page.tsx:
import { Button } from "@repo/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@repo/ui/card";
import { Badge } from "@repo/ui/badge";
import { formatCurrency, formatDate } from "@repo/utils/format";
const stats = [
{ label: "Total Revenue", value: formatCurrency(48250), change: "+12.5%" },
{ label: "Active Users", value: "2,420", change: "+8.1%" },
{ label: "Orders Today", value: "145", change: "+3.2%" },
{ label: "Conversion Rate", value: "3.6%", change: "-0.4%" },
];
const recentOrders = [
{ id: "ORD-001", customer: "Alice Johnson", amount: 299, status: "completed" as const },
{ id: "ORD-002", customer: "Bob Smith", amount: 149, status: "pending" as const },
{ id: "ORD-003", customer: "Charlie Brown", amount: 499, status: "completed" as const },
{ id: "ORD-004", customer: "Diana Ross", amount: 89, status: "failed" as const },
];
const statusVariant = {
completed: "success",
pending: "warning",
failed: "error",
} as const;
export default function AdminDashboard() {
return (
<main className="mx-auto max-w-6xl p-8">
<div className="mb-8 flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
<p className="text-gray-500">{formatDate(new Date())}</p>
</div>
<Button variant="primary">Export Report</Button>
</div>
<div className="mb-8 grid gap-4 md:grid-cols-4">
{stats.map((stat) => (
<Card key={stat.label}>
<CardContent>
<p className="text-sm text-gray-500">{stat.label}</p>
<p className="mt-1 text-2xl font-bold">{stat.value}</p>
<Badge variant={stat.change.startsWith("+") ? "success" : "error"}>
{stat.change}
</Badge>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>Recent Orders</CardTitle>
</CardHeader>
<CardContent>
<table className="w-full">
<thead>
<tr className="border-b text-left text-sm text-gray-500">
<th className="pb-3">Order ID</th>
<th className="pb-3">Customer</th>
<th className="pb-3">Amount</th>
<th className="pb-3">Status</th>
</tr>
</thead>
<tbody>
{recentOrders.map((order) => (
<tr key={order.id} className="border-b last:border-0">
<td className="py-3 font-mono text-sm">{order.id}</td>
<td className="py-3">{order.customer}</td>
<td className="py-3">{formatCurrency(order.amount)}</td>
<td className="py-3">
<Badge variant={statusVariant[order.status]}>{order.status}</Badge>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</main>
);
}الآن يتشارك كلا التطبيقين web و admin نفس مكوّنات واجهة المستخدم والأدوات المساعدة. غيّر مكوّنًا مرة واحدة، وسيتحدّث في كل مكان.
الخطوة 7: تهيئة خطوط أنابيب Turborepo
حدّث ملف turbo.json للتعامل مع جميع المهام بكفاءة:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"type-check": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}أضف السكريبتات الجديدة إلى ملف package.json الجذري:
{
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"dev:web": "turbo run dev --filter=web",
"dev:admin": "turbo run dev --filter=admin",
"lint": "turbo run lint",
"type-check": "turbo run type-check",
"test": "turbo run test",
"clean": "turbo run clean && rm -rf node_modules .turbo"
}
}فهم تبعيات المهام
رسم التبعيات هو مفتاح كفاءة Turborepo:
build task flow:
@repo/typescript-config (no build needed)
→ @repo/utils#build
→ @repo/ui#build
→ web#build (depends on ui + utils)
→ admin#build (depends on ui + utils)
يشغّل Turborepo المهمتين utils#build و ui#build بالتوازي (لأنهما مستقلتان)، ثم يشغّل web#build و admin#build بالتوازي بمجرد انتهاء تبعياتهما.
الخطوة 8: تفعيل التخزين المؤقت عن بُعد
التخزين المؤقت عن بُعد هو الميزة القاتلة في Turborepo. يتيح لك مشاركة مخرجات البناء بين جهازك المحلي وزملائك في الفريق وCI — بحيث لا يعيد أحد بناء ما بناه شخص آخر بالفعل.
ربط المستودع
npx turbo login
npx turbo linkهذا يربط مستودعك الأحادي بالتخزين المؤقت عن بُعد لـ Vercel (يتوفر مستوى مجاني). بعد الربط، سترى تأكيدًا:
✓ Linked to my-org/my-monorepo
كيف يعمل
- البناء الأول: يشغّل Turborepo المهمة، ويحسب تجزئة المدخلات (ملفات المصدر، التبعيات، متغيرات البيئة)، ويرفع المخرجات إلى التخزين المؤقت عن بُعد.
- البناء الثاني (أو CI، أو زميل في الفريق): يحسب Turborepo نفس التجزئة، ويجدها في التخزين المؤقت عن بُعد، ويُنزّل المخرجات — متخطيًا البناء الفعلي.
# First build — runs everything
pnpm build
# → web#build: cache miss, computing...
# → admin#build: cache miss, computing...
# Second build — instant replay
pnpm build
# → web#build: cache hit, replaying output
# → admin#build: cache hit, replaying outputبالنسبة للمستودعات الأحادية الكبيرة، يمكن أن يُقلّل هذا أوقات CI من 20 دقيقة إلى أقل من دقيقة واحدة.
التخزين المؤقت عن بُعد المُستضاف ذاتيًا
إذا كنت تفضّل عدم استخدام Vercel، يمكنك استضافة خادم التخزين المؤقت بنفسك. يدعم Turborepo أي تخزين متوافق مع S3:
// turbo.json
{
"remoteCache": {
"enabled": true,
"signature": true
}
}استخدم متغيرات البيئة TURBO_API و TURBO_TOKEN و TURBO_TEAM لتهيئة نقطة نهاية مُستضافة ذاتيًا.
الخطوة 9: إعداد CI/CD باستخدام GitHub Actions
أنشئ ملف .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm type-check
- name: Build
run: pnpm build
- name: Test
run: pnpm testنشر التطبيقات المتغيّرة فقط
يستطيع Turborepo اكتشاف التطبيقات التي تغيّرت ونشرها فقط. استخدم العلم --filter مع git diff:
deploy-web:
name: Deploy Web
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Check for changes
id: changes
run: |
if npx turbo-ignore web; then
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Deploy web app
if: steps.changes.outputs.skip == 'false'
run: echo "Deploying web app..."
# Add your deployment command hereيخرج أمر turbo-ignore برمز 0 إذا لم يتغيّر شيء في تلك الحزمة، مما يتيح لك تخطي عمليات النشر غير الضرورية.
الخطوة 10: أنماط متقدّمة
متغيرات البيئة في Turborepo
إذا كانت مخرجات البناء تعتمد على متغيرات البيئة، يجب إخبار Turborepo بها. وإلا، قد يقدّم بناءً مخزّنًا مؤقتًا استخدم قيم بيئة مختلفة.
// turbo.json
{
"globalEnv": ["NODE_ENV", "CI"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"env": ["DATABASE_URL", "NEXT_PUBLIC_API_URL"],
"outputs": [".next/**", "!.next/cache/**"]
}
}
}globalEnv— متغيرات تؤثّر على جميع المهام.env— متغيرات تؤثّر على مهمة محددة. التغييرات في هذه المتغيرات تُبطل التخزين المؤقت.
تصفية المهام
صيغة التصفية في Turborepo قوية:
# Run build for web and its dependencies
pnpm build --filter=web...
# Run build for everything except docs
pnpm build --filter=!docs
# Run build for packages that changed since main
pnpm build --filter=...[main]
# Run build for web and admin only
pnpm build --filter=web --filter=adminالحزم الداخلية مقابل المنشورة
حزمنا تحمل القيمة "private": true — أي أنها داخلية، تُستهلك فقط داخل المستودع الأحادي. للحزم المنشورة (npm)، ستُضيف:
{
"name": "@myorg/ui",
"version": "1.0.0",
"private": false,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts"
}
}استخدم changesets لإدارة إصدارات الحزم المنشورة.
الخطوة 11: إعداد Tailwind CSS المشترك
عندما تستخدم عدة تطبيقات Tailwind، شارك الإعدادات للحفاظ على اتساق رموز التصميم.
أنشئ ملف packages/tailwind-config/package.json:
{
"name": "@repo/tailwind-config",
"version": "0.0.1",
"private": true,
"main": "./tailwind.config.ts",
"exports": {
".": "./tailwind.config.ts"
},
"devDependencies": {
"tailwindcss": "^4.0.0"
}
}أنشئ ملف packages/tailwind-config/tailwind.config.ts:
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
theme: {
extend: {
colors: {
brand: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554",
},
},
fontFamily: {
sans: ["Inter", "system-ui", "sans-serif"],
mono: ["JetBrains Mono", "monospace"],
},
},
},
plugins: [],
};
export default config;في كل تطبيق، وسّع الإعدادات المشتركة:
// apps/web/tailwind.config.ts
import sharedConfig from "@repo/tailwind-config";
import type { Config } from "tailwindcss";
const config: Config = {
...sharedConfig,
content: [
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
"../../packages/ui/src/**/*.{ts,tsx}",
],
};
export default config;اختبار المستودع الأحادي
دعنا نتحقق من أن كل شيء يعمل من البداية إلى النهاية.
بناء جميع الحزم
pnpm buildيجب أن ترى Turborepo ينسّق البناء:
Tasks: 4 successful, 4 total
Cached: 0 cached, 4 total
Time: 12.4s
شغّله مرة أخرى
pnpm buildهذه المرة، كل شيء مخزّن مؤقتًا:
Tasks: 4 successful, 4 total
Cached: 4 cached, 4 total
Time: 0.3s >>> FULL TURBO
رسالة "FULL TURBO" تعني أن كل مهمة قُدّمت من التخزين المؤقت — لم يُنجَز أي عمل.
تشغيل خوادم التطوير
# All apps
pnpm dev
# Just the web app
pnpm dev:webاستكشاف الأخطاء وإصلاحها
أخطاء "Module not found"
إذا لم يتمكن تطبيق من العثور على حزمة مساحة عمل:
- تحقق من أن الحزمة مدرجة في تبعيات ملف
package.jsonالخاص بالتطبيق - شغّل
pnpm installمن الجذر - تأكد من أن
transpilePackagesتتضمّن الحزمة فيnext.config.ts
التخزين المؤقت لا يعمل
إذا لم تُخزَّن عمليات البناء مؤقتًا:
- شغّل
turbo run build --summarizeلفحص تجزئة المهمة - تحقق من أن
outputsفيturbo.jsonتتطابق مع مخرجات البناء الفعلية - تأكد من الإعلان عن متغيرات البيئة في
envأوglobalEnv
أخطاء مسارات TypeScript
إذا أظهر محررك أخطاء أنواع لحزم مساحة العمل:
- أعد تشغيل خادم TypeScript (
Cmd+Shift+Pثم "TypeScript: Restart TS Server") - تأكد من أن كل حزمة لديها ملف
tsconfig.jsonمناسب يمتد من الإعدادات المشتركة - تحقق من أن
exportsفيpackage.jsonتُشير إلى الملفات الصحيحة
ملخص هيكل المشروع
إليك الهيكل الكامل للمستودع الأحادي الذي بنيته:
my-monorepo/
├── apps/
│ ├── web/ # Marketing site
│ │ ├── app/page.tsx # Uses @repo/ui + @repo/utils
│ │ ├── next.config.ts # transpilePackages
│ │ └── package.json # workspace:* deps
│ └── admin/ # Admin dashboard
│ ├── app/page.tsx # Uses @repo/ui + @repo/utils
│ ├── next.config.ts
│ └── package.json
├── packages/
│ ├── ui/ # Shared UI components
│ │ └── src/
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── input.tsx
│ │ └── badge.tsx
│ ├── utils/ # Shared utilities
│ │ └── src/
│ │ ├── cn.ts
│ │ ├── format.ts
│ │ └── validators.ts
│ ├── tailwind-config/ # Shared Tailwind theme
│ ├── typescript-config/ # Shared tsconfig
│ └── eslint-config/ # Shared ESLint rules
├── .github/workflows/ci.yml # CI with remote caching
├── turbo.json # Pipeline config
├── pnpm-workspace.yaml
└── package.json
الخطوات التالية
الآن بعد أن أصبح لديك مستودع أحادي يعمل، فكّر في:
- إضافة Storybook لحزمة
uiلتوثيق المكوّنات - إعداد Changesets إذا كنت تخطط لنشر حزم على npm
- إضافة اختبارات E2E باستخدام Playwright في حزمة
packages/e2eمنفصلة - النشر على Vercel — لديها دعم أصلي لـ Turborepo مع اكتشاف تلقائي للمشاريع
- إضافة حزمة قاعدة بيانات مشتركة باستخدام Drizzle ORM للوصول المتسق للمخطط
الخلاصة
لقد بنيت مستودعًا أحاديًا جاهزًا للإنتاج باستخدام Turborepo يتضمّن:
- تطبيقان Next.js يتشاركان الشيفرة بسلاسة
- مكتبة واجهة مستخدم مشتركة بمكوّنات قابلة لإعادة الاستخدام ومُحدّدة الأنواع
- حزمة أدوات مساعدة مشتركة للمنطق المشترك
- إعدادات TypeScript و Tailwind مشتركة
- خطوط أنابيب بناء فعّالة مع التخزين المؤقت والتنفيذ المتوازي
- سير عمل CI/CD مع النشر الانتقائي
يُحوّل Turborepo إدارة المستودعات الأحادية من عبء إلى قوة خارقة. إن الجمع بين تجزئة المهام والتنفيذ المتوازي والتخزين المؤقت عن بُعد يعني أن عمليات البناء تبقى سريعة بغض النظر عن حجم قاعدة الشيفرة. ابدأ صغيرًا بحزمة مشتركة واحدة، ووسّع حسب متطلبات مشروعك.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.

Better Auth مع Next.js 15: الدليل الشامل للمصادقة في 2026
تعلم كيفية تنفيذ نظام مصادقة متكامل في Next.js 15 باستخدام Better Auth. يغطي هذا الدليل تسجيل الدخول بالبريد الإلكتروني وOAuth والجلسات وحماية المسارات والتحكم بالأدوار.

بناء تطبيق متكامل باستخدام Drizzle ORM و Next.js 15: قاعدة بيانات آمنة الأنواع من الصفر إلى الإنتاج
تعلّم كيفية بناء تطبيق متكامل آمن الأنواع باستخدام Drizzle ORM مع Next.js 15. يغطي هذا الدليل العملي تصميم المخططات والترحيلات وServer Actions وعمليات CRUD والنشر مع PostgreSQL.