Головна / Статті / Створення монорепозиторію pnpm та Turborepo для додатків Node.js крок за кроком

Створення монорепозиторію pnpm та Turborepo для додатків Node.js крок за кроком

Створіть монорепо на TypeScript у порожній папці за допомогою pnpm workspaces та Turborepo, а потім запустіть, скомпілюйте та виконайте завдання з фільтрацією для веб-додатку, API та спільних пакетів.

1702 слів

Як тільки у продукту з’являється фронтенд, бекенд та код, які їм обом потрібні, окремі репозиторії починають створювати проблеми: спільні типи відхиляються, конфігурація копіюється вручну, а одна зміна охоплює кілька запитів на отримання коду. pnpm workspaces разом із Turborepo вирішують цю проблему без необхідності об’єднання проектів у один. У цьому посібнику ми перейдемо від порожнього каталогу до двох додатків на TypeScript та спільного пакету, які можна розробляти, компілювати та керувати ними з одного кореневого каталогу.

Готова структура:

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

Що дає вам монорепозиторій

Монорепозиторій — це один репозиторій Git, який містить кілька додатків та пакетів. Альтернативою є один репозиторій на кожну функціональність:

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

У монорепозиторії вони стають папками: код, призначений для розгортання, знаходиться у папці apps, а код, який можна використовувати повторно, — у папці packages:

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

Код потім ділиться безпосередньо, замість того щоб спочатку його публікувати. Пакет типів, який використовується як фронтендом, так і бекендом, означає, що зміни в навантаженні доходять до обох частин у одному коміті:

apps/web
      ↓
packages/types
      ↑
apps/api

Де застосовується Turborepo

pnpm workspaces поєднує пакети; Turborepo визначає, як завдання виконуються між ними. Він забезпечує оркестрацію завдань, порядок виконання з урахуванням залежностей, паралельну обробку, локальне та віддалене кешування, інкрементальну збірку та підтримку workspaces.

Візьмемо репозиторій із трьома workspaces:

apps/web
apps/api
packages/types

Кожен з них може визначати однакові скрипти:

build
lint
test
dev

Замість того, щоб входити до кожного каталогу у правильному порядку, ви запускаєте їх з кореня, а Turborepo паралелізує процес там, де це можливо. Щоб дізнатися, коли такий підхід є доцільним, перегляньте яке місце займає Turborepo у NestJS monorepo та коли його можна пропустити.

Попередні вимоги

Вам потрібні Node.js, pnpm, Git та редактор. Перевірте наявність Node.js:

node -v

А також pnpm:

pnpm -v

Якщо pnpm відсутній, Corepack, який поставляється разом із Node.js, може його забезпечити. Увімкніть його:

corepack enable

Активуйте останню версію pnpm:

corepack prepare pnpm@latest --activate

Підтвердіть:

pnpm -v

Налаштування кореневого робочого простору

Створіть каталог:

mkdir my-monorepo
cd my-monorepo

Ініціалізуйте Git:

git init

Складіть маніфест кореня:

pnpm init

Що залишає:

my-monorepo/
└── package.json

Встановіть Turborepo у кореневу директорію

Turborepo обслуговує весь репозиторій, тому параметр --workspace-root встановлює його у корені, а не у пакеті:

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

У кореневому маніфесті з’являються скрипти, які делегують виконання до команди turbo run; ключове слово private запобігає публікації кореневої директорії:

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

Версія вашого інструменту turbo залежить від часу встановлення. Останні версії також вимагають наявності поля packageManager у кореневому файлі package.json; якщо turbo не може виявити ваш інструмент керування пакетами, перегляньте документацію.

Оголошення робочих просторів

pnpm знаходить пакети через кореневий файл:

pnpm-workspace.yaml

Вкажіть шаблони файлів, які потрібно вважати пакетами:

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

Кожна директорія, що знаходиться безпосередньо під ними, стає робочим простором:

