Strona główna / Artykuły / Budowa monorepo z pnpm i Turborepo dla aplikacji Node.js krok po kroku

Budowa monorepo z pnpm i Turborepo dla aplikacji Node.js krok po kroku

Utwórz monorepo w TypeScript z pustej folderu za pomocą pnpm workspaces i Turborepo, a następnie uruchom zadania budowania i filtrowania w aplikacji internetowej, API oraz wspólnych pakietach.

1702 słów

Gdy produkt ma interfejs użytkownika, warstwę backendową oraz kod, który obie one wymagają, oddzielne repozytoria zaczynają stanowić problem: typy są dzielone między repozytoria, konfiguracja jest kopiowana ręcznie, a jedna zmiana dotyczy kilku żądań pull request. pnpm workspaces w połączeniu z Turborepo rozwiązują ten problem bez konieczności łączenia projektów w jeden. Ten przewodnik pokazuje, jak z pustego katalogu stworzyć dwa aplikacje w TypeScript oraz wspólny pakiet, który można rozwijać, kompilować i filtrować z jednego korzenia.

Ostateczna struktura:

my-monorepo/
├── apps/
│   ├── web/
│   └── api/
│
├── packages/
│   ├── types/
│   └── eslint-config/
│
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
├── tsconfig.json
└── pnpm-lock.yaml

Co daje monorepo

Monorepo to jeden repozytorium Git zawierające kilka aplikacji i pakietów. Alternatywą jest jeden repozytorium na każdą kategorię:

frontend-repository
backend-repository
shared-types-repository
ui-library-repository

W monorepo te elementy stają się folderami – kod przeznaczony do wdrożenia znajduje się w katalogu apps, a kod wielokrotnie używalny w katalogu packages:

my-monorepo/
├── apps/
│   ├── web/
│   └── api/
│
└── packages/
    ├── types/
    └── ui/

Kod jest następnie udostępniany bezpośrednio, zamiast najpierw być publikowany. Pakiet typów używany zarówno przez frontend, jak i backend oznacza, że zmiana treści pakietu dociera do obu stron w ramach jednego commitu:

apps/web
      ↓
packages/types
      ↑
apps/api

Gdzie pasuje Turborepo

Przestrzenie robocze pnpm łączą pakiety; Turborepo decyduje o tym, w jaki sposób zadania są wykonywane pomiędzy nimi. Oferuje orkiestrację zadań, uporządkowanie uwzględniające zależności, wykonywanie równoległe, lokalne i zdalne cacheowanie, budowę w formie kroków oraz wsparcie dla przestrzeni roboczych.

Weźmy repozytorium z trzema przestrzeniami roboczymi:

apps/web
apps/api
packages/types

Każda z nich może definiować te same skrypty:

build
lint
test
dev

Zamiast wprowadzać każdy katalog w odpowiedniej kolejności, uruchamiasz je od korzenia, a Turborepo paralelizuje proces tam, gdzie to możliwe. Aby dowiedzieć się, kiedy ta metoda jest przydatna, sprawdź gdzie Turborepo pasuje do monorepo NestJS i kiedy można go pominąć.

Wymagania wstępne

Potrzebujesz Node.js, pnpm, Git oraz edytora. Sprawdź Node.js:

node -v

A także pnpm:

pnpm -v

Jeśli brakuje pnpm, Corepack, dostarczany wraz z Node.js, może go zapewnić. Włącz go:

corepack enable

Aktywuj najnowszą wersję pnpm:

corepack prepare pnpm@latest --activate

Potwierdź:

pnpm -v

Ustawianie przestrzeni roboczej korzeniowej

Stwórz katalog:

mkdir my-monorepo
cd my-monorepo

Zainicjuj Git:

git init

Stwórz plik manifestu korzeniowego:

pnpm init

Co pozostawia:

my-monorepo/
└── package.json

Zainstaluj Turborepo w katalogu głównym

Turborepo obsługuje całe repozytorium, dlatego opcja --workspace-root instaluje je w katalogu głównym, a nie w jakimś pakiecie:

pnpm add turbo --save-dev --workspace-root

Plik manifestu w katalogu głównym zawiera skrypty, które odwołują się do turbo run; tag private zapobiega publikacji tego katalogu:

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test"
  },
  "devDependencies": {
    "turbo": "..."
  }
}

Wersja Twojego narzędzia turbo zależy od czasu instalacji. Nowsze wersje wymagają również pola packageManager w pliku package.json znajdującym się w katalogu głównym; jeśli turbo nie może zidentyfikować Twojego menedżera pakietów, sprawdź dokumentację.

