Створення монорепозиторію pnpm та Turborepo для додатків Node.js крок за кроком
Створіть монорепо на TypeScript у порожній папці за допомогою pnpm workspaces та Turborepo, а потім запустіть, скомпілюйте та виконайте завдання з фільтрацією для веб-додатку, API та спільних пакетів.
Як тільки у продукту з’являється фронтенд, бекенд та код, які їм обом потрібні, окремі репозиторії починають створювати проблеми: спільні типи відхиляються, конфігурація копіюється вручну, а одна зміна охоплює кілька запитів на отримання коду. 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.