Пошаговое создание монорепозитория 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
Создайте для него манифест:
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 находится в режиме без кэширования и является персистентным, поскольку процесс наблюдения никогда не завершается.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.
Заключение
Хороший монорепозиторий — это совместная среда, в которой приложения и пакеты развиваются с использованием одного набора инструментов. Здесь стек инструментов минимален:
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.