@application-kit/feature-flags

Фичефлаги из окружения: набор флагов параметром, порт с адаптером окружения, SSR-состояние для Vue и фейк для спек
Install
npm install @application-kit/feature-flags
Latest Version:0.1.0
Vue:3
License:MIT
Last Updated:Oct 12, 2026
Links: GitLab  ·  npm
Maintainer: uwl

@application-kit/feature-flags

Фичефлаги из окружения: набор флагов объявляет приложение, пакет даёт порт чтения с адаптером окружения, SSR-состояние для Vue и фейк для спек. Флагов продукта в пакете нет.

Два правила держат всё остальное:

  1. Имя флага объявляется ровно один раз — в наборе приложения. Из него выводится тип имени, из него же адаптер узнаёт, какие переменные окружения существуют.
  2. Состояния «по умолчанию» у флага нет. Значение каждого флага приходит из окружения, а пропущенная переменная роняет старт процесса. Дефолт в коде сделал бы забытую настройку окружения неотличимой от сознательного выбора — а ищут как раз её, когда стенд ведёт себя не так, как договаривались.

Установка

yarn add @application-kit/feature-flags @libs-for-dev/nestjs-config-module

Peer: @libs-for-dev/nestjs-config-module (подпуть /parse-boolean — разбор булевых переменных, без зависимостей); vue — только для входа /vue.

Входы

Вход Что внутри Зависимости
@application-kit/feature-flags defineFeatureFlags, реестр FeatureFlagRegistryInterface, порт FeatureFlagsInterface, токен FEATURE_FLAGS, адаптер EnvironmentFeatureFlags, ошибки изоморфно, без Nest и DOM
@application-kit/feature-flags/vue useFeatureFlag, ключ FEATURE_FLAG_STATE, parseFeatureFlagState vue
@application-kit/feature-flags/testing FakeFeatureFlags —

Корень грузится и в клиентском бандле (набор нужен разбору SSR-состояния), поэтому связки с Nest в пакете нет: класс окружения и регистрацию адаптера под токеном делает приложение (пример ниже).

Набор флагов

Тип и значение объявляются рядом, в одном файле приложения:

import { defineFeatureFlags } from '@application-kit/feature-flags'

declare module '@application-kit/feature-flags' {
  interface FeatureFlagRegistryInterface {
    NEW_CHECKOUT: true
  }
}

export const featureFlags = defineFeatureFlags({
  NEW_CHECKOUT: { isPublic: true },
})

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

Единственное решение в записи — isPublic: уезжает ли флаг на клиент в SSR-состоянии. Наружу идёт явный список, а не всё состояние: серверный флаг (доступ к интеграции, аварийный рубильник) в разметке страницы — это утечка плана работ.

Префикс переменных — опция, по умолчанию FEATURE_: defineFeatureFlags({ … }, { environmentPrefix: 'FLAG_' }). Пустой префикс — RangeError.

Окружение

Имя переменной — префикс плюс имя флага: FEATURE_NEW_CHECKOUT. featureFlags.readEnvironment(source) разбирает окружение в состояние:

Значение Результат
false, 0, пустая строка (регистр и пробелы по краям не важны) выключен
любая другая непустая строка — true, 1, on, yes включён
переменной нет отказ — MissingFeatureFlagError со списком недостающих переменных
переменная с префиксом, которой нет в наборе отказ — UnknownFeatureFlagError со списком

Разбор значения — parseBoolean конфиг-пакета: список выключающих строк один на все булевы переменные. Обе проверки собирают полный список до броска, чтобы отказ назвал все проблемы разом; незнакомая переменная проверяется первой — опечатка даёт сразу и лишнее имя, и недостающее.

Связка с Nest (в приложении)

Разбор ставится в класс окружения, чтобы отказ пришёл при сборке графа конфигов, а не на первом запросе:

import { EnvironmentFeatureFlags } from '@application-kit/feature-flags'

export class FeatureFlagsEnvironment {
  @Expose()
  @IsObject()
  public FEATURE_FLAGS: FeatureFlagState

  // Конфиг-модуль конструирует класс без аргументов: переменных столько, сколько флагов, и объявлять их
  // по одной `@Expose()` значило бы держать вторую копию набора.
  public constructor(source: Record<string, unknown> = process.env) {
    this.FEATURE_FLAGS = featureFlags.readEnvironment(source)
  }
}

export class FeatureFlagsConfig extends EnvironmentFeatureFlags {
  public constructor(@Inject(FeatureFlagsEnvironment) environment: FeatureFlagsEnvironment) {
    super(featureFlags, environment.FEATURE_FLAGS)
  }
}

