Опция define: замена идентификаторов на значения

Опция define в Esbuild предназначена для замены идентификаторов на заданные значения во время сборки проекта. По своей сути она работает как механизм препроцессорных констант: каждое вхождение указанного идентификатора подменяется соответствующим выражением ещё до выполнения кода в браузере или среде Node.js.

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

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

Благодаря тому, что замена происходит на этапе сборки, Esbuild может дополнительно выполнять оптимизации, включая удаление недостижимого кода (tree shaking и dead code elimination).


Базовый синтаксис

Использование через CLI

esbuild src/index.js \
  --bundle \
  --define:DEBUG=true

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

if (DEBUG) {
  console.log("Отладочный режим");
}

После обработки:

if (true) {
  console.log("Отладочный режим");
}

Использование через JavaScript API

import * as esbuild from "esbuild";

await esbuild.build({
  entryPoints: ["src/index.js"],
  bundle: true,
  outfile: "dist/app.js",
  define: {
    DEBUG: "true"
  }
});

Особенность: значения передаются строками

В объекте define все значения задаются строками.

Например:

define: {
  DEBUG: "true"
}

не означает передачу строки "true".

Esbuild воспринимает содержимое как JavaScript-выражение и вставляет его напрямую в код.

Результат:

if (DEBUG)

станет

if (true)

Подстановка строковых литералов

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

Пример:

define: {
  API_URL: '"https://api.example.com"'
}

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

fetch(API_URL);

После сборки:

fetch("https://api.example.com");

Замена числовых значений

Можно подставлять числовые литералы.

Настройка:

define: {
  VERSION: "5"
}

Код:

console.log(VERSION);

Результат:

console.log(5);

Замена булевых значений

Один из наиболее частых вариантов использования.

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

define: {
  DEV: "false"
}

Код:

if (DEV) {
  console.log("Development mode");
}

После сборки:

if (false) {
  console.log("Development mode");
}

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


Работа с process.env.NODE_ENV

Это самый распространённый сценарий использования define.

Многие библиотеки определяют режим работы через:

process.env.NODE_ENV

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

define: {
  "process.env.NODE_ENV": '"production"'
}

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

if (process.env.NODE_ENV === "development") {
  console.log("Debug");
}

После замены:

if ("production" === "development") {
  console.log("Debug");
}

Минификатор затем удалит недостижимый код.


Настройка разных режимов сборки

Часто создаются отдельные конфигурации для разработки и продакшена.

Development

define: {
  __DEV__: "true"
}

Production

define: {
  __DEV__: "false"
}

Код приложения:

if (__DEV__) {
  enableDebugTools();
}

В продакшн-сборке вызов будет полностью исключён.


Удаление отладочного кода

Опция define особенно полезна для удаления логирования.

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

if (DEBUG_LOGS) {
  console.log(user);
}

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

define: {
  DEBUG_LOGS: "false"
}

После сборки:

if (false) {
  console.log(user);
}

После минификации:

/* удалено */

Таким образом можно избавляться от большого объёма служебного кода без дополнительных инструментов.


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

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

define: {
  __DEV__: "false",
  __VERSION__: '"2.1.0"',
  MAX_RETRY_COUNT: "5"
}

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

console.log(__VERSION__);

for (let i = 0; i < MAX_RETRY_COUNT; i++) {
  retry();
}

После замены:

console.log("2.1.0");

for (let i = 0; i < 5; i++) {
  retry();
}

Замена сложных выражений

В качестве значения можно передавать не только литералы, но и полноценные выражения JavaScript.

Настройка:

define: {
  IS_BROWSER: "typeof window !== 'undefined'"
}

Код:

if (IS_BROWSER) {
  initBrowserFeatures();
}

После обработки:

if (typeof window !== "undefined") {
  initBrowserFeatures();
}

Использование глобальных флагов

Во многих проектах создаются специальные глобальные константы.

Пример:

define: {
  __FEATURE_CHAT__: "true",
  __FEATURE_PAYMENTS__: "false"
}

Код:

if (__FEATURE_CHAT__) {
  loadChat();
}

if (__FEATURE_PAYMENTS__) {
  loadPayments();
}

После сборки:

if (true) {
  loadChat();
}

if (false) {
  loadPayments();
}

Неиспользуемый функционал может быть полностью удалён из бандла.


Feature Flags

Подход с feature flags позволяет выпускать разные версии приложения из одной кодовой базы.

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

define: {
  ENABLE_NEW_UI: "true"
}

Код:

const page = ENABLE_NEW_UI
  ? renderNewPage()
  : renderOldPage();

После сборки:

const page = true
  ? renderNewPage()
  : renderOldPage();

После оптимизации:

const page = renderNewPage();

Использование с переменными окружения

Часто значения читаются из среды выполнения Node.js.

define: {
  API_URL: JSON.stringify(process.env.API_URL),
  APP_NAME: JSON.stringify(process.env.APP_NAME)
}

Если:

API_URL=https://api.company.com
APP_NAME=Portal

то после сборки:

fetch("https://api.company.com");

console.log("Portal");

Отличие от обычных переменных

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

const DEBUG = false;

if (DEBUG) {
  console.log("debug");
}

и вариант:

if (__DEBUG__) {
  console.log("debug");
}

с конфигурацией:

define: {
  __DEBUG__: "false"
}

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


Ограничения define

Заменяются только идентификаторы

Работает:

define: {
  DEBUG: "true"
}

Код:

if (DEBUG)

Не работает для динамически сформированных строк:

window["DEBUG"]

или

globalThis[name]

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


Замена является текстовой

Следует помнить, что Esbuild не создаёт переменные.

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

define: {
  VALUE: "1 + 2"
}

Код:

console.log(VALUE * 10);

Результат:

console.log((1 + 2) * 10);

Происходит именно подстановка выражения.


Возможны неожиданные совпадения

Если идентификатор используется в разных местах программы, замена произойдёт везде.

Пример:

define: {
  VERSION: '"1.0.0"'
}

Код:

console.log(VERSION);

После обработки:

console.log("1.0.0");

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

__APP_VERSION__
__DEV__
__TEST__
__BUILD_DATE__

Так снижается риск конфликта с обычными переменными приложения.


Практический пример конфигурации

import * as esbuild from "esbuild";

const production = process.argv.includes("--production");

await esbuild.build({
  entryPoints: ["src/main.js"],
  bundle: true,
  minify: production,
  outfile: "dist/app.js",

  define: {
    __DEV__: String(!production),
    __VERSION__: '"3.4.1"',
    "process.env.NODE_ENV": production
      ? '"production"'
      : '"development"'
  }
});

Использование в приложении:

if (__DEV__) {
  console.log("Debug mode");
}

console.log("Version:", __VERSION__);

if (process.env.NODE_ENV === "production") {
  enableOptimizations();
}

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


Взаимодействие с минификацией

Максимальная эффективность достигается при совместном использовании define и minify.

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

define: {
  DEBUG: "false"
},
minify: true

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

if (DEBUG) {
  console.log("debug");
}

startApplication();

После замены:

if (false) {
  console.log("debug");
}

startApplication();

После минификации:

startApplication();

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