Главная / Статьи / Пошаговое создание монорепозитория 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

Создайте для него манифест:

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.