В процессе сборки JavaScript-приложений Rollup формирует единый или набор оптимизированных файлов на основе графа зависимостей. На этом этапе нередко происходит неочевидное включение данных, которые не должны попадать в клиентскую часть: API-ключей, токенов доступа, внутренних URL, конфигураций окружения и диагностической информации.
Источником таких утечек становятся:
.env файлов без фильтрации;Rollup, в отличие от серверных рантаймов, не различает «секретные» и «несекретные» данные. Любой импортируемый идентификатор, попавший в граф зависимостей и не удалённый tree-shaking-ом, может оказаться в итоговом бандле.
Одним из ключевых механизмов Rollup является статическая трансформация кода. При этом выражения вида:
const API_URL = process.env.API_URL;
могут быть заменены на конкретное значение на этапе сборки. Это создаёт риск закрепления чувствительных данных в результирующем файле.
Особенно опасны конструкции:
const TOKEN = "secret_token_123";
const KEY = config.privateKey;
При отсутствии дополнительной обработки такие значения становятся частью клиентского JavaScript и могут быть извлечены из браузера любым пользователем.
@rollup/plugin-replaceОдним из основных инструментов контроля подстановки значений является плагин замены строк.
import replace from '@rollup/plugin-replace';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es'
},
plugins: [
replace({
preventAssignment: true,
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.API_URL': JSON.stringify('https://api.example.com')
})
]
};
Механизм работы основан на прямой замене AST-узлов, что делает подстановку детерминированной. Однако при неправильной конфигурации возможна утечка значений, особенно если переменные передаются динамически или через промежуточные модули.
Безопасная работа с окружением предполагает разделение конфигурации на публичную и приватную части.
Типовая структура:
config/
public.js
private.js
public.js:
export const config = {
apiBase: 'https://api.example.com',
appVersion: '1.0.0'
};
private.js:
export const secrets = {
apiKey: process.env.API_KEY,
dbPassword: process.env.DB_PASSWORD
};
В Rollup сборке допускается импорт только публичного модуля:
import { config } from './config/public.js';
При этом приватный модуль исключается через external или
условную компиляцию.
external для предотвращения включения модулейПоле external в конфигурации Rollup позволяет исключить
зависимости из бандла.
export default {
input: 'src/index.js',
external: ['dotenv', './config/private.js']
};
Исключённые модули не попадают в граф сборки и остаются на стороне окружения выполнения. Это критично для серверных конфигураций, где секреты должны оставаться вне клиентского контекста.
Tree-shaking в Rollup удаляет недостижимый код, но его эффективность зависит от статической природы условий.
Пример:
if (process.env.SHOW_DEBUG === 'true') {
console.log('Debug mode');
}
При корректной подстановке через replace:
replace({
'process.env.SHOW_DEBUG': JSON.stringify('false')
});
код трансформируется в:
if ('false' === 'true') {
console.log('Debug mode');
}
и впоследствии может быть удалён как dead code.
Однако если значение не является статическим, например:
const flag = getRuntimeFlag();
if (flag) {
console.log('Debug');
}
Rollup не способен безопасно удалить такой блок.
.env файловДля работы с переменными окружения часто используется загрузка
.env через Node.js слой:
import dotenv from 'dotenv';
dotenv.config();
Однако прямое подключение dotenv в клиентский бандл
приводит к утечке всей структуры process.env.
Корректный подход — извлечение значений на этапе сборки:
import { config } from 'dotenv';
import replace from '@rollup/plugin-replace';
config();
export default {
plugins: [
replace({
preventAssignment: true,
'process.env.API_KEY': JSON.stringify(process.env.API_KEY)
})
]
};
После этого dotenv исключается через
external, чтобы не попасть в итоговый bundle.
Типовые причины попадания секретов в бандл:
export const config = {
apiUrl: '...',
apiKey: process.env.API_KEY
};
export const getKey = () => process.env.API_KEY;
console.log(process.env);
Rollup не различает критичность данных — любая экспортируемая сущность считается частью публичного API модуля.
Практика разделения на client/server сборки снижает вероятность утечек.
Пример конфигурации:
export default [
{
input: 'src/client.js',
output: {
file: 'dist/client.js',
format: 'es'
}
},
{
input: 'src/server.js',
output: {
file: 'dist/server.js',
format: 'cjs'
}
}
];
Серверная сборка содержит доступ к секретам, клиентская — только публичные данные.
Неконтролируемый инлайнинг может привести к попаданию чувствительных данных в неожиданные места.
Особенно критичны:
Пример проблемного кода:
const secret = process.env.API_KEY;
export const requestUrl = (path) =>
`${secret}@api.service.com/${path}`;
После сборки значение secret становится частью строки
URL, доступной в клиенте.
Безопасная модель предполагает экспорт только тех значений, которые допустимы для клиента:
Любой экспортируемый объект рассматривается как потенциально публичный:
export const publicConfig = {
apiUrl: 'https://api.example.com'
};
Часто утечки происходят не в собственном коде, а через зависимости. Некоторые пакеты могут содержать:
Rollup позволяет анализировать граф зависимостей через плагины визуализации, однако финальная ответственность за включение пакетов остаётся на уровне конфигурации сборки.
Помимо replace, применяются дополнительные
механизмы:
Пример удаления логов:
terser({
compress: {
drop_console: true,
drop_debugger: true
}
});
Любой код, попадающий в клиентскую сборку, считается публичным независимо от его источника. Это фундаментальное ограничение архитектуры frontend-сборки.
Rollup обеспечивает инструменты трансформации и оптимизации, но не обеспечивает классификацию данных. Контроль чувствительности реализуется исключительно на уровне структуры проекта и конфигурации сборки.