Skip to content

Instantly share code, notes, and snippets.

@cdvillard
Created September 27, 2025 22:16
Show Gist options
  • Select an option

  • Save cdvillard/fa0a1b74e3cac3ab0569f5802ae7af08 to your computer and use it in GitHub Desktop.

Select an option

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
/*
* ./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",
},
};
/*
* ./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 });
};
/*
* ./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;
}
/*
* ./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');
}
});
},
};
}
/*
* ./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();
}
@cdvillard
Copy link
Author

vite-plugin-flags

A 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.

  1. Install the @vercel/toolbar package
pnpm i @vercel/toolbar
npm i @vercel/toolbar
yarn add @vercel/toolbar
bun add @vercel/toolbar
  1. Link your local project to Vercel
vercel link [project-path]
  1. Add the Vercel Toolbar plugin to your Vite config
// vite.config.ts

import { vercelToolbar } from '@vercel/toolbar/plugins/vite';
import { defineConfig } from 'vite';
 
export default defineConfig({
  plugins: [vercelToolbar()],
});
  1. Mount the toolbar at the entry point of your project
// index.ts

import { mountVercelToolbar } from '@vercel/toolbar/vite';

if (import.meta.env.DEV) {
  mountVercelToolbar();
}

Setting up vite-plugin-flags

Once you have that set up, you can add the scripts above to your project, and simply add vite-plugin-flags to your Vite configuration.

import { vercelToolbar } from '@vercel/toolbar/plugins/vite';
import { flagsPlugin } from './vite-flags-plugin.js';
import { defineConfig } from 'vite';
 
export default defineConfig({
  plugins: [vercelToolbar(), flagsPlugin()],
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment