vue-feature-toggles v0.1.5: мультивариантность, переменные, группы и CLI

В первой версии плагина флаг — это boolean. Включён или выключен. Этого хватает для большинства кейсов, но на практике быстро упираешься в стену.
A/B-тест с тремя вариантами? Нужен не boolean, а строка 'v1' | 'v2' | 'control'. Тема оформления для отдельного флага? Набор переменных — цвета, лимиты, тексты — которые можно менять независимо от самого флага. Группа бета-флагов, которую можно включить одной командой? Правило, которое вычисляет значение флага на лету по OS-настройкам или роли пользователя?
Всё это требовало отдельной логики в каждом проекте. В 0.1.5 — это встроено в плагин.
Тип флага: boolean | string
Основное изменение — FlagValue теперь boolean | string. Флаг может быть не просто включён/выключен, но и нести вариант.
ts
app.use(FeatureToggles, {
flags: {
newDashboard: true,
betaSearch: false,
checkoutFlow: 'v1', // мультивариантный флаг
},
})
isEnabled('checkoutFlow') вернёт true для любого строкового значения, кроме 'false'. Получить сам вариант — через getValue:
ts
const { getValue } = useFeatureProvider()
const variant = getValue('checkoutFlow') // 'v1' | 'v2' | 'control' | boolean
Мультивариантные флаги
Для мультивариантных флагов — два интерфейса. Composable:
ts
const { variant, isVariant } = useFeatureVariant('checkoutFlow')
// variant.value === 'v1'
// isVariant('v2') === false
И компонент <FeatureVariant> с именованными слотами:
vue
<FeatureVariant name="checkoutFlow">
<template #v1>
<CheckoutV1 />
</template>
<template #v2>
<CheckoutV2 />
</template>
<template #fallback>
<CheckoutLegacy />
</template>
</FeatureVariant>
Рендерится только тот слот, чьё имя совпадает с текущим значением флага. Если флаг выключен или значение не совпало ни с одним слотом — #fallback.
Переменные флага
Флаг может нести не только состояние, но и параметры. Переменные задаются в конфиге:
ts
app.use(FeatureToggles, {
flags: { newDashboard: true },
variables: {
newDashboard: {
accentColor: '#4f46e5',
maxWidgets: 6,
welcomeText: 'Welcome to the new dashboard!',
},
},
})
В компоненте:
ts
const { getVariable, setVariable } = useFeatureProvider()
const accentColor = getVariable('newDashboard', 'accentColor') // '#4f46e5'
// Runtime override — только в памяти
setVariable('newDashboard', 'accentColor', '#e74c3c')
// Или с персистентностью
setVariable('newDashboard', 'maxWidgets', 10, { persist: true })
getVariable читает по тому же принципу приоритетов что и флаги: URL → runtime → static. Переменные удобны для A/B-тестов где вариантам нужны разные параметры без хардкода в компонентах.
Группы флагов
Группы — это именованные наборы флагов, которыми можно управлять вместе:
ts
app.use(FeatureToggles, {
flags: {
betaSearch: false,
aiSuggestions: true,
newDashboard: true,
darkMode: true,
},
groups: {
beta: ['betaSearch', 'aiSuggestions'],
layout: ['newDashboard', 'darkMode'],
},
})
ts
const { setGroup, resetGroup, isGroupEnabled } = useFeatureProvider()
setGroup('beta', true) // включить все флаги группы beta
resetGroup('layout') // сбросить runtime-оверрайды всей группы layout
isGroupEnabled('beta') // true если все флаги группы включены
Компонент <Feature> поддерживает prop group:
vue
<!-- показать если вся группа beta включена -->
<Feature group="beta">
<BetaFeaturesBadge />
</Feature>
В DevTools группы вынесены на отдельную вкладку — можно переключать группу целиком, не трогая флаги по одному.
Зависимости флагов
Флаг может зависеть от другого — автоматически выключаться если зависимость не выполнена:
ts
app.use(FeatureToggles, {
flags: {
betaSearch: false,
aiSuggestions: true, // зависит от betaSearch
},
dependencies: {
aiSuggestions: ['betaSearch'],
},
})
Если betaSearch === false — isEnabled('aiSuggestions') вернёт false независимо от значения флага. В getFlagSource будет 'dependency'. Реактивно: включаешь betaSearch — aiSuggestions сразу становится доступен.
Контекстные правила
Правило — это функция, которая вычисляет значение флага:
ts
app.use(FeatureToggles, {
rules: {
// darkMode следует OS-настройкам
darkMode: () => window.matchMedia('(prefers-color-scheme: dark)').matches,
// бета-доступ по email домену
betaFeatures: () => {
const user = useAuthStore()
return user.email.endsWith('@company.com')
},
},
})
Правила вычисляются внутри computed — реактивны. Менять не нужно: если пользователь переключит тему OS, darkMode обновится сам. Правило можно перекрыть через setFlag или URL-оверрайд — приоритеты те же.
Метаданные и срок действия
ts
app.use(FeatureToggles, {
meta: {
newDashboard: {
description: 'Redesigned dashboard UI',
owner: 'frontend',
addedAt: '2025-01-15',
ticket: 'PROJ-42',
},
christmasBanner: {
description: 'Seasonal Christmas promotional banner',
owner: 'marketing',
addedAt: '2024-11-01',
ticket: 'MKTG-12',
},
},
expiry: {
christmasBanner: '2025-01-10',
},
})
getMeta('newDashboard') возвращает метаданные флага. Expired флаги показываются в DevTools с жёлтым бейджем. CLI-команда stale находит флаги, которые давно не трогали.
Персистентные оверрайды и профили
setFlag по умолчанию хранит оверрайд в памяти — пропадает при перезагрузке. Опция persist: true записывает в localStorage:
ts
setFlag('darkMode', true, { persist: true })
resetFlag('darkMode') // убирает и из памяти, и из localStorage
Поверх этого — профили: именованные наборы оверрайдов, которые можно сохранить и переключать между ними.
ts
const { saveProfile, loadProfile, listProfiles } = useFeatureProvider()
saveProfile('qa-beta') // сохранить текущие runtime-оверрайды как профиль
loadProfile('qa-beta') // загрузить профиль
listProfiles() // ['qa-beta', 'dark-mode-test']
В DevTools профили доступны через выпадающий список в футере вкладки Flags.
Живые обновления: SSE и WebSocket
loader запрашивает флаги при инициализации. Для живых обновлений без перезагрузки — liveUpdates:
ts
app.use(FeatureToggles, {
loader: async () => fetch('/api/flags').then(r => r.json()),
liveUpdates: {
type: 'sse',
url: '/api/flags/stream',
},
})
При получении события из SSE/WebSocket плагин делает partial merge во loaderFlags — только те флаги, что пришли в апдейте, остальные не трогаются. Реактивность подхватывает — UI обновляется мгновенно.
ts
// WebSocket вместо SSE
liveUpdates: {
type: 'websocket',
url: 'wss://api.app.com/flags',
eventName: 'flags',
}
SSR-гидрация
В SSR флаги нужны синхронно на старте — до того как loader вернёт результат. Иначе гидрация видит одно состояние (без флагов), а браузер — другое (с флагами), и Vue жалуется на несоответствие.
ts
app.use(FeatureToggles, {
ssrState: {
newDashboard: true,
betaSearch: false,
checkoutFlow: 'v2',
},
loader: async () => fetch('/api/flags').then(r => r.json()),
})
ssrState заполняет loaderFlags синхронно при инициализации. isReady сразу true, isLoading — false. loader всё равно выполнится и обновит флаги, но первый рендер пройдёт с корректными значениями.
В Nuxt — передаётся из server-хэндлера:
ts
// server/api/flags.ts
export default defineEventHandler(async (event) => {
return getFlagsForUser(event)
})
// app.vue
const { data: serverFlags } = await useFetch('/api/flags')
app.use(FeatureToggles, { ssrState: serverFlags.value })
Типизация флагов
По умолчанию useFeature('anyString') принимает любую строку — нет автодополнения, нет ошибок при опечатке. В 0.1.5 — module augmentation:
ts
// feature-flags.d.ts
import 'vue-feature-toggles'
declare module 'vue-feature-toggles' {
interface FeatureFlagNames {
newDashboard: boolean
betaSearch: boolean
checkoutFlow: 'v1' | 'v2' | 'control'
aiSuggestions: boolean
christmasBanner: boolean
}
}
После этого useFeature('unknownFlag') — ошибка TypeScript. getValue('checkoutFlow') возвращает 'v1' | 'v2' | 'control'. setFlag('checkoutFlow', 'v3') — ошибка. Автодополнение в IDE работает во всех трёх интерфейсах: компонент, директива, composable.
CLI
bash
npx vue-feature-toggles list
npx vue-feature-toggles check ./src
npx vue-feature-toggles stale --months 3
Три команды, которые читают feature-toggles.config.js из корня проекта.
list — обзор всех флагов
Flag Value Source Owner Added Expiry Groups
────────────────────────────────────────────────────────────────────────────────
newDashboard true static frontend 2025-01-15 layout
betaSearch false static search-team 2025-06-01 beta
checkoutFlow v1 static checkout 2025-09-01
aiSuggestions true static ai-team 2025-10-01 beta
christmasBanner true static marketing 2024-11-01 [EXPIRED]
Expired флаги подсвечиваются жёлтым. Флаги без метаданных — без owner/ticket.
check — аудит кода
Сканирует исходники, находит все обращения к флагам через useFeature, v-feature, isEnabled. Сравнивает с известными флагами из конфига:
✅ newDashboard
✅ betaSearch
✅ checkoutFlow
❌ newCheckout — unknown flag. Did you mean: checkoutFlow?
❌ betaSearchV2 — unknown flag. Did you mean: betaSearch?
Неизвестные флаги — предложение с ближайшим совпадением по редакционному расстоянию. Выход с кодом 1 — можно встроить в CI.
stale — устаревшие флаги
bash
npx vue-feature-toggles stale --months 3
Показывает флаги, у которых meta.addedAt старше N месяцев и значение true. Это кандидаты на удаление: фича давно в проде, флаг больше не нужен, но так и остался в коде.
DevTools: три вкладки
Виджет вырос. Раньше — список флагов и несколько кнопок. Теперь — три вкладки с разным контентом и своими элементами управления в футере.
Flags

