Внешние зависимости через build.rollupOptions.external

Параметр build.rollupOptions.external используется для исключения зависимостей из итогового бандла во время сборки проекта через Vite. Настройка передаётся напрямую в конфигурацию Rollup, который используется внутри production-сборки Vite.

Механизм особенно важен при:

  • разработке библиотек;
  • создании SDK;
  • сборке плагинов;
  • публикации npm-пакетов;
  • работе с CDN;
  • интеграции в существующие приложения;
  • микрофронтенд-архитектуре.

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


Базовая структура настройки

import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue']
    }
  }
})

В этом примере пакет vue не будет встроен в итоговый файл.

Если в исходном коде присутствует импорт:

import { ref } from 'vue'

то после сборки Rollup сохранит импорт как внешний:

import { ref } from 'vue'

Вместо внедрения всего runtime Vue внутрь бандла.


Почему исключение зависимостей важно

Уменьшение размера сборки

Без external библиотека может случайно включить огромные зависимости:

  • Vue;
  • React;
  • Lodash;
  • Moment.js;
  • RxJS;
  • Three.js.

Это приводит к:

  • увеличению размера npm-пакета;
  • дублированию кода;
  • конфликтам версий;
  • ухудшению tree-shaking.

Предотвращение дублирования фреймворков

Классическая проблема — две копии React или Vue в приложении.

Например:

  • библиотека содержит встроенный React;
  • основное приложение также использует React.

В результате появляются:

  • ошибки hooks;
  • проблемы с context;
  • нарушение singleton-механизмов;
  • увеличение памяти.

Правильная стратегия — помечать фреймворк как external.


Совместимость с peerDependencies

Чаще всего external используется совместно с peerDependencies.

Пример package.json:

{
  "peerDependencies": {
    "vue": "^3.4.0"
  }
}

Конфигурация Vite:

export default defineConfig({
  build: {
    rollupOptions: {
      external: ['vue']
    }
  }
})

Такой подход означает:

  • библиотека требует Vue;
  • Vue должен установить конечный проект;
  • Vue не будет встроен в сборку библиотеки.

Исключение нескольких зависимостей

export default defineConfig({
  build: {
    rollupOptions: {
      external: [
        'vue',
        'axios',
        'lodash'
      ]
    }
  }
})

Теперь Rollup оставит все эти импорты внешними.


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

Иногда импортируются внутренние пути:

import debounce from 'lodash/debounce'

Простого указания 'lodash' недостаточно.

Необходимо:

external: [
  'lodash',
  'lodash/debounce'
]

Или использовать функцию.


external как функция

Наиболее гибкий вариант.

export default defineConfig({
  build: {
    rollupOptions: {
      external(id) {
        return id.includes('lodash')
      }
    }
  }
})

Теперь все импорты lodash автоматически становятся внешними:

lodash
lodash/map
lodash/debounce
lodash/throttle

Аргументы функции external

Rollup передаёт несколько параметров:

external(id, parentId, isResolved)

id

Идентификатор импортируемого модуля.

external(id) {
  console.log(id)
}

Пример значений:

vue
react
lodash/debounce
./utils.js

parentId

Модуль, из которого выполняется импорт.

external(id, parentId) {
  console.log(parentId)
}

Полезно для сложной логики.


isResolved

Показывает, был ли путь уже разрешён Rollup.


Исключение всех node_modules

Иногда требуется оставить внешними абсолютно все зависимости.

external(id) {
  return !id.startsWith('.') && !path.isAbsolute(id)
}

Такой подход особенно популярен при сборке Node.js-библиотек.

Полный пример:

import path from 'path'
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      external(id) {
        return !id.startsWith('.') && !path.isAbsolute(id)
      }
    }
  }
})

Исключение Node.js builtin-модулей

При разработке backend-библиотек часто исключают встроенные модули Node.js.

external: [
  'fs',
  'path',
  'os',
  'crypto'
]

Или автоматически:

import { builtinModules } from 'module'

export default defineConfig({
  build: {
    rollupOptions: {
      external: builtinModules
    }
  }
})

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

Rollup поддерживает регулярные выражения.

external: [
  /^lodash/
]

Подойдут:

lodash
lodash/map
lodash/debounce

Автоматическое исключение peerDependencies

Популярная практика — динамически читать package.json.

import { defineConfig } from 'vite'
import packageJson from './package.json'

export default defineConfig({
  build: {
    rollupOptions: {
      external: Object.keys(
        packageJson.peerDependencies || {}
      )
    }
  }
})

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


Исключение зависимостей при library mode

Наиболее частый сценарий.

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.js',
      name: 'MyLibrary',
      fileName: 'my-library'
    },
    rollupOptions: {
      external: ['vue']
    }
  }
})

Связь external и output.globals

Если сборка генерирует формат:

umd
iife

то необходимо указать глобальные переменные.

Пример:

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.js',
      name: 'MyLib',
      formats: ['umd']
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

Почему нужны globals

UMD и IIFE работают в браузере через глобальные объекты.

Если Vue исключён из сборки:

external: ['vue']

то Rollup должен понимать:

window.Vue

является реализацией пакета vue.


Пример итоговой UMD-сборки

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

import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)

  return {
    count
  }
}

Результат:

