Вынесение зависимостей в external

Вынесение зависимостей в external — один из ключевых механизмов Rollup, который определяет, какие модули попадут в итоговый бандл, а какие останутся внешними и будут загружаться отдельно в рантайме. Эта концепция напрямую влияет на размер сборки, стратегию публикации библиотек и способ интеграции с окружением (Node.js, браузер, CDN).

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

Самая простая форма — массив строк, где перечисляются имена пакетов:

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  external: ['lodash', 'axios']
};

В этом случае любые импорты:

import _ from 'lodash';
import axios from 'axios';

не будут включены в бандл. Вместо этого Rollup оставит их как есть.

Поведение при различных форматах сборки

Результат работы external зависит от формата выходного файла.

ESM

При format: 'esm' внешние зависимости сохраняются как стандартные import:

import _ from 'lodash';

Браузер или Node.js обязан самостоятельно разрешить этот импорт.

CommonJS

При format: 'cjs' зависимости преобразуются в require:

const _ = require('lodash');

Rollup не пытается резолвить модуль, оставляя ответственность системе выполнения.

UMD / IIFE

Для UMD и IIFE ситуация отличается: внешние зависимости должны быть представлены через глобальные переменные:

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.umd.js',
    format: 'umd',
    name: 'MyLib',
    globals: {
      lodash: '_'
    }
  },
  external: ['lodash']
};

В этом случае Rollup заменяет импорт на обращение к глобальной переменной _.

Функциональная форма external

Помимо массива можно использовать функцию, что позволяет реализовать более гибкую логику:

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.js',
    format: 'esm'
  },
  external: (id) => {
    return id.startsWith('lodash');
  }
};

Функция получает идентификатор модуля и возвращает true, если модуль должен считаться внешним.

Часто используется для:

  • исключения всех node_modules
  • выборочного исключения пакетов
  • условного поведения в зависимости от окружения

Типичные стратегии внешних зависимостей

Полное исключение node_modules

Распространённый паттерн для библиотек:

export default {
  external: id => !id.startsWith('.') && !id.startsWith('/')
};

Здесь все пакеты из node_modules считаются внешними, а локальные файлы остаются внутри бандла.

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

Исключение peerDependencies

Часто external синхронизируют с peerDependencies из package.json:

import pkg from './package.json' assert { type: 'json' };

export default {
  external: Object.keys(pkg.peerDependencies || {})
};

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

Связь external и package.json

В реальных проектах external часто строится на метаданных пакета.

dependencies vs peerDependencies

  • dependencies обычно не выносятся наружу в приложениях
  • peerDependencies почти всегда выносятся в external при сборке библиотек

Типичный сценарий:

import pkg from './package.json' assert { type: 'json' };

const external = [
  ...Object.keys(pkg.dependencies || {}),
  ...Object.keys(pkg.peerDependencies || {})
];

export default {
  input: 'src/index.js',
  output: {
    file: 'dist/index.js',
    format: 'esm'
  },
  external
};

Работа с путями и алиасами

external работает не только с именами пакетов, но и с путями модулей.

external: [
  'react',
  'react-dom',
  '/virtual-module'
]

Также можно учитывать alias-конфигурации, если используется плагин @rollup/plugin-alias, поскольку Rollup принимает решение об external до финального резолва модулей.

Взаимодействие с tree-shaking

external полностью исключает модуль из графа зависимостей Rollup. Это означает:

  • tree-shaking внутри external-модуля не выполняется
  • анализ кода не проводится
  • содержимое пакета не попадает в бандл вообще

Таким образом external — это “жёсткое исключение”, в отличие от оптимизаций внутри включённых модулей.

external и динамические импорты

При использовании import() поведение остаётся аналогичным:

import('lodash').then(_ => {
  console.log(_.chunk([1,2,3], 2));
});

Если lodash объявлен как external, Rollup оставит динамический импорт без изменений.

Однако важно учитывать, что в некоторых форматах (например, IIFE) динамические внешние импорты могут быть невалидны без дополнительной инфраструктуры загрузчика.

Типичные ошибки при использовании external

Ошибка 1: случайное исключение локальных модулей

external: ['src/utils']

В этом случае модуль может быть исключён из сборки, хотя должен быть встроен.

Ошибка 2: несоответствие globals

При UMD-сборке:

external: ['lodash'],
output: {
  format: 'umd',
  globals: {
    lodash: 'lodash'
  }
}

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

Ошибка 3: частичное исключение пакета

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

external: ['lodash/map']

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

Влияние external на размер и архитектуру библиотеки

Использование external напрямую определяет:

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

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

Комбинация external с output.preserveModules

При использовании:

output: {
  preserveModules: true
}

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

External и CDN-стратегии

При сборке для CDN часто внешние зависимости заменяются на глобальные переменные, а сами библиотеки подключаются через <script>:

<script src="https://cdn.jsdelivr.net/npm/lodash"></script>
<script src="bundle.js"></script>

Rollup в этом случае должен быть настроен так:

export default {
  external: ['lodash'],
  output: {
    format: 'iife',
    globals: {
      lodash: '_'
    }
  }
};

Многоуровневые конфигурации external

В сложных проектах external может формироваться динамически:

import pkg from './package.json' assert { type: 'json' };

const baseExternal = [
  ...Object.keys(pkg.peerDependencies || {})
];

const nodeExternal = id =>
  baseExternal.includes(id) || id.startsWith('node:');

export default {
  external: nodeExternal
};

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

Поведение при резолве плагинами

external применяется до большинства трансформаций, но после базового анализа входных модулей. Это означает, что плагины резолва (например, alias или node-resolve) могут повлиять на идентификаторы, которые попадут в external-функцию.

Поэтому логика external должна учитывать реальные id модулей, а не исходные пути импорта в коде.

Стратегическая роль external в Rollup-сборках

external определяет границу между:

  • тем, что контролируется сборщиком
  • тем, что контролируется окружением исполнения

Эта граница особенно важна при разработке:

  • библиотек для npm
  • UI-компонентов
  • SDK для интеграций
  • универсальных модулей для Node и браузера

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