Logo Craft Homelab Docs Нейросети Контакты Telegram
Аудит production-сборки фронтенда: что реально уезжает в dist Трендовые github проекты в нашем телеграм канале. Подпишись →
10 сентября 2026 г.

Папка dist заслуживает отдельной ревизии перед релизом

Тесты зелёные, линтер молчит, pull request одобрен. Кажется, что код проверен со всех сторон. Но ревью читает исходники, а в браузер пользователя уезжает собранный артефакт: минифицированные чанки, карты исходников, случайно попавшие JSON-конфиги. Между тем, что написано в репозитории, и тем, что лежит в dist, помещается целый класс утечек, которые не видно ни в diff, ни в .env.production.

Разберём воспроизводимую процедуру: собрать контрольный релиз, провести инвентаризацию файлов, прогнать поиск по подозрительным паттернам и — самое важное — научиться отличать ожидаемое содержимое от инцидента.

Три разных представления одного релиза

Полезно держать в голове, что у любого фронтенд-релиза есть три независимых слоя:

  • Исходный код — то, что разработчик планировал отправить пользователю.
  • Production-артефакт — то, что реально сгенерировал сборщик после подстановки переменных и минификации.
  • Процесс развёртывания — то, что из артефакта в итоге окажется на боевом домене.

Ошибки прячутся на стыках. Переменная окружения, которую забыли пометить как клиентскую; ветка кода, которую оптимизатор должен был вырезать; карта исходников, которую CI выложил рядом с бандлом. Всё это проходит ревью исходников без единого замечания.

Контрольный релиз с маркерами

Чтобы проверить процедуру, соберём заведомо неидеальный релиз. В .env.production кладём смесь легитимных клиентских значений и того, чему в браузере делать нечего:

VITE_PUBLIC_API_URL=https://api.production.example.test/v1
VITE_PUBLISHABLE_KEY=pk_test_MARKER_ONLY
VITE_SECRET_KEY=sk_test_MARKER_ONLY
VITE_STAGING_API_URL=https://staging-api.example.test/v1
VITE_EXPERIMENTAL_CHECKOUT=true

Чтобы значения действительно попали в сборку, приложение должно к ним обратиться. В src/main.js:

const clientConfig = {
  apiUrl: import.meta.env.VITE_PUBLIC_API_URL,
  publishableKey: import.meta.env.VITE_PUBLISHABLE_KEY,
  secretKeyMistake: import.meta.env.VITE_SECRET_KEY,
  stagingApiUrl: import.meta.env.VITE_STAGING_API_URL,
  experimentalCheckout: import.meta.env.VITE_EXPERIMENTAL_CHECKOUT,
  diagnosticLabel: "DEBUG_RETAINED_FALLBACK_TO_V1",
};

if (import.meta.env.DEV) {
  console.debug("DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED");
}

document.querySelector("#inventory").textContent =
  JSON.stringify(clientConfig, null, 2);

Сборщики уровня Vite подставляют import.meta.env во время билда. Любая переменная с префиксом VITE_, к которой обращается код, становится частью клиентского бандла в открытом виде. Ветка под import.meta.env.DEV в production-режиме считается мёртвым кодом и должна быть удалена из исполняемого файла.

Инвентаризация вместо слепого grep

Первый шаг — не поиск секретов, а список того, из чего вообще состоит релиз:

find dist -type f | sort

Так обнаруживаются забытые *.map, отчёты бандл-анализатора, тестовые фикстуры и конфиги, которые не читаются при просмотре исходников. Только после этого имеет смысл искать по содержимому:

rg -n 'secret|token|api|staging|debug|sourceMappingURL' dist -g '*.js'

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

Разбор находок

Одинаковая отметка «есть в бандле» скрывает совершенно разные ситуации.

Production-адрес API. Браузеру нужно знать, куда слать запросы, поэтому URL в клиентском конфиге ожидаем. Проверяем не отсутствие адресов, а соответствие содержимого ожиданиям: тот ли это домен, та ли версия API.

Два ключа с похожими именами. pk_test_… — публикуемый ключ, он рассчитан на работу в браузере и не даёт доступа к операциям аккаунта; его присутствие в JS нормально. sk_test_… — серверный ключ, он подтверждает привилегированные запросы к платёжному провайдеру. Даже тестовый серверный ключ в клиентской сборке — повод остановить развёртывание и ротировать секрет.

Staging-URL. Сам по себе адрес тестового стенда секретом не является, но мы проверяем production-артефакт, а внутри лежит конфигурация другого окружения. Причина обычно в одном из трёх: конвейер подтянул не тот .env, адрес забыли вычистить после отладки, либо он действительно нужен коду и это надо зафиксировать явно.

Фича-флаг. VITE_EXPERIMENTAL_CHECKOUT=true попадает в бандл целиком, вместе со всей логикой ветки. Клиентский флаг годится для управления интерфейсом. Если за флагом скрыт доступ к платным или приватным операциям, проверку обязательно дублировать на бэкенде: пользователь легко переключит значение через DevTools или дёрнет закрытый метод напрямую.

Debug-строка в исполняемом коде. Маркер DEBUG_RETAINED_FALLBACK_TO_V1 доступов не раскрывает, но показывает механику: служебный текст остаётся в бандле, если используется в живом коде. В крупном проекте такие строки выдают внутренние режимы, старые фоллбеки и детали незавершённых экспериментов. Строку для мониторинга оставляем осознанно, забытый след отладки убираем.

Source map хранит то, что минификатор выбросил

Вторая debug-строка, DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED, из .js действительно исчезла — оптимизатор вырезал мёртвую ветку. Но проверять только .js недостаточно:

rg -n 'DEBUG_DEV_ONLY_SHOULD_BE_STRIPPED|sourcesContent' dist -g '*.map'

В карте исходников строка на месте, в секции sourcesContent лежит исходный текст модуля целиком. Это ожидаемое поведение: задача минификатора — убрать лишнее из исполняемого файла, задача source map — сохранить оригинал для отладки. Проблема возникает, когда .map публикуется вместе с бандлом и становится доступен любому по прямой ссылке из sourceMappingURL.

Варианты решения: не генерировать карты для production, генерировать «скрытые» карты без комментария-ссылки в конце файла, либо выкладывать .map только во внутренний сервис ошибок и не пускать их на CDN.

Что вынести в CI

Ручная ревизия полезна как разовое упражнение, дальше её стоит автоматизировать:

  • шаг пайплайна, который падает при появлении в dist файлов *.map (или, наоборот, проверяет, что они загружены только в трекер ошибок);
  • поиск по бандлу известных префиксов секретов — sk_, AKIA, -----BEGIN, ghp_ — с ненулевым кодом возврата;
  • проверка, что в сборку не попали адреса непроизводственных окружений;
  • фиксация списка файлов релиза в артефактах сборки, чтобы неожиданные добавления были видны в diff между релизами.

Короткий чек-лист

  • Сначала список файлов dist, потом поиск по содержимому.
  • Для каждой находки — решение «ожидаемо / убрать / инцидент», а не просто «нашлось».
  • Серверные ключи в клиентском коде — стоп-фактор и ротация.
  • Клиентские фича-флаги всегда дублируются проверкой на бэкенде.
  • Source map либо не публикуются, либо уходят только во внутренний сервис.
  • Всё перечисленное держится проверками в пайплайне, а не дисциплиной разработчика.

Папка dist не должна оставаться чёрным ящиком. Заглядывать туда стоит до того, как релиз станет публичным.