Передача переменных окружения в конфиг

Конфигурация сборщика редко остаётся статической. В разных режимах работы требуется изменять:

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

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

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

NODE_ENV=development
NODE_ENV=production
BUILD_TARGET=modern
ENABLE_ANALYTICS=true
API_URL=https://example.com

Доступ к переменным окружения в Node.js

Конфигурация Rollup выполняется внутри Node.js, поэтому все переменные окружения доступны через объект process.env.

Пример:

console.log(process.env.NODE_ENV);

Внутри rollup.config.js:

export default {
    output: {
        sourcemap: process.env.NODE_ENV !== 'production'
    }
};

Если переменная не существует, значение будет undefined.


Передача переменных через командную строку

Самый простой способ — передать переменную во время запуска.

Linux и macOS

NODE_ENV=production rollup -c

Windows CMD

set NODE_ENV=production && rollup -c

Windows PowerShell

$env:NODE_ENV="production"; rollup -c

Проблема такого подхода — различия между платформами.


Использование cross-env

Пакет cross-env обеспечивает единый синтаксис для всех операционных систем.

Установка:

npm install --save-dev cross-env

Пример package.json:

{
    "scripts": {
        "build": "cross-env NODE_ENV=production rollup -c",
        "dev": "cross-env NODE_ENV=development rollup -c -w"
    }
}

Теперь команды работают одинаково на Linux, macOS и Windows.


Использование переменных внутри конфигурации

Переключение sourcemap

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

export default {
    output: {
        sourcemap: !isProduction
    }
};

Изменение имени файла

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

export default {
    output: {
        file: isProduction
            ? 'dist/app.min.js'
            : 'dist/app.js'
    }
};

Подключение плагинов по условию

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

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

export default {
    plugins: [
        isProduction && terser()
    ]
};

Поскольку Rollup не любит false внутри массива плагинов, обычно используется фильтрация:

plugins: [
    isProduction && terser()
].filter(Boolean)

Файл .env

При большом количестве переменных передавать их вручную неудобно. Для этого используются .env файлы.

Пример:

NODE_ENV=production
API_URL=https://api.example.com
ENABLE_ANALYTICS=true

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

Пакет dotenv загружает переменные из .env в process.env.

Установка:

npm install --save-dev dotenv

Загрузка .env в Rollup

import dotenv from 'dotenv';

dotenv.config();

console.log(process.env.API_URL);

export default {
    // конфигурация
};

После вызова config() все значения становятся доступны через process.env.


Разделение окружений

Обычно используется несколько файлов окружения:

.env
.env.development
.env.production

Загрузка нужного файла

import dotenv from 'dotenv';

const envFile =
    process.env.NODE_ENV === 'production'
        ? '.env.production'
        : '.env.development';

dotenv.config({
    path: envFile
});

export default {};

Значения по умолчанию

Иногда переменная может отсутствовать.

Небезопасный вариант:

const apiUrl = process.env.API_URL;

Безопасный вариант:

const apiUrl =
    process.env.API_URL ||
    'http://localhost:3000';

Приведение типов

Все значения из process.env являются строками.

Даже если указано:

ENABLE_CACHE=true
PORT=3000

Node.js получит:

process.env.ENABLE_CACHE === 'true'
process.env.PORT === '3000'

Булевы значения

const enableCache =
    process.env.ENABLE_CACHE === 'true';

Числа

const port = Number(process.env.PORT);

Проверка NaN

const port = Number(process.env.PORT);

if (Number.isNaN(port)) {
    throw new Error('Некорректный PORT');
}

Передача переменных в клиентский код

Переменные окружения внутри rollup.config.js доступны только во время сборки. Браузер не имеет доступа к process.env.

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


Использование @rollup/plugin-replace

Установка:

npm install --save-dev @rollup/plugin-replace

Замена process.env.NODE_ENV

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

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

Что делает replace

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

if (process.env.NODE_ENV === 'production') {
    enableAnalytics();
}

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

if ('production' === 'production') {
    enableAnalytics();
}

После этого минификатор может удалить лишние ветки.


preventAssignment

Современные версии плагина требуют:

preventAssignment: true

Это предотвращает ошибочные замены в выражениях присваивания.

Например:

process.env.NODE_ENV = 'test';

Передача собственных переменных

replace({
    preventAssignment: true,
    __API_URL__: JSON.stringify(
        process.env.API_URL
    )
})

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

fetch(__API_URL__ + '/users');

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

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

Использование объекта ENV

Иногда удобнее заменить сразу набор значений.

replace({
    preventAssignment: true,
    __ENV__: JSON.stringify({
        API_URL: process.env.API_URL,
        VERSION: process.env.VERSION
    })
})

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

console.log(__ENV__.API_URL);

Использование import.meta.env

Некоторые инструменты используют синтаксис:

import.meta.env.MODE

В чистом Rollup такой механизм отсутствует по умолчанию, но его можно реализовать через replace:

replace({
    preventAssignment: true,
    'import.meta.env.MODE': JSON.stringify(
        process.env.NODE_ENV
    )
})

Условная сборка

Переменные окружения часто применяются для выбора конфигурации.


Разные директории вывода

const outputDir =
    process.env.NODE_ENV === 'production'
        ? 'dist/prod'
        : 'dist/dev';

export default {
    output: {
        dir: outputDir
    }
};

Разные форматы

const format =
    process.env.BUILD_FORMAT || 'esm';

export default {
    output: {
        format
    }
};

Разные точки входа

