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

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

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

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


Что такое разрешение зависимостей

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

Например:

import React from "react";

Parcel должен определить:

  1. Где находится пакет react.
  2. Какой файл является точкой входа пакета.
  3. Какую версию пакета использовать.
  4. Какие внутренние зависимости React необходимо подключить.
  5. Какие форматы модулей поддерживаются данным пакетом.

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


Граф зависимостей

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

Пример структуры:

index.js
 ├── app.js
 │    ├── router.js
 │    └── api.js
 └── utils.js

Код:

// index.js
import "./app";
import "./utils";
// app.js
import "./router";
import "./api";

Каждый импорт создаёт новую связь внутри графа.

Parcel рекурсивно обходит все импорты и строит полную карту проекта.

Схематично:

index.js
   |
   +---- app.js
   |        |
   |        +---- router.js
   |        |
   |        +---- api.js
   |
   +---- utils.js

Полученный граф используется для:

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

Node Resolution Algorithm

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

Импорт:

import lodash from "lodash";

Запускает поиск:

project/
├── node_modules/
│   └── lodash/

Если пакет не найден, Parcel поднимается вверх по дереву каталогов:

project/
└── src/
    └── app/

Поиск будет происходить следующим образом:

src/app/node_modules
src/node_modules
project/node_modules

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


Локальные зависимости

Относительные импорты начинаются с:

./
../

Пример:

import Button from "./Button";

Parcel выполняет поиск:

Button.js
Button.jsx
Button.ts
Button.tsx
Button/index.js
Button/index.ts
...

Если файл найден, он становится узлом графа зависимостей.


Абсолютные зависимости

Импорт без относительного пути считается пакетом.

Пример:

import axios from "axios";

Parcel ищет пакет внутри:

node_modules/axios

Затем анализирует файл:

{
  "main": "index.js"
}

или

{
  "module": "dist/axios.esm.js"
}

или

{
  "source": "src/index.js"
}

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


Анализ package.json

Файл package.json играет ключевую роль в разрешении зависимостей.

Пример:

{
  "name": "my-library",
  "main": "dist/main.js",
  "module": "dist/module.js",
  "source": "src/index.js"
}

Parcel анализирует поля:

Поле Назначение
main Основная точка входа CommonJS
module ES Module версия
source Исходный код библиотеки
browser Замены для браузерной среды
exports Современная система экспорта
types TypeScript-описания

На основе этих данных выбирается оптимальный вариант подключения.


Поле source

Одной из важных особенностей Parcel является поддержка поля:

{
  "source": "src/index.js"
}

Если пакет содержит исходный код, Parcel способен работать непосредственно с ним.

Преимущества:

  • дополнительная оптимизация;
  • tree shaking;
  • единая система трансформации;
  • улучшённое разделение кода.

Например:

{
  "source": "src/index.ts"
}

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


Поле browser

Многие пакеты имеют разные реализации для Node.js и браузера.

Пример:

{
  "browser": {
    "./server.js": "./browser.js"
  }
}

Когда выполняется браузерная сборка:

import "./server.js";

Parcel автоматически заменяет зависимость:

import "./browser.js";

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


Поле exports

Современные версии Node.js используют механизм exports.

Пример:

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}

Допустимые импорты:

import lib from "my-lib";
import utils from "my-lib/utils";

Недопустимый импорт:

import test from "my-lib/internal/test";

Parcel соблюдает ограничения, указанные в exports.


Conditional Exports

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

Пример:

{
  "exports": {
    ".": {
      "import": "./esm/index.js",
      "require": "./cjs/index.js",
      "browser": "./browser/index.js"
    }
  }
}

Parcel определяет среду выполнения и выбирает нужный вариант.

Для браузера:

browser/index.js

Для ES-модулей:

esm/index.js

Для CommonJS:

cjs/index.js

Разрешение ES Modules

Parcel полностью поддерживает ES Modules.

Пример:

import { format } from "date-fns";

При анализе:

  1. находится пакет;
  2. определяется экспортируемый модуль;
  3. строится граф зависимостей;
  4. вычисляются используемые экспорты.

Это позволяет выполнять эффективное удаление неиспользуемого кода.


Разрешение CommonJS

Многие библиотеки всё ещё используют CommonJS.

Пример:

const express = require("express");

Parcel умеет анализировать:

require(...)

и включать такие зависимости в граф проекта.

Поддерживаются конструкции:

module.exports
exports.foo
require()

