# vue-feature-toggles — AI Reference Backend-agnostic feature flags for Vue 3 — boolean and multivariate (A/B) flags, deterministic rollout percentages, contextual rules, URL overrides, dependencies, groups, scheduling, SSR hydration, live updates (SSE/WebSocket), Vue DevTools integration, and a CLI. Single npm package with several subpath exports. Version 0.1.20. This document is hand-written for AI agents and other tools that generate code against this package: every signature, default, and behavior note below is verified directly against the TypeScript source (not summarized from prose docs), and prose is kept to the minimum needed to use the API correctly. For human-readable narrative docs (why you'd reach for each piece, worked examples), see the interactive site instead: - Full docs (EN): https://npm.vuecraft.ru/en/packages/vue-feature-toggles/guide/overview - Full docs (RU): https://npm.vuecraft.ru/packages/vue-feature-toggles/guide/overview - GitHub: https://github.com/macrulezru/vue-feature-toggles - npm: https://www.npmjs.com/package/vue-feature-toggles Links below starting with "/" are relative to https://npm.vuecraft.ru. --- ## 1. Package map — what to import from where One npm package, several subpath exports (each a separate build, excluded from the main bundle unless explicitly imported): | Import path | Install | Contains | |---|---|---| | `vue-feature-toggles` | `npm install vue-feature-toggles` | Plugin, components, directive, composables, core types (sections 3–9). | | `vue-feature-toggles/nuxt` | (same install, peer `nuxt: >=3.0.0`) | Nuxt module — auto-registers, does NOT auto-import composables (section 13). | | `vue-feature-toggles/testing` | (same install) | Test helpers for Vue Test Utils (section 14). | | `vue-feature-toggles/storybook` | (same install) | Storybook 7/8 decorator (section 15). | | `vue-feature-toggles/adapters` | (same install) | Loader factories for LaunchDarkly/Unleash/Flagsmith (section 16). | | `vue-feature-toggles/vite` | (same install, dev dependency only) | Vite plugin that strips `` from production builds (section 17). | | `vue-feature-toggles` CLI (`bin`) | same install, run via `npx vue-feature-toggles` | `list` / `check` / `stale` commands (section 18). | `@vue/devtools-api` is an **optional peer dependency** — the Vue DevTools browser-extension integration (section 10) dynamically `import()`s it and silently no-ops if it isn't installed or devtools aren't active. No peer dependency is required for the core package otherwise beyond `vue: ^3.x`. --- ## 2. Core types (verbatim from `core/types.ts`) ```ts type FlagValue = boolean | string // string = a variant name for an A/B flag // A flag definition in `flags: {...}` — either a plain value, or an // object enabling deterministic rollout: type FlagDefinition = FlagValue | { value: FlagValue; rollout: number } type FlagSource = 'url' | 'runtime' | 'rules' | 'loader' | 'static' | 'schedule' | 'default' // NOTE: no 'live' source — live-update values land in the same bucket as `loader` (see 4.6) interface FlagMeta { description?: string owner?: string addedAt?: string ticket?: string } interface FlagSchedule { from?: string // ISO date string — flag becomes active on this date to?: string // ISO date string — flag becomes inactive after this date } interface SetFlagOptions { persist?: boolean // write to localStorage (see 4.8) } interface WatchFlagOptions { debounce?: number // ms — coalesce rapid changes (e.g. DevTools toggling) into one callback immediate?: boolean // fire the callback once immediately with the current value } interface LiveUpdatesOptions { type: 'sse' | 'websocket' url: string reconnectDelay?: number // ms, default 3000 } interface FeatureTogglesOptions { flags?: Record loader?: () => Promise> reloadInterval?: number // ms — re-run `loader` on an interval. default 0 (off) urlOverrides?: boolean // default: true in dev (import.meta.env.DEV), false in prod urlPrefix?: string // default 'feature' — query param prefix, see 4.3 defaultValue?: boolean // default false — used for a completely unknown flag name userId?: string // seed for deterministic rollout hashing, see 4.2 schedule?: Record ssrState?: Record // hydration snapshot, see section 9 liveUpdates?: LiveUpdatesOptions variables?: Record> // per-flag variables, see 4.9 groups?: Record // named flag groups, see 4.10 dependencies?: Record // flag → required flags, see 4.11 rules?: Record boolean> // contextual rules, see 4.3 meta?: Record expiry?: Record // ISO date — dev-only console warning past this date } // Module augmentation point for typed flag names — see 4.1 interface FeatureFlagNames {} type FlagName = keyof FeatureFlagNames extends never ? string : keyof FeatureFlagNames ``` ### `FeatureProvider` — the full engine interface Returned by `createFeatureProvider()` (4.1) and by `useFeatureProvider()` (5.3). Every method below is described in detail in section 4. ```ts interface FeatureProvider { flags: Ref> isLoading: Ref isReady: Ref isEnabled(name: string): boolean setFlag(name: string, value: boolean, options?: SetFlagOptions): void resetFlag(name: string): void resetAll(): void reload(): Promise getFlagSource(name: string): FlagSource getVariant(name: string): string setVariant(name: string, variant: string, options?: SetFlagOptions): void getVariable(flagName: string, varName: string): Ref setVariable(flagName: string, varName: string, value: unknown): void setGroup(groupName: string, value: boolean): void resetGroup(groupName: string): void isGroupEnabled(groupName: string): boolean getDependencyViolations(): Record saveProfile(name: string, flags: Record): void loadProfile(name: string): void // name === 'default' calls resetAll() instead of loading a saved profile listProfiles(): string[] serialize(): Record // for SSR — see section 9 getFlagMeta(name: string): FlagMeta | undefined isExpired(name: string): boolean isPersisted(name: string): boolean clearPersistedFlags(): void watchFlag(name: string, callback: (value: boolean, oldValue: boolean) => void, options?: WatchFlagOptions): WatchStopHandle getRollout(name: string): number | undefined getSchedule(name: string): FlagSchedule | undefined isScheduleActive(name: string): boolean listVariables(flagName: string): string[] listGroups(): Record } ``` --- ## 3. `isFlagTruthy` semantics (applies everywhere a flag reads as boolean) ```ts function isFlagTruthy(val: FlagValue | undefined): boolean ``` - `undefined` → `false`. - `boolean` → itself. - `string` → `false` only for the exact strings `''`, `'false'`, or `'0'`; **every other string is truthy**, including a real variant name. So a variant flag currently set to `'control'` reads as `isEnabled(name) === true` — `isEnabled`/`` don't distinguish "on" from "a specific variant is active"; use `getVariant`/`useFeatureVariant`/`` to read which variant. --- ## 4. Core engine: `createFeatureProvider(options)` ```ts function createFeatureProvider(options: FeatureTogglesOptions): FeatureProvider const FEATURE_PROVIDER_KEY: unique symbol // the Vue provide/inject key, exported for advanced use ``` Not usually called directly in a Vue app — the `FeatureToggles` plugin (section 7) calls it for you and provides the result. Call it directly when you need a provider without the plugin (testing utils and the Storybook decorator both do this internally). ### 4.1 Flag name typing (module augmentation) ```ts declare module 'vue-feature-toggles' { interface FeatureFlagNames { newDashboard: true betaSearch: true } } ``` Once augmented, `FlagName` (used by `useFeature`/`useFeatureVariant`) narrows from plain `string` to a union of your actual flag names — every other API in this package still accepts a plain `string` for the flag name (only `useFeature`/`useFeatureVariant` use `FlagName`). ### 4.2 Rollout resolution — `resolveFlagDef` (evaluated ONCE, at provider creation) ```ts function hashToFloat(str: string): number // FNV-1a 32-bit hash → float in [0, 1), deterministic function resolveFlagDef(name: string, def: FlagDefinition, userId?: string): FlagValue ``` For `{ value, rollout }`: computes `hashToFloat(\`${userId ?? 'anonymous'}:${name}\`)` and resolves to `value` if the hash is `< rollout`, else `false`. **Same `userId` (or no `userId` at all) always resolves the same way** for a given flag name — this is deterministic bucketing, not a random per-page-load coin flip. **If `userId` is omitted, every anonymous visitor hashes under the literal string `'anonymous'` and gets the IDENTICAL result** — a rollout flag with no `userId` is either on for everyone or off for everyone, not "on for N% of visitors". Pass a real per-user/per-session `userId` for an actual gradual rollout. Rollout is resolved once when `createFeatureProvider()` runs — it does not re-evaluate if `userId` changes later without recreating the provider. ### 4.3 Merge/priority order for `flags` (computed reactively on every read) Exact order, highest priority first — this is the order the real `flags` computed getter checks each source in: 1. **URL override** (`urlOverrides: true`, default in dev only) — a query param `?feature:flagName=value`, prefix configurable via `urlPrefix` (default `'feature'`). Value parsing: `'false'`/`'0'` → `false`; `'true'`/`'1'` → `true`; anything else → the raw string (a variant name). Re-read reactively on `popstate`/`hashchange`. 2. **Runtime override** — `setFlag()`/`setVariant()` calls. 3. **Rules** — `rules: { name: () => boolean }`, re-evaluated reactively (wrapped in try/catch — a throwing rule silently resolves to `false`). 4. **Loader** — the async `loader()` result, and anything merged in via `liveUpdates` (section 4.6 — they share the same underlying ref). 5. **Static** — the resolved `flags` option (after rollout, 4.2). 6. **`defaultValue`** (default `false`) — for a name that isn't in any of the above. **After** that merge, two more passes run, each of which can force a flag to `false`: - **Schedule**: if `schedule[name]` exists and the current time is outside `[from, to)`, the flag is forced `false` — **unless** a URL or runtime override for that name is already active (schedule respects explicit overrides). - **Dependencies**: if `dependencies[flag]` lists any flag that `isFlagTruthy` says is currently off, `flag` is forced `false` — **this check does NOT exempt URL/runtime overrides** (unlike schedule); a dependency violation always wins, even over an explicit `setFlag(flag, true)`. ### 4.4 `isEnabled(name)` / `getVariant(name)` ```ts isEnabled(name: string): boolean // isFlagTruthy(flags.value[name]) — see section 3 getVariant(name: string): string // flags.value[name] if it's a string, else '' (including for a boolean flag) ``` In dev (`import.meta.env.DEV`), `isEnabled()` logs one console warning per unknown flag name the first time it's read after `isReady` — not an error, just a warning; the call still returns `defaultValue`. ### 4.5 `setFlag` / `setVariant` / `resetFlag` / `resetAll` ```ts setFlag(name: string, value: boolean, options?: SetFlagOptions): void setVariant(name: string, variant: string, options?: SetFlagOptions): void resetFlag(name: string): void // removes the runtime override, falls through to the next source resetAll(): void // clears every runtime override AND clears persisted storage ``` `{ persist: true }` also writes the override to `localStorage` (4.8); omitting it (or passing `{ persist: false }`) on a name that was previously persisted **removes** it from persisted storage — the persisted flag set always mirrors the most recent call's `persist` value for that name, not just the first one. ### 4.6 `reload()` / live updates ```ts reload(): Promise // re-runs `loader`, sets isLoading during the call ``` - If `loader` is set and no `ssrState`, `reload()` runs once automatically on creation; `isReady` flips `true` once it resolves. With `ssrState` present, `isReady` is `true` immediately (avoids a loading flash on hydration) and `loader` (if also set) still runs in the background to refresh past the SSR snapshot. - `reloadInterval > 0` (with a `loader`) re-runs `reload()` on that interval via `setInterval` — never cleared by the provider itself. - `liveUpdates` opens an `EventSource` (`type: 'sse'`) or `WebSocket` (`type: 'websocket'`) to `url` and merges each parsed JSON message into the **same underlying ref `loader` populates** — a live-pushed flag therefore reports `getFlagSource() === 'loader'`, there is no separate `'live'` source value (see the `FlagSource` type, section 2). Both transports auto-reconnect after `reconnectDelay` (default 3000ms) on error/close, indefinitely, with no backoff and no `close()`/teardown method exposed — a live connection lives for the page's lifetime once opened. ### 4.7 `getFlagSource(name)` ```ts getFlagSource(name: string): FlagSource ``` Returns which layer resolved the flag's current value, in the section 4.3 priority order, with one extra rule: reports `'schedule'` specifically when the flag exists in `schedule` and is currently forced off by it (and isn't overridden by URL/runtime). A name matching nothing at all returns `'default'`. ### 4.8 Persistence (`localStorage`) Two separate keys, both SSR-safe (no-op if `localStorage` is undefined, and every read/write is wrapped in try/catch): ```ts const PERSIST_KEY = 'vue-feature-toggles:overrides' // setFlag/setVariant with { persist: true } const PROFILES_KEY = 'vue-feature-toggles:profiles' // saveProfile() ``` `isPersisted(name)` / `clearPersistedFlags()` (removes only the persisted subset of runtime overrides, non-persisted runtime overrides stay) operate on the first key. Profiles (4.12) use the second, independent key. ### 4.9 Variables — `getVariable` / `setVariable` ```ts getVariable(flagName: string, varName: string): Ref // a NEW computed ref on every call — not cached/memoized setVariable(flagName: string, varName: string, value: unknown): void // runtime-only, not persisted, not exposed via SetFlagOptions ``` Resolution order for a variable's value: URL override (`?feature-var:flagName:varName=value`, parsed via `parseVarValue` — `'true'`/`'false'` → boolean, a numeric string → `number`, else the raw string) → `setVariable()` runtime override → the static `variables` option → `undefined`. Independent of the flag's own on/off state — reading a variable doesn't check `isEnabled(flagName)` first. ### 4.10 Groups ```ts setGroup(groupName: string, value: boolean): void // calls setFlag(name, value) for every member resetGroup(groupName: string): void // calls resetFlag(name) for every member isGroupEnabled(groupName: string): boolean // true only if EVERY member isFlagTruthy (AND, not OR); false for an empty/unknown group ``` `groups` just maps a group name to a flat list of flag names — there's no nesting, and members aren't otherwise special (each is a normal flag you can also read/set individually). ### 4.11 Dependencies ```ts getDependencyViolations(): Record // { flagName: [unmetRequiredFlag, ...] } for every currently-violated flag ``` See 4.3 for the forcing behavior. In dev, the first time a flag with unmet dependencies is otherwise-truthy, one console warning is logged (deduped per flag name for the provider's lifetime). ### 4.12 Profiles ```ts saveProfile(name: string, flags: Record): void loadProfile(name: string): void // name === 'default' → resetAll() instead of reading a saved profile listProfiles(): string[] ``` A profile is just a named snapshot of `{ flagName: value }` written to the `PROFILES_KEY` localStorage entry. `loadProfile` applies each entry via `setFlag`/`setVariant` (as runtime overrides, not persisted unless you separately call `setFlag(..., { persist: true })`) — it does not clear flags absent from the profile first. ### 4.13 Metadata, expiry, schedule/rollout introspection ```ts getFlagMeta(name: string): FlagMeta | undefined isExpired(name: string): boolean // expiry[name] is in the past (date-only comparison, ignores time-of-day) getRollout(name: string): number | undefined // the configured rollout fraction, or undefined if the flag wasn't defined with one getSchedule(name: string): FlagSchedule | undefined isScheduleActive(name: string): boolean // true for a flag with no schedule at all listVariables(flagName: string): string[] // union of static + runtime + URL variable names set for this flag listGroups(): Record // the configured `groups` option, verbatim ``` `isExpired` is purely informational — an expired flag is NOT automatically disabled; only a dev console warning fires (via `checkExpiry`, run once at provider creation, `import.meta.env.DEV` only). ### 4.14 `watchFlag(name, callback, options?)` ```ts watchFlag(name: string, callback: (value: boolean, oldValue: boolean) => void, options?: WatchFlagOptions): WatchStopHandle ``` Watches `isFlagTruthy(flags.value[name])` specifically (boolean, not the raw `FlagValue`) — for a variant flag this only fires on an on/off-truthiness transition per section 3's rules, not on every variant change. `debounce` coalesces rapid changes into the last value, with `oldValue` from before the burst started (not the previous individual tick). `immediate: true` fires once synchronously with the current value as both `value` and `oldValue`. ### 4.15 `serialize()` ```ts serialize(): Record // a snapshot of `flags.value` — see section 9 for the SSR flow ``` --- ## 5. Vue composables ### 5.1 `useFeature` — THREE call shapes, two different behaviors ```ts function useFeature(name: FlagName): Ref function useFeature(names: T[]): Record> function useFeature(first: FlagName, ...rest: FlagName[]): Ref ``` **This is the single easiest thing to misuse in this package — array and variadic arguments do NOT behave the same:** - `useFeature('a')` → one `Ref` for flag `'a'`. - `useFeature(['a', 'b'])` (an **array literal**) → an object `{ a: Ref, b: Ref }` — one independent ref **per** flag. - `useFeature('a', 'b')` (**multiple plain arguments**, no array) → a single `Ref` that is `true` only if **every** listed flag is enabled (logical AND) — NOT a map of individual refs. If no `FeatureProvider` is in the component tree, every ref silently resolves to `false` — `useFeature` never throws. ### 5.2 `useFeatureVariant(name)` ```ts function useFeatureVariant(name: FlagName): Ref ``` `provider?.getVariant(name) ?? ''` — same no-provider-is-silent behavior as `useFeature`. ### 5.3 `useFeatureProvider()` ```ts function useFeatureProvider(): FeatureProvider ``` **Throws** (`Error`, not a silent fallback) if called outside a component tree where the `FeatureToggles` plugin was installed — unlike `useFeature`/`useFeatureVariant`, which never throw. Use this when you need direct access to `setFlag`/`getFlagSource`/etc., not just a reactive boolean. --- ## 6. `v-feature` directive ```ts const vFeature: Directive // binding.value: string | string[]; binding.arg: 'not' | undefined ``` ```vue
...
shown when betaSearch is OFF
shown only if BOTH are on (AND)
``` - Array value → AND across all listed flags (same convention as `useFeature`'s array form, though `useFeature`'s array form returns per-flag refs while this ANDs them — don't assume parity between the two array behaviors). - Reads `provider.flags.value[name]` directly (not `isEnabled()`, but the same truthiness rule applies since it's the same source data) and **toggles `el.style.display = 'none'`** — the element is NOT removed from the DOM (unlike `v-if`); it stays mounted, just hidden. Anything that queries the DOM (tests, `document.querySelectorAll`, some a11y tooling) will still find it. - `:not` argument inverts the whole expression (after the AND, for an array). - Internally reads the provider via `binding.instance.$.appContext.provides` (raw internals) rather than the public `inject()` API — functionally equivalent, but means it only works when the directive's host component instance is available (always true for normal template usage; not usable detached from a component instance). --- ## 7. Components ### 7.1 `` ```ts interface FeatureProps { name?: string group?: string fallback?: string | Component inverted?: boolean tag?: string // wraps output in this tag; omit for a Fragment (no wrapper element) } // Slots: default, #loading, #fallback ``` ```vue ``` - **`group` takes priority over `name` if both are somehow passed**; if **neither** is passed, `enabled` is unconditionally `false` (a silent no-op — the component renders its fallback/nothing, no warning). - `group` set → checks `isGroupEnabled(group)` (AND across members, 4.10). `name` set → checks `isEnabled(name)`. - `inverted: true` flips the final enabled/disabled result (after group/name resolution). - Render priority, first match wins: `#loading` slot (only while `provider.isLoading` is true AND a `#loading` slot was actually passed) → default slot (if enabled) → `#fallback` slot (if enabled is false and a `#fallback` slot was passed) → `fallback` prop as a `Component` → `fallback` prop as a plain string (rendered as text) → nothing. ### 7.2 `` ```ts interface FeatureVariantProps { name: string } // Slots: one named slot per possible variant string, plus #fallback ``` ```vue ``` Renders the slot named after the CURRENT variant string (`getVariant(name)`) if a slot with that exact name was provided; otherwise renders `#fallback`. Not a boolean check at all — a boolean flag read through here (`getVariant` returns `''` for a non-string flag value) always falls through to `#fallback`. ### 7.3 `` ```ts interface FeatureDevToolsProps { title?: string // default 'Feature Toggles' theme?: 'light' | 'dark' | 'auto' // default 'auto' — follows prefers-color-scheme, user can override in-panel } ``` An **in-app floating UI panel** (flags/groups/history tabs, search, per-flag override controls) — place it once anywhere in your template (commonly the root `App.vue`), e.g. ``. This is separate from, and in addition to, the automatic Vue DevTools browser-extension integration (section 10), which needs no template placement at all. Strip `` from production builds with the Vite plugin (section 17) instead of manually gating every usage with `v-if`. --- ## 8. `FeatureToggles` plugin (`app.use`) ```ts const FeatureToggles: Plugin // install(app, options?: FeatureTogglesOptions) ``` ```ts import { createApp } from 'vue' import { FeatureToggles } from 'vue-feature-toggles' app.use(FeatureToggles, { flags: { newDashboard: true, betaSearch: { value: true, rollout: 0.2 } }, userId: currentUser.id, }) ``` `options` is exactly `FeatureTogglesOptions` (section 2). Installing the plugin: 1. Creates one provider via `createFeatureProvider(options)` and `app.provide`s it under `FEATURE_PROVIDER_KEY`. 2. Registers **`Feature`** and **`FeatureVariant`** as global components — **NOT `FeatureDevTools`**, which must be imported and used explicitly (it isn't auto-registered by the plugin at all). 3. Registers the `v-feature` directive globally. 4. In dev only (`import.meta.env.DEV`), dynamically imports and wires the Vue DevTools browser-extension integration (section 10). --- ## 9. SSR ```ts function serializeFlags(provider: FeatureProvider): Record // trivial wrapper: provider.serialize() ``` Plain-Vue SSR flow (Nuxt handles this automatically instead, see section 13): on the server, call `serializeFlags(provider)` after rendering and embed the result in the HTML (e.g. `window.__FEATURE_FLAGS__ = JSON.stringify(snapshot)`); on the client, pass it back in as `ssrState` — `app.use(FeatureToggles, { ssrState: window.__FEATURE_FLAGS__, loader })`. `ssrState` seeds `isReady: true` immediately (no loading flash) and is stored in the SAME ref `loader` populates (same priority tier as `'loader'` in 4.3) — the values are present from the very first render, preventing a hydration mismatch between server- and client-rendered flag-gated content. --- ## 10. Vue DevTools (browser extension) integration ```ts function setupVueDevtools(app: App, provider: FeatureProvider): void // called automatically by the plugin in dev — not meant to be called directly ``` Dynamically `import('@vue/devtools-api')`; if that import rejects (package not installed, or devtools inactive), it's caught and **silently skipped** — no error, no console warning. When available, adds: - An **Inspector** tab ("Feature Toggles") listing every current flag as a node, tagged with its value and its `getFlagSource()`. Selecting a node shows its value/source/expired/persisted state, and its `FlagMeta` fields if set. - A **Timeline layer** ("Feature Flag Changes") — one event per flag change, with old/new value and source, whenever any flag's resolved value actually changes (a `watch` on the whole `flags` object, diffed key by key). This is entirely separate from `` (7.3), which is an in-page UI component, not a browser-extension panel — the two can be used together or independently. --- ## 11. Adapters (`vue-feature-toggles/adapters`) Each is a loader **factory** — call it with its own options to get a `() => Promise>` function, then pass that as `FeatureTogglesOptions.loader`. None of these are auto-detected; you choose one explicitly. ```ts function launchDarklyLoader(opts: { clientSideId: string user: { key: string; [key: string]: unknown } baseUrl?: string // default https://app.launchdarkly.com }): () => Promise> function unleashLoader(opts: { url: string; appName: string; clientKey: string; userId?: string }): () => Promise> // Unleash flags with enabled variants resolve to the first ENABLED variant's name (not necessarily // the variant Unleash itself would pick by weight) — a plain enabled/disabled toggle resolves to that boolean. function flagsmithLoader(opts: { apiKey: string; apiUrl?: string; identity?: string // apiUrl default https://edge.api.flagsmith.com/api/v1 }): () => Promise> // feature_state_value used as the variant string when it's a non-empty string, else the plain `enabled` boolean. ``` Each throws (a plain `Error`) if the underlying `fetch` response isn't `ok` — that rejection propagates out of `loader()`/`reload()`, so `isLoading` stays true and `isReady` never flips on failure unless you catch it yourself (the provider's `reload()` doesn't swallow loader errors, see 4.6). --- ## 12. `vue-feature-toggles/testing` ```ts function createTestFeatureProvider( flags?: Record, options?: Omit ): { install(app: App): void; provider: FeatureProvider } function withFeatures( flags?: Record, options?: Omit ): { global: { plugins: [ReturnType] } } // spread into Vue Test Utils' mount(Component, withFeatures(...)) async function setTestFlag(name: string, value: FlagValue): Promise // setFlag or setVariant on the module-level "current" provider, then awaits nextTick() function resetTestProvider(): void // clears the module-level reference; call in afterEach/afterAll ``` `setTestFlag` operates on a **module-level singleton** — the most recently created test provider (via `withFeatures`/ `createTestFeatureProvider`) — not the one from any specific `mount()` call. Running two tests that each create a provider without calling `resetTestProvider()` between them means `setTestFlag` in the second test silently targets the second provider (fine in practice with serial test execution, but `resetTestProvider()` in `afterEach` is the documented safe pattern to avoid cross-test leakage). `loader` and `reloadInterval` are excluded from the options type on purpose — tests should set flags directly, not through an async loader. --- ## 13. `vue-feature-toggles/nuxt` ```ts // nuxt.config.ts export default defineNuxtConfig({ modules: ['vue-feature-toggles/nuxt'], featureToggles: { // config key: 'featureToggles' flags: { newDashboard: true }, defaultValue: false, // default false urlPrefix: 'feature', // default 'feature' urlOverrides: true, // default: NODE_ENV !== 'production' reloadInterval: 0, // default 0 // groups / dependencies / meta / expiry / schedule / variables / userId / liveUpdates: passed through as-is }, }) ``` - **`loader` and `rules` are NOT accepted here** — `NuxtFeatureTogglesOptions = Omit` — function values can't survive JSON serialization into `runtimeConfig.public`. There's no alternate config-based way to supply them through the module; if you need a `loader` or `rules`, install the plain package and call `app.use(FeatureToggles, ...)` yourself from a custom plugin instead of using this module. - **`ssrState` is handled automatically** — the module's runtime plugin hooks `app:rendered` on the server to stash `provider.serialize()` into the Nuxt payload (`nuxtApp.payload.featureFlags`), and reads it back on the client before creating the client-side provider. No manual `serializeFlags()` call needed in a Nuxt app (unlike plain Vue SSR, section 9). - **The module registers global components (`Feature`, `FeatureVariant`) and the `v-feature` directive — but does NOT auto-import the composables** (`useFeature`, `useFeatureVariant`, `useFeatureProvider` still need an explicit `import from 'vue-feature-toggles'` in a Nuxt app; there is no `addImports` call in the module at all). - The provider is also exposed as `$featureToggles` via Nuxt's plugin-provide mechanism: `const { $featureToggles } = useNuxtApp()` — a way to reach the full `FeatureProvider` outside component `setup()` without `useFeatureProvider()`. --- ## 14. `vue-feature-toggles/storybook` ```ts function withFeatureToggles( defaultFlags?: Record, options?: Omit ): (story: () => unknown, context: { parameters?: { featureToggles?: Record } }) => Component ``` ```ts // .storybook/preview.ts import { withFeatureToggles } from 'vue-feature-toggles/storybook' export const decorators = [withFeatureToggles()] ``` ```ts // MyComponent.stories.ts export const WithBetaSearch: Story = { parameters: { featureToggles: { betaSearch: true, newDashboard: false } }, } ``` Per-story `parameters.featureToggles` is merged **on top of** `defaultFlags` (story wins on key conflicts) — a fresh `createFeatureProvider` is created per story render, so state from one story never leaks into another. `Feature`/`FeatureVariant` are registered locally on the decorator's own wrapper component (not globally) — they work inside any story without a separate global registration step. --- ## 15. `vue-feature-toggles/vite` (dev dependency) ```ts function featureTogglesPlugin(options?: { stripDevTools?: boolean }): Plugin // stripDevTools default true ``` ```ts // vite.config.ts import { featureTogglesPlugin } from 'vue-feature-toggles/vite' export default defineConfig({ plugins: [vue(), featureTogglesPlugin()] }) ``` Only runs during `vite build` (checks `config.command === 'build'` via `configResolved`) — a no-op in dev serve mode, so `` still works normally while developing. During a production build, it regex-strips ``/`` usage and its import statement (including cleaning it out of a multi-name import) from every `.vue`/`.ts`/`.tsx`/`.js`/`.jsx`/`.mts`/`.mjs` file that mentions it — a source-text transform, not an AST-based one; it looks for the literal identifier `FeatureDevTools` in import statements and JSX/template-like tag syntax. --- ## 16. CLI (`npx vue-feature-toggles `) ``` vue-feature-toggles list Show all flags from config, with metadata vue-feature-toggles check [--src ] Scan source files for references to UNKNOWN flags (default --src: src) vue-feature-toggles stale [--months N] Find flags that have been `true` for longer than N months (default 3) --config Path to config file (default: feature-toggles.config.{js,mjs,json} in root) --root Project root (default: current directory) ``` Config resolution order: (1) `--config` path if given, else `feature-toggles.config.{js,mjs,json}` in `--root`; (2) if no config file exists, the CLI **statically scans source files for `app.use(FeatureToggles, { ... })`** and parses the object literal out of the source text (a hand-rolled brace/string-aware extractor, not a real JS parser — works for a literal config object, not one built from variables/spread/computed values). `check` flags any flag name referenced in source (via `isEnabled`/`useFeature`/etc. call sites, matched by regex) that isn't declared in the resolved config's `flags`. `stale` reads `meta[name].addedAt`/`expiry`-adjacent info to flag long-lived `true` flags worth revisiting — cleanup hygiene, not a runtime feature. --- ## 17. Consolidated gotcha list Cross-cutting facts most likely to produce subtly wrong generated code if missed — each is explained in full where it first applies above, listed here for a fast pre-flight check: 1. `useFeature(['a', 'b'])` (array) returns `{ a: Ref, b: Ref }` — independent refs. `useFeature('a', 'b')` (variadic, no array) returns ONE `Ref` that's the AND of both. Easy to conflate (5.1). 2. A rollout flag with no `userId` set resolves identically for every visitor (hashes under the literal `'anonymous'`) — it is not a random per-visitor coin flip unless a real per-user `userId` is provided (4.2). 3. `isFlagTruthy` treats any non-empty string other than `'false'`/`'0'` as truthy — a variant flag reads as "enabled" via `isEnabled()` regardless of WHICH variant is active (section 3). 4. Dependency-violation forcing (4.11) overrides even an explicit `setFlag(x, true)`/URL override — unlike schedule forcing (which respects an active override), a flag with an unmet dependency is always forced off (4.3). 5. Live-pushed flag updates (SSE/WebSocket) share the same underlying state — and therefore the same `FlagSource` — as the async `loader`. There is no `'live'` value in the `FlagSource` type (4.6). 6. `v-feature` hides elements via `el.style.display = 'none'`, not by removing them from the DOM like `v-if` — DOM queries still find a "disabled" element (section 6). 7. `` with neither `name` nor `group` silently renders nothing (or its fallback) — no warning (7.1). 8. `` (an in-template UI panel) and the Vue DevTools browser-extension integration (automatic, no template placement) are two separate, independently-available things (7.3, section 10) — only `` needs the Vite plugin (section 15) to be stripped from production. 9. The Nuxt module registers global components and the directive, but does **not** auto-import the composables, and does **not** accept `loader`/`rules` in its config at all (section 13). 10. `useFeature`/`useFeatureVariant` never throw without a provider (silently `false`/`''`); `useFeatureProvider()` DOES throw without one — the two composable families have opposite error-handling philosophies (5.1–5.3). 11. `getVariable()` returns a brand-new `computed` ref on every call — don't call it inside a template expression repeatedly expecting a stable reference; call it once and store the result (4.9).