Zadeklaruj przestrzenie robocze

pnpm znajduje pakiety za pomocą pliku w katalogu głównym:

pnpm-workspace.yaml

Wymień nazwy plików, które mają być traktowane jako pakiety:

packages:
  - "apps/*"
  - "packages/*"

Każdy katalog znajdujący się bezpośrednio pod nimi staje się przestrzenią roboczą:

apps/*
packages/*

Stwórz foldery dla obu aplikacji oraz pakietu types:

mkdir -p apps/web
mkdir -p apps/api
mkdir -p packages/types

Drzewo struktury do tej pory:

my-monorepo/
├── apps/
│   ├── web/
│   └── api/
│
├── packages/
│   └── types/
│
├── package.json
└── pnpm-workspace.yaml

Dodawanie dwóch aplikacji

Aplikacja internetowa

Aby skupić uwagę na monorepo, aplikacja internetowa jest na razie zwykłym projektem Node.js. Wprowadź ją:

cd apps/web

Daj jej manifest:

pnpm init

Docelowa struktura:

apps/web/
├── package.json
└── src/
    └── index.ts

Stwórz plik wejściowy:

mkdir src
touch src/index.ts

Dodaj zastępczy element:

console.log("Hello from Web application");

Każde środowisko pracy wymaga TypeScript, więc wróć do katalogu głównego:

cd ../..

I zainstaluj go raz:

pnpm add typescript --save-dev --workspace-root

API

Ten sam schemat: wprowadź i zainicjalizuj:

cd apps/api
pnpm init

Stwórz plik wejściowy:

mkdir src
touch src/index.ts

Dodaj jego zastępczy element:

console.log("Hello from API application");

Obyie aplikacje są teraz zgodne:

apps/
├── web/
│   ├── src/
│   │   └── index.ts
│   └── package.json
│
└── api/
    ├── src/
    │   └── index.ts
    └── package.json

Dzielenie się konfiguracją TypeScript

Wróć do katalogu głównego:

cd ../..

Stwórz podstawową konfigurację:

tsconfig.json

Zawiera opcje wspólne dla wszystkich przestrzeni roboczych: nowoczesny cel, rozwiązanie NodeNext oraz ścisłą weryfikację:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

Każda aplikacja ją rozszerza i dodaje tylko ustawienia lokalne. Dla aplikacji internetowej utwórz:

apps/web/tsconfig.json

Wskazuje na plik główny, ustawia folder wyjściowy i kompiluje tylko katalog src:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"]
}

API otrzymuje ten sam plik:

apps/api/tsconfig.json

Ze identyczną zawartością:

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src"]
}

Zmiany dotyczące ścisłości lub celu odbywają się teraz w jednym miejscu.

Dostarczanie skryptów budowania dla każdej przestrzeni roboczej

Turborepo uruchamia skrypty zdefiniowane przez przestrzenie robocze. Otwórz manifest internetowy:

apps/web/package.json

Ustaw nazwę z zakresem i trzy skrypty: build za pomocą tsc, dev w trybie obserwacji, lint za pomocą ESLint:

{
  "name": "@repo/web",
  "private": true,
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "lint": "eslint ."
  }
}

Następnie manifest API:

apps/api/package.json

Z własną nazwą:

{
  "name": "@repo/api",
  "private": true,
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "lint": "eslint ."
  }
}

Filtrzy i zależności przestrzeni roboczej odnoszą się do tych nazw @repo/.... Skrypt dev wymaga pliku tsx:

pnpm add tsx --save-dev --workspace-root

Skrypt lint zakłada również, że ESLint jest zainstalowany i skonfigurowany; należy go dodać albo usunąć ten skrypt, w przeciwnym razie pnpm lint nie zadziała.

Konfiguracja łańcucha zadań

turbo.json opisuje, jak zachowują się poszczególne zadania. Utwórz go w katalogu głównym:

turbo.json

Zdefiniuj zadania:

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

Na co zwrócić uwagę:

  • "dependsOn": ["^build"] sprawia, że pakiet czeka na zakończenie budowy pakietów przestrzeni roboczej, od których zależy; znak kierownika oznacza zależności.
  • outputs określa, co należy zapamiętać w pamięci cache i przywrócić, dzięki czemu nieprzemienione pakiety nie są ponownie budowane.
  • dev jest niezapisywany w cache i ma właściwość persistent, ponieważ proces nadzoru nigdy się nie kończy.
  • lint również najpierw uruchamia zależności.
  • tasks to obecny klucz; starsze wersje używały pipeline, więc starsze przykłady mogą wymagać dostosowania.

    Uruchamianie i budowanie od korzenia

    Rozpocznij wszystkie procesy deweloperskie:

    pnpm dev
    

    To działa, ponieważ skrypt dev w korzeniu jest:

    "dev": "turbo run dev"
    

    Turborepo znajduje pliki dev w każdym miejscu pracy i uruchamia je razem, używając jednego terminala dla aplikacji webowej:

    cd apps/web
    pnpm dev
    

    A innego dla API:

    cd apps/api
    pnpm dev
    

    Przy użyciu jednego polecenia w korzeniu:

    pnpm dev
    

    Buduj wszystko w ten sam sposób:

    pnpm build
    

    Turborepo uruchamia budowanie według zależności pakietów, najpierw te wspólne:

    pnpm build
          │
          ▼
    turbo run build
          │
          ├── packages/types
          │
          ├── apps/api
          │
          └── apps/web
    

    Taka kolejność wynika z deklarowanych zależności: dopóki packages/types nie będzie miał pliku package.json i aplikacje od niego będą zależeć, Turborepo nie uruchomi jego budowania jako pierwszego.

    Celowanie w pojedyncze środowisko pracy

    Parametr --filter w pnpm uruchamia skrypt w konkretnym środowisku pracy, na przykład serwer rozwojowy API:

    pnpm --filter @repo/api dev
    

    Lub budowanie strony internetowej:

    pnpm --filter @repo/web build
    

    Własny filtr Turborepo zarządza cache’em oraz kolejnością uruchamiania zadań:

    pnpm turbo run build --filter=@repo/api
    

    Polecenia, których będziesz używać codziennie

    Zainstaluj wszystko:

    pnpm install
    

    Rozpocznij rozwój:

    pnpm dev
    

    Zbuduj wszystko:

    pnpm build
    

    Przeprowadź analizę stylu we wszystkich plikach:

    pnpm lint
    

    Zbuduj jeden pakiet:

    pnpm --filter @repo/api build
    

    Uruchom jedną aplikację:

    pnpm --filter @repo/web dev
    

    Dodaj zależność do jednego środowiska pracy:

    pnpm --filter @repo/api add express
    

    Zależność od lokalnego pakietu; workspace:* łączy kopię repozytorium zamiast pobierać ją z rejestru:

    pnpm --filter @repo/api add @repo/types@workspace:*
    

    Dlaczego nie poprzestać na workspacesach pnpm?

    Moglibyśmy polegać wyłącznie na workspacesach:

    apps/
    packages/
    

    Jednak w miarę rozwoju repozytorium coraz więcej rzeczy koordynuje się ręcznie:

    build
    test
    lint
    typecheck
    dev
    dependencies
    task ordering
    caching
    

    Turborepo dodaje mechanizmy orkiestracji: jeden polecenie rozumie relacje między pakietami, pomija te, które się nie zmieniły, i uruchamia niezależne zadania równolegle:

    pnpm turbo run build
    

    Dla jednej aplikacji i jednego pakietu zwykłe workspacesy mogą wystarczyć. Nadal wybierasz menedżer pakietów? Przeczytaj nasze porównanie npm i pnpm.

    Podsumowanie

    Dobry monorepo to wspólne środowisko, w którym aplikacje i pakiety rozwijają się przy użyciu jednego zestawu narzędzi. Tutaj stos jest prosty:

    pnpm
    +
    Turborepo
    +
    TypeScript
    

    Zacznij od dwóch aplikacji:

    apps/
    ├── web/
    └── api/
    

    Rozwijaj je w coraz więcej usług:

    apps/
    ├── web/
    ├── admin/
    ├── api/
    └── worker/
    

    Wspierane przez wspólne pakiety:

    packages/
    ├── ui/
    ├── types/
    ├── database/
    ├── auth/
    └── utils/
    

    Zaletą jest możliwość dzielenia się kodem, typami, konfiguracją i procesami pracy, przy jednoczesnym zachowaniu niezależnej organizacji każdej aplikacji. W miarę jej rozbudowywania:

    • deklaruj lokalne zależności za pomocą workspace:*, aby budowanie odbywało się we właściwej kolejności
    • wymień wszystkie pliki w sekcji outputs, w przeciwnym razie przywracanie z pamięci podręcznej je pominie
    • przechowuj podstawową konfigurację w katalogu głównym i rozwijaj ją dalej
    • dodaj packageManager oraz narzędzia takie jak ESLint, zanim będziesz polegać na skryptach w katalogu głównym w środowisku CI

    Źródła: dokumentacja Turborepo, repozytorium Turborepo, dokumentacja pnpm oraz dokumentacja Node.js.

    Literatura pokrewna