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

Монорепозиторий (Monorepo) представляет собой единое хранилище исходного кода, содержащее несколько взаимосвязанных пакетов, приложений и библиотек. Такой подход широко используется для организации крупных проектов, где множество модулей развиваются синхронно и имеют общую инфраструктуру.

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

project/
├── package.json
├── packages/
│   ├── ui/
│   │   ├── package.json
│   │   └── src/
│   ├── utils/
│   │   ├── package.json
│   │   └── src/
│   └── core/
│       ├── package.json
│       └── src/
└── apps/
    ├── admin/
    └── client/

В подобных проектах Parcel решает сразу несколько задач:

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

Главным преимуществом Parcel является возможность работать с монорепозиторием практически без сложной конфигурации.


Организация рабочих пространств

Современные монорепозитории обычно используют механизм Workspaces.

Пример настройки в корневом package.json:

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

Поддержка рабочих пространств существует в:

  • npm Workspaces;
  • Yarn Workspaces;
  • pnpm Workspaces.

Parcel корректно распознаёт структуру зависимостей, создаваемую менеджером пакетов.

Например:

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

и

{
  "name": "@company/admin",
  "dependencies": {
    "@company/ui": "^1.0.0"
  }
}

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


Сборка внутренних библиотек

Часто монорепозиторий содержит набор внутренних библиотек.

Пример библиотеки:

packages/ui
├── package.json
└── src
    └── index.js

Файл:

export function Button() {
    console.log("Button");
}

Настройка библиотеки:

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

Запуск сборки:

parcel build

Parcel анализирует поле source и автоматически создаёт выходные файлы, указанные в целях (targets).

Результат:

dist/
├── main.js
└── module.js

Таким образом каждая библиотека может собираться независимо от остальных частей монорепозитория.


Использование Targets для разных пакетов

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

Пример:

{
  "targets": {
    "main": {
      "context": "node",
      "outputFormat": "commonjs"
    },
    "module": {
      "context": "browser",
      "outputFormat": "esmodule"
    }
  }
}

После сборки будут созданы:

dist/
├── main.js
└── module.js

Такой подход позволяет одновременно поддерживать:

  • Node.js;
  • браузеры;
  • современные ESM-системы;
  • старые CommonJS-проекты.

Особенно полезно это для библиотек общего назначения внутри монорепозитория.


Взаимозависимости между пакетами

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

packages/
├── core
└── ui

Пакет ui использует core.

import { formatDate } from "@company/core";

export function renderDate(date) {
    return formatDate(date);
}

Parcel строит единый граф зависимостей:

ui
└── core

Во время сборки:

  1. обнаруживается импорт;
  2. определяется пакет-источник;
  3. подключается исходный код;
  4. выполняются трансформации;
  5. создаётся итоговый бандл.

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


Автоматическое разрешение локальных зависимостей

При использовании Workspaces менеджер пакетов создаёт символические ссылки между пакетами.

Например:

node_modules/
└── @company/
    └── ui -> ../. ./packages/ui

Parcel корректно обрабатывает такие связи.

Импорт:

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

будет разрешён в исходный код внутреннего пакета.

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


Сборка приложений, использующих внутренние пакеты

Допустим существует приложение:

apps/admin

Файл:

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

Button();

Сборка:

parcel build src/index.js

Parcel автоматически включает код библиотеки в итоговый граф.

Схематично процесс выглядит так:

Admin App
    ↓
@company/ui
    ↓
@company/core

Все зависимости анализируются как единая система.


Изоляция пакетов через Package Exports

Современные библиотеки часто используют поле exports.

Пример:

{
  "exports": {
    ".": "./src/index.js",
    "./hooks": "./src/hooks.js"
  }
}

Теперь доступны только явно объявленные точки входа.

Разрешены:

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

Недоступен:

import data from "@company/ui/src/internal/data";

Parcel учитывает правила exports при построении графа зависимостей.

Это помогает поддерживать стабильные публичные API внутри монорепозитория.


Общие ресурсы между пакетами