// провайдер модуля
{ provide: FEATURE_FLAGS, useExisting: FeatureFlagsConfig }

Чтение на сервере

Через порт по токену, а не чтением process.env:

import type { FeatureFlagsInterface } from '@application-kit/feature-flags'
import { FEATURE_FLAGS } from '@application-kit/feature-flags'

export class SomeHandler {
  public constructor(@Inject(FEATURE_FLAGS) private readonly featureFlags: FeatureFlagsInterface) {}

  public execute(): void {
    if (this.featureFlags.isEnabled('NEW_CHECKOUT')) { /* … */ }
  }
}

Чтение синхронное, и это решение: асинхронный isEnabled заставил бы ждать каждого потребителя, включая шаблон Vue, где await в разметке невозможен. Адаптер с хранением в БД остаётся возможным — он держит обновляемый снимок в памяти и встаёт под тот же токен без правок потребителей.

Клиент (Vue)

Публичный срез уезжает на страницу SSR-состоянием:

// сервер, перед renderToString
const state = featureFlagsPort.publicState()
app.provide(FEATURE_FLAG_STATE, state)
// … состояние сериализуется в страницу строкой (JSON.stringify)

// клиент, до app.mount()
app.provide(FEATURE_FLAG_STATE, parseFeatureFlagState(featureFlags, window.ssrFeatureFlags))

В компоненте:

<script setup lang="ts">
import { useFeatureFlag } from '@application-kit/feature-flags/vue'

const isNewCheckoutEnabled = useFeatureFlag('NEW_CHECKOUT')
</script>

<template>
  <NewCheckout v-if="isNewCheckoutEnabled" />
</template>
  • Без provide всё выключено: до прихода SSR-состояния помеченного фрагмента на странице быть не должно — показать лишнее и убрать при гидрации хуже, чем не показать.
  • Имя, которого нет в предоставленном состоянии, — UnknownFeatureFlagNameError со списком известных: имя из данных без проверки дало бы undefined, и фрагмент молча исчез бы со страницы.
  • parseFeatureFlagState не бросает ни на какой форме испорченного провода: разбор стоит до app.mount(), и бросок убил бы весь бутстрап клиента. Незнакомое имя на проводе игнорируется, пропущенное и не-булево — выключено: страница старой версии не роняет клиент новой во время выкатки.

Тесты потребителя

Через фейк порта, без process.env и vi.stubEnv. База фейка — «всё выключено», поэтому спека, которой флаг нужен включённым, говорит это явно:

import { FakeFeatureFlags } from '@application-kit/feature-flags/testing'

const port = new FakeFeatureFlags(featureFlags, { NEW_CHECKOUT: true })

Снимок publicState() фейка строится как у адаптера: непубличные флаги в нём выключены. Для компонента — provide состояния:

mount(SomeComponent, {
  global: { provide: { [FEATURE_FLAG_STATE]: featureFlags.stateOf((name) => name === 'NEW_CHECKOUT') } },
})

Типовые отказы

Симптом Причина Что делать
MissingFeatureFlagError: Missing feature flags in environment: FEATURE_X на старте флаг есть в наборе, переменная в окружении не задана дописать переменную в окружение каждого процесса
UnknownFeatureFlagError: Unknown feature flags in environment: FEATURE_X на старте переменная есть, флага в наборе нет — опечатка или флаг снят из кода и забыт в окружении поправить имя или убрать переменную
UnknownFeatureFlagNameError: Unknown feature flag requested: X. Known flags: … при рендере опечатка в имени у useFeatureFlag поправить имя по списку в тексте ошибки
Переменная выставлена, а на клиенте флаг выключен флаг непубличный (isPublic: false) проверить isPublic в наборе

Чего здесь нет

Адаптера с хранением в БД, процента раскатки, таргетинга по пользователю, UI управления флагами.

Related Packages

vue-feature-toggles

Backend-agnostic feature flags for Vue 3 — boolean and multivariate flags, conte...

3,189 0
@ops-ai/nuxt-toggly-server

Server-side feature flag utilities for Nuxt with Nitro support

1,631 5
@ops-ai/nuxt-toggly-client

Client-side feature flag composables and components for Nuxt/Vue 3

1,745 5
vue-server-renderer

server renderer for Vue 2.0

13,054,069 212,644

Version History

Version Released Vue License
0.1.0 ^3.5.0 MIT
0.0.0 ^3.5.0 MIT
Pre-releases (1)
Version Released License
0.0.0-stage