(function(global, factory) {
  factory(global.Vue)
})(this, function(Vue) {

})

Ошибки без globals

Если забыть output.globals, Rollup может выдать:

No name was provided for external module

или браузер получит:

Vue is not defined

Внешние зависимости и CDN

external часто используется совместно с CDN-подключением.

Например:

<script src="https://unpkg.com/vue@3"></script>
<script src="my-lib.js"></script>

Конфигурация:

external: ['vue']

Отличие external от alias

resolve.alias изменяет путь импорта:

resolve: {
  alias: {
    '@': '/src'
  }
}

external полностью исключает модуль из бандла.

Это разные механизмы.


Отличие external от optimizeDeps.exclude

Многие путают эти настройки.

optimizeDeps.exclude

Работает только в dev-режиме.

Влияет на pre-bundling через esbuild.


build.rollupOptions.external

Работает только во время production-сборки.

Управляет содержимым итогового бандла.


Отличие от ssr.external

Vite также поддерживает:

ssr: {
  external: []
}

Это отдельная система для SSR-сборок.

Она не связана напрямую с build.rollupOptions.external.


External и tree-shaking

Внешние зависимости не участвуют в tree-shaking внутри текущего бандла.

Например:

external: ['lodash']

Rollup больше не анализирует содержимое lodash.


Проблемы при неправильной настройке

Дублирование зависимостей

Если забыть external:

React duplicated
Vue duplicated

Огромный размер библиотеки

Библиотека может весить:

2 KB → 700 KB

из-за встроенного фреймворка.


Runtime-ошибки

Особенно критично для:

  • React hooks;
  • Vue reactivity;
  • singleton-хранилищ;
  • dependency injection.

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

Например:

Library → Vue 3.3
Application → Vue 3.5

Встроенная копия может привести к нестабильной работе.


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

После build полезно анализировать итоговые файлы.

Если external настроен правильно:

  • размер бандла заметно меньше;
  • импорт остаётся внешним;
  • зависимости отсутствуют внутри output-файла.

Анализ через Rollup Visualizer

Популярный инструмент:

npm install rollup-plugin-visualizer -D

Подключение:

import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer()
  ]
})

Позволяет увидеть:

  • какие зависимости встроены;
  • размер модулей;
  • структуру chunks;
  • ошибки external-конфигурации.

External в монорепозиториях

В monorepo часто исключают внутренние пакеты.

external: [
  '@company/ui',
  '@company/core'
]

Это предотвращает:

  • циклические зависимости;
  • дублирование пакетов;
  • повторную упаковку workspace-модулей.

Комбинация с preserveModules

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

output: {
  preserveModules: true
}

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


External для React-библиотеки

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

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.jsx',
      formats: ['es', 'umd']
    },
    rollupOptions: {
      external: [
        'react',
        'react-dom'
      ],
      output: {
        globals: {
          react: 'React',
          'react-dom': 'ReactDOM'
        }
      }
    }
  }
})

External для Vue-библиотеки

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.js',
      formats: ['es']
    },
    rollupOptions: {
      external: ['vue']
    }
  }
})

External для Node.js SDK

import { builtinModules } from 'module'

export default defineConfig({
  build: {
    target: 'node18',
    rollupOptions: {
      external: [
        ...builtinModules,
        'axios'
      ]
    }
  }
})

Когда не следует использовать external

Не стоит исключать зависимости, если:

  • приложение должно быть полностью автономным;
  • библиотека не предполагает peerDependencies;
  • dependency должна гарантированно присутствовать;
  • требуется единый self-contained bundle.

Практическая стратегия для библиотек

Наиболее распространённая схема:

В peerDependencies

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

В external

external: ['react']

В globals

globals: {
  react: 'React'
}

Автоматизация external через helper-функцию

import packageJson from './package.json'

function getExternalPackages() {
  return [
    ...Object.keys(
      packageJson.dependencies || {}
    ),
    ...Object.keys(
      packageJson.peerDependencies || {}
    )
  ]
}

export default defineConfig({
  build: {
    rollupOptions: {
      external: getExternalPackages()
    }
  }
})

Влияние на ESM-сборки

Для формата:

es

внешние импорты сохраняются как ES imports:

import React from 'react'

Это оптимальный вариант для современных bundlers.


Влияние на CommonJS

При генерации CommonJS:

cjs

внешние зависимости преобразуются:

const React = require('react')

Влияние на browser environment

Если dependency external, то браузер обязан получить её отдельно:

  • через CDN;
  • через import map;
  • через bundler;
  • через script tag.

Иначе приложение не сможет найти модуль во время выполнения.


Наиболее распространённые ошибки

Исключение относительных путей

Неверно:

external: ['./utils.js']

Обычно internal-модули не должны быть external.


Забытые подмодули

lodash/debounce
rxjs/operators
date-fns/format

Отсутствие globals для UMD

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


Несовпадение peerDependencies и external

Если пакет находится в peerDependencies, но отсутствует в external, он всё равно попадёт в бандл.


Рекомендуемый production-подход

Для библиотек:

  • framework → external;
  • peerDependencies → external;
  • builtin modules → external;
  • тяжёлые optional dependencies → external.

Для обычных SPA-приложений external используется значительно реже, поскольку приложения обычно собираются в self-contained bundle.