Monorepo architektúra felépítése Next.js és Turborepo használatával

A modern szoftverfejlesztés egyik legnagyobb kihívása a projektek méretének és komplexitásának növekedése. Ahogy a csapatok és az alkalmazások bővülnek, a kódkezelés, a függőségek kezelése és a fejlesztési munkafolyamatok optimalizálása kulcsfontosságúvá válik. Ebben a kontextusban egyre népszerűbbé válnak a monorepo architektúrák, különösen a JavaScript ökoszisztémában. Ez a cikk arra fókuszál, hogyan építhetünk fel egy robusztus és hatékony monorepót Next.js front-end alkalmazásokhoz, a Turborepo erejével kiegészítve.

Miért érdemes a Monorepo-t választani?

Hagyományosan, ha több alkalmazásunk vagy könyvtárunk van, hajlamosak vagyunk mindegyiket külön-külön, saját verziókövetési tárhelyen (git repository) kezelni – ez az ún. „multirepo” megközelítés. Bár elsőre logikusnak tűnik, idővel komoly problémákhoz vezethet:

  • Kódmegosztás nehézségei: A közös UI komponensek, segédprogramok vagy API kliensek frissítése és szinkronizálása a különböző repók között macerás. Gyakran bonyolult publikálási folyamatokat igényel (npm registry).
  • Függőségi káosz: Különböző verziók használata ugyanabból a külső könyvtárból a projektek között, ami inkonzisztenciához és futásidejű hibákhoz vezethet.
  • Refaktorálás rémálma: Egy alapvető funkció vagy interfész módosítása több repóban is változást igényelhet, ami hatalmas koordinációs erőfeszítést jelent.
  • Közös tooling hiánya: Nehéz egységesíteni a lintereket, formázókat, build eszközöket a különböző projektek között.

A monorepo ezzel szemben egyetlen verziókövetési tárhelyet jelent, amely több, egymással összefüggő projekt kódját tartalmazza. Nem keverendő össze a monolitikus architektúrával, ahol minden egyetlen, hatalmas kódbázisban van. A monorepo sokkal inkább egy gyűjtőhely a logikailag elkülönülő projektek számára.

A Monorepo előnyei:

  • Egyszerűsített kódmegosztás: A közös kód, UI komponensek, type definition-ök könnyen importálhatók a belső csomagokból, mintha egy külső npm csomagot használnánk. Nincs szükség publikálásra a változások azonnali elérhetőségéhez.
  • Egységesített tooling: A gyökérszintű konfigurációk (ESLint, Prettier, TypeScript) biztosítják a konzisztenciát az összes projektben.
  • Atomikus változások: Egyetlen commit-ben refaktorálhatunk több alkalmazást és könyvtárat, biztosítva, hogy minden változás szinkronban legyen. Ez drámaian javítja a fejlesztői élményt.
  • Átláthatóság: A teljes kódbázis könnyebben átlátható és navigálható.
  • Egyszerűsített függőségkezelés: A workspace menedzserek (mint a pnpm vagy yarn) segítenek abban, hogy a függőségek ne legyenek duplikálva, csökkentve ezzel a telepítési időt és a lemezhasználatot.

Hátrányok és megoldások:

Természetesen a monorepo sem csodaszer. Komplexitást hozhat a kezdeti beállítás és a nagy kódbázis kezelése. A lassú build idők, a cache-elés hiánya és a CI/CD folyamatok optimalizálása kihívást jelenthet. Pontosan ezen problémák megoldására nyújt hatékony eszközt a Turborepo, amit hamarosan részletesebben is megvizsgálunk.

Next.js: Az Ideális Frontend Keretrendszer Monorepóban

A Next.js egy React keretrendszer, amely lehetővé teszi a szerveroldali renderelést (SSR), statikus oldalgenerálást (SSG) és inkrementális statikus regenerálást (ISR). Ezek a funkciók optimalizálják a teljesítményt és a SEO-t, miközben kiváló fejlesztői élményt biztosítanak.

