إطار Next.js

المستودع الأحادي مع Next.js

28 دقيقة الدرس 33 من 40

مقدمة إلى بنية المستودع الأحادي

المستودع الأحادي (monorepo) هو استراتيجية تطوير برمجيات حيث يتم تخزين كود مشاريع متعددة في مستودع واحد. بالنسبة لتطبيقات Next.js، تتيح المستودعات الأحادية مشاركة الكود والأدوات المتسقة والتغييرات الذرية عبر تطبيقات وحزم متعددة.

لماذا المستودع الأحادي؟ المستودعات الأحادية مثالية للمؤسسات التي تبني تطبيقات متعددة ذات صلة (تطبيق ويب، تطبيق جوال، لوحة إدارة) تشارك منطق الأعمال المشترك ومكونات واجهة المستخدم والأدوات المساعدة.

نظرة عامة على Turborepo

Turborepo هو نظام بناء عالي الأداء لقواعد أكواد JavaScript و TypeScript، محسّن للمستودعات الأحادية. يوفر التخزين المؤقت الذكي والتنفيذ المتوازي وإدارة التبعيات الفعالة.

الميزات الرئيسية

  • التخزين المؤقت عن بُعد: مشاركة نتائج البناء عبر الأجهزة و CI/CD
  • البناء التدريجي: إعادة بناء ما تغير فقط
  • خطوط أنابيب المهام: تحديد التبعيات بين المهام
  • التنفيذ المتوازي: تشغيل المهام عبر الحزم في وقت واحد
  • التجزئة الواعية بالمحتوى: التخزين المؤقت بناءً على محتوى الملف، وليس الطوابع الزمنية

إعداد Turborepo

التثبيت

# إنشاء مستودع أحادي جديد مع Next.js
npx create-turbo@latest my-monorepo

# أو الإضافة إلى مستودع أحادي موجود
npm install turbo --save-dev

بنية المستودع الأحادي الأساسية

my-monorepo/
├── apps/
│   ├── web/              # تطبيق Next.js الرئيسي
│   │   ├── app/
│   │   ├── package.json
│   │   └── next.config.js
│   ├── admin/            # لوحة الإدارة Next.js
│   │   ├── app/
│   │   ├── package.json
│   │   └── next.config.js
│   └── docs/             # موقع التوثيق
│       ├── app/
│       └── package.json
├── packages/
│   ├── ui/               # مكونات واجهة المستخدم المشتركة
│   │   ├── src/
│   │   ├── package.json
│   │   └── tsconfig.json
│   ├── config/           # التكوينات المشتركة
│   │   ├── eslint/
│   │   ├── typescript/
│   │   └── package.json
│   └── utils/            # الأدوات المساعدة المشتركة
│       ├── src/
│       └── package.json
├── package.json          # package.json الجذر
├── turbo.json           # تكوين Turborepo
└── pnpm-workspace.yaml  # تكوين مساحة العمل

تكوين مساحة العمل

استخدام pnpm (موصى به)

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
# package.json الجذر
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "clean": "turbo run clean"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  },
  "packageManager": "pnpm@8.15.0"
}

استخدام npm Workspaces

# package.json الجذر
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev"
  }
}

نصيحة: يوصى باستخدام pnpm للمستودعات الأحادية نظرًا لاستخدامه الفعال لمساحة القرص وأوقات التثبيت السريعة. يستخدم مخزنًا قابلًا للعنونة بالمحتوى للحزم.

تكوين Turborepo

turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [".env", ".env.local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "clean": {
      "cache": false
    }
  }
}

شرح خط الأنابيب:

  • ^build - قم بتشغيل مهام البناء للتبعيات أولاً
  • outputs - الملفات المراد تخزينها مؤقتًا للتشغيلات المستقبلية
  • inputs - الملفات التي تؤثر على مخرجات المهمة
  • cache: false - تعطيل التخزين المؤقت لمهام dev/watch
  • persistent: true - استمرار تشغيل المهمة (لخوادم التطوير)

إنشاء حزم مشتركة

حزمة مكونات واجهة المستخدم المشتركة

# packages/ui/package.json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.tsx",
  "types": "./src/index.tsx",
  "scripts": {
    "lint": "eslint . --max-warnings 0"
  },
  "dependencies": {
    "react": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.0",
    "typescript": "^5.3.0"
  }
}
// packages/ui/src/Button.tsx
import { ReactNode } from 'react';

interface ButtonProps {
  children: ReactNode;
  onClick?: () => void;
  variant?: 'primary' | 'secondary';
}

export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={`btn btn-${variant}`}
    >
      {children}
    </button>
  );
}
// packages/ui/src/index.tsx
export { Button } from './Button';
export { Card } from './Card';
export { Input } from './Input';

حزمة تكوين TypeScript

# packages/config/typescript/package.json
{
  "name": "@repo/typescript-config",
  "version": "0.0.0",
  "private": true,
  "main": "index.js",
  "files": [
    "base.json",
    "nextjs.json",
    "react-library.json"
  ]
}
// packages/config/typescript/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "skipLibCheck": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true
  }
}
// packages/config/typescript/nextjs.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["src", "next-env.d.ts"],
  "exclude": ["node_modules"]
}

استخدام الحزم المشتركة في التطبيقات

تثبيت الحزم الداخلية

