Created
September 27, 2025 22:16
-
-
Save cdvillard/fa0a1b74e3cac3ab0569f5802ae7af08 to your computer and use it in GitHub Desktop.
`vite-plugin-flags`: a Vite plugin and supporting scripts for enabling Vercel's Flags Explorer in local development for frontend-only projects
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ./vercel/flags.config.ts | |
| * Feature flags configuration for local development and Vercel middleware. | |
| * Add/edit flags and attributes here in FLAG_DEFS to expose them via | |
| * `/.well-known/vercel/flags` and to allow URL → cookie overrides in middleware.ts. | |
| */ | |
| export type FlagValue = string | number | boolean | null; | |
| export type FlagDef = { | |
| type: "enum" | "boolean" | "string" | "featureFlag"; | |
| origin?: string; | |
| options?: { value: string | number | boolean | null; label?: string }[]; | |
| default?: FlagValue; | |
| description?: string; | |
| }; | |
| export const FLAG_DEFS: Record<string, FlagDef> = { | |
| theme: { | |
| type: "enum", | |
| options: [{ value: "light" }, { value: "dark" }], | |
| description: "Selection for color scheme", | |
| default: "light", | |
| }, | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ./vercel/flags.ts | |
| * Feature flags logic for local development and Vercel middleware. | |
| * Defines the logic and endpoints used in delivering flags. | |
| */ | |
| import { parse as parseCookie } from 'cookie'; | |
| import { decryptOverrides, version } from "flags"; | |
| import { FLAG_DEFS, type FlagDef, type FlagValue } from "./flags.config.js"; | |
| export const ENDPOINTS = { | |
| FLAGS_DISCOVERY: "/.well-known/vercel/flags", | |
| FLAG_VALUES: "/api/flag-values", | |
| FLAG_VALUES_MODULE: "/api/flag-values.js", | |
| } as const; | |
| const HEADERS = { | |
| AUTHORIZATION: "authorization", | |
| COOKIE: "cookie", | |
| CONTENT_TYPE: "content-type", | |
| CACHE_CONTROL: "cache-control", | |
| } as const; | |
| const CONTENT_TYPES = { | |
| JSON: "application/json", | |
| HTML: "text/html; charset=utf-8", | |
| JS: "text/javascript; charset=utf-8", | |
| } as const; | |
| export type JsonObject = Record<string, unknown>; | |
| export const getOverridesCookie = async ( | |
| req: Request, | |
| ): Promise<{ environment?: string; [definition: string]: any }> => { | |
| const rawOverrideCookie = req.headers.get('cookie'); | |
| const overrideCookie = rawOverrideCookie | |
| ? parseCookie(rawOverrideCookie)?.['vercel-flag-overrides'] | |
| : null; | |
| return overrideCookie ? ((await decryptOverrides(overrideCookie)) ?? {}) : {}; | |
| }; | |
| export const flagDefaultsFromDefs = (defs: Record<string, FlagDef>): Record<string, FlagValue> => { | |
| const out: Record<string, FlagValue> = {}; | |
| for (const [key, def] of Object.entries(defs)) { | |
| if (def.default !== undefined) { | |
| out[key] = def.default; | |
| } | |
| } | |
| return out; | |
| }; | |
| export const resolveFlagValues = async (request: Request) => { | |
| const overrides = await getOverridesCookie(request); | |
| const defaults = flagDefaultsFromDefs(FLAG_DEFS); | |
| return { ...defaults, ...overrides } as Record<string, FlagValue>; | |
| }; | |
| export const jsonResponse = ( | |
| data: unknown, | |
| options: { status?: number; headers?: Record<string, string> } = {} | |
| ) => { | |
| const { status = 200, headers = {} } = options; | |
| return new Response(JSON.stringify(data), { | |
| status, | |
| headers: { [HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON, ...headers }, | |
| }); | |
| }; | |
| export const noStoreHeaders = { | |
| [HEADERS.CACHE_CONTROL]: "no-store, must-revalidate", | |
| }; | |
| export const handleFlagsDiscovery = async () => | |
| new Response( | |
| JSON.stringify({ definitions: FLAG_DEFS }), | |
| { | |
| status: 200, | |
| headers: { | |
| [HEADERS.CONTENT_TYPE]: CONTENT_TYPES.JSON, | |
| "x-flags-sdk-version": version, | |
| }, | |
| } | |
| ); | |
| export const handleFlagValues = async (request: Request) => { | |
| const values = await resolveFlagValues(request); | |
| return jsonResponse(values, { headers: noStoreHeaders }); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ./middleware.ts | |
| * Defines a handler for Vercel to use in Vercel previews | |
| * for cookie-based overrides | |
| */ | |
| import { | |
| ENDPOINTS, | |
| handleFlagValues as handleFlagValuesEndpoint, | |
| handleFlagsDiscovery as handleFlagsDiscoveryEndpoint, | |
| } from "./vercel/flags.js"; | |
| export default async function middleware(request: Request) { | |
| const url = new URL(request.url); | |
| // Handle different endpoints using a declarative approach | |
| const endpointHandlers = { | |
| [ENDPOINTS.FLAGS_DISCOVERY]: () => handleFlagsDiscoveryEndpoint(), | |
| [ENDPOINTS.FLAG_VALUES]: async () => await handleFlagValuesEndpoint(request), | |
| } as const; | |
| const handler = endpointHandlers[url.pathname as unknown as keyof typeof endpointHandlers]; | |
| if (handler) { | |
| return await handler(); | |
| } | |
| // Fall through to static assets | |
| return undefined; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ./vite-plugin-flags.ts | |
| * The plugin hooking into Vite's `configureServer` that allows for | |
| * local development using the Vercel Toolbar to toggle values instead | |
| * of manually pluging them in in markup | |
| */ | |
| import type { Plugin } from 'vite'; | |
| import { readFileSync, existsSync } from 'fs'; | |
| import { join } from 'path'; | |
| import type { ServerResponse } from 'http'; | |
| // Helper function to handle response writing | |
| function writeResponse(res: ServerResponse, response: Response) { | |
| // Set response headers | |
| for (const [key, value] of response.headers.entries()) { | |
| res.setHeader(key, value); | |
| } | |
| res.statusCode = response.status; | |
| return response.text(); | |
| } | |
| // Load FLAGS_SECRET from .env.local for development | |
| function loadFlagsSecret() { | |
| const envLocalPath = join(process.cwd(), '.env.local'); | |
| if (existsSync(envLocalPath)) { | |
| try { | |
| const envContent = readFileSync(envLocalPath, 'utf8'); | |
| const lines = envContent.split('\n'); | |
| for (const line of lines) { | |
| const [key, ...valueParts] = line.split('='); | |
| if (key && key.trim() === 'FLAGS_SECRET') { | |
| const value = valueParts.join('=').trim().replace(/^["']|["']$/g, ''); | |
| process.env.FLAGS_SECRET = value; | |
| // FLAGS_SECRET successfully loaded | |
| break; | |
| } | |
| } | |
| } catch (error) { | |
| console.warn('⚠️ Could not load .env.local:', error); | |
| } | |
| } | |
| } | |
| export function flagsPlugin(): Plugin { | |
| // Load FLAGS_SECRET when the plugin is initialized | |
| loadFlagsSecret(); | |
| return { | |
| name: 'flags-dev-server', | |
| configureServer(server) { | |
| server.middlewares.use('/.well-known/vercel/flags', async (_req, res) => { | |
| try { | |
| const { handleFlagsDiscovery } = await import('./vercel/flags.js'); | |
| const response = await handleFlagsDiscovery(); | |
| const responseText = await writeResponse(res, response); | |
| res.end(responseText); | |
| } catch (error) { | |
| console.error('Error handling flags discovery:', error); | |
| res.statusCode = 500; | |
| res.end('Internal Server Error'); | |
| } | |
| }); | |
| server.middlewares.use('/api/flag-values', async (req, res) => { | |
| try { | |
| const { handleFlagValues } = await import('./vercel/flags.js'); | |
| // Create a proper Request object | |
| const url = `http://localhost:${server.config.server.port}${req.url}`; | |
| const request = new Request(url, { | |
| method: req.method || 'GET', | |
| headers: req.headers as Record<string, string>, | |
| }); | |
| const response = await handleFlagValues(request); | |
| const responseText = await writeResponse(res, response); | |
| res.end(responseText); | |
| } catch (error) { | |
| console.error('Error handling flag values:', error); | |
| res.statusCode = 500; | |
| res.end('Internal Server Error'); | |
| } | |
| }); | |
| }, | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * ./public/vite-script-flags.ts | |
| * An example of functions you might need to inject into `index.html` | |
| * via a script tag to load the flags once they're toggled on or off. | |
| * This example shows how you might add them as attributes or a specific | |
| * property of an element. | |
| * The `init` function will need to change depending on how and where you | |
| * need to set the resulting values. In theory, you can set them in | |
| * sessionStorage or in the global scope. | |
| */ | |
| // Load flag definitions to identify feature flags | |
| const loadFlagDefinitions = async () => { | |
| const res = await fetch('/.well-known/vercel/flags', { cache: 'no-store' }); | |
| const data = await res.json(); | |
| return data.definitions; | |
| }; | |
| // Import current flag values as a JSON module and apply; fallback to fetch if needed | |
| const loadFlags = async () => { | |
| const res = await fetch('/api/flag-values', { cache: 'no-store' }); | |
| return res.json(); | |
| }; | |
| const separateFlags = (flags, definitions) => { | |
| const = {}; | |
| const featureFlags = {}; | |
| Object.entries(flags).forEach(([key, value]) => { | |
| const flagDef = definitions[key]; | |
| if (flagDef && flagDef.type === 'featureFlag') { | |
| featureFlags[key] = value; | |
| } else { | |
| attributeValues[key] = value; | |
| } | |
| }); | |
| return { attributeValues, featureFlags }; | |
| }; | |
| const applyAttributes = (header, flags) => { | |
| Object.entries(flags).forEach(([key, value]) => { | |
| if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) { | |
| // Prefer setting the property if it exists to avoid later resets, then reflect to attribute | |
| if (key in header) { | |
| try { header[key] = value; } catch { } | |
| } | |
| header.setAttribute(key, String(value)); | |
| } | |
| }); | |
| }; | |
| const applyFeatureFlags = (header, featureFlags) => { | |
| // Merge feature flags with existing featureFlags | |
| header.featureFlags = { | |
| ...(header.featureFlags || {}), | |
| ...featureFlags | |
| }; | |
| }; | |
| const init = async () => { | |
| const [flags, definitions] = await Promise.all([ | |
| loadFlags(), | |
| loadFlagDefinitions() | |
| ]); | |
| const { attributeValues, featureFlags } = separateFlags(flags, definitions); | |
| await customElements.whenDefined('branded-header'); | |
| const header = document.querySelector('branded-header'); | |
| if (header) { | |
| // Apply regular flags to header properties/attributes | |
| applyAttributes(header, attributeValues); | |
| // Apply feature flags to featureFlags property | |
| applyFeatureFlags(header, featureFlags); | |
| } | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
vite-plugin-flagsA Vite plugin geared towards frontend projects supporting local enablement of Vercel's Flags Explorer toolbar feature
These scripts are less a prescription and more a set of guidelines to enable Vercel's Flags Explorer locally in Vite-based projects that don't serve APIs. You'll need to add the Vercel Toolbar to your project as a dependency before these scripts will work.
Setting up the Vercel Toolbar in a Vite-based project.
Setting up
vite-plugin-flagsOnce you have that set up, you can add the scripts above to your project, and simply add
vite-plugin-flagsto your Vite configuration.