Shimming глобальных переменных (process, Buffer и др.)

Современные JavaScript-приложения часто пишутся так, будто они выполняются в единой среде. На практике существует как минимум два крупных контекста выполнения: браузер и Node.js. Эти среды предоставляют разные наборы глобальных объектов и API, что приводит к несовместимости кода при переносе между ними.

В Node.js доступны такие сущности, как process, Buffer, __dirname, __filename. В браузере они отсутствуют. В браузере, в свою очередь, доступны window, document, fetch (в зависимости от окружения), но их нет в Node.js.

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

Понятие shim и назначение подмены глобальных объектов

Shim (подмена или полифил) в контексте сборки — это механизм, который имитирует отсутствие глобальных переменных или API в целевой среде.

В Rollup shim используется для следующих задач:

  • эмуляция Node.js-глобалов в браузере
  • замена браузерных глобалов в Node.js-сборке
  • предотвращение runtime-ошибок при обращении к несуществующим объектам
  • обеспечение совместимости сторонних библиотек, написанных под Node.js

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

Глобальные переменные Node.js и их проблемы в браузере

Наиболее проблемные глобальные объекты:

process

process — один из самых часто используемых глобальных объектов Node.js. Он предоставляет доступ к:

  • переменным окружения (process.env)
  • информации о процессе (process.version, process.platform)
  • управлению жизненным циклом процесса

В браузере process отсутствует полностью. Однако многие библиотеки используют конструкции вида:

if (process.env.NODE_ENV === 'production') {
  // оптимизированный код
}

Без shim такой код вызовет ReferenceError.

Buffer

Buffer используется для работы с бинарными данными:

  • обработка файлов
  • сетевые протоколы
  • криптография

В браузере аналог существует частично (например, Uint8Array), но прямого Buffer нет.

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

const buf = Buffer.from('hello');

Без подмены это также приводит к ошибке выполнения.

__dirname и __filename

Эти переменные предоставляют информацию о текущем пути файла в Node.js. В браузере концепция файловой системы отсутствует, поэтому такие переменные не имеют смысла.

Механизм shimming в Rollup

Rollup не предоставляет встроенной полной эмуляции Node.js. Вместо этого используется комбинация плагинов и ручной конфигурации.

Основные подходы:

  • @rollup/plugin-replace
  • @rollup/plugin-inject
  • rollup-plugin-node-globals
  • rollup-plugin-node-polyfills
  • кастомные виртуальные модули

Каждый подход решает свою часть задачи.

Подмена process через plugin-replace

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

Пример конфигурации:

import replace from '@rollup/plugin-replace';

export default {
  plugins: [
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify('production')
    })
  ]
};

Здесь process.env.NODE_ENV заменяется строковым литералом на этапе сборки.

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

  • высокая производительность
  • отсутствие runtime-зависимостей
  • простота

Недостатки:

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

Глубокая эмуляция process через inject

Для более сложных случаев используется @rollup/plugin-inject.

import inject from '@rollup/plugin-inject';

export default {
  plugins: [
    inject({
      process: 'process/browser'
    })
  ]
};

Это приводит к автоматической подстановке импорта:

import process from 'process/browser';

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

Реализация process/browser обычно берётся из пакета process, который частично эмулирует Node.js API.

Полноценные polyfills для Buffer

Для Buffer используется отдельный полифилл:

import inject from '@rollup/plugin-inject';

export default {
  plugins: [
    inject({
      Buffer: ['buffer', 'Buffer']
    })
  ]
};

Это трансформируется в:

import { Buffer } from 'buffer';

Модуль buffer предоставляет реализацию поверх Uint8Array.

node-globals: автоматическая эмуляция окружения

Плагин rollup-plugin-node-globals выполняет более агрессивную подмену:

  • process
  • global
  • Buffer

Он создаёт глобальные shim-объекты, имитирующие Node.js окружение.

Пример:

import globals from 'rollup-plugin-node-globals';

export default {
  plugins: [
    globals()
  ]
};

Механизм работы:

  • анализ AST
  • обнаружение глобальных идентификаторов
  • подстановка импортов или polyfill-кода

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

node-polyfills: комплексная стратегия совместимости

Плагин rollup-plugin-node-polyfills решает проблему комплексно, добавляя набор стандартных Node.js модулей:

  • path
  • crypto
  • stream
  • buffer
  • util
  • events

Он полезен при портировании библиотек из Node.js в браузер.

Пример:

import polyfills from 'rollup-plugin-node-polyfills';

export default {
  plugins: [
    polyfills()
  ]
};

Однако этот подход приводит к значительному увеличению размера сборки, так как многие Node.js API эмулируются поверх браузерных примитивов.

Гибридная стратегия shimming

В реальных проектах редко используется один подход. Чаще применяется комбинация:

  • replace для статических значений
  • inject для глобальных объектов
  • частичные polyfills только для используемых модулей

Пример конфигурации:

import replace from '@rollup/plugin-replace';
import inject from '@rollup/plugin-inject';

export default {
  plugins: [
    replace({
      preventAssignment: true,
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    inject({
      process: 'process/browser',
      Buffer: ['buffer', 'Buffer']
    })
  ]
};

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

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

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

Основные причины:

  • добавление полифиллов для Node.js API
  • дублирование функциональности, уже существующей в браузере
  • отсутствие tree-shaking для некоторых shim-модулей

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

  • stream
  • crypto
  • buffer

Эти модули могут добавлять десятки килобайт к бандлу.

Ошибки при shimming и их диагностика

Типичные проблемы:

ReferenceError: process is not defined

Причина: отсутствие inject или replace для process.

Buffer is not defined

Причина: отсутствие polyfill для buffer.

Неожиданное поведение библиотек

Причина: частичная эмуляция Node.js API, когда библиотека ожидает полную совместимость.

Диагностика обычно включает:

  • анализ итогового бандла
  • проверку зависимостей через npm ls
  • просмотр AST трансформаций Rollup

Ограничения shimming подхода

Shim не является полноценной виртуализацией среды. Его ограничения:

  • невозможность точно воспроизвести Node.js runtime
  • несовместимость с native bindings
  • неполная поддержка потоков и файловой системы
  • различия в event loop модели

Shim решает только задачу синтаксической и частично семантической совместимости.

Архитектурные альтернативы shimming

Вместо массового использования shim иногда применяется иной подход:

  • разделение кода на browser и node entry points
  • использование conditional exports в package.json
  • изоляция платформенно-зависимых модулей

Пример conditional exports:

{
  "exports": {
    "browser": "./dist/browser.js",
    "node": "./dist/node.js"
  }
}

Это уменьшает необходимость в shim и делает сборку более предсказуемой.

Роль Rollup в управлении глобальными зависимостями

Rollup работает на уровне ES-модулей и не навязывает runtime. Это означает, что:

  • он не предоставляет Node.js API
  • не эмулирует окружение по умолчанию
  • оставляет управление глобальными зависимостями разработчику

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