Во многих проектах требуется изменять значения глобальных идентификаторов ещё на этапе сборки. Наиболее распространённые сценарии:
В Esbuild для подобных задач используется параметр define. Он позволяет заменять указанные идентификаторы на произвольные выражения во время компиляции.
В отличие от присваивания значений в рантайме, такая замена выполняется до запуска приложения, что даёт дополнительные возможности для оптимизации и удаления неиспользуемого кода.
Базовый пример:
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
outfile: 'dist/app.js',
define: {
DEBUG: 'false'
}
});
Исходный код:
if (DEBUG) {
console.log('Отладка включена');
}
После сборки:
if (false) {
console.log('Отладка включена');
}
При включённой минификации Esbuild дополнительно удалит недостижимый код:
// код блока исчезнет полностью
Таким образом, механизм работает не как обычное присваивание значения переменной, а как текстовая подстановка корректного JavaScript-выражения с последующим анализом дерева программы.
Особенность параметра define заключается в том, что
значения передаются в виде строк, содержащих JavaScript-код.
Например:
define: {
API_URL: '"https://api.example.com"'
}
Исходный код:
fetch(API_URL);
Результат:
fetch("https://api.example.com");
Если забыть внутренние кавычки:
define: {
API_URL: 'https://api.example.com'
}
Esbuild воспримет содержимое как JavaScript-код и выдаст ошибку синтаксиса.
Правильный вариант всегда должен содержать строковый литерал:
define: {
API_URL: '"https://api.example.com"'
}
Чаще всего define используется именно для логических
флагов.
Конфигурация:
define: {
IS_DEV: 'true',
IS_PROD: 'false'
}
Код приложения:
if (IS_DEV) {
console.log('Режим разработки');
}
if (IS_PROD) {
enableAnalytics();
}
После сборки:
if (true) {
console.log('Режим разработки');
}
if (false) {
enableAnalytics();
}
После минификации:
console.log("Режим разработки");
Весь продакшен-код будет удалён.
Define подходит не только для строк и булевых значений.
Конфигурация:
define: {
VERSION_MAJOR: '2',
VERSION_MINOR: '5'
}
Исходный код:
const version = `${VERSION_MAJOR}.${VERSION_MINOR}`;
После сборки:
const version = `${2}.${5}`;
Один из самых распространённых сценариев связан с переменной окружения Node.js.
Многие библиотеки проверяют:
process.env.NODE_ENV
Чтобы передать нужное значение:
define: {
'process.env.NODE_ENV': '"production"'
}
Исходный код:
if (process.env.NODE_ENV !== 'production') {
console.log('Отладочная информация');
}
После обработки:
if ("production" !== 'production') {
console.log('Отладочная информация');
}
После оптимизации условие исчезнет.
Часто значения берутся непосредственно из окружения Node.js.
Пример:
const isProduction =
process.env.NODE_ENV === 'production';
await esbuild.build({
define: {
__DEV__: String(!isProduction)
}
});
Либо:
await esbuild.build({
define: {
API_URL: JSON.stringify(process.env.API_URL)
}
});
Если переменная содержит:
https://api.company.com
то результатом станет:
define: {
API_URL: '"https://api.company.com"'
}
Использование JSON.stringify() считается наиболее
безопасным способом передачи строковых значений.
Define может содержать не только литералы.
Например:
define: {
BUILD_DATE: 'Date.now()'
}
Код:
console.log(BUILD_DATE);
После замены:
console.log(Date.now());
Однако подобный подход используется редко. Обычно рекомендуется подставлять уже вычисленные константы.
Лучший вариант:
define: {
BUILD_DATE: String(Date.now())
}
После сборки:
console.log(1717286400000);
Для уменьшения вероятности конфликтов обычно применяются специальные имена:
define: {
__DEV__: 'true',
__TEST__: 'false',
__BUILD_VERSION__: '"2.1.0"'
}
Пример:
if (__DEV__) {
enableDebugTools();
}
Такой стиль широко распространён в экосистеме JavaScript и позволяет сразу отличать сборочные константы от обычных переменных.
В одном объекте define может находиться любое количество замен.
define: {
__DEV__: 'false',
__API_URL__: '"https://api.example.com"',
__VERSION__: '"3.4.1"',
__FEATURE_CHAT__: 'true',
__FEATURE_PAYMENTS__: 'false'
}
Использование:
console.log(__VERSION__);
if (__FEATURE_CHAT__) {
startChat();
}
if (__FEATURE_PAYMENTS__) {
initPayments();
}
Во время сборки все идентификаторы будут заменены соответствующими значениями.
Define особенно полезен совместно с tree shaking.
Исходный код:
function debugLog(message) {
console.log(message);
}
if (__DEV__) {
debugLog('Инициализация');
}
Конфигурация:
define: {
__DEV__: 'false'
}
После преобразования:
if (false) {
debugLog('Инициализация');
}
После оптимизации:
// блок удалён
Если функция больше нигде не используется, Esbuild может удалить и её.
Feature flags позволяют включать и отключать функциональность на этапе сборки.
Конфигурация:
define: {
FEATURE_NEW_UI: 'true',
FEATURE_BETA_API: 'false'
}
Код:
if (FEATURE_NEW_UI) {
renderNewInterface();
}
if (FEATURE_BETA_API) {
useBetaApi();
}
После минификации останется только реально используемый функционал.
Подобный подход часто применяется в крупных приложениях для формирования разных вариантов сборки.
await esbuild.build({
define: {
__DEV__: 'true',
__API_URL__: '"http://localhost:3000"'
}
});
await esbuild.build({
define: {
__DEV__: 'false',
__API_URL__: '"https://api.company.com"'
}
});
Исходный код остаётся одинаковым:
if (__DEV__) {
console.log('Запуск');
}
fetch(__API_URL__);
Различия появляются исключительно на этапе сборки.
Параметр доступен не только через JavaScript API.
Пример:
esbuild src/index.js \
--bundle \
--define:DEBUG=false \
--outfile=dist/app.js
Для строк:
esbuild src/index.js \
--define:API_URL=\"https://api.example.com\"
Либо:
esbuild src/index.js \
--define:process.env.NODE_ENV=\"production\"
TypeScript не знает о существовании переменных, определённых через
define.
Код:
if (__DEV__) {
console.log('debug');
}
Компилятор выдаст ошибку:
Cannot find name '__DEV__'
Необходимо объявить глобальную константу.
Файл:
declare const __DEV__: boolean;
Либо:
declare const __VERSION__: string;
После этого TypeScript сможет корректно выполнять проверку типов.
Работает:
define: {
DEBUG: 'true'
}
Не работает:
const DEBUG = false;
console.log(DEBUG);
Локальная переменная перекрывает глобальный идентификатор.
Define не является системой поиска и замены строк.
Например:
define: {
hello: '"world"'
}
Не приведёт к изменению строки:
console.log("hello");
Заменяются только элементы синтаксического дерева JavaScript.
Плохой вариант:
define: {
VERSION: '"1.0.0"'
}
Если в проекте появится переменная:
const VERSION = 'test';
возникнет неоднозначность.
Гораздо безопаснее:
define: {
__APP_VERSION__: '"1.0.0"'
}
Переменная окружения:
process.env.API_URL
определяется во время выполнения программы.
Define:
__API_URL__
заменяется во время сборки.
Сравнение:
| Характеристика | process.env | define |
|---|---|---|
| Доступна во время выполнения | Да | Нет |
| Заменяется при сборке | Нет | Да |
| Позволяет удалять код | Нет | Да |
| Участвует в tree shaking | Нет | Да |
| Подходит для feature flags | Частично | Да |
import * as esbuild from 'esbuild';
const production =
process.env.NODE_ENV === 'production';
await esbuild.build({
entryPoints: ['src/index.js'],
bundle: true,
minify: production,
outfile: 'dist/app.js',
define: {
__DEV__: String(!production),
__VERSION__: JSON.stringify('2.4.0'),
__API_URL__: JSON.stringify(
production
? 'https://api.company.com'
: 'http://localhost:3000'
),
'process.env.NODE_ENV': JSON.stringify(
production
? 'production'
: 'development'
)
}
});
Использование в приложении:
console.log('Версия:', __VERSION__);
if (__DEV__) {
console.log('Режим разработки');
}
fetch(`${__API_URL__}/users`);
В результате каждая сборка получает собственный набор констант, а Esbuild может удалить весь код, который никогда не будет выполнен в выбранном окружении.