Условная логика: dev, staging, prod

В реальных проектах сборка JavaScript почти всегда зависит от окружения выполнения: development, staging и production. Каждое окружение предъявляет разные требования к коду, оптимизациям, логированию и подключаемым зависимостям. Esbuild предоставляет быстрый и предсказуемый механизм управления условной логикой через параметры сборки, переменные окружения и подстановки значений на этапе компиляции.


Базовый принцип разделения окружений

Ключевая идея заключается в том, что различия между окружениями должны фиксироваться на этапе сборки, а не во время выполнения. Это позволяет:

  • удалять лишний код из production-сборки
  • включать расширенное логирование в development
  • использовать разные API endpoints
  • управлять feature flags без runtime-издержек

Esbuild реализует это через опцию define, переменные окружения и конфигурационные скрипты Node.js.


Использование process.env в Esbuild

Наиболее распространённый подход — использование переменных окружения:

NODE_ENV=development node build.js
NODE_ENV=production node build.js

В Windows часто применяется cross-env:

cross-env NODE_ENV=production node build.js

Внутри конфигурации esbuild:

import * as esbuild from 'esbuild';

const env = process.env.NODE_ENV;

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  define: {
    'process.env.NODE_ENV': JSON.stringify(env)
  }
});

Ключевой момент: значение должно быть сериализовано через JSON.stringify, иначе в код попадёт некорректный литерал.


Условная логика внутри кода

После подстановки process.env.NODE_ENV код можно разделять на ветки:

if (process.env.NODE_ENV === 'production') {
  console.log = () => {};
}

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


Удаление debug-кода через define

Опция define является центральным инструментом условной компиляции.

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  outfile: 'dist/app.js',
  define: {
    __DEV__: JSON.stringify(process.env.NODE_ENV !== 'production'),
    __PROD__: JSON.stringify(process.env.NODE_ENV === 'production')
  }
});

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

if (__DEV__) {
  console.log('Development mode');
}

При production-сборке условие становится статическим:

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

После минификации блок полностью удаляется.


Разделение конфигураций сборки

Для сложных проектов применяется разделение конфигураций:

build/
  esbuild.dev.js
  esbuild.staging.js
  esbuild.prod.js

Development-конфигурация

import * as esbuild from 'esbuild';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  sourcemap: true,
  outfile: 'dist/bundle.js',
  define: {
    'process.env.NODE_ENV': '"development"'
  }
});

Особенности:

  • включены source maps
  • отключена минификация
  • сохраняется читаемость кода

Production-конфигурация

import * as esbuild from 'esbuild';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  sourcemap: false,
  outfile: 'dist/bundle.js',
  define: {
    'process.env.NODE_ENV': '"production"'
  }
});

Особенности:

  • минимизация кода
  • отсутствие source maps (или их отдельная генерация)
  • агрессивное удаление неиспользуемых веток

Staging-конфигурация

Staging часто используется как промежуточная среда для тестирования production-поведения:

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  sourcemap: true,
  outfile: 'dist/bundle.js',
  define: {
    'process.env.NODE_ENV': '"staging"'
  }
});

Feature flags через define

Esbuild позволяет реализовать feature flags без runtime-логики:

define: {
  __FEATURE_CHAT__: 'true',
  __FEATURE_ANALYTICS__: 'false'
}

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

if (__FEATURE_CHAT__) {
  import('./chat.js').then(initChat);
}

При сборке неподдерживаемые ветки исчезают после tree-shaking.


Условные импорты модулей

Esbuild поддерживает динамическое разбиение кода, но условность должна быть статически определима:

let logger;

if (__DEV__) {
  logger = await import('./logger.dev.js');
} else {
  logger = await import('./logger.prod.js');
}

При правильной конфигурации сборщик создаёт отдельные чанки для каждого варианта.


Разделение точек входа по окружениям

Иногда используется подход с разными entry points:

entryPoints: [
  'src/index.dev.js'
]

Или динамическое переключение:

const entryPoints =
  process.env.NODE_ENV === 'production'
    ? ['src/index.prod.js']
    : ['src/index.dev.js'];

Использование plugins для окружений

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

Пример плагина подстановки API URL:

const envPlugin = {
  name: 'env-plugin',
  setup(build) {
    const env = process.env.NODE_ENV;

    build.onResolve({ filter: /^@env$/ }, () => ({
      path: '@env',
      namespace: 'env-ns'
    }));

    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => {
      return {
        contents: `
          export const API_URL =
            ${env === 'production'
              ? '"https://api.prod.com"'
              : '"http://localhost:3000"'}
        `
      };
    });
  }
};

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

import { API_URL } from '@env';

Изоляция конфигурации через build script

Типовая структура build-скрипта:

import * as esbuild from 'esbuild';

const isProd = process.env.NODE_ENV === 'production';

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: isProd,
  sourcemap: !isProd,
  outfile: 'dist/app.js',
  define: {
    __DEV__: JSON.stringify(!isProd),
    __PROD__: JSON.stringify(isProd),
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  }
});

Такая схема централизует всю условную логику в одном месте, исключая расхождение конфигураций.


Комбинирование с TypeScript и типизацией

При использовании TypeScript условные переменные объявляются через global.d.ts:

declare const __DEV__: boolean;
declare const __PROD__: boolean;

Это позволяет сохранять статическую проверку без runtime-определений.


Оптимизация условного кода

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

  • сворачивание констант
  • удаление недостижимых веток
  • tree-shaking модулей
  • инлайнинг простых выражений

Пример:

if (false) {
  expensiveFunction();
}

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

// блок удалён

Сценарии реального использования окружений

Логирование

if (__DEV__) {
  console.debug('Debug info');
}

Подключение аналитики

if (__PROD__) {
  import('./analytics.js');
}

Разные API

const API =
  process.env.NODE_ENV === 'production'
    ? 'https://api.service.com'
    : 'http://localhost:4000';

Типичные ошибки при условной логике

  • использование runtime-вычислений вместо compile-time значений
  • отсутствие JSON.stringify в define
  • смешивание окружений в одном bundle без разделения конфигураций
  • попытка менять process.env во время выполнения
  • использование сложных выражений, которые не могут быть оптимизированы Esbuild

Роль детерминированной сборки

Условная логика в Esbuild работает эффективно только при детерминированных значениях. Сборщик опирается на статический анализ, поэтому любые условия должны быть вычислимы до этапа bundling. Это позволяет превращать конфигурацию окружений в механизм компиляции, а не выполнения.