Workspaces: yarn, npm, pnpm

Понятие Workspaces и их роль в монорепозиториях

Современные проекты нередко состоят из нескольких взаимосвязанных пакетов. Например, в одном репозитории могут одновременно находиться:

  • веб-приложение;
  • серверная часть;
  • общая библиотека компонентов;
  • набор утилит;
  • внутренние SDK.

Подход, при котором несколько пакетов располагаются внутри одного репозитория, называется монорепозиторием (monorepo).

Для управления такими структурами менеджеры пакетов предоставляют механизм Workspaces. Он позволяет:

  • объединять множество пакетов в единый проект;
  • централизованно устанавливать зависимости;
  • создавать локальные связи между пакетами без публикации в npm;
  • ускорять установку зависимостей;
  • упрощать разработку и тестирование.

Parcel полностью поддерживает проекты, построенные на Workspaces, независимо от того, используется Yarn, npm или pnpm.


Структура монорепозитория

Типичная структура проекта выглядит следующим образом:

my-monorepo/
├── package.json
├── packages/
│   ├── ui/
│   │   ├── package.json
│   │   └── src/
│   ├── utils/
│   │   ├── package.json
│   │   └── src/
│   └── app/
│       ├── package.json
│       └── src/
└── node_modules/

Корневой каталог содержит общий package.json, а каждый пакет обладает собственным файлом package.json.


Workspaces в npm

Начиная с npm 7 механизм Workspaces входит в состав стандартного менеджера пакетов.

Корневой файл:

{
  "name": "my-monorepo",
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

Пакет библиотеки:

{
  "name": "@company/utils",
  "version": "1.0.0"
}

Пакет приложения:

{
  "name": "@company/app",
  "version": "1.0.0",
  "dependencies": {
    "@company/utils": "1.0.0"
  }
}

После выполнения:

npm install

npm автоматически создаёт локальную связь между пакетами внутри монорепозитория.


Workspaces в Yarn

Yarn стал одним из первых популярных инструментов, реализовавших поддержку монорепозиториев.

Корневой файл:

{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

Установка зависимостей выполняется обычной командой:

yarn install

Yarn анализирует все рабочие пространства и формирует единое дерево зависимостей.

В современных версиях Yarn поддерживаются:

  • Workspaces;
  • Plug’n’Play (PnP);
  • Zero Install;
  • ограничения версий через Constraints;
  • расширенные механизмы кэширования.

Parcel корректно работает как с классическим режимом node_modules, так и с современными возможностями Yarn.


Workspaces в pnpm

pnpm использует собственную высокоэффективную модель хранения пакетов.

Корневой файл:

packages:
  - "packages/*"

Файл сохраняется как:

pnpm-workspace.yaml

Дополнительно присутствует стандартный корневой package.json:

{
  "name": "my-monorepo",
  "private": true
}

Установка зависимостей:

pnpm install

Главные особенности pnpm:

  • экономия дискового пространства;
  • использование жёстких ссылок;
  • высокая скорость установки;
  • строгая изоляция зависимостей.

Parcel полностью совместим с проектами на pnpm.


Parcel внутри монорепозитория

Рассмотрим структуру:

packages/
├── app/
└── ui/

Пакет ui:

{
  "name": "@company/ui",
  "source": "src/index.js"
}

Пакет app:

{
  "dependencies": {
    "@company/ui": "workspace:*"
  }
}

Использование:

import { Button } from "@company/ui";

Parcel обнаруживает локальную зависимость и включает её в граф сборки так же, как обычный пакет.

Дополнительная публикация библиотеки не требуется.


Разрешение зависимостей между пакетами

Parcel строит единый граф зависимостей для всего проекта.

Например:

app
 ├── ui
 │    └── utils
 └── api
      └── utils

Parcel определяет:

  • какие файлы используются;
  • какие зависимости являются общими;
  • какие модули необходимо вынести в отдельные чанки.

Благодаря этому исключается дублирование кода.


Использование протокола workspace

Во многих монорепозиториях применяется специальный протокол:

{
  "dependencies": {
    "@company/ui": "workspace:*"
  }
}

Также возможны варианты:

{
  "dependencies": {
    "@company/ui": "workspace:^"
  }
}

или

{
  "dependencies": {
    "@company/ui": "workspace:~"
  }
}

Такой синтаксис явно сообщает менеджеру пакетов, что зависимость должна разрешаться через локальное рабочее пространство.

Parcel получает уже корректно разрешённый путь к пакету и не требует дополнительной настройки.


Автоматическое отслеживание изменений

Одно из преимуществ Workspaces заключается в том, что изменения библиотек моментально доступны приложениям.

Например:

packages/
├── ui/
└── app/

После изменения:

// ui/src/Button.js
export function Button() {
  return "Updated";
}

Parcel фиксирует изменение файла внутри пакета ui и запускает перестроение зависимых модулей.

При использовании режима разработки:

parcel serve src/index.html

обновление происходит автоматически через механизм HMR.


Общие зависимости в Workspaces

Часто несколько пакетов используют одну библиотеку.

Пример:

app
 ├── react
 └── ui
      └── react

Parcel анализирует дерево зависимостей и определяет, что используется одна и та же версия React.

Это позволяет:

  • избежать дублирования;
  • уменьшить размер сборки;
  • сократить объём передаваемого кода.

Hoisting зависимостей

Большинство менеджеров пакетов стараются поднимать общие зависимости вверх по дереву каталогов.

Например:

node_modules/
├── react
├── lodash
└── typescript

Вместо:

packages/
├── app/node_modules/react
└── ui/node_modules/react

Такой механизм называется hoisting.

Parcel не зависит от конкретной структуры размещения пакетов и использует стандартный механизм разрешения модулей Node.js.


Особенности работы с pnpm

В pnpm зависимости размещаются иначе.

Физическая структура может выглядеть значительно сложнее:

node_modules/
└── .pnpm/

Однако Parcel работает через официальные алгоритмы разрешения модулей и корректно находит пакеты независимо от внутреннего устройства каталога.

Поэтому специальные настройки обычно не требуются.


Общий конфигурационный пакет

Распространённая практика — выделение отдельного пакета для конфигурации.

Пример:

packages/
├── eslint-config/
├── ui/
└── app/

Пакет:

{
  "name": "@company/eslint-config"
}

Использование:

{
  "extends": [
    "@company/eslint-config"
  ]
}

Тот же подход применяется для:

  • TypeScript-конфигураций;
  • Babel-конфигураций;
  • PostCSS-конфигураций;
  • внутренних инструментов разработки.

Parcel легко взаимодействует с подобной организацией проекта.


TypeScript и Workspaces

Типичная структура:

packages/
├── types/
├── ui/
└── app/

Пакет типов:

{
  "name": "@company/types"
}

Использование:

import type { User } from "@company/types";

Parcel корректно отслеживает зависимости между TypeScript-пакетами и перестраивает проект при изменениях.


Алиасы и Workspaces

Иногда используются алиасы:

{
  "alias": {
    "@ui": "./src/ui"
  }
}

Импорт:

import Button from "@ui/Button";

Алиасы могут сосуществовать с Workspaces.

Например:

import { Button } from "@company/ui";
import Header from "@ui/Header";

Parcel разрешает оба типа импортов через собственный резолвер.


Кэширование в больших монорепозиториях

Крупные проекты могут содержать:

  • десятки пакетов;
  • сотни модулей;
  • тысячи файлов.

Parcel использует интеллектуальное кэширование:

  • графа зависимостей;
  • результатов трансформации;
  • промежуточных артефактов.

В сочетании с Workspaces это позволяет существенно сократить время повторных сборок.


Публикация пакетов из монорепозитория

Не все пакеты предназначены только для внутреннего использования.

Например:

packages/
├── ui
├── utils
└── app

Библиотеки могут публиковаться в npm:

npm publish

или:

pnpm publish

Parcel при этом используется исключительно как инструмент сборки.

Пакеты внутри монорепозитория продолжают использовать локальные ссылки во время разработки и опубликованные версии после релиза.


Типовой сценарий использования Parcel в Workspaces

Структура:

packages/
├── ui/
├── icons/
├── utils/
└── app/

Зависимости:

app
 ├── ui
 │    ├── icons
 │    └── utils
 └── utils

Процесс работы выглядит следующим образом:

  1. Менеджер пакетов создаёт связи между рабочими пространствами.
  2. Parcel строит граф зависимостей всех пакетов.
  3. Изменения библиотек автоматически отслеживаются.
  4. Общие зависимости оптимизируются.
  5. Выполняется разделение кода на чанки.
  6. Создаются финальные сборки приложения.

Благодаря поддержке Yarn Workspaces, npm Workspaces и pnpm Workspaces Parcel одинаково эффективно работает как в небольших проектах из нескольких пакетов, так и в крупных корпоративных монорепозиториях с десятками внутренних библиотек и приложений.