Monorepo környezetben a Next.js különösen jól teljesít, mert:

  • Önálló alkalmazások: Lehetővé teszi több, különálló, de mégis összefüggő webalkalmazás (pl. admin panel, marketing oldal, ügyfélportál) hatékony kezelését egyetlen repóban.
  • API Routes: A beépített API route funkcionalitásnak köszönhetően könnyen építhetünk kis háttérszolgáltatásokat, amelyek szorosan kapcsolódnak az UI-hoz, és megoszthatnak kódot a frontend réteggel.
  • Könnyű integráció: Zökkenőmentesen integrálható megosztott komponenskönyvtárakkal és segédprogramokkal, amik a monorepo `packages` mappájában helyezkednek el.
  • Moduláris felépítés: A Next.js modularitása illeszkedik a monorepo „project-per-package” filozófiájához, ahol minden alkalmazás vagy könyvtár önálló egységként működik.

Turborepo: A Monorepo Teljesítmény Motorja

A Turborepo egy nagy teljesítményű build rendszer JavaScript/TypeScript monorepókhoz, melyet a Vercel fejleszt. Fő célja, hogy drámaian felgyorsítsa a build, test, lint és egyéb feladatok futtatását azáltal, hogy csak a szükséges munkát végzi el.

Hogyan működik a Turborepo?

  • Intelligens Task Futás: A Turborepo egy függőségi gráfot épít az összes projekt és azok feladatai között. Csak azokat a taskokat futtatja újra, amelyeknek a bemenete megváltozott.
  • Fast Incremental Builds: Ha egy fájl megváltozik egy csomagban, a Turborepo csak azt a csomagot és az összes rá támaszkodó csomagot építi újra. A már elkészült build eredményeket a cache-ből veszi.
  • Content-Aware Caching: A Turborepo nem csak azt nézi, hogy egy fájl megváltozott-e, hanem a tartalmát is figyelembe veszi. Egyedi hash-eket generál a fájlok tartalmából, így pontosan tudja, mikor változott meg valami érdemben.
  • Remote Caching: A Turborepo képes a build cache-t megosztani a csapat tagjai és a CI/CD környezetek között (pl. Vercel Artifacts, AWS S3). Ez azt jelenti, hogy ha valaki már lefuttatott egy buildet, és a cache-t feltöltötte, a többiek letölthetik ezt a cache-t, és nem kell újra építeniük ugyanazt a kódot. Ez óriási időmegtakarítást jelent, különösen a CI/CD pipeline-okban.
  • Parallel Execution: Egyszerre több feladatot is futtat, maximalizálva a CPU kihasználtságát.

A Turborepo kulcsfontosságú a skálázható monorepo architektúrákban, mert a build idők optimalizálásával fenntartja a gyors fejlesztési ciklust még akkor is, ha a kódbázis gigantikusra nő.

A Monorepo Architektúra Felépítése: Lépésről Lépésre

Nézzük meg, hogyan hozhatjuk létre a monorepót. Ebben a példában a pnpm-et használjuk workspace menedzserként, ami kiválóan alkalmas a monorepók kezelésére a hatékony függőségkezelése miatt.

1. Kezdeti beállítás és gyökér `package.json`

Hozzuk létre a projekt gyökérkönyvtárát, és inicializáljuk a pnpm-et:


mkdir my-nextjs-turborepo
cd my-nextjs-turborepo
pnpm init

A gyökér package.json fájlunkat módosítsuk, hogy tartalmazza a "private": true beállítást és a "workspaces" definíciót:


{
  "name": "my-nextjs-turborepo-root",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev --parallel",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "latest"
  },
  "workspaces": [
    "apps/*",
    "packages/*"
  ]
}

Telepítsük a Turborepo-t fejlesztői függőségként:


pnpm add -D turbo