# apps/web/package.json
{
  "name": "web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*",
    "next": "14.1.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@repo/typescript-config": "workspace:*",
    "@repo/eslint-config": "workspace:*",
    "typescript": "^5.3.0"
  }
}
// apps/web/app/page.tsx
import { Button } from '@repo/ui';
import { formatDate } from '@repo/utils';

export default function HomePage() {
  return (
    <div>
      <h1>مرحبًا بك في تطبيقي</h1>
      <Button variant="primary">اضغط هنا</Button>
      <p>اليوم هو {formatDate(new Date())}</p>
    </div>
  );
}

تكوين TypeScript

// apps/web/tsconfig.json
{
  "extends": "@repo/typescript-config/nextjs.json",
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

تحذير: استخدم دائمًا بروتوكول workspace:* في تبعيات package.json عند الإشارة إلى الحزم الداخلية. هذا يضمن استخدام إصدار مساحة العمل المحلية.

خطوط أنابيب البناء

وضع التطوير

# تشغيل جميع التطبيقات في وضع التطوير
pnpm dev

# تشغيل تطبيق محدد
pnpm --filter web dev

# تشغيل تطبيقات محددة متعددة
pnpm --filter web --filter admin dev

بناء الإنتاج

# بناء جميع التطبيقات
pnpm build

# البناء مع تصور الرسم البياني
turbo run build --graph

# بناء تطبيق معين وتبعياته
pnpm --filter web build

التنفيذ المتوازي

# تشغيل lint عبر جميع الحزم بالتوازي
turbo run lint

# تشغيل الاختبارات مع أقصى توازٍ
turbo run test --concurrency=10

التخزين المؤقت عن بُعد

ذاكرة التخزين المؤقت عن بُعد لـ Vercel (مجانية)

# الربط بـ Vercel
npx turbo login
npx turbo link

# ستستخدم عمليات البناء اللاحقة ذاكرة التخزين المؤقت عن بُعد
pnpm build

ذاكرة التخزين المؤقت عن بُعد المخصصة

# .turborc
{
  "teamId": "your-team-id",
  "apiUrl": "https://your-cache-server.com"
}

نصيحة: يمكن أن يقلل التخزين المؤقت عن بُعد من أوقات بناء CI/CD بنسبة 40-60٪ من خلال مشاركة نتائج البناء عبر أعضاء الفريق وأجهزة CI.

متغيرات البيئة

متغيرات البيئة المشتركة

# .env الجذر
DATABASE_URL="postgresql://..."
REDIS_URL="redis://..."
# apps/web/.env.local
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_APP_NAME="تطبيق الويب"
# apps/admin/.env.local
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_APP_NAME="لوحة الإدارة"

حزمة متغيرات البيئة

// packages/env/src/index.ts
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  REDIS_URL: z.string().url(),
  NODE_ENV: z.enum(['development', 'production', 'test'])
});

export const env = envSchema.parse(process.env);

// الاستخدام في التطبيقات
import { env } from '@repo/env';
console.log(env.DATABASE_URL);

الاختبار في المستودع الأحادي

تكوين Jest

// packages/config/jest/jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  transform: {
    '^.+\\.ts$': 'ts-jest'
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/*.test.ts'
  ]
};
// apps/web/jest.config.js
const base = require('@repo/jest-config');

module.exports = {
  ...base,
  displayName: 'web',
  testEnvironment: 'jsdom'
};

CI/CD مع Turborepo

GitHub Actions

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm turbo run build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

      - name: Test
        run: pnpm turbo run test

      - name: Lint
        run: pnpm turbo run lint

تمرين: بناء نظام تجارة إلكترونية بمستودع أحادي

أنشئ مستودعًا أحاديًا يحتوي على:

  1. التطبيقات:
    • متجر موجه للعملاء (Next.js)
    • لوحة تحكم الإدارة (Next.js)
    • توثيق API (Next.js)
  2. الحزم:
    • @repo/ui - مكونات مشتركة (Button، Card، Modal)
    • @repo/database - مخطط Prisma والأدوات المساعدة
    • @repo/auth - منطق المصادقة
    • @repo/config - تكوينات مشتركة (ESLint، TypeScript)
  3. تكوين Turborepo مع التخزين المؤقت المناسب
  4. إعداد GitHub Actions لـ CI/CD
  5. تنفيذ التخزين المؤقت عن بُعد

مكافأة: أضف مساحة عمل تطبيق الجوال باستخدام React Native مع منطق الأعمال المشترك.

أفضل الممارسات

إدارة التبعيات

  • حافظ على مزامنة التبعيات المشتركة عبر الحزم
  • استخدم الإصدارات الدقيقة للتبعيات الحرجة
  • قم بتحديث التبعيات بانتظام باستخدام pnpm update --latest

تنظيم الكود

  • اجعل الحزم صغيرة ومركزة
  • استخدم اصطلاحات تسمية واضحة (@repo/package-name)
  • قم بتوثيق واجهات برمجة التطبيقات للحزم باستخدام TypeScript والتعليقات

الأداء

  • قم بتمكين التخزين المؤقت عن بُعد للتعاون الجماعي
  • استخدم --filter لتشغيل المهام فقط حيث تكون مطلوبة
  • قم بتكوين outputs المناسبة للتخزين المؤقت
  • تجنب التبعيات الدائرية بين الحزم

نصيحة محترف: استخدم turbo run build --dry-run لرؤية ما سيتم تخزينه مؤقتًا وما هي التبعيات الموجودة دون تشغيل المهام فعليًا.