const input =
    process.env.TARGET === 'admin'
        ? 'src/admin.js'
        : 'src/main.js';

export default {
    input
};

Создание конфигурации как функции

Rollup позволяет экспортировать функцию вместо объекта.

Это особенно удобно при работе с переменными окружения.


Пример

export default () => {
    const isProduction =
        process.env.NODE_ENV === 'production';

    return {
        output: {
            sourcemap: !isProduction
        }
    };
};

Получение аргументов CLI

Rollup передаёт аргументы функции конфигурации.

export default commandLineArgs => {
    console.log(commandLineArgs);

    return {};
};

Запуск:

rollup -c --environment TARGET:mobile

Параметр –environment

Rollup умеет передавать переменные без сторонних инструментов.

rollup -c --environment NODE_ENV:production

Несколько значений:

rollup -c --environment NODE_ENV:production,MINIFY:true

Использование environment внутри конфигурации

export default () => {
    console.log(process.env.NODE_ENV);
    console.log(process.env.MINIFY);

    return {};
};

Rollup автоматически добавляет значения в process.env.


Комбинирование dotenv и CLI

CLI-параметры могут переопределять .env.

import dotenv from 'dotenv';

dotenv.config();

const mode =
    process.env.NODE_ENV || 'development';

Запуск:

rollup -c --environment NODE_ENV:production

В этом случае значение из CLI станет приоритетным.


Проверка обязательных переменных

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

if (!process.env.API_URL) {
    throw new Error(
        'Переменная API_URL обязательна'
    );
}

Централизация конфигурации

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


env.js

import dotenv from 'dotenv';

dotenv.config();

export const config = {
    isProduction:
        process.env.NODE_ENV === 'production',

    apiUrl:
        process.env.API_URL ||
        'http://localhost:3000',

    enableAnalytics:
        process.env.ENABLE_ANALYTICS === 'true'
};

rollup.config.js

import { config } from './env.js';

export default {
    output: {
        sourcemap: !config.isProduction
    }
};

Защита секретных данных

Нельзя передавать в клиентский код:

  • токены;
  • пароли;
  • приватные ключи;
  • секреты API;
  • строки подключения к БД.

Опасный пример:

replace({
    preventAssignment: true,
    __SECRET_KEY__: JSON.stringify(
        process.env.SECRET_KEY
    )
})

После сборки значение окажется внутри JavaScript-файла браузера.


Публичные и приватные переменные

Распространённая практика — разделять переменные по префиксам.

Пример:

PUBLIC_API_URL=https://example.com
SECRET_KEY=123456

В клиент передаются только значения с PUBLIC_.


Автоматическая фильтрация публичных переменных

const publicEnv = Object.fromEntries(
    Object.entries(process.env).filter(
        ([key]) => key.startsWith('PUBLIC_')
    )
);

Передача публичных переменных в replace

replace({
    preventAssignment: true,
    __PUBLIC_ENV__: JSON.stringify(publicEnv)
})

Использование нескольких конфигураций

Разные окружения могут формировать полностью разные сборки.

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

const config = isProduction
    ? productionConfig
    : developmentConfig;

export default config;

Асинхронная загрузка переменных

Иногда значения приходят из внешнего источника.


Асинхронная конфигурация

export default async () => {
    const response = await fetchConfig();

    return {
        plugins: [
            replace({
                preventAssignment: true,
                __API_URL__: JSON.stringify(
                    response.apiUrl
                )
            })
        ]
    };
};

Использование JSON-файлов вместо .env

Некоторые проекты предпочитают JSON-конфигурации.


config.json

{
    "apiUrl": "https://example.com",
    "debug": false
}

Подключение

import config from './config.json';

export default {
    plugins: [
        replace({
            preventAssignment: true,
            __CONFIG__: JSON.stringify(config)
        })
    ]
};

Типичные ошибки

Отсутствие JSON.stringify

Неправильно:

replace({
    'process.env.NODE_ENV': 'production'
})

Результат:

if (production === 'production')

Правильно:

replace({
    'process.env.NODE_ENV': JSON.stringify(
        'production'
    )
})

Использование process.env в браузере без replace

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

console.log(process.env.NODE_ENV);

В браузере:

ReferenceError: process is not defined

Загрузка dotenv после использования переменных

Неправильно:

const mode = process.env.NODE_ENV;

dotenv.config();

Правильно:

dotenv.config();

const mode = process.env.NODE_ENV;

Хранение секретов в репозитории

Файлы .env часто добавляют в .gitignore.

.env
.env.production

Практическая структура проекта

project/
├── src/
├── dist/
├── .env
├── .env.production
├── .env.development
├── env.js
├── package.json
└── rollup.config.js

Комплексный пример конфигурации

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

dotenv.config({
    path:
        process.env.NODE_ENV === 'production'
            ? '.env.production'
            : '.env.development'
});

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

const publicEnv = Object.fromEntries(
    Object.entries(process.env).filter(
        ([key]) => key.startsWith('PUBLIC_')
    )
);

export default {
    input: 'src/main.js',

    output: {
        file: isProduction
            ? 'dist/app.min.js'
            : 'dist/app.js',

        format: 'esm',

        sourcemap: !isProduction
    },

    plugins: [
        replace({
            preventAssignment: true,

            'process.env.NODE_ENV':
                JSON.stringify(
                    process.env.NODE_ENV
                ),

            __PUBLIC_ENV__:
                JSON.stringify(publicEnv)
        }),

        isProduction && terser()
    ].filter(Boolean)
};