Гибридные пакеты

Некоторые библиотеки одновременно поддерживают CommonJS и ES Modules.

Пример:

{
  "main": "dist/index.cjs",
  "module": "dist/index.mjs"
}

Parcel автоматически выбирает наиболее подходящий формат.

Обычно предпочтение отдаётся ES Module версии, поскольку она предоставляет больше возможностей для оптимизации.


Разрешение TypeScript-модулей

Импорт:

import User from "./User";

может быть разрешён в:

User.ts

или:

User.tsx

Parcel учитывает расширения TypeScript и корректно интегрирует их в общий граф зависимостей.

Поддерживаются:

.ts
.tsx
.cts
.mts

Разрешение JSX и React-компонентов

Импорт:

import App from "./App";

может ссылаться на:

App.jsx

или

App.tsx

Parcel автоматически определяет тип файла и запускает соответствующий трансформер.


Импорт ресурсов как зависимостей

В Parcel зависимостью является не только JavaScript.

Пример:

import "./styles.css";

CSS-файл становится отдельным узлом графа.

Аналогично:

import logo from "./logo.svg";
import image from "./photo.png";
import data from "./users.json";

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


CSS-зависимости

Файл:

@import "./variables.css";

создаёт новую связь в графе.

Также анализируются конструкции:

background-image: url("./bg.png");

Parcel автоматически находит ресурс:

bg.png

и включает его в процесс сборки.


URL-зависимости

Parcel умеет отслеживать ресурсы через URL.

Пример:

new URL("./worker.js", import.meta.url);

или

new URL("./image.png", import.meta.url);

Такие ссылки рассматриваются как полноценные зависимости проекта.


Web Workers и разрешение модулей

Создание Worker:

new Worker(
  new URL("./worker.js", import.meta.url)
);

Приводит к созданию отдельного графа зависимостей.

Parcel:

  1. находит worker-файл;
  2. строит независимый граф;
  3. создаёт отдельный бандл.

Динамические зависимости

Динамический импорт:

import("./settings.js");

создаёт асинхронную зависимость.

Parcel выделяет её в отдельный пакет:

main.bundle.js
settings.bundle.js

Связь между ними сохраняется в графе проекта.


Разрешение зависимостей в монорепозиториях

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

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

Пакет:

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

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

Parcel корректно работает с:

  • npm workspaces;
  • Yarn workspaces;
  • pnpm workspaces.

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


Многие менеджеры пакетов создают символьные ссылки.

Например:

node_modules/ui
    -> ../. ./packages/ui

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


Кэширование результатов разрешения

Поиск зависимостей является дорогой операцией.

Parcel сохраняет результаты:

  • анализа package.json;
  • поиска файлов;
  • вычисления графа;
  • проверки версий.

При повторной сборке информация берётся из кэша, что значительно ускоряет работу.


Ошибки разрешения зависимостей

Типичная ошибка:

import api from "./api";

при отсутствии файла:

api.js

Parcel выдаёт подробную диагностику:

Cannot resolve dependency './api'

Часто дополнительно показываются:

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

Конфликты версий

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

Пример:

react@18
react@19

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

Благодаря этому корректно работают даже сложные деревья зависимостей.


Tree Shaking и разрешение зависимостей

После построения графа Parcel анализирует используемые экспорты.

Исходный код:

import { add } from "./math";

Модуль:

export function add() {}
export function sub() {}
export function mul() {}

Если используются только:

add()

Parcel может удалить:

sub()
mul()

из финальной сборки.

Качество tree shaking напрямую зависит от точности разрешения зависимостей.


Влияние графа зависимостей на разделение кода

После построения полного графа Parcel определяет:

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

Например:

import("./admin");
import("./profile");

может привести к созданию:

main.js
admin.js
profile.js
shared.js

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


Пользовательские резолверы

Архитектура Parcel поддерживает плагины-резолверы.

Схема работы:

Import
   ↓
Resolver
   ↓
Asset
   ↓
Transformer

Резолвер может:

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

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


Внутренний процесс разрешения зависимостей

Упрощённая последовательность действий Parcel выглядит следующим образом:

Импорт найден
      ↓
Определение типа зависимости
      ↓
Поиск файла или пакета
      ↓
Чтение package.json
      ↓
Определение точки входа
      ↓
Проверка условий exports
      ↓
Создание узла графа
      ↓
Рекурсивный обход новых зависимостей
      ↓
Формирование полного Dependency Graph

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