A "workspaces" alatt definiáljuk azokat a mappákat, ahol a projektjeink és könyvtáraink (ún. „workspac”-ek) találhatóak. A "apps/*" és "packages/*" azt jelenti, hogy az apps és packages mappákban található összes almappát workspace-ként fogja kezelni a pnpm.

2. Mappastruktúra kialakítása

Hozzuk létre az alapvető mappastruktúrát:


mkdir apps packages
  • apps/: Ide kerülnek a futtatható alkalmazások, mint például a Next.js front-endek, Storybook instance-ek, vagy backend szolgáltatások.
  • packages/: Itt lesznek a megosztott könyvtárak és komponensgyűjtemények (pl. UI komponensek, utility függvények, TypeScript konfigurációk, ESLint konfigurációk).

3. Turborepo konfiguráció (`turbo.json`)

Hozzuk létre a turbo.json fájlt a gyökérkönyvtárban. Ez a fájl mondja meg a Turborepo-nak, hogyan kezelje a különböző task-okat.


// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}

A "pipeline" szekció definiálja a task-okat (pl. build, lint, dev).

  • "build": A "^build" azt jelenti, hogy mielőtt egy csomagot építene, előbb megpróbálja építeni az összes olyan csomagot, amitől az adott csomag függ. Az "outputs" megadja, mely fájlokat kell cache-elni a build után.
  • "lint": Nem generál kimeneti fájlokat, ezért az "outputs" üres.
  • "dev": A "cache": false kikapcsolja a cache-t, mert fejlesztés közben mindig friss eredményre van szükségünk. A "persistent": true azt jelenti, hogy a dev parancs folyamatosan fut, nem áll le azonnal.

4. Next.js alkalmazás hozzáadása

Navigáljunk az apps mappába, és hozzunk létre egy Next.js alkalmazást:


cd apps
pnpm create next-app@latest my-web-app --typescript --eslint --tailwind --app

A prompt-ok során válasszuk a nekünk megfelelő beállításokat. A folyamat befejeztével térjünk vissza a gyökérkönyvtárba:


cd ..

Ahhoz, hogy a Next.js alkalmazásunk megfelelően működjön a monorepóban, és képes legyen transzpilálni a megosztott csomagokat, módosítanunk kell a next.config.js fájlt az apps/my-web-app mappában (Next.js 13 és újabb verziókhoz):


// apps/my-web-app/next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  transpilePackages: ['@repo/ui', '@repo/utils'], // Például a megosztott csomagjaink
};

module.exports = nextConfig;

A transpilePackages beállítás kulcsfontosságú, mert a Next.js alapértelmezetten nem transzpilálja a node_modules-on kívüli csomagokat. Ezzel jelezzük neki, hogy a saját, belső csomagjainkat is fordítsa le.

5. Megosztott csomagok létrehozása

Most hozzunk létre néhány megosztott csomagot a packages mappában.

Példa: UI komponenskönyvtár (`@repo/ui`)


mkdir packages/ui
cd packages/ui
pnpm init

A packages/ui/package.json fájlunk:


{
  "name": "@repo/ui",
  "version": "0.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "license": "MIT",
  "scripts": {
    "lint": "eslint .",
    "generate:component": "turbo gen react-component"
  },
  "devDependencies": {
    "@repo/eslint-config": "*",
    "@repo/typescript-config": "*",
    "@types/react": "^18.2.37",
    "@types/node": "^20.9.0",
    "eslint": "^8.54.0",
    "react": "^18.2.0",
    "typescript": "^5.2.2"
  }
}

Figyeljük meg a "main" és "types" mezőket, amelyek az index fájlra mutatnak. A "@repo/eslint-config": "*" és "@repo/typescript-config": "*" azt jelzi, hogy ezek a csomagok belső hivatkozások más workspace-ekre, és a pnpm kezeli a szimbolikus linkelést. A "*" azt jelenti, hogy az adott package legújabb verzióját fogja használni a monorepon belül.