Список всех флагов с источником, бейджем expired, инлайн-переключением. Новое:
- Строковые флаги (мультивариантные) — иконка карандаша рядом с значением, по клику появляется
<input>прямо в строке. Enter подтверждает, Escape отменяет - Кнопка
▸разворачивает секцию переменных для флага. Видны все переменные с текущими значениями, поля для редактирования, кнопка Set - Бейдж
depна флаге чья зависимость не выполнена
Footer: выбор профиля из дропдауна, поле для сохранения нового профиля, Reset all, Copy URL (формирует URL с оверрайдами), Export JSON, Import JSON.
Groups

Список групп из конфига. Для каждой — флаги-чипы, статус (все включены / частично / выключены), кнопки Enable all / Reset group.
Footer: Reset all groups.
History

Лог событий: все изменения флагов и переменных с источником и временем. Реактивно — новые события появляются сверху.
Footer: кнопка Clear + счётчик записей.
Drag в пределах вьюпорта
Панель перетаскивается. Позиция зажата в границах окна браузера с учётом реальных размеров панели:
ts
const onMove = (ev: MouseEvent) => {
const pw = panelRef.value?.offsetWidth ?? 380
const ph = panelRef.value?.offsetHeight ?? 420
pos.value = {
x: Math.max(0, Math.min(ev.clientX - startX, window.innerWidth - pw)),
y: Math.max(0, Math.min(ev.clientY - startY, window.innerHeight - ph)),
}
}
Панель нельзя утащить за пределы экрана — остаток всегда виден.
Testing и Storybook
ts
// в тестах
import { createFeatureTogglesMock } from 'vue-feature-toggles/testing'
const wrapper = mount(MyComponent, {
global: {
plugins: [
createFeatureTogglesMock({
newDashboard: true,
betaSearch: false,
}),
],
},
})
createFeatureTogglesMock возвращает упрощённый провайдер без загрузчика и реактивности — для тестов где нужно просто проверить рендер при конкретных значениях флагов.
Storybook:
ts
// .storybook/preview.ts
import { featureTogglesDecorator } from 'vue-feature-toggles/storybook'
export const decorators = [
featureTogglesDecorator({
newDashboard: true,
betaSearch: false,
}),
]
ts
// MyComponent.stories.ts
export const WithBeta = {
parameters: {
featureToggles: { betaSearch: true },
},
}
parameters.featureToggles перекрывают дефолтные значения из декоратора. Каждая история запускается с нужным набором флагов — без моков, без обёрток вручную.
NPM: https://www.npmjs.com/package/vue-feature-toggles
GitHub: https://github.com/macrulezru/vue-feature-toggles