Нередко несколько пакетов используют одни и те же ресурсы:

packages/
├── assets
├── ui
└── marketing

Например:

import logo from "@company/assets/logo.svg";

Parcel автоматически обрабатывает:

  • SVG;
  • PNG;
  • JPEG;
  • WebP;
  • шрифты;
  • CSS;
  • JSON.

Дополнительная настройка в большинстве случаев не требуется.


Работа с TypeScript в монорепозитории

Структура проекта:

packages/
├── core
├── ui
└── utils

Каждый пакет содержит TypeScript-код.

export function sum(a: number, b: number): number {
    return a + b;
}

Parcel автоматически использует конфигурацию:

tsconfig.json

или

tsconfig.base.json

в корне монорепозитория.

Типичный вариант:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true
  }
}

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


Использование путевых алиасов

В больших монорепозиториях активно используются алиасы.

Пример:

{
  "compilerOptions": {
    "paths": {
      "@core/*": [
        "packages/core/src/*"
      ]
    }
  }
}

Импорт:

import { logger } from "@core/logger";

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

Это упрощает поддержку больших кодовых баз.


Общий кэш для всего репозитория

Одним из главных преимуществ Parcel является агрессивное кэширование.

При первой сборке создаётся каталог:

.parcel-cache

Кэш содержит результаты:

  • трансформаций;
  • минификации;
  • анализа зависимостей;
  • обработки изображений;
  • компиляции TypeScript.

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

Например:

core изменён
↓
ui зависит от core
↓
admin зависит от ui

Parcel пересоберёт только необходимую цепочку зависимостей.

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


Watch-режим для нескольких пакетов

Во время разработки обычно используется:

parcel watch src/index.html

Parcel отслеживает изменения сразу во всех пакетах, участвующих в графе.

Изменение файла:

packages/core/src/date.js

приведёт к автоматической пересборке всех зависимых частей проекта.

Механизм работает даже через несколько уровней вложенности зависимостей.


Разделение кода между приложениями

Монорепозиторий часто содержит несколько клиентских приложений:

apps/
├── admin
├── crm
└── dashboard

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

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

Parcel анализирует общий граф и способен эффективно выполнять:

  • tree shaking;
  • code splitting;
  • устранение дублирования модулей;
  • создание общих чанков.

В результате уменьшается объём загружаемого кода.


Tree Shaking в монорепозитории

Предположим библиотека экспортирует множество функций.

export function a() {}
export function b() {}
export function c() {}
export function d() {}

Приложение использует только одну:

import { a } from "@company/utils";

Во время production-сборки Parcel удалит неиспользуемые экспорты.

Итоговый бандл будет содержать только необходимый код.

На больших внутренних библиотеках это позволяет существенно снизить размер сборки.


Управление зависимостями через Peer Dependencies

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

Пример:

{
  "peerDependencies": {
    "react": "^19.0.0"
  }
}

Подобная схема позволяет избежать:

  • дублирования React;
  • конфликтов версий;
  • появления нескольких экземпляров библиотек.

Parcel корректно учитывает peer-зависимости при построении графа.


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

Даже если код хранится в едином репозитории, библиотеки могут публиковаться независимо.

Пример пакета:

{
  "name": "@company/core",
  "version": "2.1.0",
  "source": "src/index.js",
  "main": "dist/main.js"
}

Сборка:

parcel build

Публикация:

npm publish

Каждый пакет получает собственный артефакт сборки, сохраняя при этом преимущества совместной разработки внутри монорепозитория.


Типичная архитектура монорепозитория с Parcel

Крупный проект может иметь следующую организацию:

project/
├── apps/
│   ├── web
│   ├── mobile-web
│   └── admin
│
├── packages/
│   ├── core
│   ├── ui
│   ├── api
│   ├── analytics
│   └── design-tokens
│
├── package.json
├── tsconfig.json
└── .parcelrc

Связи между пакетами:

web
 ├── ui
 ├── api
 └── analytics

admin
 ├── ui
 ├── api
 └── core

ui
 ├── design-tokens
 └── core

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