apps/*
packages/*

Створіть папки для обох додатків та пакету types:

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

Структура наразі:

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

Додавання двох додатків

Веб-додаток

Щоб зосередитися на monorepo, веб-додаток поки що є звичайним Node.js. Введіть його:

cd apps/web

Додайте до нього manifest:

pnpm init

Бажана форма:

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

Створіть файл входу:

mkdir src
touch src/index.ts

Додайте місце для заміни:

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

Кожному робочому простору потрібен TypeScript, тож поверніться до кореня:

cd ../..

І встановіть його один раз:

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

API

Та сама схема: введіть та ініціалізуйте:

cd apps/api
pnpm init

Створіть файл входу:

mkdir src
touch src/index.ts

Додайте його місце для заміни:

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

Тепер обидва додатки відповідають один одному:

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

Обмін конфігурацією TypeScript

Повернутися до кореня:

cd ../..

Створити базову конфігурацію:

tsconfig.json

Вона містить параметри, якими користуються всі робочі простори: сучасну мету, резолюцію NodeNext та сувору перевірку:

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

Кожен додаток розширює її та додає лише локальні налаштування. Для веб-додатку створіть:

apps/web/tsconfig.json

Вона вказує на кореневий файл, встановлює папку для зберігання результатів та компілює лише src:

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

API отримує той самий файл:

apps/api/tsconfig.json

З ідентичним вмістом:

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

Зміни щодо суворості чи мети тепер відбуваються в одному місці.

Надання скриптів будування кожному робочому простору

Turborepo виконує скрипти, визначені робочими просторами. Відкрийте веб-маніфест:

apps/web/package.json

Встановіть ім’я з обмеженим діапазоном та три скрипти: build за допомогою tsc, dev у режимі спостереження, lint за допомогою ESLint:

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

Потім маніфест API:

apps/api/package.json

З власною назвою:

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

Фільтри та залежності робочого простору посилаються на ці імена @repo/.... Скрипт dev потребує tsx:

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

Скрипт lint також передбачає, що ESLint встановлений та налаштований; додайте його або видаліть скрипт, інакше pnpm lint завершиться невдачею.

Налаштування конвеєра завдань

turbo.json описує, як поводиться кожне завдання. Створіть його у кореневій директорії:

turbo.json

Визначте завдання:

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

Що варто зазначити:

  • "dependsOn": ["^build"] змушує пакет чекати на завершення будови пакетів робочого простору, від яких він залежить; символ кершен означає залежності.
  • outputs визначає, що потрібно зберігати в кеші та відновлювати, щоб незмінні пакети не перебудовувалися.
  • dev не зберігається в кеші та є persistent, оскільки процес спостереження ніколи не закінчується.
  • lint також спочатку запускає залежності.
  • tasks — це поточний ключ; старіші версії використовували pipeline, тому старі приклади можуть потребувати корекції.

    Запуск та будування з кореня

    Запустіть усі процеси розробки:

    pnpm dev
    

    Це працює тому, що скрипт dev у корені є:

    "dev": "turbo run dev"
    

    Turborepo знаходить dev у кожному робочому просторі та запускає їх разом, використовуючи один термінал для веб-додатку:

    cd apps/web
    pnpm dev
    

    І інший — для API:

    cd apps/api
    pnpm dev
    

    За допомогою однієї команди з кореня:

    pnpm dev
    

    Усе будується однаковим способом:

    pnpm build
    

    Turborepo виконує збірку за принципом залежностей пакетів, спочатку обробляючи спільні пакети:

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

    Цей порядок визначається оголошеними залежностями: поки у packages/types немає файлу package.json, а додатки не залежать від нього, Turborepo не буде спочатку його обробляти.

    Робота з одним простором роботи

    Параметр --filter у pnpm запускає скрипт у певному просторі роботи, наприклад сервері розробки API:

    pnpm --filter @repo/api dev
    

    Або для створення веб-версії:

    pnpm --filter @repo/web build
    

    Власний фільтр Turborepo забезпечує кешування та правильний порядок обробки:

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

    Команди, якими ви будете користуватися щодня

    Встановити все:

    pnpm install
    

    Розпочати розробку:

    pnpm dev
    

    Створити збірку для всього:

    pnpm build
    

    Перевірити код на помилки для всього:

    pnpm lint
    

    Створити збірку для одного пакета:

    pnpm --filter @repo/api build
    

    Запустити один додаток:

    pnpm --filter @repo/web dev
    

    Додати залежність до одного простору роботи:

    pnpm --filter @repo/api add express
    

    Залежність від локального пакета; workspace:* посилається на копію репозиторію замість отримання даних з реєстру:

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

    Чому не обмежитися лише workspaces pnpm

    Ви можете покладатися виключно на workspaces:

    apps/
    packages/
    

    Однак у міру зростання репозиторію координація стає більш ручною:

    build
    test
    lint
    typecheck
    dev
    dependencies
    task ordering
    caching
    

    Turborepo додає механізми оркестрації: одна команда розуміє взаємозв’язки пакетів, пропускає незмінні частини та виконує окремі завдання паралельно:

    pnpm turbo run build
    

    Для одного додатку та одного пакета можуть вистачити звичайні workspaces. Ще вибираєте менеджер пакетів? Дивіться нашу порівняння npm та pnpm.

    Підсумок

    Хороший monorepo — це спільне середовище, де додатки та пакети розвиваються за допомогою одного набору інструментів. Тут кількість компонентів невелика:

    pnpm
    +
    Turborepo
    +
    TypeScript
    

    Почніть з двох додатків:

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

    Розширюйте до більшої кількості сервісів:

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

    Підтримується за рахунок спільних пакетів:

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

    Перевага полягає у можливості спільного використання коду, типів, конфігурації та процесів роботи, при цьому кожен додаток залишається незалежно організованим. Коли ви його розширюєте:

    • оголошуйте локальні залежності за допомогою workspace:*, щоб компіляція відбувалася у правильному порядку
    • вказуйте кожен артефакт у полі outputs, інакше відновлення кешу його пропустить
    • зберігайте базову конфігурацію у корені та розширюйте її
    • додайте packageManager та інструменти на кшталт ESLint перед тим, як покладатися на скрипти у корені в середовищі CI

    Джерела: документація Turborepo, репозиторій Turborepo, документація pnpm та документація Node.js.