├── .nvmrc ├── .node-version ├── README.md ├── .husky └── pre-commit ├── packages ├── vercel-flags-core │ ├── .gitignore │ ├── README.md │ ├── tsconfig.json │ ├── src │ │ ├── store.ts │ │ ├── data-source │ │ │ ├── in-memory-data-source.ts │ │ │ └── interface.ts │ │ ├── lib │ │ │ └── report-value.ts │ │ ├── utils.ts │ │ └── client.test.ts │ ├── tsup.config.js │ ├── vitest.config.ts │ └── CHANGELOG.md ├── adapter-vercel │ ├── README.md │ ├── tsconfig.json │ ├── tsup.config.js │ ├── vitest.config.ts │ ├── CHANGELOG.md │ └── package.json ├── adapter-flagsmith │ ├── .gitignore │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ └── tsup.config.js ├── adapter-openfeature │ ├── .gitignore │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── CHANGELOG.md │ └── tsup.config.js ├── adapter-split │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── CHANGELOG.md │ ├── tsup.config.js │ └── package.json ├── adapter-optimizely │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── CHANGELOG.md │ ├── tsup.config.js │ └── package.json ├── flags │ ├── assets │ │ ├── hero-dark.png │ │ └── hero-light.png │ ├── src │ │ ├── spec-extension │ │ │ ├── cookies.ts │ │ │ └── adapters │ │ │ │ └── reflect.ts │ │ ├── analytics.ts │ │ ├── react │ │ │ └── index.test.tsx │ │ ├── sveltekit │ │ │ ├── index.test.ts │ │ │ ├── env.ts │ │ │ └── types.ts │ │ ├── next │ │ │ ├── overrides.ts │ │ │ └── create-flags-discovery-endpoint.ts │ │ ├── lib │ │ │ ├── normalize-options.ts │ │ │ ├── merge-provider-data.ts │ │ │ └── async-memoize-one.ts │ │ └── index.ts │ ├── vitest.config.ts │ ├── tsconfig.json │ ├── tsup.config.js │ └── LICENSE.md ├── adapter-growthbook │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── tsup.config.js │ └── CHANGELOG.md ├── adapter-hypertune │ ├── src │ │ ├── index.ts │ │ └── provider │ │ │ └── index.ts │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── tsup.config.js │ ├── CHANGELOG.md │ └── package.json ├── adapter-posthog │ ├── tsconfig.json │ ├── tsup.config.js │ ├── src │ │ ├── provider │ │ │ └── index.test.ts │ │ └── types.ts │ ├── CHANGELOG.md │ └── README.md ├── adapter-reflag │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── CHANGELOG.md │ ├── tsup.config.js │ └── README.md ├── adapter-statsig │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── tsup.config.js │ ├── CHANGELOG.md │ └── README.md ├── adapter-edge-config │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── tsup.config.js │ ├── CHANGELOG.md │ └── package.json └── adapter-launchdarkly │ ├── tsconfig.json │ ├── vitest.config.ts │ ├── tsup.config.js │ └── CHANGELOG.md ├── tests ├── next-15 │ ├── README.md │ ├── port.ts │ ├── app │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistVF.woff │ │ │ └── GeistMonoVF.woff │ │ ├── flags.tsx │ │ ├── page.tsx │ │ ├── globals.css │ │ ├── page.spec.tsx │ │ ├── app-router-precomputed │ │ │ └── [code] │ │ │ │ ├── page.tsx │ │ │ │ └── page.spec.tsx │ │ └── layout.tsx │ ├── .env │ ├── public │ │ ├── vercel.svg │ │ ├── window.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── next.config.ts │ ├── CHANGELOG.md │ ├── postcss.config.mjs │ ├── tailwind.config.ts │ ├── .gitignore │ ├── flags.tsx │ ├── tsconfig.json │ ├── package.json │ ├── pages-specs │ │ ├── pages-router.spec.tsx │ │ └── pages-router-precomputed │ │ │ └── [code] │ │ │ └── page.spec.tsx │ └── pages │ │ └── pages-router.tsx ├── next-16 │ ├── README.md │ ├── port.ts │ ├── app │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistVF.woff │ │ │ └── GeistMonoVF.woff │ │ ├── flags.tsx │ │ ├── globals.css │ │ ├── page.tsx │ │ ├── page.spec.tsx │ │ ├── layout.tsx │ │ └── app-router-precomputed │ │ │ └── [code] │ │ │ ├── page.spec.tsx │ │ │ └── page.tsx │ ├── .env │ ├── public │ │ ├── vercel.svg │ │ ├── window.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── CHANGELOG.md │ ├── postcss.config.mjs │ ├── next.config.ts │ ├── tailwind.config.ts │ ├── .gitignore │ ├── flags.tsx │ ├── package.json │ ├── pages-specs │ │ ├── pages-router.spec.tsx │ │ └── pages-router-precomputed │ │ │ └── [code] │ │ │ └── page.spec.tsx │ ├── tsconfig.json │ └── pages │ │ ├── pages-router.tsx │ │ └── pages-router-precomputed │ │ └── [code] │ │ └── index.tsx ├── sveltekit-e2e │ ├── .npmrc │ ├── src │ │ ├── port.ts │ │ ├── lib │ │ │ ├── index.ts │ │ │ └── precomputed-flags.ts │ │ ├── hooks.server.ts │ │ ├── routes │ │ │ ├── precomputed │ │ │ │ └── [code] │ │ │ │ │ ├── +page.svelte │ │ │ │ │ └── +page.server.ts │ │ │ ├── +layout.server.ts │ │ │ ├── +page.svelte │ │ │ ├── +page.server.ts │ │ │ ├── api │ │ │ │ └── reroute │ │ │ │ │ └── +server.ts │ │ │ ├── +layout.svelte │ │ │ ├── page.spec.ts │ │ │ └── [x+2e]well-known │ │ │ │ └── vercel │ │ │ │ └── flags │ │ │ │ └── __+server.ts │ │ ├── hooks.ts │ │ ├── app.d.ts │ │ └── app.html │ ├── static │ │ └── favicon.png │ ├── .env │ ├── .gitignore │ ├── vite.config.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── README.md └── README.md ├── .npmrc ├── examples ├── sveltekit-example │ ├── .npmrc │ ├── src │ │ ├── app.css │ │ ├── lib │ │ │ ├── index.ts │ │ │ └── precomputed-flags.ts │ │ ├── hooks.server.ts │ │ ├── routes │ │ │ ├── examples │ │ │ │ ├── dashboard-pages │ │ │ │ │ └── +page.server.ts │ │ │ │ ├── marketing-pages │ │ │ │ │ └── [code] │ │ │ │ │ │ └── +page.server.ts │ │ │ │ └── (marketing-pages-manual-approach) │ │ │ │ │ ├── marketing-pages-variant-a │ │ │ │ │ └── +page.svelte │ │ │ │ │ └── marketing-pages-variant-b │ │ │ │ │ └── +page.svelte │ │ │ ├── +layout.svelte │ │ │ ├── api │ │ │ │ ├── reroute-manual │ │ │ │ │ └── +server.ts │ │ │ │ └── reroute │ │ │ │ │ └── +server.ts │ │ │ └── [x+2e]well-known │ │ │ │ └── vercel │ │ │ │ └── flags │ │ │ │ └── __+server.ts │ │ ├── app.d.ts │ │ ├── app.html │ │ └── hooks.ts │ ├── static │ │ └── favicon.png │ ├── .env │ ├── .gitignore │ ├── vite.config.ts │ ├── svelte.config.js │ ├── tsconfig.json │ ├── CHANGELOG.md │ ├── package.json │ └── README.md ├── shirt-shop │ ├── vercel.json │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── .well-known │ │ │ └── vercel │ │ │ │ └── flags │ │ │ │ └── route.ts │ │ ├── free-delivery.tsx │ │ ├── [code] │ │ │ ├── cart │ │ │ │ ├── proceed-to-checkout.tsx │ │ │ │ ├── order-summary.tsx │ │ │ │ └── page.tsx │ │ │ └── add-to-cart.tsx │ │ ├── summer-sale.tsx │ │ └── layout.tsx │ ├── public │ │ └── images │ │ │ ├── pool.jpg │ │ │ └── product │ │ │ ├── shirt-black.png │ │ │ ├── shirt-blue.png │ │ │ └── shirt-white.png │ ├── .env │ ├── postcss.config.mjs │ ├── components │ │ ├── utils │ │ │ ├── cart-types.ts │ │ │ ├── images.ts │ │ │ └── product-detail-page-context.tsx │ │ ├── main.tsx │ │ ├── banners │ │ │ └── free-delivery-banner.tsx │ │ ├── shopping-cart │ │ │ ├── shopping-cart-list.tsx │ │ │ └── proceed-to-checkout-button.tsx │ │ ├── product-detail-page │ │ │ ├── product-header.tsx │ │ │ └── product-reviews.tsx │ │ ├── dev-tools.tsx │ │ └── image-gallery.tsx │ ├── next.config.mjs │ ├── tailwind.config.ts │ ├── lib │ │ ├── identify.ts │ │ ├── get-cart-id.ts │ │ └── get-stable-id.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ └── proxy.ts └── snippets │ ├── app │ ├── favicon.ico │ ├── fonts │ │ ├── GeistVF.woff │ │ └── GeistMonoVF.woff │ ├── examples │ │ ├── feature-flags-in-proxy │ │ │ ├── variant-on │ │ │ │ └── page.tsx │ │ │ ├── variant-off │ │ │ │ └── page.tsx │ │ │ ├── flags.ts │ │ │ ├── proxy.ts │ │ │ ├── handlers.ts │ │ │ └── shared.tsx │ │ ├── suspense-fallbacks │ │ │ ├── flags.tsx │ │ │ └── proxy.tsx │ │ ├── marketing-pages │ │ │ ├── regenerate-id-button.tsx │ │ │ ├── proxy.tsx │ │ │ ├── [code] │ │ │ │ └── page.tsx │ │ │ └── get-or-generate-visitor-id.tsx │ │ └── dashboard-pages │ │ │ ├── flags.ts │ │ │ └── page.tsx │ ├── concepts │ │ ├── precompute │ │ │ ├── manual │ │ │ │ ├── variant-a │ │ │ │ │ └── page.tsx │ │ │ │ ├── variant-b │ │ │ │ │ └── page.tsx │ │ │ │ ├── flags.tsx │ │ │ │ └── proxy.ts │ │ │ └── automatic │ │ │ │ └── [code] │ │ │ │ ├── proxy.ts │ │ │ │ ├── flags.tsx │ │ │ │ └── page.tsx │ │ ├── adapters │ │ │ ├── page.tsx │ │ │ └── flags.tsx │ │ ├── identify │ │ │ ├── basic │ │ │ │ ├── page.tsx │ │ │ │ └── flags.ts │ │ │ └── full │ │ │ │ └── flags.ts │ │ └── dedupe │ │ │ └── page.tsx │ ├── getting-started │ │ └── overview │ │ │ ├── reload-button.tsx │ │ │ └── page.tsx │ ├── .well-known │ │ └── vercel │ │ │ └── flags │ │ │ └── route.ts │ └── layout.tsx │ ├── public │ ├── vercel.svg │ ├── prince-akachi-LWkFHEGpleE-unsplash.jpg │ ├── window.svg │ ├── file.svg │ ├── globe.svg │ └── next.svg │ ├── flags.ts │ ├── postcss.config.mjs │ ├── lib │ ├── utils.ts │ └── pages-router-precomputed │ │ ├── README.md │ │ ├── flags.ts │ │ └── proxy.ts │ ├── components │ ├── content.tsx │ ├── theme-provider.tsx │ ├── demo-flag.tsx │ ├── ui │ │ ├── input.tsx │ │ └── tooltip.tsx │ └── pages-layout.tsx │ ├── .env │ ├── components.json │ ├── .gitignore │ ├── hooks │ └── use-mobile.tsx │ ├── pages │ └── examples │ │ ├── pages-router-dynamic │ │ └── index.tsx │ │ └── pages-router-precomputed │ │ └── [code] │ │ └── index.tsx │ ├── tsconfig.json │ ├── next.config.ts │ └── package.json ├── vitest.workspace.js ├── assets └── hero-dark.png ├── .prettierignore ├── pnpm-workspace.yaml ├── apps └── shirt-shop-api │ ├── app │ └── favicon.ico │ ├── postcss.config.mjs │ ├── README.md │ ├── next.config.ts │ ├── package.json │ ├── .gitignore │ └── tsconfig.json ├── renovate.json ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ └── feedback.md └── workflows │ ├── unit-tests.yml │ └── playwright.yml ├── .changeset ├── README.md └── config.json ├── tsconfig-base.json ├── .zed └── settings.json ├── biome.json ├── LICENSE.md └── docs └── REVIEWS.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 20.x 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/flags/README.md -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | -------------------------------------------------------------------------------- /tests/next-15/README.md: -------------------------------------------------------------------------------- 1 | # Next.js 15 e2e tests 2 | -------------------------------------------------------------------------------- /tests/next-15/port.ts: -------------------------------------------------------------------------------- 1 | export const port = 4015; 2 | -------------------------------------------------------------------------------- /tests/next-16/README.md: -------------------------------------------------------------------------------- 1 | # Next.js 15 e2e tests 2 | -------------------------------------------------------------------------------- /tests/next-16/port.ts: -------------------------------------------------------------------------------- 1 | export const port = 4016; 2 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact = true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /examples/sveltekit-example/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /vitest.workspace.js: -------------------------------------------------------------------------------- 1 | export default ['./packages/*']; 2 | -------------------------------------------------------------------------------- /packages/adapter-vercel/README.md: -------------------------------------------------------------------------------- 1 | # `@flags-sdk/vercel` 2 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/README.md: -------------------------------------------------------------------------------- 1 | # `@vercel/flags-core` 2 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/port.ts: -------------------------------------------------------------------------------- 1 | export const port = 5173; 2 | -------------------------------------------------------------------------------- /packages/adapter-flagsmith/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .env*.local 3 | -------------------------------------------------------------------------------- /packages/adapter-openfeature/.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .env*.local 3 | -------------------------------------------------------------------------------- /assets/hero-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/assets/hero-dark.png -------------------------------------------------------------------------------- /packages/adapter-split/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getProviderData } from './provider'; 2 | -------------------------------------------------------------------------------- /examples/shirt-shop/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "installCommand": "pnpm install:compat" 3 | } 4 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getProviderData } from './provider'; 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | pnpm-lock.yaml 4 | .next 5 | .vercel 6 | .vscode 7 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/typography"; 3 | -------------------------------------------------------------------------------- /tests/next-15/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-15/app/favicon.ico -------------------------------------------------------------------------------- /tests/next-16/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-16/app/favicon.ico -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "tests/*" 4 | - "examples/*" 5 | - "apps/*" 6 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/apps/shirt-shop-api/app/favicon.ico -------------------------------------------------------------------------------- /examples/shirt-shop/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/shirt-shop/app/favicon.ico -------------------------------------------------------------------------------- /examples/snippets/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/snippets/app/favicon.ico -------------------------------------------------------------------------------- /packages/adapter-split/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/flags/assets/hero-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/packages/flags/assets/hero-dark.png -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /packages/adapter-growthbook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapter'; 2 | export { getProviderData } from './provider'; 3 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-posthog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-reflag/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-statsig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/flags/assets/hero-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/packages/flags/assets/hero-light.png -------------------------------------------------------------------------------- /tests/next-15/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-15/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /tests/next-16/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-16/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /examples/snippets/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/snippets/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /packages/adapter-edge-config/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-launchdarkly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/adapter-openfeature/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base"] 4 | } 5 | -------------------------------------------------------------------------------- /tests/next-15/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-15/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /tests/next-16/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/next-16/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /tests/sveltekit-e2e/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/tests/sveltekit-e2e/static/favicon.png -------------------------------------------------------------------------------- /examples/shirt-shop/public/images/pool.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/shirt-shop/public/images/pool.jpg -------------------------------------------------------------------------------- /examples/snippets/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/snippets/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /examples/sveltekit-example/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/sveltekit-example/static/favicon.png -------------------------------------------------------------------------------- /tests/next-15/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | -------------------------------------------------------------------------------- /tests/next-16/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/shirt-shop/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | -------------------------------------------------------------------------------- /examples/sveltekit-example/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | -------------------------------------------------------------------------------- /examples/shirt-shop/public/images/product/shirt-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/shirt-shop/public/images/product/shirt-black.png -------------------------------------------------------------------------------- /examples/shirt-shop/public/images/product/shirt-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/shirt-shop/public/images/product/shirt-blue.png -------------------------------------------------------------------------------- /examples/shirt-shop/public/images/product/shirt-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/shirt-shop/public/images/product/shirt-white.png -------------------------------------------------------------------------------- /examples/snippets/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/flags/src/spec-extension/cookies.ts: -------------------------------------------------------------------------------- 1 | export { 2 | RequestCookies, 3 | ResponseCookies, 4 | stringifyCookie, 5 | } from '@edge-runtime/cookies'; 6 | -------------------------------------------------------------------------------- /tests/next-15/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-16/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/snippets/public/prince-akachi-LWkFHEGpleE-unsplash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/flags/HEAD/examples/snippets/public/prince-akachi-LWkFHEGpleE-unsplash.jpg -------------------------------------------------------------------------------- /packages/adapter-reflag/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-split/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "resolveJsonModule": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/next-15/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /packages/adapter-edge-config/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-flagsmith/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-growthbook/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-openfeature/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/adapter-statsig/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /examples/snippets/flags.ts: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'example-flag', 5 | decide() { 6 | return true; 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/snippets/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /packages/adapter-launchdarkly/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | plugins: [], 5 | test: { environment: 'node' }, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/next-15/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # next-15 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [fb50709] 8 | - Updated dependencies [87503b6] 9 | - @vercel/flags@2.7.0 10 | -------------------------------------------------------------------------------- /tests/next-15/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /tests/next-16/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # next-15 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [fb50709] 8 | - Updated dependencies [87503b6] 9 | - @vercel/flags@2.7.0 10 | -------------------------------------------------------------------------------- /tests/next-16/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /tests/next-15/app/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'example-flag', 5 | decide: () => true, 6 | defaultValue: false, 7 | }); 8 | -------------------------------------------------------------------------------- /tests/next-16/app/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'example-flag', 5 | decide: () => true, 6 | defaultValue: false, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/shirt-shop/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /tests/next-16/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | cacheComponents: true, 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /examples/snippets/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/README.md: -------------------------------------------------------------------------------- 1 | # shirt-shop-api 2 | 3 | This project serves as the backend for all Shirt Shop examples. 4 | 5 | We use this separate backend as we don't want to force people cloning the template to set up a KV store. 6 | -------------------------------------------------------------------------------- /packages/adapter-split/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/split 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 3c66284: initialize 14 | -------------------------------------------------------------------------------- /examples/snippets/lib/pages-router-precomputed/README.md: -------------------------------------------------------------------------------- 1 | This folder contains supporting files for the Pages Router example, as those files can not live within the `pages/` folder as they would otherwise count as actualy Next.js pages. 2 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .vercel 12 | .env*.local 13 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/optimizely 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 3c66284: initialize 14 | -------------------------------------------------------------------------------- /examples/snippets/components/content.tsx: -------------------------------------------------------------------------------- 1 | export function Content({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/sveltekit-example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | vite.config.js.timestamp-* 10 | vite.config.ts.timestamp-* 11 | .vercel 12 | .env*.local 13 | -------------------------------------------------------------------------------- /packages/adapter-flagsmith/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "lib": ["esnext"], 6 | "resolveJsonModule": true, 7 | "target": "ES2020" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/adapter-openfeature/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/openfeature 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - fe72a22: enhance README 14 | -------------------------------------------------------------------------------- /packages/flags/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | environment: 'node', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/utils/cart-types.ts: -------------------------------------------------------------------------------- 1 | export interface CartItem { 2 | id: string; 3 | color: string; 4 | size: string; 5 | quantity: number; 6 | } 7 | 8 | export interface Cart { 9 | id: string; 10 | items: CartItem[]; 11 | } 12 | -------------------------------------------------------------------------------- /examples/snippets/.env: -------------------------------------------------------------------------------- 1 | # this is a random secret that can be used for testing 2 | FLAGS_SECRET=testing-secret-2pMFwxPkTiHKcrwRfFllrhT1KAc8 3 | EDGE_CONFIG="https://edge-config.vercel.com/ecfg_fvhsocnj9jbqprcriyscdnre3aix?token=6c514eae-1bf8-45b6-9277-ee6560280140" 4 | -------------------------------------------------------------------------------- /packages/flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig-base.json", 3 | "include": ["src"], 4 | "compilerOptions": { 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "resolveJsonModule": true, 7 | "target": "ES2020" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { createHandle } from 'flags/sveltekit'; 2 | import { FLAGS_SECRET } from '$env/static/private'; 3 | import * as flags from '$lib/flags'; 4 | 5 | export const handle = createHandle({ secret: FLAGS_SECRET, flags }); 6 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/main.tsx: -------------------------------------------------------------------------------- 1 | export function Main({ children }: { children: React.ReactNode }) { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/shirt-shop/next.config.mjs: -------------------------------------------------------------------------------- 1 | import withVercelToolbar from '@vercel/toolbar/plugins/next'; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | cacheComponents: true, 6 | }; 7 | 8 | export default withVercelToolbar()(nextConfig); 9 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { createHandle } from 'flags/sveltekit'; 2 | import { FLAGS_SECRET } from '$env/static/private'; 3 | import * as flags from '$lib/flags'; 4 | 5 | export const handle = createHandle({ secret: FLAGS_SECRET, flags }); 6 | -------------------------------------------------------------------------------- /packages/adapter-flagsmith/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/flagsmith 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - 5fa7258: Add `@flags-sdk/flagsmith` adapter 14 | -------------------------------------------------------------------------------- /packages/flags/src/analytics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @deprecated It is no longer necessary to call this function. Web Analytics now automatically detects flags present in the DOM. 3 | */ 4 | export const injectFlags = ( 5 | _params: { keys?: string[] } | undefined, 6 | ): void => {}; 7 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/variant-on/page.tsx: -------------------------------------------------------------------------------- 1 | import { Shared } from '../shared'; 2 | 3 | export default async function Page() { 4 | // we statically know that the flag is true as this is the variant-on page 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /packages/flags/src/react/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { FlagDefinitions } from '.'; 3 | 4 | describe('FlagDefinitions', () => { 5 | it('is a function', () => { 6 | expect(typeof FlagDefinitions).toBe('function'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { vercelToolbar } from '@vercel/toolbar/plugins/vite'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit(), vercelToolbar()], 7 | }); 8 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/variant-off/page.tsx: -------------------------------------------------------------------------------- 1 | import { Shared } from '../shared'; 2 | 3 | export default async function Page() { 4 | // we statically know that the flag is false as this is the variant-off page 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/banners/free-delivery-banner.tsx: -------------------------------------------------------------------------------- 1 | export function FreeDeliveryBanner() { 2 | return ( 3 |
4 | Get free delivery on orders over $30 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/manual/variant-a/page.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { manualPrecomputeFlag } from '../flags'; 3 | 4 | export default function Page() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/manual/variant-b/page.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { manualPrecomputeFlag } from '../flags'; 3 | 4 | export default function Page() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/flags.ts: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const basicProxyFlag = flag({ 4 | key: 'basic-proxy-flag', 5 | decide({ cookies }) { 6 | return cookies.get('basic-proxy-flag')?.value === '1'; 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/store.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'node:async_hooks'; 2 | 3 | /** 4 | * A store to avoid reading the Edge Config for every flag evaluation, 5 | * and instead reading it once per request. 6 | */ 7 | export const store = new AsyncLocalStorage(); 8 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/precomputed/[code]/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

{data.flag}

9 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @custom-variant dark (&:is(.dark *)); 4 | 5 | @theme { 6 | --color-link: rgb(0 112 243 / 1); 7 | --color-success: rgb(0 112 243 / 1); 8 | --color-background: white; 9 | --color-success-dark: rgb(7 97 209 / 1); 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log 9 | 10 | .turbo 11 | .DS_Store 12 | .vscode 13 | .idea 14 | .vercel 15 | .env*.local 16 | .svelte-kit 17 | **/test-results/ 18 | **/playwright-report/ 19 | **/blob-report/ 20 | **/playwright/.cache/ 21 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/manual/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const manualPrecomputeFlag = flag({ 4 | key: 'manual-precompute-flag', 5 | description: 'Manual precompute example', 6 | decide() { 7 | return Math.random() > 0.5; 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feedback.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feedback 3 | about: Leave feedback about the Flags SDK and flags-sdk.dev 4 | title: "[Feedback]" 5 | labels: feedback 6 | assignees: dferber90 7 | --- 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/examples/dashboard-pages/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { showNewDashboard } from '$lib/flags'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | const dashboard = await showNewDashboard(); 6 | 7 | return { dashboard }; 8 | }; 9 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/+layout.server.ts: -------------------------------------------------------------------------------- 1 | import { showDashboard } from '$lib/flags'; 2 | import type { LayoutServerLoad } from './$types'; 3 | 4 | export const load: LayoutServerLoad = async () => { 5 | const dashboard = await showDashboard(); 6 | return { title: dashboard ? 'new dashboard' : 'old dashboard' }; 7 | }; 8 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/.well-known/vercel/flags/route.ts: -------------------------------------------------------------------------------- 1 | import { createFlagsDiscoveryEndpoint, getProviderData } from 'flags/next'; 2 | import * as flags from '../../../../flags'; 3 | 4 | export const GET = createFlagsDiscoveryEndpoint(() => { 5 | const providerData = getProviderData(flags); 6 | return providerData; 7 | }) as any; 8 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/adapters/page.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { customAdapterFlag } from './flags'; 3 | 4 | export default async function Page() { 5 | const customAdapter = await customAdapterFlag(); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/hooks.ts: -------------------------------------------------------------------------------- 1 | export async function reroute({ url, fetch }) { 2 | if (url.pathname === '/precomputed') { 3 | const destination = new URL('/api/reroute', url); 4 | destination.searchParams.set('pathname', url.pathname); 5 | 6 | return fetch(destination).then((response) => response.text()); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/snippets/lib/pages-router-precomputed/flags.ts: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'pages-router-precomputed-example-flag', 5 | decide() { 6 | return true; 7 | }, 8 | options: [false, true], 9 | }); 10 | 11 | export const exampleFlags = [exampleFlag]; 12 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/identify/basic/page.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { basicIdentifyExampleFlag } from './flags'; 3 | 4 | export default async function Page() { 5 | const basic = await basicIdentifyExampleFlag(); 6 | 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/suspense-fallbacks/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const hasAuthCookieFlag = flag({ 4 | key: 'has-auth-cookie', 5 | decide({ cookies }) { 6 | return cookies.has('suspense-fallbacks-user-id'); 7 | }, 8 | }); 9 | 10 | export const coreFlags = [hasAuthCookieFlag]; 11 | -------------------------------------------------------------------------------- /examples/sveltekit-example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import tailwindcss from '@tailwindcss/vite'; 3 | import { vercelToolbar } from '@vercel/toolbar/plugins/vite'; 4 | import { defineConfig } from 'vite'; 5 | 6 | export default defineConfig({ 7 | plugins: [sveltekit(), vercelToolbar(), tailwindcss()], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/adapter-reflag/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/reflag 2 | 3 | ## 1.0.1 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - 1d662a7: Update to @bucketco/node-sdk@1.8.3 14 | 15 | ## 0.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 79b25f6: Introduce Bucket adapter 20 | -------------------------------------------------------------------------------- /packages/adapter-vercel/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: true, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-posthog/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-reflag/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-edge-config/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-openfeature/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/adapter-edge-config/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/edge-config 2 | 3 | ## 0.1.2 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.1.1 10 | 11 | ### Patch Changes 12 | 13 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 14 | 15 | ## 0.1.0 16 | 17 | ### Minor Changes 18 | 19 | - 48cbe45: initial release 20 | -------------------------------------------------------------------------------- /packages/adapter-split/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/openfeature.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: true, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-growthbook/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-statsig/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/adapter-vercel/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig(({ mode }) => ({ 5 | test: { 6 | environment: 'node', 7 | server: { 8 | deps: { 9 | inline: ['@vercel/flags'], 10 | }, 11 | }, 12 | env: loadEnv(mode, process.cwd(), ''), 13 | }, 14 | })); 15 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | async redirects() { 6 | return [ 7 | { 8 | source: '/', 9 | destination: 'https://flags-sdk.dev', 10 | permanent: false, 11 | }, 12 | ]; 13 | }, 14 | }; 15 | 16 | export default nextConfig; 17 | -------------------------------------------------------------------------------- /examples/snippets/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 4 | import type * as React from 'react'; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /packages/adapter-launchdarkly/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/provider/index.ts'], 5 | format: ['esm', 'cjs'], 6 | splitting: true, 7 | sourcemap: true, 8 | minify: false, 9 | clean: false, 10 | skipNodeModulesBundle: true, 11 | dts: true, 12 | external: ['node_modules'], 13 | }); 14 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig(({ mode }) => ({ 5 | test: { 6 | environment: 'node', 7 | server: { 8 | deps: { 9 | inline: ['@vercel/flags'], 10 | }, 11 | }, 12 | env: loadEnv(mode, process.cwd(), ''), 13 | }, 14 | })); 15 | -------------------------------------------------------------------------------- /tests/next-15/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-16/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/snippets/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-15/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-16/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/snippets/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/shirt-shop/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | plugins: [ 9 | require('@tailwindcss/forms'), 10 | require('@tailwindcss/typography'), 11 | require('@tailwindcss/aspect-ratio'), 12 | ], 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /examples/shirt-shop/lib/identify.ts: -------------------------------------------------------------------------------- 1 | import type { Identify } from 'flags'; 2 | import { dedupe } from 'flags/next'; 3 | import { getStableId } from './get-stable-id'; 4 | 5 | export type EvaluationContext = { 6 | stableId?: string; 7 | }; 8 | 9 | export const identify = dedupe(async () => { 10 | const stableId = await getStableId(); 11 | 12 | return { stableId: stableId.value }; 13 | }) satisfies Identify; 14 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/adapters/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | import { createEdgeConfigAdapter } from './edge-config-adapter'; 3 | 4 | const edgeConfigAdapter = createEdgeConfigAdapter(process.env.EDGE_CONFIG!); 5 | 6 | export const customAdapterFlag = flag({ 7 | key: 'custom-adapter-flag', 8 | description: 'Shows how to use a custom flags adapter', 9 | adapter: edgeConfigAdapter(), 10 | }); 11 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/shopping-cart/shopping-cart-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AnimatePresence } from 'motion/react'; 4 | 5 | export function ShoppingCartList({ children }: { children: React.ReactNode }) { 6 | return ( 7 |
    8 | 9 | {children} 10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/adapter-flagsmith/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | // eslint-disable-next-line import/no-default-export -- [@vercel/style-guide@5 migration] 4 | export default defineConfig({ 5 | entry: ['src/index.ts'], 6 | format: 'esm', 7 | splitting: true, 8 | sourcemap: true, 9 | minify: false, 10 | clean: false, 11 | skipNodeModulesBundle: true, 12 | dts: true, 13 | external: ['node_modules'], 14 | }); 15 | -------------------------------------------------------------------------------- /tests/next-15/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookieFlag, exampleFlag, hostFlag } from '../flags'; 2 | 3 | export default async function Home() { 4 | const example = await exampleFlag(); 5 | const host = await hostFlag(); 6 | const cookie = await cookieFlag(); 7 | return ( 8 |
9 |

Example App Router Flag Value: {example ? 'true' : 'false'}

10 |

Host: {host}

11 |

Cookie: {cookie}

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /tests/next-15/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /tests/next-16/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --background: #ffffff; 7 | --foreground: #171717; 8 | } 9 | 10 | @media (prefers-color-scheme: dark) { 11 | :root { 12 | --background: #0a0a0a; 13 | --foreground: #ededed; 14 | } 15 | } 16 | 17 | body { 18 | color: var(--foreground); 19 | background: var(--background); 20 | font-family: Arial, Helvetica, sans-serif; 21 | } 22 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/utils/images.ts: -------------------------------------------------------------------------------- 1 | import type { StaticImageData } from 'next/image'; 2 | import black from '@/public/images/product/shirt-black.png'; 3 | import blue from '@/public/images/product/shirt-blue.png'; 4 | import white from '@/public/images/product/shirt-white.png'; 5 | 6 | export const images = [black, white, blue]; 7 | 8 | export const colorToImage: Record = { 9 | Black: black, 10 | White: white, 11 | Blue: blue, 12 | }; 13 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/proxy.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from 'next/server'; 2 | import { basicProxyFlag } from './flags'; 3 | 4 | export async function featureFlagsInProxy(request: NextRequest) { 5 | const active = await basicProxyFlag(); 6 | const variant = active ? 'variant-on' : 'variant-off'; 7 | 8 | return NextResponse.rewrite( 9 | new URL(`/examples/feature-flags-in-proxy/${variant}`, request.url), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | adapter: adapter(), 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /examples/snippets/lib/pages-router-precomputed/proxy.ts: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/next'; 2 | import { type NextRequest, NextResponse } from 'next/server'; 3 | import { exampleFlags } from './flags'; 4 | 5 | export async function pagesRouterProxy(request: NextRequest) { 6 | // precompute the flags 7 | const code = await precompute(exampleFlags); 8 | 9 | return NextResponse.rewrite( 10 | new URL(`/examples/pages-router-precomputed/${code}`, request.url), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |

{data.post.title}

9 |
{@html data.post.content}
10 |
11 |
12 |
Dashboard Flag Value: {@html data.dashboard}
13 |
Host: {@html data.host}
14 |
Cookie: {@html data.cookie}
15 | -------------------------------------------------------------------------------- /examples/snippets/app/getting-started/overview/reload-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { Button } from '@/components/ui/button'; 5 | 6 | export function ReloadButton() { 7 | const router = useRouter(); 8 | 9 | return ( 10 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/data-source/in-memory-data-source.ts: -------------------------------------------------------------------------------- 1 | import type { Packed } from '../types'; 2 | import type { DataSource } from './interface'; 3 | 4 | export class InMemoryDataSource implements DataSource { 5 | private data: Packed.Data; 6 | public projectId?: string; 7 | 8 | constructor(data: Packed.Data, projectId?: string) { 9 | this.data = data; 10 | this.projectId = projectId; 11 | } 12 | 13 | async getData() { 14 | return this.data; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/free-delivery.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { track } from '@vercel/analytics'; 4 | import { useEffect } from 'react'; 5 | import { FreeDeliveryBanner } from '@/components/banners/free-delivery-banner'; 6 | 7 | export function FreeDelivery(props: { show: boolean }) { 8 | useEffect(() => { 9 | if (props.show) track('free_delivery_banner:viewed'); 10 | }, [props.show]); 11 | 12 | if (!props.show) return null; 13 | 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /packages/adapter-posthog/src/provider/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { getAppHost } from '.'; 3 | 4 | describe('getAppHost', () => { 5 | it('maps us.i.posthog.com', () => { 6 | expect(getAppHost('https://us.i.posthog.com')).toBe( 7 | 'https://us.posthog.com', 8 | ); 9 | }); 10 | it('maps eu.i.posthog.com', () => { 11 | expect(getAppHost('https://eu.i.posthog.com')).toBe( 12 | 'https://eu.posthog.com', 13 | ); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/next-15/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: 'var(--background)', 13 | foreground: 'var(--foreground)', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /tests/next-16/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: 'var(--background)', 13 | foreground: 'var(--foreground)', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | export default config; 20 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | {@render children()} 15 |
16 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/product-detail-page/product-header.tsx: -------------------------------------------------------------------------------- 1 | import { ProductReviews } from './product-reviews'; 2 | 3 | export function ProductHeader() { 4 | return ( 5 |
6 |
7 |

Circles T-Shirt

8 |

20.00 USD

9 |
10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/suspense-fallbacks/proxy.tsx: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/next'; 2 | import { type NextRequest, NextResponse } from 'next/server'; 3 | import { coreFlags } from './flags'; 4 | 5 | export async function pprShellsProxy(request: NextRequest) { 6 | // precompute the flags 7 | const code = await precompute(coreFlags); 8 | 9 | // rewrite the page with the code 10 | return NextResponse.rewrite( 11 | new URL(`/examples/suspense-fallbacks/${code}`, request.url), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/handlers.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export function actAsFlaggedInUser() { 4 | document.cookie = 'basic-proxy-flag=1; Path=/'; 5 | window.location.reload(); 6 | } 7 | 8 | export function actAsFlaggedOutUser() { 9 | document.cookie = 'basic-proxy-flag=0; Path=/'; 10 | window.location.reload(); 11 | } 12 | 13 | export function clear() { 14 | document.cookie = 15 | 'basic-proxy-flag=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'; 16 | window.location.reload(); 17 | } 18 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/automatic/[code]/proxy.ts: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/next'; 2 | import { type NextRequest, NextResponse } from 'next/server'; 3 | import { marketingFlags } from './flags'; 4 | 5 | export async function automaticPrecomputeProxy(request: NextRequest) { 6 | // precompute the flags 7 | const code = await precompute(marketingFlags); 8 | 9 | // rewrite the page with the code 10 | return NextResponse.rewrite( 11 | new URL(`/concepts/precompute/automatic/${code}`, request.url), 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/manual/proxy.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from 'next/server'; 2 | import { manualPrecomputeFlag } from './flags'; 3 | 4 | export async function manualPrecomputeProxy(request: NextRequest) { 5 | // use the flag 6 | const value = await manualPrecomputeFlag(); 7 | 8 | // rewrite the page to the variant 9 | return NextResponse.rewrite( 10 | new URL( 11 | `/concepts/precompute/manual/${value ? 'variant-a' : 'variant-b'}`, 12 | request.url, 13 | ), 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/dedupe/page.tsx: -------------------------------------------------------------------------------- 1 | import { dedupe } from 'flags/next'; 2 | 3 | const dedupeExample = dedupe(() => { 4 | return Math.random().toString().substring(0, 8); 5 | }); 6 | 7 | export default async function Page() { 8 | const random1 = await dedupeExample(); 9 | const random2 = await dedupeExample(); 10 | const random3 = await dedupeExample(); 11 | 12 | return ( 13 |
14 | {random1} {random2} {random3} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/marketing-pages/regenerate-id-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | 5 | export function RegenerateIdButton() { 6 | return ( 7 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "shirt-shop", 12 | "svelte-example", 13 | "next-14", 14 | "next-15", 15 | "next-16", 16 | "sveltekit-e2e", 17 | "snippets" 18 | ], 19 | "snapshot": { 20 | "useCalculatedVersion": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/automatic/[code]/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const firstPrecomputedFlag = flag({ 4 | key: 'first-precomputed-flag', 5 | decide: () => Math.random() > 0.5, 6 | }); 7 | 8 | export const secondPrecomputedFlag = flag({ 9 | key: 'second-precomputed-flag', 10 | decide: () => Date.now() % 2 === 0, 11 | }); 12 | 13 | // a group of feature flags to be precomputed 14 | export const marketingFlags = [ 15 | firstPrecomputedFlag, 16 | secondPrecomputedFlag, 17 | ] as const; 18 | -------------------------------------------------------------------------------- /examples/snippets/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": false, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { cookieFlag, hostFlag, showDashboard } from '$lib/flags'; 2 | import type { PageServerLoad } from './$types'; 3 | 4 | export const load: PageServerLoad = async () => { 5 | const dashboard = await showDashboard(); 6 | const host = await hostFlag(); 7 | const cookie = await cookieFlag(); 8 | 9 | return { 10 | post: { 11 | title: dashboard ? 'New Dashboard' : `Old Dashboard`, 12 | content: `Content for page goes here`, 13 | }, 14 | dashboard, 15 | host, 16 | cookie, 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/shirt-shop/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /packages/flags/src/sveltekit/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest'; 2 | import { flag, getProviderData } from '.'; 3 | 4 | vi.mock('$env/dynamic/private', () => ({ FLAGS_SECRET: 'secret' })); 5 | 6 | describe('getProviderData', () => { 7 | it('is a function', () => { 8 | expect(typeof getProviderData).toBe('function'); 9 | }); 10 | }); 11 | 12 | describe('flag', () => { 13 | it('defines a key', async () => { 14 | const f = flag({ key: 'first-flag', decide: () => false }); 15 | expect(f).toHaveProperty('key', 'first-flag'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/adapter-growthbook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/growthbook 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - 0da9c49: Auto-refresh flag definitions with a stale-while-revalidate strategy 8 | 9 | ## 0.1.3 10 | 11 | ### Patch Changes 12 | 13 | - 5f3757a: drop tsconfig dependency 14 | 15 | ## 0.1.2 16 | 17 | ### Patch Changes 18 | 19 | - d43589a: support strings in Edge Config 20 | 21 | ## 0.1.1 22 | 23 | ### Patch Changes 24 | 25 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 26 | 27 | ## 0.1.0 28 | 29 | ### Minor Changes 30 | 31 | - 6239e2d: initialize 32 | -------------------------------------------------------------------------------- /packages/flags/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | const defaultConfig = { 4 | format: ['esm', 'cjs'], 5 | splitting: true, 6 | sourcemap: true, 7 | minify: false, 8 | clean: true, 9 | skipNodeModulesBundle: true, 10 | dts: true, 11 | external: [/^node:.*/, 'node_modules'], 12 | }; 13 | 14 | export default defineConfig({ 15 | entry: { 16 | index: 'src/index.ts', 17 | next: 'src/next/index.ts', 18 | sveltekit: 'src/sveltekit/index.ts', 19 | react: 'src/react/index.tsx', 20 | analytics: 'src/analytics.ts', 21 | }, 22 | ...defaultConfig, 23 | }); 24 | -------------------------------------------------------------------------------- /packages/flags/src/next/overrides.ts: -------------------------------------------------------------------------------- 1 | import { decryptOverrides } from '..'; 2 | import { memoizeOne } from '../lib/async-memoize-one'; 3 | 4 | const memoizedDecrypt = memoizeOne( 5 | (text: string) => decryptOverrides(text), 6 | (a, b) => a[0] === b[0], // only the first argument gets compared 7 | { cachePromiseRejection: true }, 8 | ); 9 | 10 | export async function getOverrides(cookie: string | undefined) { 11 | if (typeof cookie === 'string' && cookie !== '') { 12 | const cookieOverrides = await memoizedDecrypt(cookie); 13 | return cookieOverrides ?? null; 14 | } 15 | 16 | return null; 17 | } 18 | -------------------------------------------------------------------------------- /tests/next-16/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { cookieFlag, exampleFlag, hostFlag } from '../flags'; 3 | 4 | async function Home() { 5 | const example = await exampleFlag(); 6 | const host = await hostFlag(); 7 | const cookie = await cookieFlag(); 8 | return ( 9 |
10 |

Example App Router Flag Value: {example ? 'true' : 'false'}

11 |

Host: {host}

12 |

Cookie: {cookie}

13 |
14 | ); 15 | } 16 | 17 | export default function Page() { 18 | return ( 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/api/reroute/+server.ts: -------------------------------------------------------------------------------- 1 | import { text } from '@sveltejs/kit'; 2 | import { computeInternalRoute, createVisitorId } from '$lib/precomputed-flags'; 3 | 4 | export async function GET({ url, request, cookies }) { 5 | let visitorId = cookies.get('visitorId'); 6 | 7 | if (!visitorId) { 8 | visitorId = createVisitorId(); 9 | cookies.set('visitorId', visitorId, { path: '/' }); 10 | request.headers.set('x-visitorId', visitorId); // cookie is not available on the initial request 11 | } 12 | 13 | return text( 14 | await computeInternalRoute(url.searchParams.get('pathname')!, request), 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/precomputed/[code]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { generatePermutations } from 'flags/sveltekit'; 2 | import { precomputedFlag } from '$lib/flags'; 3 | import { precomputedFlags } from '$lib/precomputed-flags'; 4 | import type { PageServerLoad } from './$types'; 5 | 6 | export const prerender = true; 7 | 8 | export async function entries() { 9 | return (await generatePermutations(precomputedFlags)).map((code) => ({ 10 | code, 11 | })); 12 | } 13 | 14 | export const load: PageServerLoad = async ({ params }) => { 15 | return { 16 | flag: await precomputedFlag(params.code, precomputedFlags), 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /examples/snippets/components/demo-flag.tsx: -------------------------------------------------------------------------------- 1 | export function DemoFlag({ value, name }: { value: boolean; name: string }) { 2 | return ( 3 |
6 |

9 | The feature flag {name} evaluated 10 | to {JSON.stringify(value)}. 11 |

12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/flags/src/lib/normalize-options.ts: -------------------------------------------------------------------------------- 1 | import type { FlagOption, GenerousOption } from '../types'; 2 | 3 | export function normalizeOptions( 4 | flagOptions: GenerousOption[] | undefined, 5 | ): FlagOption[] | undefined { 6 | if (!Array.isArray(flagOptions)) return flagOptions; 7 | 8 | return flagOptions.map((option) => { 9 | if (typeof option === 'boolean') return { value: option }; 10 | if (typeof option === 'number') return { value: option }; 11 | if (typeof option === 'string') return { value: option }; 12 | if (option === null) return { value: option }; 13 | 14 | return option; 15 | }) as FlagOption[]; 16 | } 17 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 17 |
18 | 19 |
20 | {data.title} 21 | 22 | {@render children?.()} 23 |
24 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/hypertune 2 | 3 | ## 0.3.2 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.3.1 10 | 11 | ### Patch Changes 12 | 13 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 14 | 15 | ## 0.3.0 16 | 17 | ### Minor Changes 18 | 19 | - a7b9be8: Nested flags are now supported, and a `key` is now passed to `createSource` to prevent conflicts with your `getHypertune` setup. 20 | 21 | ## 0.2.0 22 | 23 | ### Minor Changes 24 | 25 | - 9ba13cc: Introducing the Hypertune Adapter for Flags SDK 26 | 27 | ## 0.1.0 28 | 29 | ### Minor Changes 30 | 31 | - 0cf8dd4: Initialize 32 | -------------------------------------------------------------------------------- /tests/next-15/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | !.env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /tests/next-16/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | !.env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shirt-shop-api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --turbopack --port 3030", 8 | "check": "biome check", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@upstash/redis": "1.34.4", 13 | "next": "16.0.10", 14 | "react": "^19.0.0", 15 | "react-dom": "^19.0.0" 16 | }, 17 | "devDependencies": { 18 | "@tailwindcss/postcss": "^4", 19 | "@types/node": "^20", 20 | "@types/react": "^19", 21 | "@types/react-dom": "^19", 22 | "tailwindcss": "^4", 23 | "typescript": "^5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/sveltekit-example/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // You can switch out the Vercel adapter with another one, though keep in mind that the precompute 12 | // approach needs another approach then, as middleware.ts is Vercel concept. 13 | adapter: adapter(), 14 | }, 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /tests/next-15/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'example-flag', 5 | decide: () => true, 6 | defaultValue: false, 7 | options: [false, true], 8 | }); 9 | 10 | export const hostFlag = flag({ 11 | key: 'host', 12 | decide: ({ headers }) => headers.get('host') || 'no host', 13 | options: ['no host', 'localhost'], 14 | }); 15 | 16 | export const cookieFlag = flag({ 17 | key: 'cookie', 18 | decide: ({ cookies }) => cookies.get('example-cookie')?.value || 'no cookie', 19 | options: ['no cookie'], 20 | }); 21 | 22 | export const precomputedFlags = [exampleFlag, hostFlag, cookieFlag]; 23 | -------------------------------------------------------------------------------- /tests/next-16/flags.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | 3 | export const exampleFlag = flag({ 4 | key: 'example-flag', 5 | decide: () => true, 6 | defaultValue: false, 7 | options: [false, true], 8 | }); 9 | 10 | export const hostFlag = flag({ 11 | key: 'host', 12 | decide: ({ headers }) => headers.get('host') || 'no host', 13 | options: ['no host', 'localhost'], 14 | }); 15 | 16 | export const cookieFlag = flag({ 17 | key: 'cookie', 18 | decide: ({ cookies }) => cookies.get('example-cookie')?.value || 'no cookie', 19 | options: ['no cookie'], 20 | }); 21 | 22 | export const precomputedFlags = [exampleFlag, hostFlag, cookieFlag]; 23 | -------------------------------------------------------------------------------- /examples/snippets/app/getting-started/overview/page.tsx: -------------------------------------------------------------------------------- 1 | import { flag } from 'flags/next'; 2 | import { DemoFlag } from '@/components/demo-flag'; 3 | import { ReloadButton } from './reload-button'; 4 | 5 | // declare a feature flag 6 | const randomFlag = flag({ 7 | key: 'random-flag', 8 | decide() { 9 | // this flag will be on for 50% of visitors 10 | return Math.random() > 0.5; 11 | }, 12 | }); 13 | 14 | export default async function Page() { 15 | // use the feature flag 16 | const overview = await randomFlag(); 17 | 18 | return ( 19 | <> 20 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/snippets/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | .env*.local 42 | 43 | certificates -------------------------------------------------------------------------------- /apps/shirt-shop-api/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | .env*.local 43 | -------------------------------------------------------------------------------- /examples/snippets/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener('change', onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener('change', onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/lib/precomputed-flags.ts: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/sveltekit'; 2 | import { precomputedFlag } from './flags'; 3 | 4 | export const precomputedFlags = [precomputedFlag]; 5 | 6 | /** 7 | * Given a user-visible pathname, precompute the internal route using the flags used on that page 8 | * 9 | * e.g. /precomputed -> /precomputed/asd-qwe-123 10 | */ 11 | export async function computeInternalRoute(pathname: string, request: Request) { 12 | if (pathname === '/precomputed') { 13 | return `/precomputed/${await precompute(precomputedFlags, request)}`; 14 | } 15 | 16 | return pathname; 17 | } 18 | 19 | export function createVisitorId() { 20 | return 'visitorId'; 21 | } 22 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/lib/report-value.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json'; 2 | import type { OutcomeType, ResolutionReason } from '../types'; 3 | 4 | /** 5 | * Only used interally for now. 6 | */ 7 | export function internalReportValue( 8 | key: string, 9 | value: unknown, 10 | data: { 11 | originProjectId?: string; 12 | originProvider?: 'vercel'; 13 | outcomeType?: OutcomeType; 14 | reason?: ResolutionReason | 'override'; 15 | }, 16 | ) { 17 | const symbol = Symbol.for('@vercel/request-context'); 18 | const ctx = Reflect.get(globalThis, symbol)?.get(); 19 | ctx?.flags?.reportValue(key, value, { 20 | sdkVersion: version, 21 | ...data, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/[code]/cart/proceed-to-checkout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { track } from '@vercel/analytics'; 4 | import { toast } from 'sonner'; 5 | import { ProceedToCheckoutButton } from '@/components/shopping-cart/proceed-to-checkout-button'; 6 | 7 | export function ProceedToCheckout({ color }: { color: string }) { 8 | return ( 9 | { 12 | track('proceed_to_checkout:clicked'); 13 | toast('End reached', { 14 | className: 'my-classname', 15 | description: 'The checkout flow is not implemented in this template.', 16 | duration: 5000, 17 | }); 18 | }} 19 | /> 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /examples/shirt-shop/lib/get-cart-id.ts: -------------------------------------------------------------------------------- 1 | import { dedupe } from 'flags/next'; 2 | import { nanoid } from 'nanoid'; 3 | import { cookies, headers } from 'next/headers'; 4 | 5 | /** 6 | * Reads the cart id from the cookie or returns a new cart id 7 | */ 8 | export const getCartId = dedupe(async () => { 9 | const cookiesStore = await cookies(); 10 | const header = await headers(); 11 | 12 | const generatedCartId = header.get('x-generated-cart-id'); 13 | 14 | if (generatedCartId) { 15 | return { value: generatedCartId, isFresh: false }; 16 | } 17 | 18 | const cartId = cookiesStore.get('cart-id')?.value; 19 | if (!cartId) return { value: nanoid(), isFresh: true }; 20 | return { value: cartId, isFresh: false }; 21 | }); 22 | -------------------------------------------------------------------------------- /tests/next-15/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /tests/next-15/app/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/`); 6 | await expect( 7 | page.getByText('Example App Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | }); 15 | 16 | test('can read cookies', async ({ page }) => { 17 | await page.goto(`http://localhost:${port}/`); 18 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/next-16/app/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/`); 6 | await expect( 7 | page.getByText('Example App Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | }); 15 | 16 | test('can read cookies', async ({ page }) => { 17 | await page.goto(`http://localhost:${port}/`); 18 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/next-16/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-16", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --port 4016 --turbopack", 8 | "check": "biome check", 9 | "start": "next start --port 4016", 10 | "test:e2e": "playwright test" 11 | }, 12 | "dependencies": { 13 | "flags": "workspace:*", 14 | "next": "16.0.10", 15 | "react": "19.2.0", 16 | "react-dom": "19.2.0" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "1.51.1", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "postcss": "^8", 24 | "tailwindcss": "^3.4.1", 25 | "typescript": "^5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/shirt-shop/lib/get-stable-id.ts: -------------------------------------------------------------------------------- 1 | import { dedupe } from 'flags/next'; 2 | import { nanoid } from 'nanoid'; 3 | import { cookies, headers } from 'next/headers'; 4 | 5 | /** 6 | * Reads the stable id from the cookie or returns a new stable id 7 | */ 8 | export const getStableId = dedupe(async () => { 9 | const cookiesStore = await cookies(); 10 | const header = await headers(); 11 | 12 | const generatedStableId = header.get('x-generated-stable-id'); 13 | 14 | if (generatedStableId) { 15 | return { value: generatedStableId, isFresh: false }; 16 | } 17 | 18 | const stableId = cookiesStore.get('stable-id')?.value; 19 | if (!stableId) return { value: nanoid(), isFresh: true }; 20 | return { value: stableId, isFresh: false }; 21 | }); 22 | -------------------------------------------------------------------------------- /examples/snippets/pages/examples/pages-router-dynamic/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 2 | import { DemoFlag } from '@/components/demo-flag'; 3 | import PagesLayout from '@/components/pages-layout'; 4 | import { exampleFlag } from '@/flags'; 5 | 6 | export const getServerSideProps = (async ({ req }) => { 7 | const example = await exampleFlag(req); 8 | return { props: { example } }; 9 | }) satisfies GetServerSideProps<{ example: boolean }>; 10 | 11 | export default function PageRouter({ 12 | example, 13 | }: InferGetServerSidePropsType) { 14 | return ( 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /tests/next-15/app/app-router-precomputed/[code]/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookieFlag, exampleFlag, hostFlag, precomputedFlags } from '@/flags'; 2 | 3 | export const generateStaticParams = () => { 4 | return []; 5 | }; 6 | 7 | export default async function Page({ 8 | params, 9 | }: { 10 | params: Promise<{ code: string }>; 11 | }) { 12 | const { code } = await params; 13 | const example = await exampleFlag(code, precomputedFlags); 14 | const host = await hostFlag(code, precomputedFlags); 15 | const cookie = await cookieFlag(code, precomputedFlags); 16 | return ( 17 |
18 |

Example App Router Flag Value: {example ? 'true' : 'false'}

19 |

Host: {host}

20 |

Cookie: {cookie}

21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/data-source/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Packed } from '../types'; 2 | 3 | /** 4 | * DataSource interface for the Vercel Flags client 5 | */ 6 | export interface DataSource { 7 | /** 8 | * The datafile 9 | */ 10 | getData(): Promise; 11 | /** 12 | * The project for which these flags were loaded for 13 | */ 14 | projectId?: string; 15 | /** 16 | * Initialize the data source by fetching the initial file or setting up polling or 17 | * subscriptions. 18 | * 19 | * @see https://openfeature.dev/specification/sections/providers#requirement-241 20 | */ 21 | initialize?: () => Promise; 22 | 23 | /** 24 | * End polling or subscriptions. 25 | */ 26 | shutdown?(): void; 27 | } 28 | -------------------------------------------------------------------------------- /tests/next-15/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-15", 3 | "version": "0.1.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --port 4015 --turbopack", 8 | "check": "biome check", 9 | "start": "next start --port 4015", 10 | "test:e2e": "playwright test" 11 | }, 12 | "dependencies": { 13 | "flags": "workspace:*", 14 | "next": "15.5.9", 15 | "react": "19.0.0-rc-02c0e824-20241028", 16 | "react-dom": "19.0.0-rc-02c0e824-20241028" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "1.51.1", 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "postcss": "^8", 24 | "tailwindcss": "^3.4.1", 25 | "typescript": "^5" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /examples/sveltekit-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "noUncheckedIndexedAccess": true, 16 | "preserveWatchOutput": true, 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "moduleResolution": "node", 20 | "module": "ES2020", 21 | "types": ["@types/node"], 22 | "jsx": "react" 23 | }, 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /tests/next-15/pages-specs/pages-router.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/pages-router`); 6 | await expect( 7 | page.getByText('Example Pages Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/pages-router`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | }); 15 | 16 | test('can read cookies', async ({ page }) => { 17 | await page.goto(`http://localhost:${port}/pages-router`); 18 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/next-16/pages-specs/pages-router.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/pages-router`); 6 | await expect( 7 | page.getByText('Example Pages Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/pages-router`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | }); 15 | 16 | test('can read cookies', async ({ page }) => { 17 | await page.goto(`http://localhost:${port}/pages-router`); 18 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | // Folder-specific settings 2 | // 3 | // For a full list of overridable settings, and general information on folder-specific settings, 4 | // see the documentation: https://zed.dev/docs/configuring-zed#settings-files 5 | { 6 | "languages": { 7 | "JavaScript": { "formatter": { "language_server": { "name": "biome" } } }, 8 | "TypeScript": { "formatter": { "language_server": { "name": "biome" } } }, 9 | "TSX": { "formatter": { "language_server": { "name": "biome" } } }, 10 | "JSON": { "formatter": { "language_server": { "name": "biome" } } }, 11 | "JSONC": { "formatter": { "language_server": { "name": "biome" } } }, 12 | "CSS": { "formatter": { "language_server": { "name": "biome" } } } 13 | }, 14 | "auto_install_extensions": { 15 | "biome": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/[code]/cart/order-summary.tsx: -------------------------------------------------------------------------------- 1 | import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'; 2 | import { proceedToCheckoutColorFlag } from '@/flags'; 3 | import { ProceedToCheckout } from './proceed-to-checkout'; 4 | 5 | export async function OrderSummary({ 6 | showSummerBanner, 7 | freeDelivery, 8 | }: { 9 | showSummerBanner: boolean; 10 | freeDelivery: boolean; 11 | }) { 12 | // This is a fast feature flag so we don't suspend on it 13 | const proceedToCheckoutColor = await proceedToCheckoutColorFlag(); 14 | 15 | return ( 16 | } 20 | /> 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/identify/full/flags.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlyRequestCookies } from 'flags'; 2 | import { dedupe, flag } from 'flags/next'; 3 | 4 | interface Entities { 5 | user?: { id: string }; 6 | } 7 | 8 | const identify = dedupe( 9 | ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => { 10 | const userId = cookies.get('identify-example-user-id')?.value; 11 | return { user: userId ? { id: userId } : undefined }; 12 | }, 13 | ); 14 | 15 | export const fullIdentifyExampleFlag = flag({ 16 | key: 'full-identify-example-flag', 17 | identify, 18 | description: 'Full identify example', 19 | decide({ entities }) { 20 | console.log(entities); 21 | if (!entities?.user) return false; 22 | return entities.user.id === 'user1'; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/lib/precomputed-flags.ts: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/sveltekit'; 2 | import { firstMarketingABTest, secondMarketingABTest } from './flags'; 3 | 4 | export const marketingFlags = [firstMarketingABTest, secondMarketingABTest]; 5 | 6 | /** 7 | * Given a user-visible pathname, precompute the internal route using the flags used on that page 8 | * 9 | * e.g. /marketing -> /marketing/asd-qwe-123 10 | */ 11 | export async function computeInternalRoute(pathname: string, request: Request) { 12 | if (pathname === '/examples/marketing-pages') { 13 | return `/examples/marketing-pages/${await precompute(marketingFlags, request)}`; 14 | } 15 | 16 | return pathname; 17 | } 18 | 19 | export function createVisitorId() { 20 | return crypto.randomUUID().replace(/-/g, ''); 21 | } 22 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/identify/basic/flags.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlyRequestCookies } from 'flags'; 2 | import { dedupe, flag } from 'flags/next'; 3 | 4 | interface Entities { 5 | user?: { id: string }; 6 | } 7 | 8 | const identify = dedupe( 9 | ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => { 10 | const userId = cookies.get('identify-example-user-id')?.value; 11 | return { user: userId ? { id: userId } : undefined }; 12 | }, 13 | ); 14 | 15 | export const basicIdentifyExampleFlag = flag({ 16 | key: 'basic-identify-example-flag', 17 | identify, 18 | description: 'Basic identify example', 19 | decide({ entities }) { 20 | console.log(entities); 21 | if (!entities?.user) return false; 22 | return entities.user.id === 'user1'; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /examples/snippets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /apps/shirt-shop-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /examples/shirt-shop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "react-jsx", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | }, 23 | "target": "ES2017" 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts" 31 | ], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/api/reroute-manual/+server.ts: -------------------------------------------------------------------------------- 1 | import { text } from '@sveltejs/kit'; 2 | import { marketingABTestManualApproach } from '$lib/flags.js'; 3 | 4 | export async function GET({ request, cookies }) { 5 | let flag = cookies.get('marketingManual'); 6 | 7 | if (!flag) { 8 | flag = String(Math.random() < 0.5); 9 | cookies.set('marketingManual', flag, { 10 | path: '/', 11 | httpOnly: false, // So that we can reset the visitor Id on the client in the examples 12 | }); 13 | request.headers.set('x-marketingManual', flag); // cookie is not available on the initial request 14 | } 15 | 16 | return text( 17 | (await marketingABTestManualApproach()) 18 | ? '/examples/marketing-pages-variant-a' 19 | : '/examples/marketing-pages-variant-b', 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svelte-example 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [fb50709] 8 | - Updated dependencies [87503b6] 9 | - @vercel/flags@2.7.0 10 | 11 | ## 0.0.5 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [69f8df0] 16 | - Updated dependencies [65d3d73] 17 | - @vercel/flags@2.6.3 18 | 19 | ## 0.0.4 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [80e0737] 24 | - @vercel/flags@2.6.2 25 | 26 | ## 0.0.3 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [9c379a8] 31 | - @vercel/flags@2.6.1 32 | 33 | ## 0.0.2 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies [7b9aa17] 38 | - Updated dependencies [b049c26] 39 | - Updated dependencies [c12f4d4] 40 | - Updated dependencies [1be4799] 41 | - @vercel/flags@2.6.0 42 | -------------------------------------------------------------------------------- /examples/sveltekit-example/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # svelte-example 2 | 3 | ## 0.0.6 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [fb50709] 8 | - Updated dependencies [87503b6] 9 | - @vercel/flags@2.7.0 10 | 11 | ## 0.0.5 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies [69f8df0] 16 | - Updated dependencies [65d3d73] 17 | - @vercel/flags@2.6.3 18 | 19 | ## 0.0.4 20 | 21 | ### Patch Changes 22 | 23 | - Updated dependencies [80e0737] 24 | - @vercel/flags@2.6.2 25 | 26 | ## 0.0.3 27 | 28 | ### Patch Changes 29 | 30 | - Updated dependencies [9c379a8] 31 | - @vercel/flags@2.6.1 32 | 33 | ## 0.0.2 34 | 35 | ### Patch Changes 36 | 37 | - Updated dependencies [7b9aa17] 38 | - Updated dependencies [b049c26] 39 | - Updated dependencies [c12f4d4] 40 | - Updated dependencies [1be4799] 41 | - @vercel/flags@2.6.0 42 | -------------------------------------------------------------------------------- /tests/next-16/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts", 31 | "**/*.mts" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/shopping-cart/proceed-to-checkout-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | const colorMap: Record = { 4 | blue: 'bg-blue-600 hover:bg-blue-700', 5 | red: 'bg-red-600 hover:bg-red-700', 6 | green: 'bg-green-600 hover:bg-green-700', 7 | }; 8 | 9 | export function ProceedToCheckoutButton({ 10 | color, 11 | onClick, 12 | }: { 13 | color: string; 14 | onClick: () => void; 15 | }) { 16 | return ( 17 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/summer-sale.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { track } from '@vercel/analytics'; 4 | import { useEffect } from 'react'; 5 | import { toast } from 'sonner'; 6 | import { SummerSaleBanner } from '@/components/banners/summer-sale-banner'; 7 | 8 | export function SummerSale(props: { show: boolean }) { 9 | useEffect(() => { 10 | if (props.show) track('summer_banner:viewed'); 11 | }, [props.show]); 12 | 13 | if (!props.show) return null; 14 | 15 | return ( 16 | { 18 | track('summer_banner:clicked'); 19 | toast('End reached', { 20 | className: 'my-classname', 21 | description: 22 | 'The summer sale is not implemented in this template. Try adding to the cart instead.', 23 | }); 24 | }} 25 | /> 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/adapter-statsig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/statsig 2 | 3 | ## 0.2.5 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.2.4 10 | 11 | ### Patch Changes 12 | 13 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 14 | 15 | ## 0.2.3 16 | 17 | ### Patch Changes 18 | 19 | - 48adbf2: bump statsig-node-lite to v0.5.2 20 | 21 | ## 0.2.2 22 | 23 | ### Patch Changes 24 | 25 | - 1e3c8df: If using an Edge Config adapter, reduce minimum sync delay for config specs from 5000ms->1000ms 26 | 27 | ## 0.2.1 28 | 29 | ### Patch Changes 30 | 31 | - 9a687cb: accept consoleApiKey from getProviderData 32 | 33 | ## 0.2.0 34 | 35 | ### Minor Changes 36 | 37 | - bd7e10a: Initial support for Feature Gates and Dynamic Configs 38 | 39 | ## 0.1.0 40 | 41 | ### Minor Changes 42 | 43 | - 3c66284: initialize 44 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from '@vercel/analytics/next'; 2 | import { VercelToolbar } from '@vercel/toolbar/next'; 3 | import type { Metadata } from 'next'; 4 | import { Toaster } from 'sonner'; 5 | 6 | import './globals.css'; 7 | import { ExamplesBanner } from '@/components/banners/examples-banner'; 8 | 9 | export const metadata: Metadata = { 10 | title: 'Flags SDK Example', 11 | description: 'A Flags SDK example for Ecommerce', 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | {children} 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/adapter-posthog/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/posthog 2 | 3 | ## 0.2.2 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.2.1 10 | 11 | ### Patch Changes 12 | 13 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 14 | 15 | ## 0.2.0 16 | 17 | ### Minor Changes 18 | 19 | - 5586443: List flags includes inactive ones to discover all flags 20 | 21 | ## 0.1.0 22 | 23 | ### Minor Changes 24 | 25 | - 44bada7: Publish PostHog adapter 26 | 27 | ## 0.1.0 28 | 29 | ### Minor Changes 30 | 31 | - postHogAdapter: Default adapter is available 32 | - postHogAdapter.isFeatureEnabled: Check if a feature flag is enabled 33 | - postHogAdapter.featureFlagValue: Get the value of a feature flag 34 | - postHogAdapter.featureFlagPayload: Get the payload of a feature flag 35 | - postHogAdapter.client: Access the PostHog client 36 | -------------------------------------------------------------------------------- /packages/adapter-posthog/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Adapter } from 'flags'; 2 | 3 | type JsonType = 4 | | string 5 | | number 6 | | boolean 7 | | null 8 | | { 9 | [key: string]: JsonType; 10 | } 11 | | Array; 12 | 13 | interface PostHogEntities { 14 | distinctId: string; 15 | } 16 | 17 | type PostHogAdapter = { 18 | isFeatureEnabled: (options?: { 19 | sendFeatureFlagEvents?: boolean; 20 | }) => Adapter; 21 | featureFlagValue: (options?: { 22 | sendFeatureFlagEvents?: boolean; 23 | }) => Adapter; 24 | featureFlagPayload: ( 25 | getValue: (payload: JsonType) => T, 26 | options?: { 27 | sendFeatureFlagEvents?: boolean; 28 | }, 29 | ) => Adapter; 30 | }; 31 | 32 | export type { Adapter, PostHogEntities, PostHogAdapter, JsonType }; 33 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function is used to check for exhaustiveness in switch statements. 3 | * 4 | * @param _ - The value to check. 5 | * 6 | * @example 7 | * Given `type Union = 'a' | 'b' | 'c'`, the following code will not compile: 8 | * ```ts 9 | * switch (union) { 10 | * case 'a': 11 | * return 'a'; 12 | * case 'b': 13 | * return 'b'; 14 | * default: 15 | * exhaustivenessCheck(union); // This will throw an error 16 | * } 17 | * ``` 18 | * This is because `value` has been narrowed to `'c'` by the `default` arm, 19 | * which is not assignable to `never`. If we covered the `'c'` case, the type 20 | * would narrow to `never`, which is assignable to `never` and would not cause an error. 21 | */ 22 | export function exhaustivenessCheck(_: never): never { 23 | throw new Error('Exhaustiveness check failed'); 24 | } 25 | -------------------------------------------------------------------------------- /tests/next-15/pages/pages-router.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 2 | import { cookieFlag, exampleFlag, hostFlag } from '../flags'; 3 | 4 | export const getServerSideProps = (async ({ req }) => { 5 | const example = await exampleFlag(req); 6 | const host = await hostFlag(req); 7 | const cookie = await cookieFlag(req); 8 | return { props: { example, host, cookie } }; 9 | }) satisfies GetServerSideProps<{ 10 | example: boolean; 11 | host: string; 12 | cookie: string; 13 | }>; 14 | 15 | export default function PagesRouter({ 16 | example, 17 | host, 18 | cookie, 19 | }: InferGetServerSidePropsType) { 20 | return ( 21 |
22 |

Example Pages Router Flag Value: {example ? 'true' : 'false'}

23 |

Host: {host}

24 |

Cookie: {cookie}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /tests/next-16/pages/pages-router.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 2 | import { cookieFlag, exampleFlag, hostFlag } from '../flags'; 3 | 4 | export const getServerSideProps = (async ({ req }) => { 5 | const example = await exampleFlag(req); 6 | const host = await hostFlag(req); 7 | const cookie = await cookieFlag(req); 8 | return { props: { example, host, cookie } }; 9 | }) satisfies GetServerSideProps<{ 10 | example: boolean; 11 | host: string; 12 | cookie: string; 13 | }>; 14 | 15 | export default function PagesRouter({ 16 | example, 17 | host, 18 | cookie, 19 | }: InferGetServerSidePropsType) { 20 | return ( 21 |
22 |

Example Pages Router Flag Value: {example ? 'true' : 'false'}

23 |

Host: {host}

24 |

Cookie: {cookie}

25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | env: 4 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 5 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: [main] 12 | 13 | jobs: 14 | test: 15 | name: "Test" 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup pnpm 25 | uses: pnpm/action-setup@v2 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | cache: "pnpm" 32 | 33 | - name: Install dependencies 34 | run: pnpm install 35 | 36 | - name: Run tests 37 | run: pnpm test 38 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/dashboard-pages/flags.ts: -------------------------------------------------------------------------------- 1 | import type { ReadonlyRequestCookies } from 'flags'; 2 | import { dedupe, flag } from 'flags/next'; 3 | 4 | interface Entities { 5 | user?: { id: string }; 6 | } 7 | 8 | const identify = dedupe( 9 | ({ cookies }: { cookies: ReadonlyRequestCookies }): Entities => { 10 | const userId = cookies.get('dashboard-user-id')?.value; 11 | return { user: userId ? { id: userId } : undefined }; 12 | }, 13 | ); 14 | 15 | export const dashboardFlag = flag({ 16 | key: 'dashboard-flag', 17 | identify, 18 | description: 'Flag used on the Dashboard Pages example', 19 | decide({ entities }) { 20 | if (!entities?.user) return false; 21 | // Allowed users could be loaded from Edge Config or elsewhere 22 | const allowedUsers = ['user1']; 23 | 24 | return allowedUsers.includes(entities.user.id); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/flags/src/lib/merge-provider-data.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderData } from '../types'; 2 | 3 | export async function mergeProviderData( 4 | itemsPromises: (Promise | ProviderData)[], 5 | ): Promise { 6 | const items = await Promise.all( 7 | itemsPromises.map((p) => Promise.resolve(p).catch(() => null)), 8 | ); 9 | 10 | return items 11 | .filter((item): item is ProviderData => Boolean(item)) 12 | .reduce( 13 | (acc, item) => { 14 | Object.entries(item.definitions).forEach(([key, definition]) => { 15 | if (!acc.definitions[key]) acc.definitions[key] = {}; 16 | Object.assign(acc.definitions[key], definition); 17 | }); 18 | 19 | if (Array.isArray(item.hints)) acc.hints.push(...item.hints); 20 | 21 | return acc; 22 | }, 23 | { definitions: {}, hints: [] }, 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /tests/next-15/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import localFont from 'next/font/local'; 3 | import './globals.css'; 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900', 9 | }); 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900', 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tests/next-16/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import localFont from 'next/font/local'; 3 | import './globals.css'; 4 | 5 | const geistSans = localFont({ 6 | src: './fonts/GeistVF.woff', 7 | variable: '--font-geist-sans', 8 | weight: '100 900', 9 | }); 10 | const geistMono = localFont({ 11 | src: './fonts/GeistMonoVF.woff', 12 | variable: '--font-geist-mono', 13 | weight: '100 900', 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: 'Create Next App', 18 | description: 'Generated by create next app', 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/`); 6 | await expect(page.getByText('Dashboard Flag Value: true')).toBeVisible(); 7 | }); 8 | 9 | test('can read request headers', async ({ page }) => { 10 | await page.goto(`http://localhost:${port}/`); 11 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 12 | }); 13 | 14 | test('can read cookies', async ({ page }) => { 15 | await page.context().addCookies([ 16 | { 17 | name: 'example-cookie', 18 | value: 'example-cookie-value', 19 | url: `http://localhost:${port}/`, 20 | }, 21 | ]); 22 | await page.goto(`http://localhost:${port}/`); 23 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/snippets/app/concepts/precompute/automatic/[code]/page.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { 3 | firstPrecomputedFlag, 4 | marketingFlags, 5 | secondPrecomputedFlag, 6 | } from './flags'; 7 | 8 | type Params = Promise<{ code: string }>; 9 | 10 | export default async function Page({ params }: { params: Params }) { 11 | const { code } = await params; 12 | // access the precomputed result by passing params.code and the group of 13 | // flags used during precomputation of this route segment 14 | const firstPrecomputed = await firstPrecomputedFlag(code, marketingFlags); 15 | const secondPrecomputed = await secondPrecomputedFlag(code, marketingFlags); 16 | 17 | return ( 18 |
19 | 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /examples/snippets/next.config.ts: -------------------------------------------------------------------------------- 1 | import { withVercelToolbar } from '@vercel/toolbar/plugins/next'; 2 | import type { NextConfig } from 'next'; 3 | 4 | const nextConfig: NextConfig = { 5 | images: { 6 | remotePatterns: [ 7 | { 8 | protocol: 'https' as const, 9 | hostname: 'assets.vercel.com', 10 | port: '', 11 | pathname: '/image/upload/**', 12 | }, 13 | ], 14 | }, 15 | async headers() { 16 | return [ 17 | { 18 | source: '/(.*)', 19 | headers: [ 20 | { 21 | key: 'Content-Security-Policy', 22 | value: 23 | "frame-ancestors 'self' https://flags-sdk.com https://www.flags-sdk.com https://flags-sdk.dev https://www.flags-sdk.dev http://localhost:* https://*.vercel.sh", 24 | }, 25 | ], 26 | }, 27 | ]; 28 | }, 29 | }; 30 | 31 | export default withVercelToolbar()(nextConfig); 32 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @vercel/flags-core 2 | 3 | ## 0.1.8 4 | 5 | ### Patch Changes 6 | 7 | - 620974c: [internal] change label to note 8 | 9 | ## 0.1.7 10 | 11 | ### Patch Changes 12 | 13 | - 43293a3: depend directly on @vercel/edge-config (removed as peer dep) 14 | 15 | ## 0.1.6 16 | 17 | ### Patch Changes 18 | 19 | - 5f3757a: drop tsconfig dependency 20 | - Updated dependencies [5f3757a] 21 | - flags@4.0.2 22 | 23 | ## 0.1.5 24 | 25 | ### Patch Changes 26 | 27 | - 6a7313a: publish cjs bundles besides esm 28 | 29 | ## 0.1.4 30 | 31 | ### Patch Changes 32 | 33 | - df76e2c: export evaluate fn 34 | 35 | ## 0.1.3 36 | 37 | ### Patch Changes 38 | 39 | - 9ecc4de: export Packed type 40 | 41 | ## 0.1.2 42 | 43 | ### Patch Changes 44 | 45 | - bfe9080: export DataSource type 46 | 47 | ## 0.1.1 48 | 49 | ### Patch Changes 50 | 51 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 52 | -------------------------------------------------------------------------------- /tests/next-15/pages-specs/pages-router-precomputed/[code]/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../../../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/pages-router-static`); 6 | await expect( 7 | page.getByText('Pages Router Precomputed Example: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/pages-router-static`); 13 | await expect( 14 | page.getByText(`Pages Router Precomputed Host: localhost:${port}`), 15 | ).toBeVisible(); 16 | }); 17 | 18 | test('can read cookies', async ({ page }) => { 19 | await page.goto(`http://localhost:${port}/pages-router-static`); 20 | await expect( 21 | page.getByText(`Pages Router Precomputed Cookie: example-cookie-value`), 22 | ).toBeVisible(); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/next-16/pages-specs/pages-router-precomputed/[code]/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../../../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/pages-router-static`); 6 | await expect( 7 | page.getByText('Pages Router Precomputed Example: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/pages-router-static`); 13 | await expect( 14 | page.getByText(`Pages Router Precomputed Host: localhost:${port}`), 15 | ).toBeVisible(); 16 | }); 17 | 18 | test('can read cookies', async ({ page }) => { 19 | await page.goto(`http://localhost:${port}/pages-router-static`); 20 | await expect( 21 | page.getByText(`Pages Router Precomputed Cookie: example-cookie-value`), 22 | ).toBeVisible(); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/next-15/app/app-router-precomputed/[code]/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../../../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/app-router-static`); 6 | await expect( 7 | page.getByText('Example App Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | const res = await page.goto(`http://localhost:${port}/app-router-static`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | 15 | // ensure we were rewritten 16 | expect(res?.headers()['x-middleware-rewrite']).toBeDefined(); 17 | }); 18 | 19 | test('can read cookies', async ({ page }) => { 20 | await page.goto(`http://localhost:${port}/app-router-static`); 21 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-e2e", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "start": "vite preview --port 5173", 9 | "check": "biome check", 10 | "svelte:check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "test:e2e": "playwright test" 13 | }, 14 | "dependencies": { 15 | "flags": "workspace:*", 16 | "@vercel/toolbar": "0.1.36" 17 | }, 18 | "devDependencies": { 19 | "@playwright/test": "1.51.1", 20 | "@sveltejs/adapter-vercel": "^5.6.0", 21 | "@sveltejs/kit": "^2.20.2", 22 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 23 | "svelte": "^5.0.0", 24 | "svelte-check": "^4.0.0", 25 | "typescript": "^5.5.0", 26 | "vite": "^5.4.4" 27 | }, 28 | "type": "module" 29 | } 30 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/examples/marketing-pages/[code]/+page.server.ts: -------------------------------------------------------------------------------- 1 | import { firstMarketingABTest, secondMarketingABTest } from '$lib/flags'; 2 | import { marketingFlags } from '$lib/precomputed-flags'; 3 | import type { PageServerLoad } from './$types'; 4 | // import { generatePermutations } from 'flags/sveltekit'; 5 | 6 | // Use Vercel ISR: 7 | export const config = { 8 | isr: { 9 | expiration: false, 10 | }, 11 | }; 12 | 13 | // You could also prerender at build time by doing: 14 | // 15 | // export const prerender = true; 16 | // 17 | // export async function entries() { 18 | // return (await generatePermutations(marketingFlags)).map((code) => ({ code })); 19 | // } 20 | 21 | export const load: PageServerLoad = async ({ params }) => { 22 | const flag1 = await firstMarketingABTest(params.code, marketingFlags); 23 | const flag2 = await secondMarketingABTest(params.code, marketingFlags); 24 | 25 | return { flag1, flag2 }; 26 | }; 27 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/product-detail-page/product-reviews.tsx: -------------------------------------------------------------------------------- 1 | import { StarIcon } from '@heroicons/react/20/solid'; 2 | import clsx from 'clsx'; 3 | 4 | export function ProductReviews() { 5 | return ( 6 |
7 |
8 |

4.9

9 |
10 | {[0, 1, 2, 3, 4].map((rating) => ( 11 |
18 | 21 |
22 | See all 312 reviews 23 |
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/api/reroute/+server.ts: -------------------------------------------------------------------------------- 1 | import { text } from '@sveltejs/kit'; 2 | import { computeInternalRoute, createVisitorId } from '$lib/precomputed-flags'; 3 | 4 | export async function GET({ url, request, cookies, setHeaders }) { 5 | let visitorId = cookies.get('visitorId'); 6 | 7 | if (!visitorId) { 8 | visitorId = createVisitorId(); 9 | cookies.set('visitorId', visitorId, { 10 | path: '/', 11 | httpOnly: false, // So that we can reset the visitor Id on the client in the examples 12 | }); 13 | request.headers.set('x-visitorId', visitorId); // cookie is not available on the initial request 14 | } 15 | 16 | // Add cache headers to not request the API as much (as the visitor id is not changing) 17 | setHeaders({ 18 | 'Cache-Control': 'private, max-age=300, stale-while-revalidate=600', 19 | }); 20 | 21 | return text( 22 | await computeInternalRoute(url.searchParams.get('pathname')!, request), 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/dev-tools.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ArrowPathIcon } from '@heroicons/react/24/outline'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | export function DevTools() { 7 | const router = useRouter(); 8 | 9 | const deleteCookie = () => { 10 | document.cookie = 11 | 'stable-id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; 12 | router.refresh(); 13 | }; 14 | 15 | return ( 16 |
17 | Dev Tools 18 | 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /examples/snippets/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = 'Input'; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /tests/next-15/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-16/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/snippets/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # e2e tests 2 | 3 | We run e2e tests for multiple versions of Next.js. 4 | 5 | We run them for the latest release of every major release from Next.js 13 onwards. 6 | 7 | Locally the tests run against the dev server. Playwright will either start the dev server or reuse an already running instance if there is one on the configured port. 8 | 9 | In CI the tests run against the production server. We use `turbo.json` to first build and then start the server. 10 | 11 | The test applications are not deployed anywhere to keep the action fast. 12 | 13 | ## Ports 14 | 15 | The port of each application indicates the Next.js version 16 | 17 | - next-14 runs on 4014 18 | - next-15 runs on 4015 19 | - next-16 runs on 4016 20 | - sveltekit runs on 5173 21 | 22 | ## Developing locally 23 | 24 | If you want to write or debug e2e tests for a specific Next.js version you can 25 | 26 | Terminal 1 27 | 28 | - `pnpm next-14` 29 | 30 | Terminal 2 31 | 32 | - `cd tests/next-14` 33 | - `pnpm playwright test` or `pnpm playwright test --ui` 34 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/[code]/add-to-cart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { track } from '@vercel/analytics'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useEffect, useState } from 'react'; 6 | import { AddToCartButton } from '@/components/product-detail-page/add-to-cart-button'; 7 | import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'; 8 | import { addToCart } from '@/lib/actions'; 9 | 10 | export function AddToCart() { 11 | const router = useRouter(); 12 | const { color, size } = useProductDetailPageContext(); 13 | const [isLoading, setIsLoading] = useState(false); 14 | 15 | useEffect(() => { 16 | track('add_to_cart:viewed'); 17 | }, []); 18 | 19 | return ( 20 | { 23 | setIsLoading(true); 24 | track('add_to_cart:clicked'); 25 | await addToCart({ id: 'shirt', color, size, quantity: 1 }); 26 | router.push('/cart'); 27 | }} 28 | /> 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/adapter-reflag/README.md: -------------------------------------------------------------------------------- 1 | # Flags SDK - Reflag Provider 2 | 3 | The [Reflag provider](https://flags-sdk.dev/docs/api-reference/adapters/reflag) for the [Flags SDK](https://flags-sdk.dev/) contains support for Reflag's feature flags. 4 | 5 | ## Setup 6 | 7 | The Reflag provider is available in the `@flags-sdk/reflag` module. You can install it with 8 | 9 | ```bash 10 | pnpm i @flags-sdk/reflag 11 | ``` 12 | 13 | ## Provider Instance 14 | 15 | You can import the default adapter instance `reflagAdapter` from `@flags-sdk/reflag`: 16 | 17 | ```ts 18 | import { reflagAdapter } from "@flags-sdk/reflag"; 19 | ``` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { flag } from "flags/next"; 25 | import { reflagAdapter } from "@flags-sdk/reflag"; 26 | 27 | export const huddleFlag = flag({ 28 | key: "huddle", 29 | adapter: reflagAdapter.isEnabled(), 30 | }); 31 | ``` 32 | 33 | ## Documentation 34 | 35 | Please check out the [Reflag provider documentation](https://flags-sdk.dev/docs/api-reference/adapters/reflag) for more information. 36 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/feature-flags-in-proxy/shared.tsx: -------------------------------------------------------------------------------- 1 | import { DemoFlag } from '@/components/demo-flag'; 2 | import { Button } from '@/components/ui/button'; 3 | import { basicProxyFlag } from './flags'; 4 | import { actAsFlaggedInUser, actAsFlaggedOutUser, clear } from './handlers'; 5 | 6 | // This component does not actually use the feature flag, but the 7 | // variant-on and variant-off pages know about the value statically. 8 | export function Shared({ variant }: { variant: 'on' | 'off' }) { 9 | return ( 10 | <> 11 | 12 |
13 | 16 | 19 | 22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/sveltekit-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-example", 3 | "version": "0.0.6", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "biome check", 10 | "svelte:check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "lint": "biome check" 13 | }, 14 | "dependencies": { 15 | "@tailwindcss/typography": "0.5.16", 16 | "@tailwindcss/vite": "4.0.15", 17 | "@vercel/edge": "^1.2.1", 18 | "@vercel/toolbar": "0.1.36", 19 | "cookie": "^0.6.0", 20 | "flags": "workspace:*", 21 | "tailwindcss": "4.0.15" 22 | }, 23 | "devDependencies": { 24 | "@sveltejs/adapter-vercel": "^5.6.0", 25 | "@sveltejs/kit": "^2.20.2", 26 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 27 | "svelte": "^5.0.0", 28 | "svelte-check": "^4.0.0", 29 | "typescript": "^5.5.0", 30 | "vite": "^5.4.4" 31 | }, 32 | "type": "module" 33 | } 34 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/src/provider/index.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderData } from 'flags'; 2 | 3 | export async function getProviderData(options: { 4 | token: string; 5 | }): Promise { 6 | if (!options.token) { 7 | return { 8 | definitions: {}, 9 | hints: [ 10 | { key: 'hypertune/missing-token', text: 'Missing Hypertune token' }, 11 | ], 12 | }; 13 | } 14 | 15 | const response = await fetch( 16 | `https://edge.hypertune.com/vercel-flag-definitions`, 17 | { 18 | headers: { Authorization: `Bearer ${options.token}` }, 19 | cache: 'no-store', 20 | }, 21 | ); 22 | 23 | if (response.status !== 200) { 24 | return { 25 | definitions: {}, 26 | hints: [ 27 | { 28 | key: 'hypertune/response-not-ok', 29 | text: `Failed to fetch Hypertune flag definitions (received ${response.status} response)`, 30 | }, 31 | ], 32 | }; 33 | } 34 | 35 | const definitions = (await response.json()) as ProviderData['definitions']; 36 | return { definitions, hints: [] }; 37 | } 38 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/[x+2e]well-known/vercel/flags/__+server.ts: -------------------------------------------------------------------------------- 1 | // This file is currently disabled as it is renamed to `__+server.ts`. 2 | // 3 | // The `createHandle` function in your `hooks.server.ts` will inject the 4 | // `/.well-known/vercel/flags` endpoint, so this file is not needed. 5 | // 6 | // For more control you can disable the default injection by not passing `flags` to `createHandle({ secret, flags })`. 7 | // You can then enable this file by renaming it to `+server.ts` 8 | // 9 | // This folder needs to be called [x+2e]well-known as folders starting with a 10 | // dot like .well-known cause issues, so the [x+2e] encoding is necessary. 11 | // See https://github.com/sveltejs/kit/discussions/7562#discussioncomment-4206530 12 | import { createFlagsDiscoveryEndpoint, getProviderData } from 'flags/sveltekit'; 13 | import { FLAGS_SECRET } from '$env/static/private'; 14 | import * as flags from '$lib/flags'; 15 | 16 | export const GET = createFlagsDiscoveryEndpoint( 17 | async () => { 18 | return getProviderData(flags); 19 | }, 20 | { secret: FLAGS_SECRET }, 21 | ); 22 | -------------------------------------------------------------------------------- /tests/next-16/app/app-router-precomputed/[code]/page.spec.tsx: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | import { port } from '../../../port'; 3 | 4 | test('displays the flag value', async ({ page }) => { 5 | await page.goto(`http://localhost:${port}/app-router-static`); 6 | await expect( 7 | page.getByText('Example App Router Flag Value: true'), 8 | ).toBeVisible(); 9 | }); 10 | 11 | test('can read request headers', async ({ page }) => { 12 | await page.goto(`http://localhost:${port}/app-router-static`); 13 | await expect(page.getByText(`Host: localhost:${port}`)).toBeVisible(); 14 | }); 15 | 16 | test('keeps page static', async ({ page }) => { 17 | const res = await page.goto(`http://localhost:${port}/app-router-static`); 18 | expect(res?.headers()['x-nextjs-cache']).toBe('HIT'); 19 | expect(res?.headers()['x-nextjs-prerender']).toBe('1'); 20 | }); 21 | 22 | test('can read cookies', async ({ page }) => { 23 | await page.goto(`http://localhost:${port}/app-router-static`); 24 | await expect(page.getByText(`Cookie: example-cookie-value`)).toBeVisible(); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /examples/sveltekit-example/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.7/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false 10 | }, 11 | "formatter": { 12 | "enabled": true, 13 | "indentStyle": "space" 14 | }, 15 | "linter": { 16 | "enabled": true, 17 | "rules": { 18 | "recommended": true, 19 | "correctness": { 20 | "noUnusedFunctionParameters": "off" 21 | }, 22 | "suspicious": { 23 | "noExplicitAny": "off", 24 | "noDocumentCookie": "off", 25 | "noUnknownAtRules": "off" 26 | }, 27 | "a11y": { 28 | "noSvgWithoutTitle": "off" 29 | }, 30 | "style": { 31 | "useNodejsImportProtocol": "off", 32 | "noNonNullAssertion": "off" 33 | } 34 | } 35 | }, 36 | "javascript": { 37 | "formatter": { 38 | "quoteStyle": "single" 39 | } 40 | }, 41 | "assist": { 42 | "enabled": true, 43 | "actions": { 44 | "source": { 45 | "organizeImports": "on" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/adapter-statsig/README.md: -------------------------------------------------------------------------------- 1 | # Flags SDK — Statsig Provider 2 | 3 | The [Statsig provider](https://flags-sdk.dev/docs/api-reference/adapters/statsig) for the [Flags SDK](https://flags-sdk.dev/) contains support for Statsig's Feature Gates, Dynamic Config, Experiments, Autotune and Layers. 4 | 5 | ## Setup 6 | 7 | The Statsig provider is available in the `@flags-sdk/statsig` module. You can install it with 8 | 9 | ```bash 10 | pnpm i @flags-sdk/statsig 11 | ``` 12 | 13 | ## Provider Instance 14 | 15 | You can import the default adapter instance `statigAdapter` from `@flags-sdk/statsig`: 16 | 17 | ```ts 18 | import { statsigAdapter } from "@flags-sdk/statsig"; 19 | ``` 20 | 21 | ## Example 22 | 23 | ```ts 24 | import { flag } from "flags/next"; 25 | import { statsigAdapter } from "@flags-sdk/statsig"; 26 | 27 | export const marketingGate = flag({ 28 | key: "marketing_gate", 29 | adapter: statsigAdapter.featureGate((config) => config.value), 30 | }); 31 | ``` 32 | 33 | ## Documentation 34 | 35 | Please check out the [Statsig provider documentation](https://flags-sdk.dev/docs/api-reference/adapters/statsig) for more information. 36 | -------------------------------------------------------------------------------- /tests/next-16/app/app-router-precomputed/[code]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from 'next/navigation'; 2 | import { Suspense } from 'react'; 3 | import { cookieFlag, exampleFlag, hostFlag, precomputedFlags } from '@/flags'; 4 | 5 | const PLACEHOLDER = '__placeholder__'; 6 | 7 | export const generateStaticParams = () => { 8 | return [{ code: PLACEHOLDER }]; 9 | }; 10 | 11 | async function Home({ params }: { params: Promise<{ code: string }> }) { 12 | const { code } = await params; 13 | if (code === PLACEHOLDER) return notFound(); 14 | 15 | const example = await exampleFlag(code, precomputedFlags); 16 | const host = await hostFlag(code, precomputedFlags); 17 | const cookie = await cookieFlag(code, precomputedFlags); 18 | return ( 19 |
20 |

Example App Router Flag Value: {example ? 'true' : 'false'}

21 |

Host: {host}

22 |

Cookie: {cookie}

23 |
24 | ); 25 | } 26 | 27 | export default function Page({ 28 | params, 29 | }: { 30 | params: Promise<{ code: string }>; 31 | }) { 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/flags/src/spec-extension/adapters/reflect.ts: -------------------------------------------------------------------------------- 1 | // copied from Next.js, and reduced 2 | // https://github.com/vercel/next.js/tree/canary/packages/next/src/server/web/spec-extension 3 | // biome-ignore lint/complexity/noStaticOnlyClass: Copied over from Next.js 4 | export class ReflectAdapter { 5 | static get( 6 | target: T, 7 | prop: string | symbol, 8 | receiver: unknown, 9 | ): any { 10 | const value = Reflect.get(target, prop, receiver); 11 | if (typeof value === 'function') { 12 | return value.bind(target); 13 | } 14 | 15 | return value; 16 | } 17 | 18 | static set( 19 | target: T, 20 | prop: string | symbol, 21 | value: any, 22 | receiver: any, 23 | ): boolean { 24 | return Reflect.set(target, prop, value, receiver); 25 | } 26 | 27 | static has(target: T, prop: string | symbol): boolean { 28 | return Reflect.has(target, prop); 29 | } 30 | 31 | static deleteProperty( 32 | target: T, 33 | prop: string | symbol, 34 | ): boolean { 35 | return Reflect.deleteProperty(target, prop); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/snippets/components/pages-layout.tsx: -------------------------------------------------------------------------------- 1 | import localFont from 'next/font/local'; 2 | import '../app/globals.css'; 3 | import { VercelToolbar } from '@vercel/toolbar/next'; 4 | import { ThemeProvider } from './theme-provider'; 5 | 6 | const geistSans = localFont({ 7 | src: '../app/fonts/GeistVF.woff', 8 | variable: '--font-geist-sans', 9 | weight: '100 900', 10 | }); 11 | 12 | const geistMono = localFont({ 13 | src: '../app/fonts/GeistMonoVF.woff', 14 | variable: '--font-geist-mono', 15 | weight: '100 900', 16 | }); 17 | 18 | export default function PagesLayout({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | const shouldInjectToolbar = process.env.NODE_ENV === 'development'; 24 | return ( 25 |
28 | 34 | {children} 35 | 36 | {shouldInjectToolbar && } 37 |
38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/flags/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/snippets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snippets", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev --turbopack", 8 | "check": "biome check", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-dialog": "1.1.2", 13 | "@radix-ui/react-separator": "1.1.0", 14 | "@radix-ui/react-slot": "1.1.0", 15 | "@radix-ui/react-tooltip": "1.1.4", 16 | "@vercel/edge-config": "^1.4.3", 17 | "@vercel/toolbar": "0.1.36", 18 | "class-variance-authority": "0.7.1", 19 | "clsx": "2.0.0", 20 | "flags": "workspace:*", 21 | "nanoid": "5.0.7", 22 | "next": "16.0.10", 23 | "next-themes": "0.4.4", 24 | "react": "19.0.0-rc.1", 25 | "react-dom": "19.0.0-rc.1", 26 | "tailwind-merge": "2.5.5", 27 | "tailwindcss-animate": "1.0.7" 28 | }, 29 | "devDependencies": { 30 | "@tailwindcss/typography": "0.5.10", 31 | "@types/node": "^20", 32 | "@types/react": "^19", 33 | "@types/react-dom": "^19", 34 | "postcss": "^8", 35 | "shiki": "1.24.0", 36 | "tailwindcss": "^3.4.1", 37 | "typescript": "^5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/flags/src/index.ts: -------------------------------------------------------------------------------- 1 | export { version } from '../package.json'; 2 | export { 3 | createAccessProof, 4 | decryptFlagDefinitions, 5 | decryptFlagValues, 6 | decryptOverrides, 7 | encryptFlagDefinitions, 8 | encryptFlagValues, 9 | encryptOverrides, 10 | verifyAccessProof, 11 | } from './lib/crypto'; 12 | export { mergeProviderData } from './lib/merge-provider-data'; 13 | export { reportValue } from './lib/report-value'; 14 | export { safeJsonStringify } from './lib/safe-json-stringify'; 15 | export { setTracerProvider } from './lib/tracing'; 16 | export { verifyAccess } from './lib/verify-access'; 17 | export { 18 | HeadersAdapter, 19 | type ReadonlyHeaders, 20 | } from './spec-extension/adapters/headers'; 21 | export { 22 | type ReadonlyRequestCookies, 23 | RequestCookiesAdapter, 24 | } from './spec-extension/adapters/request-cookies'; 25 | export type { 26 | Adapter, 27 | ApiData, 28 | Decide, 29 | FlagDeclaration, 30 | FlagDefinitionsType, 31 | FlagDefinitionType, 32 | FlagOptionType, 33 | FlagOverridesType, 34 | FlagValuesType, 35 | GenerousOption, 36 | Identify, 37 | JsonValue, 38 | Origin, 39 | ProviderData, 40 | } from './types'; 41 | -------------------------------------------------------------------------------- /packages/flags/src/sveltekit/env.ts: -------------------------------------------------------------------------------- 1 | // We're doing this dance so that the flags package is usable both in the SvelteKit environment 2 | // as well as other environments that don't know about '$env/dynamic/private', such as Routing Middleware 3 | let default_secret: string | undefined = process.env.FLAGS_SECRET; 4 | 5 | export async function tryGetSecret(secret?: string): Promise { 6 | if (!default_secret) { 7 | try { 8 | // @ts-expect-error SvelteKit will know about this 9 | // use static instead of dynamic because only the latter is available during prerendering, 10 | // and in case it's not available through that it should be via process.env above. 11 | const env = await import('$env/static/private'); 12 | default_secret = env.FLAGS_SECRET; 13 | } catch { 14 | // ignore, could happen when importing from an environment that doesn't know this import 15 | } 16 | } 17 | 18 | secret = secret || default_secret; 19 | 20 | if (!secret) { 21 | throw new Error( 22 | 'flags: No secret provided. Set an environment variable FLAGS_SECRET or provide a secret to the function.', 23 | ); 24 | } 25 | 26 | return secret; 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | 3 | env: 4 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 5 | TURBO_TEAM: ${{ vars.TURBO_TEAM }} 6 | 7 | on: 8 | push: 9 | branches: [main] 10 | pull_request: 11 | branches: ["*"] 12 | jobs: 13 | test: 14 | timeout-minutes: 5 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup pnpm 19 | uses: pnpm/action-setup@v2 20 | with: 21 | version: 9 22 | - uses: actions/setup-node@v3 23 | with: 24 | node-version-file: ".node-version" 25 | cache: "pnpm" 26 | - name: Install dependencies 27 | run: pnpm install --frozen-lockfile 28 | - name: Install Playwright Browsers 29 | run: pnpm exec playwright install chromium --with-deps --no-shell 30 | - name: Run Playwright tests 31 | run: pnpm test:e2e 32 | env: 33 | FLAGS_SECRET: ${{ secrets.FLAGS_SECRET }} 34 | - uses: actions/upload-artifact@v4 35 | if: ${{ !cancelled() }} 36 | with: 37 | name: playwright-report 38 | path: playwright-report/ 39 | retention-days: 30 40 | -------------------------------------------------------------------------------- /packages/flags/src/sveltekit/types.ts: -------------------------------------------------------------------------------- 1 | import type { FlagOption } from '../types'; 2 | 3 | type FlagsMeta = { 4 | key: string; 5 | description?: string; 6 | origin?: string | Record; 7 | options?: FlagOption[]; 8 | }; 9 | 10 | type RegularFlag = { 11 | (): ReturnValue | Promise; 12 | ( 13 | /** Only provide this if you're retrieving the flag value outside of the lifecycle of the `handle` hook, e.g. when calling it inside routing functions. */ 14 | request?: Request, 15 | secret?: string, 16 | ): ReturnValue | Promise; 17 | } & FlagsMeta; 18 | 19 | type PrecomputedFlag = { 20 | (): never; 21 | ( 22 | /** The route parameter that contains the precomputed flag values */ 23 | code: string, 24 | /** The flags which were used to create the code (i.e. the same array you passed to `precompute(...)`) */ 25 | flagsArray: FlagsArray, 26 | ): ReturnValue | Promise; 27 | } & FlagsMeta; 28 | 29 | export type Flag = 30 | | RegularFlag 31 | | PrecomputedFlag; 32 | 33 | export type FlagsArray = readonly Flag[]; 34 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/examples/(marketing-pages-manual-approach)/marketing-pages-variant-a/+page.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | The feature flag marketingABTestManualApproach evaluated to 4 | true. 5 |
6 |
7 | 8 |
9 | 18 |
19 | -------------------------------------------------------------------------------- /packages/adapter-vercel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/vercel 2 | 3 | ## 0.1.8 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [620974c] 8 | - @vercel/flags-core@0.1.8 9 | 10 | ## 0.1.7 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [43293a3] 15 | - @vercel/flags-core@0.1.7 16 | 17 | ## 0.1.6 18 | 19 | ### Patch Changes 20 | 21 | - 5f3757a: drop tsconfig dependency 22 | - Updated dependencies [5f3757a] 23 | - @vercel/flags-core@0.1.6 24 | - flags@4.0.2 25 | 26 | ## 0.1.5 27 | 28 | ### Patch Changes 29 | 30 | - 6a7313a: publish cjs bundles besides esm 31 | - Updated dependencies [6a7313a] 32 | - @vercel/flags-core@0.1.5 33 | 34 | ## 0.1.4 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies [df76e2c] 39 | - @vercel/flags-core@0.1.4 40 | 41 | ## 0.1.3 42 | 43 | ### Patch Changes 44 | 45 | - Updated dependencies [9ecc4de] 46 | - @vercel/flags-core@0.1.3 47 | 48 | ## 0.1.2 49 | 50 | ### Patch Changes 51 | 52 | - Updated dependencies [bfe9080] 53 | - @vercel/flags-core@0.1.2 54 | 55 | ## 0.1.1 56 | 57 | ### Patch Changes 58 | 59 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 60 | - Updated dependencies [ff052f0] 61 | - @vercel/flags-core@0.1.1 62 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/image-gallery.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import clsx from 'clsx'; 4 | import Image from 'next/image'; 5 | import { colorToImage, images } from '@/components/utils/images'; 6 | import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'; 7 | 8 | export function ImageGallery() { 9 | const { color } = useProductDetailPageContext(); 10 | 11 | const orderedImages = [...images].sort((a, b) => { 12 | if (a === colorToImage[color]) return -1; 13 | if (b === colorToImage[color]) return 1; 14 | return 0; 15 | }); 16 | 17 | return ( 18 |
19 |
20 | {orderedImages.map((image, index) => ( 21 | Product Image 30 | ))} 31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/routes/examples/(marketing-pages-manual-approach)/marketing-pages-variant-b/+page.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | The feature flag marketingABTestManualApproach evaluated to 4 | false. 5 |
6 |
7 | 8 |
9 | 18 |
19 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/marketing-pages/proxy.tsx: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/next'; 2 | import { type NextRequest, NextResponse } from 'next/server'; 3 | import { marketingFlags } from './flags'; 4 | import { getOrGenerateVisitorId } from './get-or-generate-visitor-id'; 5 | 6 | export async function marketingProxy(request: NextRequest) { 7 | // assign a cookie to the visitor 8 | const visitorId = await getOrGenerateVisitorId( 9 | request.cookies, 10 | request.headers, 11 | ); 12 | 13 | // precompute the flags 14 | const code = await precompute(marketingFlags); 15 | 16 | // rewrite the page with the code and set the cookie 17 | return NextResponse.rewrite( 18 | new URL(`/examples/marketing-pages/${code}`, request.url), 19 | { 20 | headers: { 21 | // Set the cookie on the response 22 | 'Set-Cookie': `marketing-visitor-id=${visitorId}; Path=/`, 23 | // Add a request header, so the page knows the generated id even 24 | // on the first-ever request which has no request cookie yet. 25 | // 26 | // This is later used by the getOrGenerateVisitorId function. 27 | 'x-marketing-visitor-id': visitorId, 28 | }, 29 | }, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/adapter-edge-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flags-sdk/edge-config", 3 | "version": "0.1.2", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.js", 17 | "typesVersions": { 18 | "*": { 19 | ".": [ 20 | "dist/*.d.ts", 21 | "dist/*.d.cts" 22 | ] 23 | } 24 | }, 25 | "files": [ 26 | "dist" 27 | ], 28 | "scripts": { 29 | "build": "rimraf dist && tsup", 30 | "dev": "tsup --watch --clean=false", 31 | "check": "biome check", 32 | "test": "vitest --run", 33 | "test:watch": "vitest", 34 | "type-check": "tsc --noEmit" 35 | }, 36 | "dependencies": { 37 | "@vercel/edge-config": "^1.4.3" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "20.11.17", 41 | "flags": "workspace:*", 42 | "rimraf": "6.0.1", 43 | "tsup": "8.0.1", 44 | "typescript": "5.6.3", 45 | "vite": "5.1.1", 46 | "vitest": "1.4.0" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/shirt-shop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shirt-shop", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "install:compat": "node scripts/install.mjs", 9 | "check": "biome check", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^2.2.0", 14 | "@heroicons/react": "2.2.0", 15 | "@tailwindcss/aspect-ratio": "0.4.2", 16 | "@tailwindcss/forms": "0.5.10", 17 | "@tailwindcss/postcss": "^4.0.9", 18 | "@tailwindcss/typography": "0.5.16", 19 | "@vercel/analytics": "1.5.0", 20 | "@vercel/edge": "1.2.1", 21 | "@vercel/edge-config": "^1.4.3", 22 | "@vercel/toolbar": "0.1.36", 23 | "clsx": "2.1.1", 24 | "flags": "workspace:*", 25 | "js-xxhash": "4.0.0", 26 | "motion": "12.12.1", 27 | "nanoid": "5.1.2", 28 | "next": "16.0.10", 29 | "react": "^19.0.0", 30 | "react-dom": "^19.0.0", 31 | "sonner": "2.0.1" 32 | }, 33 | "devDependencies": { 34 | "@types/node": "^22.13.5", 35 | "@types/react": "^19.0.10", 36 | "@types/react-dom": "^19.0.4", 37 | "postcss": "^8.5.3", 38 | "tailwindcss": "^4.0.9", 39 | "typescript": "^5.7.3" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/marketing-pages/[code]/page.tsx: -------------------------------------------------------------------------------- 1 | import { generatePermutations } from 'flags/next'; 2 | import { DemoFlag } from '@/components/demo-flag'; 3 | import { 4 | marketingAbTest, 5 | marketingFlags, 6 | secondMarketingAbTest, 7 | } from '../flags'; 8 | import { RegenerateIdButton } from '../regenerate-id-button'; 9 | 10 | // Generate all permutations (all combinations of flag 1 and flag 2). 11 | export async function generateStaticParams() { 12 | const permutations = await generatePermutations(marketingFlags); 13 | return permutations.map((code) => ({ code })); 14 | } 15 | 16 | export default async function Page({ 17 | params, 18 | }: { 19 | params: Promise<{ code: string }>; 20 | }) { 21 | const awaitedParams = await params; 22 | const abTest = await marketingAbTest(awaitedParams.code, marketingFlags); 23 | const secondAbTest = await secondMarketingAbTest( 24 | awaitedParams.code, 25 | marketingFlags, 26 | ); 27 | 28 | return ( 29 | <> 30 | 31 | 32 |
33 | 34 |
35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /packages/adapter-split/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flags-sdk/split", 3 | "version": "0.1.1", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.js", 17 | "typesVersions": { 18 | "*": { 19 | ".": [ 20 | "dist/*.d.ts", 21 | "dist/*.d.cts" 22 | ] 23 | } 24 | }, 25 | "files": [ 26 | "dist", 27 | "CHANGELOG.md" 28 | ], 29 | "scripts": { 30 | "build": "rimraf dist && tsup", 31 | "dev": "tsup --watch --clean=false", 32 | "check": "biome check", 33 | "test": "vitest --run", 34 | "test:watch": "vitest", 35 | "type-check": "tsc --noEmit" 36 | }, 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "@types/node": "20.11.17", 40 | "flags": "workspace:*", 41 | "msw": "2.6.4", 42 | "rimraf": "6.0.1", 43 | "tsup": "8.0.1", 44 | "typescript": "5.6.3", 45 | "vite": "5.1.1", 46 | "vitest": "1.4.0" 47 | }, 48 | "peerDependencies": {}, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/adapter-optimizely/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flags-sdk/optimizely", 3 | "version": "0.1.1", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.js", 17 | "typesVersions": { 18 | "*": { 19 | ".": [ 20 | "dist/*.d.ts", 21 | "dist/*.d.cts" 22 | ] 23 | } 24 | }, 25 | "files": [ 26 | "dist", 27 | "CHANGELOG.md" 28 | ], 29 | "scripts": { 30 | "build": "rimraf dist && tsup", 31 | "dev": "tsup --watch --clean=false", 32 | "check": "biome check", 33 | "test": "vitest --run", 34 | "test:watch": "vitest", 35 | "type-check": "tsc --noEmit" 36 | }, 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "@types/node": "20.11.17", 40 | "flags": "workspace:*", 41 | "msw": "2.6.4", 42 | "rimraf": "6.0.1", 43 | "tsup": "8.0.1", 44 | "typescript": "5.6.3", 45 | "vite": "5.1.1", 46 | "vitest": "1.4.0" 47 | }, 48 | "peerDependencies": {}, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/adapter-posthog/README.md: -------------------------------------------------------------------------------- 1 | # Flags SDK — PostHog Adapter 2 | 3 | The PostHog adapter for [Flags SDK](https://flags-sdk.dev/) supports dynamic server side feature flags powered by [PostHog](https://posthog.com/). 4 | 5 | ## Setup 6 | 7 | Install the adapter 8 | 9 | ```bash 10 | pnpm i @flags-sdk/posthog 11 | ``` 12 | 13 | ## Example Usage 14 | 15 | ```ts 16 | import { flag } from "flags/next"; 17 | import { postHogAdapter } from "@flags-sdk/posthog"; 18 | 19 | export const marketingGate = flag({ 20 | // The key in PostHog 21 | key: "my_posthog_flag_key_here", 22 | // The PostHog feature to use (isFeatureEnabled, featureFlagValue, featureFlagPayload) 23 | adapter: postHogAdapter.featureFlagValue(), 24 | }); 25 | ``` 26 | 27 | ## Runtimes 28 | 29 | | Runtime | Supported | 30 | | ------------ | --------- | 31 | | Node | ✅ | 32 | | Edge Runtime | ❌ | 33 | 34 | Note: `posthog-node` does not support the Edge Runtime. 35 | 36 | To use with Routing Middleware and precompute, read more: [Middleware now supports Node.js](https://vercel.com/changelog/middleware-now-supports-node-js) 37 | 38 | ## Documentation 39 | 40 | View more PostHog documentation at [posthog.com](https://posthog.com?utm_source=github&utm_campaign=flags_sdk). 41 | -------------------------------------------------------------------------------- /examples/snippets/pages/examples/pages-router-precomputed/[code]/index.tsx: -------------------------------------------------------------------------------- 1 | import { generatePermutations } from 'flags/next'; 2 | import type { 3 | GetStaticPaths, 4 | GetStaticProps, 5 | InferGetStaticPropsType, 6 | } from 'next'; 7 | import { DemoFlag } from '@/components/demo-flag'; 8 | import PagesLayout from '@/components/pages-layout'; 9 | import { 10 | exampleFlag, 11 | exampleFlags, 12 | } from '@/lib/pages-router-precomputed/flags'; 13 | 14 | export const getStaticPaths = (async () => { 15 | const codes = await generatePermutations(exampleFlags); 16 | 17 | return { 18 | paths: codes.map((code) => ({ params: { code } })), 19 | fallback: 'blocking', 20 | }; 21 | }) satisfies GetStaticPaths; 22 | 23 | export const getStaticProps = (async (context) => { 24 | if (typeof context.params?.code !== 'string') return { notFound: true }; 25 | 26 | const example = await exampleFlag(context.params.code, exampleFlags); 27 | return { props: { example } }; 28 | }) satisfies GetStaticProps<{ example: boolean }>; 29 | 30 | export default function PageRouter({ 31 | example, 32 | }: InferGetStaticPropsType) { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/adapter-hypertune/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flags-sdk/hypertune", 3 | "version": "0.3.2", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.js", 17 | "typesVersions": { 18 | "*": { 19 | ".": [ 20 | "dist/*.d.ts", 21 | "dist/*.d.cts" 22 | ] 23 | } 24 | }, 25 | "files": [ 26 | "dist", 27 | "CHANGELOG.md" 28 | ], 29 | "scripts": { 30 | "build": "rimraf dist && tsup", 31 | "dev": "tsup --watch --clean=false", 32 | "check": "biome check", 33 | "test": "vitest --run", 34 | "test:watch": "vitest", 35 | "type-check": "tsc --noEmit" 36 | }, 37 | "dependencies": { 38 | "@vercel/edge-config": "^1.4.3", 39 | "hypertune": "2.8.3" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "20.11.17", 43 | "flags": "workspace:*", 44 | "msw": "2.6.4", 45 | "rimraf": "6.0.1", 46 | "tsup": "8.0.1", 47 | "typescript": "5.6.3", 48 | "vite": "5.1.1", 49 | "vitest": "1.4.0" 50 | }, 51 | "publishConfig": { 52 | "access": "public" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/marketing-pages/get-or-generate-visitor-id.tsx: -------------------------------------------------------------------------------- 1 | import type { ReadonlyHeaders, ReadonlyRequestCookies } from 'flags'; 2 | import { dedupe } from 'flags/next'; 3 | import { nanoid } from 'nanoid'; 4 | import type { NextRequest } from 'next/server'; 5 | 6 | const generateId = dedupe(async () => nanoid()); 7 | 8 | // This function is not deduplicated, as it is called with 9 | // two different cookies objects, so it can not be deduplicated. 10 | // 11 | // However, the generateId function will always generate the same id for the 12 | // same request, so it is safe to call it multiple times within the same runtime. 13 | export const getOrGenerateVisitorId = async ( 14 | cookies: ReadonlyRequestCookies | NextRequest['cookies'], 15 | headers: ReadonlyHeaders | NextRequest['headers'], 16 | ) => { 17 | // check cookies first 18 | const cookieVisitorId = cookies.get('marketing-visitor-id')?.value; 19 | if (cookieVisitorId) return cookieVisitorId; 20 | 21 | // check headers in case proxy set a cookie on the response, as it will 22 | // not be present on the initial request 23 | const headerVisitorId = headers.get('x-marketing-visitor-id'); 24 | if (headerVisitorId) return headerVisitorId; 25 | 26 | // if no visitor id is found, generate a new one 27 | return generateId(); 28 | }; 29 | -------------------------------------------------------------------------------- /tests/sveltekit-e2e/src/routes/[x+2e]well-known/vercel/flags/__+server.ts: -------------------------------------------------------------------------------- 1 | // This file is currently disabled as it is renamed to `__+server.ts`. 2 | // 3 | // The `createHandle` function in your `hooks.server.ts` will inject the 4 | // `/.well-known/vercel/flags` endpoint, so this file is not needed. 5 | // 6 | // For more control you can disable the default injection by not passing `flags` to `createHandle({ secret, flags })`. 7 | // You can then enable this file by renaming it to `+server.ts` 8 | // 9 | // This folder needs to be called [x+2e]well-known as folders starting with a 10 | // dot like .well-known cause issues, so we the [x+2e] encoding is necessary. 11 | // See https://github.com/sveltejs/kit/discussions/7562#discussioncomment-4206530 12 | import { error, json } from '@sveltejs/kit'; 13 | import { verifyAccess } from 'flags'; 14 | import { getProviderData } from 'flags/sveltekit'; 15 | import { FLAGS_SECRET } from '$env/static/private'; 16 | import * as flags from '$lib/flags'; 17 | import type { RequestHandler } from './$types'; 18 | 19 | export const GET: RequestHandler = async ({ request }) => { 20 | const access = await verifyAccess( 21 | request.headers.get('Authorization'), 22 | FLAGS_SECRET, 23 | ); 24 | if (!access) error(401); 25 | 26 | return json({ definitions: getProviderData(flags) }); 27 | }; 28 | -------------------------------------------------------------------------------- /docs/REVIEWS.md: -------------------------------------------------------------------------------- 1 | # How to review pull requests for packages 2 | 3 | This documentation is intended for maintainers of this repository. 4 | 5 | ## Checking out a branch of a fork 6 | 7 | Follow the following steps to review a pull request from a fork. 8 | The example uses hypertunehq. 9 | 10 | - `git remote add hypertunehq git@github.com:hypertunehq/flags.git` 11 | - `git fetch hypertunehq` 12 | - `git checkout hypertunehq/update-hypertune-adapter` 13 | 14 | Or replace the last step with: 15 | 16 | - `git checkout -b update-hypertune-adapter hypertunehq:update-hypertune-adapter` 17 | 18 | ## Testing with examples 19 | 20 | You can try an updates to adapters with the existing examples in [vercel/examples](https://github.com/vercel/examples/tree/main/flags-sdk). 21 | 22 | 1. Build the adapter 23 | 24 | - `cd packages/adapter-hypertune` 25 | - `pnpm build` 26 | 27 | 2. Try it out 28 | 29 | - Clone https://github.com/vercel/examples/ 30 | - Change into `flags-sdk/hypertune` 31 | - Run `pnpm install` 32 | - Run `vc link` and link to `Vercel Examples` team and `flags-sdk-hypertune` package 33 | - Run `vc env pull` 34 | - Change the `@flags-sdk/launchdarkly` dependency of `flags-sdk/hypertune/package.json` to a relative path 35 | - `"@flags-sdk/launchdarkly": "file:../../../flags/packages/adapter-hypertune"` 36 | - Run `pnpm install` 37 | -------------------------------------------------------------------------------- /examples/snippets/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-15/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/next-16/pages/pages-router-precomputed/[code]/index.tsx: -------------------------------------------------------------------------------- 1 | import type { 2 | GetStaticPaths, 3 | GetStaticProps, 4 | InferGetStaticPropsType, 5 | } from 'next'; 6 | import { 7 | cookieFlag, 8 | exampleFlag, 9 | hostFlag, 10 | precomputedFlags, 11 | } from '../../../flags'; 12 | 13 | export const getStaticPaths = (async () => { 14 | return { 15 | paths: [], 16 | fallback: 'blocking', 17 | }; 18 | }) satisfies GetStaticPaths; 19 | 20 | export const getStaticProps = (async (context) => { 21 | if (typeof context.params?.code !== 'string') return { notFound: true }; 22 | 23 | const example = await exampleFlag(context.params.code, precomputedFlags); 24 | const host = await hostFlag(context.params.code, precomputedFlags); 25 | const cookie = await cookieFlag(context.params.code, precomputedFlags); 26 | 27 | return { props: { example, host, cookie } }; 28 | }) satisfies GetStaticProps<{ example: boolean; host: string; cookie: string }>; 29 | 30 | export default function Page({ 31 | example, 32 | cookie, 33 | host, 34 | }: InferGetStaticPropsType) { 35 | return ( 36 |
37 |

Pages Router Precomputed Example: {example ? 'true' : 'false'}

38 |

Pages Router Precomputed Cookie: {cookie}

39 |

Pages Router Precomputed Host: {host}

40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /tests/next-16/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/sveltekit-example/src/hooks.ts: -------------------------------------------------------------------------------- 1 | // `reroute` is called on both the server and client during dev, because `middleware.ts` is unknown to SvelteKit. 2 | // In production it's called on the client only because `middleware.ts` will handle the first page visit. 3 | // As a result, when visiting a page you'll get rerouted accordingly in all situations in both dev and prod. 4 | export async function reroute({ url, fetch }) { 5 | if (url.pathname === '/examples/marketing-pages-manual-approach') { 6 | const destination = new URL('/api/reroute-manual', url); 7 | 8 | // Since `reroute` runs on the client and the cookie with the flag info is not available to it, 9 | // we do a server request to get the internal route. 10 | return fetch(destination).then((response) => response.text()); 11 | } 12 | 13 | if ( 14 | url.pathname === '/examples/marketing-pages' 15 | // add more paths here if you want to run A/B tests on other pages, e.g. 16 | // || url.pathname === '/something-else' 17 | ) { 18 | const destination = new URL('/api/reroute', url); 19 | destination.searchParams.set('pathname', url.pathname); 20 | 21 | // Since `reroute` runs on the client and the cookie with the flag info is not available to it, 22 | // we do a server request to get the internal route. 23 | return fetch(destination).then((response) => response.text()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/shirt-shop/components/utils/product-detail-page-context.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { createContext, useContext, useMemo, useState } from 'react'; 4 | 5 | export type ProductDetailPageContextType = { 6 | color: string; 7 | size: string; 8 | setColor: (color: string) => void; 9 | setSize: (size: string) => void; 10 | }; 11 | 12 | export function useProductDetailPageContext(): ProductDetailPageContextType { 13 | return useContext(ProductDetailPageContext); 14 | } 15 | 16 | export const ProductDetailPageContext = 17 | createContext({ 18 | color: 'Black', 19 | size: 'S', 20 | setColor: () => {}, 21 | setSize: () => {}, 22 | }); 23 | 24 | export function ProductDetailPageProvider({ 25 | children, 26 | }: { 27 | children: React.ReactNode; 28 | }) { 29 | const [state, setState] = useState({ 30 | color: 'Black', 31 | size: 'S', 32 | }); 33 | 34 | const context = useMemo( 35 | () => ({ 36 | color: state.color, 37 | size: state.size, 38 | setColor: (color: string) => setState({ ...state, color }), 39 | setSize: (size: string) => setState({ ...state, size }), 40 | }), 41 | [state], 42 | ); 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /packages/adapter-launchdarkly/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @flags-sdk/launchdarkly 2 | 3 | ## 0.3.4 4 | 5 | ### Patch Changes 6 | 7 | - 5f3757a: drop tsconfig dependency 8 | 9 | ## 0.3.3 10 | 11 | ### Patch Changes 12 | 13 | - ff052f0: upgrade internal @vercel/edge-config dependency to v1.4.3 14 | 15 | ## 0.3.2 16 | 17 | ### Patch Changes 18 | 19 | - e1def0e: Significantly improve performance by upgrading to `@launchdarkly/vercel-server-sdk` v1.3.34. 20 | 21 | This release avoids JSON.stringify and JSON.parse overhead which earlier versions of `@launchdarkly/vercel-server-sdk` had. 22 | 23 | See 24 | 25 | - https://github.com/launchdarkly/js-core/releases/tag/vercel-server-sdk-v1.3.34 26 | - https://github.com/launchdarkly/js-core/pull/918 27 | 28 | ## 0.3.1 29 | 30 | ### Patch Changes 31 | 32 | - 595e9d0: only read edge config once per request 33 | 34 | ## 0.3.0 35 | 36 | ### Minor Changes 37 | 38 | - 917ef42: change API from ldAdapter() to ldAdapter.variation() 39 | 40 | ### Patch Changes 41 | 42 | - b375e4e: add metadata to package.json 43 | - 917ef42: expose ldClient on default ldAdapter 44 | 45 | ## 0.2.1 46 | 47 | ### Patch Changes 48 | 49 | - fbc886a: expose ldAdapter.ldClient 50 | - fbc886a: expose as ldAdapter 51 | 52 | ## 0.2.0 53 | 54 | ### Minor Changes 55 | 56 | - 2fcc446: Add LaunchDarkly adapter 57 | 58 | ## 0.1.0 59 | 60 | ### Minor Changes 61 | 62 | - 3c66284: initialize 63 | -------------------------------------------------------------------------------- /examples/shirt-shop/app/[code]/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import { Main } from '@/components/main'; 2 | import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'; 3 | import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'; 4 | import { 5 | proceedToCheckoutColorFlag, 6 | productFlags, 7 | showFreeDeliveryBannerFlag, 8 | showSummerBannerFlag, 9 | } from '@/flags'; 10 | import { ProceedToCheckout } from './proceed-to-checkout'; 11 | 12 | export default async function CartPage({ 13 | params, 14 | }: { 15 | params: Promise<{ code: string }>; 16 | }) { 17 | const { code } = await params; 18 | const showSummerBanner = await showSummerBannerFlag(code, productFlags); 19 | const freeDeliveryBanner = await showFreeDeliveryBannerFlag( 20 | code, 21 | productFlags, 22 | ); 23 | const proceedToCheckoutColor = await proceedToCheckoutColorFlag( 24 | code, 25 | productFlags, 26 | ); 27 | 28 | return ( 29 |
30 |
31 | 32 | 37 | } 38 | /> 39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /packages/adapter-vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@flags-sdk/vercel", 3 | "version": "0.1.8", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "sideEffects": false, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "main": "./dist/index.js", 17 | "typesVersions": { 18 | "*": { 19 | ".": [ 20 | "dist/*.d.ts", 21 | "dist/*.d.cts" 22 | ] 23 | } 24 | }, 25 | "files": [ 26 | "dist", 27 | "CHANGELOG.md" 28 | ], 29 | "scripts": { 30 | "build": "tsup", 31 | "dev": "tsup --watch", 32 | "check": "biome check", 33 | "test": "vitest --run", 34 | "test:watch": "vitest", 35 | "type-check": "tsc --noEmit" 36 | }, 37 | "dependencies": { 38 | "jose": "5.2.1", 39 | "js-xxhash": "4.0.0", 40 | "@vercel/flags-core": "workspace:*", 41 | "@vercel/edge-config": "^1.4.3" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "20.11.17", 45 | "flags": "workspace:*", 46 | "msw": "2.6.4", 47 | "next": "16.0.10", 48 | "tsup": "8.0.1", 49 | "typescript": "5.6.3", 50 | "vite": "6.0.3", 51 | "vitest": "2.1.8" 52 | }, 53 | "peerDependencies": { 54 | "flags": "*" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/flags/src/lib/async-memoize-one.ts: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/microlinkhq/async-memoize-one 2 | // and https://github.com/alexreardon/memoize-one 3 | 4 | type MemoizeOneOptions = { 5 | cachePromiseRejection?: boolean; 6 | }; 7 | 8 | type MemoizedFn any> = ( 9 | this: ThisParameterType, 10 | ...args: Parameters 11 | ) => ReturnType; 12 | 13 | /** 14 | * Memoizes an async function, but only keeps the latest result 15 | */ 16 | export function memoizeOne any>( 17 | fn: TFunc, 18 | isEqual: (a: Parameters, b: Parameters) => boolean, 19 | { cachePromiseRejection = false }: MemoizeOneOptions = {}, 20 | ): MemoizedFn { 21 | let calledOnce = false; 22 | let oldArgs: Parameters; 23 | let lastResult: any; 24 | 25 | function memoized( 26 | this: ThisParameterType, 27 | ...newArgs: Parameters 28 | ) { 29 | if (calledOnce && isEqual(newArgs, oldArgs)) return lastResult; 30 | 31 | lastResult = fn.apply(this, newArgs); 32 | 33 | if (!cachePromiseRejection && lastResult.catch) { 34 | lastResult.catch(() => { 35 | calledOnce = false; 36 | }); 37 | } 38 | 39 | calledOnce = true; 40 | oldArgs = newArgs; 41 | 42 | return lastResult; 43 | } 44 | 45 | return memoized; 46 | } 47 | -------------------------------------------------------------------------------- /examples/shirt-shop/proxy.ts: -------------------------------------------------------------------------------- 1 | import { precompute } from 'flags/next'; 2 | import { type NextRequest, NextResponse } from 'next/server'; 3 | import { productFlags } from '@/flags'; 4 | import { getCartId } from './lib/get-cart-id'; 5 | import { getStableId } from './lib/get-stable-id'; 6 | 7 | export const config = { 8 | matcher: ['/', '/cart'], 9 | }; 10 | 11 | export async function proxy(request: NextRequest) { 12 | const stableId = await getStableId(); 13 | const cartId = await getCartId(); 14 | const code = await precompute(productFlags); 15 | 16 | // rewrites the request to the variant for this flag combination 17 | const nextUrl = new URL( 18 | `/${code}${request.nextUrl.pathname}${request.nextUrl.search}`, 19 | request.url, 20 | ); 21 | 22 | // Add a header to the request to indicate that the stable id is generated, 23 | // as it will not be present on the cookie request header on the first-ever request. 24 | if (cartId.isFresh) { 25 | request.headers.set('x-generated-cart-id', cartId.value); 26 | } 27 | 28 | if (stableId.isFresh) { 29 | request.headers.set('x-generated-stable-id', stableId.value); 30 | } 31 | 32 | // response headers 33 | const headers = new Headers(); 34 | headers.append('set-cookie', `stable-id=${stableId.value}`); 35 | headers.append('set-cookie', `cart-id=${cartId.value}`); 36 | return NextResponse.rewrite(nextUrl, { request, headers }); 37 | } 38 | -------------------------------------------------------------------------------- /examples/snippets/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as TooltipPrimitive from '@radix-ui/react-tooltip'; 4 | import * as React from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider; 9 | 10 | const Tooltip = TooltipPrimitive.Root; 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger; 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ComponentRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 19 | 28 | 29 | )); 30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 31 | 32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 33 | -------------------------------------------------------------------------------- /examples/snippets/app/.well-known/vercel/flags/route.ts: -------------------------------------------------------------------------------- 1 | import { type ApiData, verifyAccess } from 'flags'; 2 | import { getProviderData } from 'flags/next'; 3 | import { type NextRequest, NextResponse } from 'next/server'; 4 | import * as topLevelFlags from '../../../../flags'; 5 | import * as adapterFlags from '../../../concepts/adapters/flags'; 6 | import * as basicIdentifyFlags from '../../../concepts/identify/basic/flags'; 7 | import * as fullIdentifyFlags from '../../../concepts/identify/full/flags'; 8 | import * as dashboardFlags from '../../../examples/dashboard-pages/flags'; 9 | import * as basicProxyFlags from '../../../examples/feature-flags-in-proxy/flags'; 10 | // The @/ import is not working in the ".well-known" folder due do the dot in the path. 11 | // We need to use relative paths instead. This seems like a TypeScript issue. 12 | import * as marketingFlags from '../../../examples/marketing-pages/flags'; 13 | 14 | export async function GET(request: NextRequest) { 15 | const access = await verifyAccess(request.headers.get('Authorization')); 16 | if (!access) return NextResponse.json(null, { status: 401 }); 17 | 18 | return NextResponse.json( 19 | getProviderData({ 20 | ...marketingFlags, 21 | ...dashboardFlags, 22 | ...topLevelFlags, 23 | ...adapterFlags, 24 | ...basicProxyFlags, 25 | ...basicIdentifyFlags, 26 | ...fullIdentifyFlags, 27 | }), 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/vercel-flags-core/src/client.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { createClient } from './client'; 3 | import { InMemoryDataSource } from './data-source/in-memory-data-source'; 4 | 5 | describe('createClient', () => { 6 | it('should be a function', () => { 7 | expect(typeof createClient).toBe('function'); 8 | }); 9 | 10 | it('should allow a custom data source', () => { 11 | const inlineDataSource = new InMemoryDataSource({ definitions: {} }); 12 | const flagsClient = createClient({ 13 | dataSource: inlineDataSource, 14 | environment: 'production', 15 | }); 16 | 17 | expect(flagsClient.environment).toEqual('production'); 18 | expect(flagsClient.dataSource).toEqual(inlineDataSource); 19 | }); 20 | 21 | it('should evaluate', async () => { 22 | const customDataSource = new InMemoryDataSource({ 23 | definitions: { 24 | 'summer-sale': { environments: { production: 0 }, variants: [false] }, 25 | }, 26 | }); 27 | const flagsClient = createClient({ 28 | dataSource: customDataSource, 29 | environment: 'production', 30 | }); 31 | 32 | await expect( 33 | flagsClient.evaluate('summer-sale', { 34 | entities: {}, 35 | headers: new Headers(), 36 | }), 37 | ).resolves.toEqual({ 38 | value: false, 39 | reason: 'paused', 40 | outcomeType: 'value', 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/flags/src/next/create-flags-discovery-endpoint.ts: -------------------------------------------------------------------------------- 1 | // Must not import anything other than types from next/server, as importing 2 | // the real next/server would prevent flags/next from working in Pages Router. 3 | import type { NextRequest } from 'next/server'; 4 | import { version } from '..'; 5 | import { verifyAccess } from '../lib/verify-access'; 6 | import type { ApiData } from '../types'; 7 | 8 | /** 9 | * Creates the Flags Discovery Endpoint for Next.js, which is a well-known endpoint used 10 | * by Flags Explorer to discover the flags of your application. 11 | * 12 | * @param getApiData a function returning the API data 13 | * @param options accepts a secret 14 | * @returns a Next.js Route Handler 15 | */ 16 | export function createFlagsDiscoveryEndpoint( 17 | getApiData: (request: NextRequest) => Promise | ApiData, 18 | options?: { 19 | secret?: string | undefined; 20 | }, 21 | ) { 22 | return async (request: NextRequest): Promise => { 23 | const access = await verifyAccess( 24 | request.headers.get('Authorization'), 25 | options?.secret, 26 | ); 27 | if (!access) return Response.json(null, { status: 401 }); 28 | 29 | const apiData = await getApiData(request); 30 | return new Response(JSON.stringify(apiData), { 31 | headers: { 32 | 'x-flags-sdk-version': version, 33 | 'content-type': 'application/json', 34 | }, 35 | }); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /examples/snippets/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import localFont from 'next/font/local'; 3 | import './globals.css'; 4 | import { VercelToolbar } from '@vercel/toolbar/next'; 5 | import { ThemeProvider } from '@/components/theme-provider'; 6 | 7 | const geistSans = localFont({ 8 | src: './fonts/GeistVF.woff', 9 | variable: '--font-geist-sans', 10 | weight: '100 900', 11 | }); 12 | const geistMono = localFont({ 13 | src: './fonts/GeistMonoVF.woff', 14 | variable: '--font-geist-mono', 15 | weight: '100 900', 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: 'Flags SDK by Vercel', 20 | description: 'The feature flags SDK by Vercel for Next.js and SvelteKit', 21 | }; 22 | 23 | export default function RootLayout({ 24 | children, 25 | }: Readonly<{ 26 | children: React.ReactNode; 27 | }>) { 28 | const shouldInjectToolbar = process.env.NODE_ENV === 'development'; 29 | return ( 30 | 31 | 34 | 40 | {children} 41 | 42 | {shouldInjectToolbar && } 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/snippets/app/examples/dashboard-pages/page.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from 'next/headers'; 2 | import { DemoFlag } from '@/components/demo-flag'; 3 | import { Button } from '@/components/ui/button'; 4 | import { dashboardFlag } from './flags'; 5 | 6 | export default async function Page() { 7 | const dashboard = await dashboardFlag(); 8 | 9 | return ( 10 | <> 11 | 12 |
13 | 23 | 33 | 43 |
44 | 45 | ); 46 | } 47 | --------------------------------------------------------------------------------