Hozzuk létre a src/Button.tsx komponenst:


// packages/ui/src/Button.tsx
import * as React from 'react';

export interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
}

export function Button({ children, onClick }: ButtonProps) {
  return (
    <button
      className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      onClick={onClick}
    >
      {children}
    </button>
  );
}

És az exportot az src/index.ts fájlban:


// packages/ui/src/index.ts
export * from './Button';

Példa: Utility függvények (`@repo/utils`)


mkdir packages/utils
cd packages/utils
pnpm init

A packages/utils/package.json fájlunk:


{
  "name": "@repo/utils",
  "version": "0.0.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "license": "MIT",
  "scripts": {
    "lint": "eslint ."
  },
  "devDependencies": {
    "@repo/eslint-config": "*",
    "@repo/typescript-config": "*",
    "typescript": "^5.2.2"
  }
}

Hozzuk létre a src/format.ts segédprogramot:


// packages/utils/src/format.ts
export function formatCurrency(amount: number, currency: string = 'USD'): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: currency,
  }).format(amount);
}

És az exportot az src/index.ts fájlban:


// packages/utils/src/index.ts
export * from './format';

Megosztott TypeScript és ESLint konfigurációk

Érdemes ezeket is megosztani, hogy egységes legyen a kódstílus és a TypeScript beállítás az összes projektben.


# packages/typescript-config
mkdir packages/typescript-config
cd packages/typescript-config
pnpm init
# ... package.json ...
# tsconfig.base.json, next.json, react-library.json fájlok létrehozása

# packages/eslint-config
mkdir packages/eslint-config
cd packages/eslint-config
pnpm init
# ... package.json ...
# library.js, next.js fájlok létrehozása

Ezeknek a csomagoknak a részletes tartalmát a Turborepo hivatalos példatárában találhatjuk meg, vagy a create-turbo paranccsal generált mintaprojektben.

6. Csomagok használata Next.js alkalmazásban

Most, hogy létrehoztuk a megosztott csomagokat, használjuk őket a my-web-app alkalmazásban. Először is, térjünk vissza a gyökérkönyvtárba, és telepítsük az összes függőséget:


cd ..
pnpm install

Ez létrehozza a szimbolikus linkeket a node_modules mappában, így a my-web-app hivatkozhat a @repo/ui és @repo/utils csomagokra.

Nyissuk meg az apps/my-web-app/app/page.tsx fájlt, és használjuk a megosztott komponenst és segédprogramot:


// apps/my-web-app/app/page.tsx
'use client';

import { Button } from '@repo/ui'; // Import a megosztott UI csomagból
import { formatCurrency } from '@repo/utils'; // Import a megosztott utils csomagból

export default function Home() {
  const handleButtonClick = () => {
    alert('Gomb megnyomva!');
  };

  return (
    <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100">
      <h1 className="text-4xl font-bold mb-8">Üdv a Next.js App-ban!</h1>
      <Button onClick={handleButtonClick}>Kattints rám!</Button>
      <p className="mt-4 text-lg">
        Példa formázott értékre: {formatCurrency(12345.67, 'EUR')}
      </p>
    </div>
  );
}

Most futtassuk az alkalmazást. A gyökérkönyvtárból:


pnpm dev --filter=my-web-app

A --filter=my-web-app paraméterrel csak a my-web-app project dev parancsát futtatjuk. Ha nem használnánk a filtert, a Turborepo megpróbálná futtatni az összes project dev parancsát.

Fejlesztési Munkafolyamat és Karbantartás

