[];
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 |
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 |
--------------------------------------------------------------------------------