A monorepo beállítása után a mindennapi munkafolyamat rendkívül hatékony lesz:

  • Egyszerű parancsok: A turbo run dev vagy turbo run build parancsok a gyökérből indítják az összes releváns projektet, vagy szűrhetünk rájuk (pl. pnpm dev --filter=my-web-app).
  • Azonnali változások: Ha módosítunk egy komponenst a @repo/ui csomagban, a változások azonnal megjelennek a my-web-app-ban, nincs szükség npm publish-ra vagy linkelésre.
  • Villámgyors build-ek: A Turborepo cache-elése és inkrementális buildjei miatt a teljes monorepo buildelése (vagy akár csak egyetlen projekté) rendkívül gyors lesz, különösen a második futtatástól kezdve.
  • Egységes tesztelés és lintelés: A turbo run test és turbo run lint parancsokkal könnyedén futtathatjuk az összes projekt tesztjeit és lintereit, biztosítva a magas kódminőséget.

CI/CD Integráció és Deploy

A Turborepo kiválóan illeszkedik a modern CI/CD pipeline-okba. A távoli cache (remote cache) használatával drámaian csökkenthetjük a build idők hosszát a CI szervereken. Ha egy build már egyszer megtörtént és a cache-be került, a következő futtatáskor, vagy akár egy másik branchen, a CI rendszer letöltheti a cache-t, és kihagyhatja azokat a lépéseket, amelyeknek az eredménye már rendelkezésre áll.

Például, a Vercel automatikusan felismeri a Turborepo-t, és használja a távoli cache-t. Más szolgáltatók esetén (pl. GitHub Actions) manuálisan kell beállítani a cache-elést, de a Turborepo dokumentációja ehhez részletes útmutatást nyújt.

A --filter opcióval célzottan deploy-olhatunk. Például, ha csak az my-web-app változott, a CI pipeline csak azt fogja buildelni és deploy-olni:


# Build csak a my-web-app-ot és a függőségeit
turbo run build --filter=my-web-app
# Deploy csak a my-web-app-ot
# (Ez a deploy parancs projektfüggő, pl. Vercel CLI, Netlify CLI stb.)

Gyakorlati Tippek és Bevált Módszerek

  • Kisebb, fókuszált csomagok: Törekedjünk arra, hogy a packages mappában lévő könyvtárak minél kisebbek és egyetlen felelősséggel rendelkezők legyenek (pl. @repo/ui, @repo/utils, @repo/api-client).
  • Egységesítés: Használjunk közös ESLint, Prettier, TypeScript konfigurációkat a packages/eslint-config és packages/typescript-config mappákban. Ez biztosítja a kódkonzisztenciát és csökkenti a konfik kezelésével járó terheket.
  • Dokumentáció: Egy monorepo könnyen összetetté válhat. Győződjünk meg róla, hogy minden csomag és alkalmazás megfelelően dokumentált.
  • Tesztelés: Integráljuk a teszteket a Turborepo pipeline-ba, és használjuk a turbo run test parancsot az összes teszt futtatására.
  • Függőségek karbantartása: Rendszeresen frissítsük a függőségeket a pnpm up -Lr vagy a pnpm recursive update parancsokkal, hogy elkerüljük az elavulást.

Összefoglalás és Jövőbeli Kilátások

A monorepo architektúra, különösen a Next.js és Turborepo kombinációjával, egy rendkívül erős eszköz a modern, skálázható webfejlesztéshez. Lehetővé teszi a gyorsabb fejlesztési ciklusokat, a könnyebb kódmegosztást, a konzisztens tooling-ot és a hatékony CI/CD folyamatokat.

Bár a kezdeti beállítás igényel némi befektetést, a hosszú távú előnyök – különösen nagyobb csapatok és összetett projektek esetén – messze felülmúlják a kezdeti kihívásokat. Ha Ön is több Next.js alkalmazást, közös komponenseket és segédprogramokat kezel, fontolja meg komolyan egy ilyen monorepo felépítését. A Turborepo segítségével a teljesítmény és a fejlesztői élmény garantáltan magas szinten marad, és a projektjei készen állnak a jövőbeli skálázhatóságra.

Reméljük, ez a részletes útmutató segít elindulni a monorepo világában, és kihasználni a benne rejlő potenciált!

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük