The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .changeset
    ├── README.md
    └── config.json
├── .github
    └── workflows
    │   ├── main.yml
    │   └── release.yml
├── .gitignore
├── .npmrc
├── .turbo
    └── config.json
├── LICENSE
├── README.md
├── example
    ├── .env
    ├── .gitignore
    ├── CHANGELOG.md
    ├── README.md
    ├── app
    │   ├── .well-known
    │   │   └── vercel
    │   │   │   └── flags
    │   │   │       └── route.ts
    │   ├── demo
    │   │   ├── client-components
    │   │   │   ├── page-content.tsx
    │   │   │   └── page.tsx
    │   │   ├── layout.tsx
    │   │   └── server-components
    │   │   │   └── page.tsx
    │   └── layout.tsx
    ├── components
    │   ├── Content.tsx
    │   ├── Divider.tsx
    │   ├── EdgeFunctionContent.tsx
    │   ├── HelpBox.tsx
    │   ├── Layout.tsx
    │   ├── Nav.tsx
    │   ├── NavLink-12.tsx
    │   ├── NavLink-13.tsx
    │   ├── Performance.tsx
    │   ├── Result.tsx
    │   └── Switch.tsx
    ├── flags
    │   ├── client.ts
    │   ├── config.ts
    │   ├── edge.ts
    │   └── server.ts
    ├── middleware.ts
    ├── next-env.d.ts
    ├── next.config.js
    ├── package.json
    ├── pages
    │   ├── _app.tsx
    │   ├── demo
    │   │   ├── basic-usage.tsx
    │   │   ├── client-side-rendering.tsx
    │   │   ├── context.tsx
    │   │   ├── disabled-revalidation.tsx
    │   │   ├── dynamics.tsx
    │   │   ├── middleware
    │   │   │   └── [variant].tsx
    │   │   ├── rollouts.tsx
    │   │   ├── server-side-rendering-hybrid.tsx
    │   │   ├── server-side-rendering-pure.tsx
    │   │   ├── static-site-generation-hybrid.tsx
    │   │   ├── static-site-generation-pure.tsx
    │   │   ├── targeting-by-traits.tsx
    │   │   ├── targeting-by-user.tsx
    │   │   └── targeting-by-visitor-key.tsx
    │   ├── docs
    │   │   └── public-api.tsx
    │   ├── index.tsx
    │   └── notes
    │   │   └── simultaneous-invocations-of-use-flags-detected.tsx
    ├── postcss.config.js
    ├── public
    │   ├── favicon.png
    │   ├── github.svg
    │   ├── logo.svg
    │   └── sitemap.xml
    ├── tailwind.config.js
    ├── tsconfig.json
    └── vercel.json
├── package.json
├── package
    ├── ARCHITECTURE.md
    ├── CHANGELOG.md
    ├── README.md
    ├── api-route
    │   └── package.json
    ├── babel.config.js
    ├── client
    │   └── package.json
    ├── config
    │   └── package.json
    ├── context
    │   └── package.json
    ├── edge
    │   └── package.json
    ├── evaluate
    │   └── package.json
    ├── jest.config.js
    ├── jest
    │   ├── delete-all-cookies.ts
    │   └── mutation-observer.js
    ├── package.json
    ├── server
    │   └── package.json
    ├── src
    │   ├── api-route.ts
    │   ├── client.csr.spec.ts
    │   ├── client.spec.ts
    │   ├── client.ssg.spec.ts
    │   ├── client.ssr.spec.ts
    │   ├── client.ts
    │   ├── config.ts
    │   ├── context.ts
    │   ├── edge.spec.ts
    │   ├── edge.ts
    │   ├── evaluate.spec.ts
    │   ├── evaluate.ts
    │   ├── evaluation-types.ts
    │   ├── internal
    │   │   ├── apply-configuration-defaults.ts
    │   │   ├── errors.ts
    │   │   ├── murmur.ts
    │   │   ├── resolve-flag-to-variant.spec.ts
    │   │   ├── resolve-flag-to-variant.ts
    │   │   ├── types.ts
    │   │   └── utils.ts
    │   ├── server.spec.ts
    │   └── server.ts
    ├── tsconfig.json
    └── tsup.config.ts
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json


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


--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
 3 |   "changelog": "@changesets/cli/changelog",
 4 |   "commit": false,
 5 |   "fixed": [],
 6 |   "linked": [],
 7 |   "access": "public",
 8 |   "baseBranch": "master",
 9 |   "updateInternalDependencies": "patch",
10 |   "ignore": []
11 | }
12 | 


--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
 1 | name: CI
 2 | on: [push]
 3 | jobs:
 4 |   build:
 5 |     runs-on: ubuntu-latest
 6 | 
 7 |     steps:
 8 |       - name: Begin CI...
 9 |         uses: actions/checkout@v3
10 | 
11 |       - uses: pnpm/action-setup@v2.2.2
12 |         with:
13 |           version: 8
14 | 
15 |       - name: Use Node
16 |         uses: actions/setup-node@v2
17 |         with:
18 |           node-version: 18.17
19 |           cache: "pnpm"
20 | 
21 |       - name: Use cached node_modules
22 |         uses: actions/cache@v3
23 |         with:
24 |           path: node_modules
25 |           key: nodeModules-${{ hashFiles('**/pnpm-lock.yaml') }}
26 |           restore-keys: |
27 |             nodeModules-
28 | 
29 |       - name: Install dependencies
30 |         run: pnpm install --frozen-lockfile
31 |         env:
32 |           CI: true
33 | 
34 |       - name: Test
35 |         run: pnpm turbo run test -- --ci --coverage --maxWorkers=2
36 |         env:
37 |           CI: true
38 | 
39 |       - name: Build
40 |         run: pnpm build
41 |         env:
42 |           CI: true
43 |           NEXT_PUBLIC_FLAGS_ENDPOINT: https://happykit.dev/api/flags
44 |           NEXT_PUBLIC_FLAGS_ENV_KEY: flags_pub_development_289861443285680649
45 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
 1 | name: Release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches:
 6 |       - master
 7 | 
 8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
 9 | 
10 | jobs:
11 |   release:
12 |     name: Release
13 |     runs-on: ubuntu-latest
14 |     steps:
15 |       - name: Checkout Repo
16 |         uses: actions/checkout@v3
17 | 
18 |       - uses: pnpm/action-setup@v2
19 |         with:
20 |           version: 8
21 | 
22 |       - name: Use Node
23 |         uses: actions/setup-node@v3
24 |         with:
25 |           node-version: 18.17
26 |           cache: "pnpm"
27 | 
28 |       - name: Use cached node_modules
29 |         uses: actions/cache@v3
30 |         with:
31 |           path: node_modules
32 |           key: nodeModules-${{ hashFiles('**/pnpm-lock.yaml') }}
33 |           restore-keys: |
34 |             nodeModules-
35 | 
36 |       - name: Install dependencies
37 |         run: pnpm install --frozen-lockfile
38 |         env:
39 |           CI: true
40 | 
41 |       - name: Create Release Pull Request or Publish to npm
42 |         id: changesets
43 |         uses: changesets/action@v1
44 |         with:
45 |           # This expects you to have a script called release which does a build for your packages and calls changeset publish
46 |           publish: pnpm release
47 |           version: pnpm version-packages
48 |         env:
49 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 | ideas.md
7 | .vercel
8 | .turbo
9 | 


--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | strict-peer-dependencies=false
2 | 


--------------------------------------------------------------------------------
/.turbo/config.json:
--------------------------------------------------------------------------------
1 | {
2 |   "apiurl": "https://vercel.com/api",
3 |   "loginurl": "https://vercel.com",
4 |   "teamid": "team_ItYtoTLMzROnOUyTcndn0dN3"
5 | }


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2021 Dominik Ferber
 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 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
 1 | <a id="nav">
 2 |   <img src="https://i.imgur.com/MS2Gtkj.png" width="100%" />
 3 | </a>
 4 | 
 5 | <div align="right">
 6 |   <a href="https://github.com/happykit/flags/tree/master/package">Documentation</a>
 7 |   <span>&nbsp;•&nbsp;</span>
 8 |   <a href="https://flags.happykit.dev/" target="_blank">Examples</a>
 9 |   <span>&nbsp;•&nbsp;</span>
10 |   <a href="https://medium.com/frontend-digest/using-feature-flags-in-next-js-c5c8d0795a2?source=friends_link&sk=d846a29f376acf9cfa41e926883923ab" target="_blank">Full Tutorial</a>
11 |   <span>&nbsp;•&nbsp;</span>
12 |   <a href="https://happykit.dev/solutions/flags" target="_blank">happykit.dev</a>
13 |   <span>&nbsp;•&nbsp;</span>
14 |   <a href="https://twitter.com/happykitdev" target="_blank">@happykitdev</a>
15 | </div>
16 | 
17 | <br />
18 | 
19 | &nbsp;
20 | 
21 | Add Feature Flags to your Next.js application with a single React Hook. This package integrates your Next.js application with HappyKit Flags. Create a free [happykit.dev](https://happykit.dev/signup) account to get started.
22 | 
23 | ## Key Features
24 | 
25 | - written for Next.js
26 | - integrate using a simple `useFlags()` hook or `getFlags()` function
27 | - supports App Router (Server Components & Client Components)
28 | - only 2 kB gzipped size
29 | - extremely fast flag responses (~50ms)
30 | - supports Server-Side Rendering and Static Site Generation
31 | - supports Middleware and Edge Functions
32 | - supports User Targeting, Custom Rules and Rollouts
33 | 
34 | <br />
35 | 
36 | <details>
37 |   <summary><b>Want to see a demo?</b></summary>
38 | 
39 |   <img alt="HappyKit Flags Demo" src="https://user-images.githubusercontent.com/1765075/94278500-90819000-ff53-11ea-912a-a59cfb491406.gif" />
40 |   <br /><br />
41 | </details>
42 | 
43 | <br />
44 | 
45 | ## Documentation
46 | 
47 | See the [full documentation](https://github.com/happykit/flags/tree/master/package) for setup instructions and usage guides.
48 | 
49 | ## Examples
50 | 
51 | This is roughly what the usage of feature flags looks like once you're up and running.
52 | 
53 | ```js
54 | // pages/index.js
55 | import { useFlags } from "flags/client";
56 | 
57 | export default function IndexPage(props) {
58 |   const flagBag = useFlags();
59 | 
60 |   return flagBag.flags.greeting === "dog" ? "Who's a good boye" : "Hello";
61 | }
62 | ```
63 | 
64 | The self documenting examples at [flags.happykit.dev](https://flags.happykit.dev/) show how to use `@happykit/flags` for client-side, static and server-side rendering.
65 | 
66 | ## Full Tutorial
67 | 
68 | A full tutorial including setup instructions is published on [frontend-digest.com](https://medium.com/frontend-digest/using-feature-flags-in-next-js-c5c8d0795a2?source=friends_link&sk=d846a29f376acf9cfa41e926883923ab).
69 | 


--------------------------------------------------------------------------------
/example/.env:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_FLAGS_ENV_KEY="flags_pub_289861443285680649"
2 | NEXT_PUBLIC_FLAGS_ENDPOINT="https://happykit.dev/api/flags"
3 | # This is a read-only token so it's okay to have visible in this example
4 | HAPPYKIT_API_TOKEN="00f23793-3b03-4d7a-ad5b-72b043ec5292"
5 | # This is used for the Vercel Toolbar. It's okay to commit this for this example
6 | FLAGS_SECRET="aaZXjTRCmxzF3GvKCyA8hkyplk83zJGRmi2sX3Tumuo"
7 | 


--------------------------------------------------------------------------------
/example/.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 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # next.js
12 | /.next/
13 | /out/
14 | 
15 | # production
16 | /build
17 | 
18 | # misc
19 | .DS_Store
20 | *.pem
21 | 
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | 
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 | 
33 | # vercel
34 | .vercel
35 | .env*.local
36 | 


--------------------------------------------------------------------------------
/example/CHANGELOG.md:
--------------------------------------------------------------------------------
 1 | # example
 2 | 
 3 | ## 0.1.7
 4 | 
 5 | ### Patch Changes
 6 | 
 7 | - Updated dependencies [79d13e9]
 8 |   - @happykit/flags@3.3.0
 9 | 
10 | ## 0.1.6
11 | 
12 | ### Patch Changes
13 | 
14 | - Updated dependencies [e473304]
15 |   - @happykit/flags@3.2.0
16 | 
17 | ## 0.1.5
18 | 
19 | ### Patch Changes
20 | 
21 | - Updated dependencies [21a540c]
22 |   - @happykit/flags@3.1.3
23 | 
24 | ## 0.1.4
25 | 
26 | ### Patch Changes
27 | 
28 | - Updated dependencies [c53e69a]
29 |   - @happykit/flags@3.1.2
30 | 
31 | ## 0.1.3
32 | 
33 | ### Patch Changes
34 | 
35 | - Updated dependencies [d509435]
36 |   - @happykit/flags@3.1.1
37 | 
38 | ## 0.1.2
39 | 
40 | ### Patch Changes
41 | 
42 | - Updated dependencies [b3c53da]
43 |   - @happykit/flags@3.1.0
44 | 
45 | ## 0.1.1
46 | 
47 | ### Patch Changes
48 | 
49 | - Updated dependencies [1822587]
50 |   - @happykit/flags@3.0.0
51 | 


--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
 1 | # Example Next.js application using˘ `@happykit/flags`
 2 | 
 3 | ## Files
 4 | 
 5 | ### `pages/_app.tsx`
 6 | 
 7 | This file contians the setup of `@happykit/flags`. The Environment Key is preconfigured in `.env.development` to load the flags from a HappyKit example project.
 8 | 
 9 | Note that in your own application, you'll need to configure an environment variable called `NEXT_PUBLIC_FLAGS_ENV_KEY` and set it to your Flags Environment Key. You can find this key in your project settings on [happykit.dev](https://happykit.dev).
10 | 
11 | 
12 | 
13 | 
14 | 


--------------------------------------------------------------------------------
/example/app/.well-known/vercel/flags/route.ts:
--------------------------------------------------------------------------------
 1 | import { type NextRequest, NextResponse } from "next/server";
 2 | import { type ApiData, verifyAccess } from "@vercel/flags";
 3 | import { getHappyKitData } from "@vercel/flags/providers/happykit";
 4 | 
 5 | export async function GET(request: NextRequest) {
 6 |   const access = await verifyAccess(request.headers.get("Authorization"));
 7 |   if (!access) return NextResponse.json(null, { status: 401 });
 8 | 
 9 |   const apiData = await getHappyKitData({
10 |     apiToken: process.env.HAPPYKIT_API_TOKEN!,
11 |     envKey: process.env.NEXT_PUBLIC_FLAGS_ENV_KEY!,
12 |   });
13 | 
14 |   return NextResponse.json<ApiData>(apiData);
15 | }
16 | 


--------------------------------------------------------------------------------
/example/app/demo/client-components/page-content.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | import { Result } from "components/Result";
 3 | import { useFlags } from "flags/client";
 4 | 
 5 | export function PageContent() {
 6 |   // pure client-side rendering without initial server-side data
 7 |   const flagBag = useFlags();
 8 | 
 9 |   return (
10 |     <article className="py-4 prose max-w-prose">
11 |       <p>
12 |         This demo shows how to use <code>@happykit/flags</code> for client
13 |         components in Next.js App Router.
14 |       </p>
15 |       <Result key="server-components" label="Flags" value={flagBag} />
16 |     </article>
17 |   );
18 | }
19 | 


--------------------------------------------------------------------------------
/example/app/demo/client-components/page.tsx:
--------------------------------------------------------------------------------
 1 | import { Content } from "components/Content";
 2 | import { PageContent } from "./page-content";
 3 | 
 4 | const title = "Client Components";
 5 | export const metadata = {
 6 |   title: `${title} · HappyKit Flags Documentation`,
 7 |   description: "Feature Flags for Next.js",
 8 | };
 9 | 
10 | export default async function ClientComponentPage() {
11 |   return (
12 |     <Content
13 |       title={title}
14 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/app/demo/client-components/page.tsx`}
15 |     >
16 |       {/* Page Content is extracted so we can make this a client-component demo  */}
17 |       <PageContent />
18 |     </Content>
19 |   );
20 | }
21 | 


--------------------------------------------------------------------------------
/example/app/demo/layout.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | import * as React from "react";
  3 | import { Nav } from "components/Nav";
  4 | import { NavLink } from "components/NavLink-13";
  5 | import { Performance } from "components/Performance";
  6 | import { Transition } from "@tailwindui/react";
  7 | 
  8 | export default function Layout(props: { children: React.ReactNode }) {
  9 |   const [expanded, setExpanded] = React.useState<boolean>(false);
 10 | 
 11 |   return (
 12 |     <div className="h-screen flex overflow-hidden bg-gray-100">
 13 |       {/* Off-canvas menu for mobile, show/hide based on off-canvas menu state. */}
 14 |       {expanded && (
 15 |         <div className="md:hidden">
 16 |           <div className="fixed inset-0 flex z-40">
 17 |             <Transition
 18 |               show={expanded}
 19 |               enter="transition-opacity ease-linear duration-300"
 20 |               enterFrom="opacity-0"
 21 |               enterTo="opacity-100"
 22 |               leave="transition-opacity ease-linear duration-300"
 23 |               leaveFrom="opacity-100"
 24 |               leaveTo="opacity-0"
 25 |             >
 26 |               <div className="fixed inset-0">
 27 |                 <div className="absolute inset-0 bg-gray-600 opacity-75" />
 28 |               </div>
 29 |             </Transition>
 30 |             <Transition
 31 |               show={expanded}
 32 |               enter="transition ease-in-out duration-300 transform"
 33 |               enterFrom="-translate-x-full"
 34 |               enterTo="translate-x-0"
 35 |               leave="transition ease-in-out duration-300 transform"
 36 |               leaveFrom="translate-x-0"
 37 |               leaveTo="-translate-x-full"
 38 |             >
 39 |               <div className="relative flex-1 flex flex-col max-w-xs w-full h-full bg-white">
 40 |                 <div className="absolute top-0 right-0 -mr-12 pt-2">
 41 |                   <button
 42 |                     type="button"
 43 |                     className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
 44 |                     onClick={() => setExpanded(false)}
 45 |                   >
 46 |                     <span className="sr-only">Close sidebar</span>
 47 |                     {/* Heroicon name: outline/x */}
 48 |                     <svg
 49 |                       className="h-6 w-6 text-white"
 50 |                       xmlns="http://www.w3.org/2000/svg"
 51 |                       fill="none"
 52 |                       viewBox="0 0 24 24"
 53 |                       stroke="currentColor"
 54 |                       aria-hidden="true"
 55 |                     >
 56 |                       <path
 57 |                         strokeLinecap="round"
 58 |                         strokeLinejoin="round"
 59 |                         strokeWidth={2}
 60 |                         d="M6 18L18 6M6 6l12 12"
 61 |                       />
 62 |                     </svg>
 63 |                   </button>
 64 |                 </div>
 65 |                 <div className="flex-shrink-0 flex items-center px-4 pt-5">
 66 |                   <img
 67 |                     className="h-8 w-auto"
 68 |                     src="/logo.svg"
 69 |                     alt="HappyKit Logo"
 70 |                   />
 71 |                 </div>
 72 |                 <div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
 73 |                   <nav
 74 |                     className="mt-5 flex-1 pl-2 pr-4 space-y-1 bg-white"
 75 |                     aria-label="Sidebar"
 76 |                   >
 77 |                     <Nav NavLink={NavLink} />
 78 |                   </nav>
 79 |                 </div>
 80 |                 <div className="flex-shrink-0 flex border-t border-gray-200 p-4">
 81 |                   <a
 82 |                     href="https://github.com/happykit/flags"
 83 |                     className="flex-shrink-0 group block"
 84 |                   >
 85 |                     <div className="flex items-center">
 86 |                       <div>
 87 |                         <img
 88 |                           className="inline-block h-10 w-10 rounded-full"
 89 |                           src="/github.svg"
 90 |                           alt=""
 91 |                         />
 92 |                       </div>
 93 |                       <div className="ml-3">
 94 |                         <p className="text-sm font-mono font-light text-gray-700 group-hover:text-gray-900">
 95 |                           @happykit/flags
 96 |                         </p>
 97 |                         <p className="text-xs font-medium text-gray-500 group-hover:text-gray-700">
 98 |                           Open on GitHub
 99 |                         </p>
100 |                       </div>
101 |                     </div>
102 |                   </a>
103 |                 </div>
104 |               </div>
105 |               <div className="flex-shrink-0 w-14">
106 |                 {/* Force sidebar to shrink to fit close icon */}
107 |               </div>
108 |             </Transition>
109 |           </div>
110 |         </div>
111 |       )}
112 |       {/* Static sidebar for desktop */}
113 |       <div className="hidden md:flex md:flex-shrink-0">
114 |         <div className="flex flex-col w-80">
115 |           <div className="flex flex-col h-0 flex-1 border-r border-gray-200 bg-white">
116 |             <div className="flex items-center flex-shrink-0 px-4 pt-5">
117 |               <img className="h-8 w-auto" src="/logo.svg" alt="HappyKit Logo" />
118 |             </div>
119 |             <div className="flex-1 flex flex-col pb-4 overflow-y-auto">
120 |               <div className="mt-3 flex-1 px-2 bg-white space-y-1">
121 |                 <div className="mt-3 flex-grow flex flex-col">
122 |                   <nav
123 |                     className="flex-1 px-2 space-y-1 bg-white"
124 |                     aria-label="Sidebar"
125 |                   >
126 |                     <Nav NavLink={NavLink} />
127 |                   </nav>
128 |                 </div>
129 |               </div>
130 |             </div>
131 |             <div className="flex-shrink-0 flex border-t border-gray-200 p-4">
132 |               <a
133 |                 href="https://github.com/happykit/flags"
134 |                 className="flex-shrink-0 w-full group block"
135 |               >
136 |                 <div className="flex items-center">
137 |                   <div>
138 |                     <img
139 |                       className="inline-block h-9 w-9 rounded-full"
140 |                       src="/github.svg"
141 |                       alt=""
142 |                     />
143 |                   </div>
144 |                   <div className="ml-3">
145 |                     <p className="text-sm font-mono font-light text-gray-700 group-hover:text-gray-900">
146 |                       @happykit/flags
147 |                     </p>
148 |                     <p className="text-xs font-medium text-gray-500 group-hover:text-gray-700">
149 |                       Open on GitHub
150 |                     </p>
151 |                   </div>
152 |                 </div>
153 |               </a>
154 |             </div>
155 |           </div>
156 |         </div>
157 |       </div>
158 |       <div className="flex flex-col w-0 flex-1 overflow-hidden">
159 |         <div className="md:hidden pl-1 pt-1 sm:pl-3 sm:pt-3">
160 |           <button
161 |             className="-ml-0.5 -mt-0.5 h-12 w-12 inline-flex items-center justify-center rounded-md text-gray-500 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
162 |             type="button"
163 |             onClick={() => setExpanded(true)}
164 |           >
165 |             <span className="sr-only">Open sidebar</span>
166 |             {/* Heroicon name: outline/menu */}
167 |             <svg
168 |               className="h-6 w-6"
169 |               xmlns="http://www.w3.org/2000/svg"
170 |               fill="none"
171 |               viewBox="0 0 24 24"
172 |               stroke="currentColor"
173 |               aria-hidden="true"
174 |             >
175 |               <path
176 |                 strokeLinecap="round"
177 |                 strokeLinejoin="round"
178 |                 strokeWidth={2}
179 |                 d="M4 6h16M4 12h16M4 18h16"
180 |               />
181 |             </svg>
182 |           </button>
183 |         </div>
184 |         <main
185 |           className="flex-1 relative z-0 overflow-y-auto focus:outline-none flex flex-col min-h-screen"
186 |           tabIndex={0}
187 |         >
188 |           {props.children}
189 |           <Performance flagBag={null} />
190 |         </main>
191 |       </div>
192 |     </div>
193 |   );
194 | }
195 | 


--------------------------------------------------------------------------------
/example/app/demo/server-components/page.tsx:
--------------------------------------------------------------------------------
 1 | import { Content } from "components/Content";
 2 | import { Result } from "components/Result";
 3 | import { getFlags } from "flags/server";
 4 | import { cookies } from "next/headers";
 5 | 
 6 | const title = "Server Components";
 7 | export const metadata = {
 8 |   title: `${title} · HappyKit Flags Documentation`,
 9 |   description: "Feature Flags for Next.js",
10 | };
11 | 
12 | export default async function ServerComponentPage() {
13 |   const visitorKey = cookies().get("hkvk")?.value;
14 |   const flags = await getFlags({ visitorKey });
15 | 
16 |   return (
17 |     <Content
18 |       title={title}
19 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/app/demo/server-components/page.tsx`}
20 |     >
21 |       <article className="py-4 prose max-w-prose">
22 |         <p>
23 |           This demo shows how to use <code>@happykit/flags</code> for server
24 |           components in Next.js App Router.
25 |         </p>
26 |         <p>
27 |           Since this page is rendered on the server only, there is no{" "}
28 |           <code>flagBag</code>. Instead, the values are shown directly.
29 |         </p>
30 |         <Result key="server-components" label="Flags" value={flags} />
31 |       </article>
32 |     </Content>
33 |   );
34 | }
35 | 


--------------------------------------------------------------------------------
/example/app/layout.tsx:
--------------------------------------------------------------------------------
 1 | import "tailwindcss/tailwind.css";
 2 | import { VercelToolbar } from "@vercel/toolbar/next";
 3 | 
 4 | export const metadata = {
 5 |   title: "HappyKit Flags Documentation",
 6 |   description: "Feature Flags for Next.js",
 7 | };
 8 | 
 9 | export default function RootLayout({
10 |   children,
11 | }: {
12 |   children: React.ReactNode;
13 | }) {
14 |   return (
15 |     <html lang="en">
16 |       <head>
17 |         <link rel="icon" href="/favicon.png" />
18 |         {process.env.NODE_ENV === "production" && (
19 |           <script
20 |             async
21 |             defer
22 |             src="https://plausible.io/js/plausible.js"
23 |             data-domain="flags.happykit.dev"
24 |           />
25 |         )}
26 |       </head>
27 |       <body>
28 |         {children}
29 |         {
30 |           // shows the toolbar when developing locally or in preview, but not in production
31 |           process.env.NEXT_PUBLIC_FLAGS_ENV_KEY &&
32 |           /^flags_pub_(development|preview)_/.test(
33 |             process.env.NEXT_PUBLIC_FLAGS_ENV_KEY
34 |           ) ? (
35 |             <VercelToolbar />
36 |           ) : null
37 |         }
38 |       </body>
39 |     </html>
40 |   );
41 | }
42 | 


--------------------------------------------------------------------------------
/example/components/Content.tsx:
--------------------------------------------------------------------------------
 1 | export function Content(props: {
 2 |   title: string;
 3 |   source?: string;
 4 |   children: React.ReactNode;
 5 | }) {
 6 |   return (
 7 |     <div className="py-6 max-w-prose flex-auto">
 8 |       <div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
 9 |         <h1 className="text-2xl font-semibold text-gray-900">{props.title}</h1>
10 |         {props.source && (
11 |           <div className="mt-4 rounded-md bg-blue-50 border border-blue-200 p-4">
12 |             <div className="flex">
13 |               <div className="flex-shrink-0">
14 |                 {/* Heroicon name: solid/information-circle */}
15 |                 <svg
16 |                   className="h-5 w-5 text-blue-400"
17 |                   xmlns="http://www.w3.org/2000/svg"
18 |                   viewBox="0 0 20 20"
19 |                   fill="currentColor"
20 |                   aria-hidden="true"
21 |                 >
22 |                   <path
23 |                     fillRule="evenodd"
24 |                     d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
25 |                     clipRule="evenodd"
26 |                   />
27 |                 </svg>
28 |               </div>
29 |               <div className="ml-3">
30 |                 <p className="text-sm text-blue-600">
31 |                   This demo is easiest to understand if you open its source code
32 |                   in a parallel tab.
33 |                 </p>
34 |                 <p className="mt-3 text-sm">
35 |                   <a
36 |                     href={props.source}
37 |                     target="_blank"
38 |                     className="whitespace-nowrap font-medium text-blue-600 hover:text-blue-700"
39 |                   >
40 |                     Source code <span aria-hidden="true">→</span>
41 |                   </a>
42 |                 </p>
43 |               </div>
44 |             </div>
45 |           </div>
46 |         )}
47 |       </div>
48 |       <div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
49 |         {props.children}
50 |       </div>
51 |     </div>
52 |   );
53 | }
54 | 


--------------------------------------------------------------------------------
/example/components/Divider.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | export function Divider(props: { children: React.ReactNode }) {
 4 |   return (
 5 |     <div className="relative">
 6 |       <div className="absolute inset-0 flex items-center" aria-hidden="true">
 7 |         <div className="w-full border-t border-gray-300" />
 8 |       </div>
 9 |       <div className="relative flex justify-center">
10 |         <span className="px-2 bg-gray-100 text-sm text-gray-500">
11 |           {props.children}
12 |         </span>
13 |       </div>
14 |     </div>
15 |   );
16 | }
17 | 


--------------------------------------------------------------------------------
/example/components/EdgeFunctionContent.tsx:
--------------------------------------------------------------------------------
 1 | import React from "react";
 2 | import type { AppFlags } from "flags/config";
 3 | 
 4 | export function EdgeFunctionContent(props: {
 5 |   checkoutVariant: AppFlags["checkout"];
 6 | }) {
 7 |   return (
 8 |     <React.Fragment>
 9 |       <p>
10 |         This demo shows how to use <code>@happykit/flags</code> in{" "}
11 |         <a
12 |           href="https://nextjs.org/blog/next-12#introducing-middleware"
13 |           rel="noreferrer noopener"
14 |         >
15 |           Next.js Middleware
16 |         </a>
17 |         .
18 |       </p>
19 |       <pre>
20 |         You have been served the "<code>{props.checkoutVariant}</code>" checkout
21 |         variant.
22 |       </pre>
23 |       <button
24 |         type="button"
25 |         className="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
26 |         onClick={() => {
27 |           document.cookie =
28 |             "hkvk=; path=/; maxAge=0; expires=Thu, 01 Jan 1970 00:00:01 GMT";
29 |           window.location.reload();
30 |         }}
31 |       >
32 |         Remove cookie and reload
33 |       </button>
34 |       <p>
35 |         This example uses a <code>middleware</code> file at{" "}
36 |         <code>/middleware.ts</code> to statically render different variants for
37 |         the <code>/demo/middleware</code> path. The different variants live
38 |         under <code>pages/demo/middleware/[variant].tsx</code>.
39 |       </p>
40 |       <p>
41 |         The middleware loads the flags and rewrites the incoming request either
42 |         to <code>/demo/middleware/short</code>,{" "}
43 |         <code>/demo/middleware/medium</code> or{" "}
44 |         <code>/demo/middleware/full</code> depending on the resolved flag
45 |         variant.
46 |       </p>
47 |       <p>
48 |         The request to any of those paths will then be handled by{" "}
49 |         <code>/demo/middleware/[variant].tsx</code>. That file will have
50 |         staticaly generated a version for each variant. The request will thus
51 |         get answered statically.
52 |       </p>
53 |       <p>
54 |         Since the resulting page is served statically from the edge, the first
55 |         render will use no visitor key. This is necessary as the concept of a
56 |         visitor does not exist during static site generation. Thus all rules and
57 |         percentage-based rollouts targeting a visitor resolve to{" "}
58 |         <code>null</code>.
59 |       </p>
60 |       <p>
61 |         The middleware loads the flags purely to decide where to rewrite the
62 |         request to. It does not send any resolved flags into the application
63 |         itself.
64 |       </p>
65 |       <p>
66 |         You are however free to call <code>useFlags()</code> on the client and
67 |         combine this approach with the middleware.
68 |       </p>
69 |     </React.Fragment>
70 |   );
71 | }
72 | 


--------------------------------------------------------------------------------
/example/components/HelpBox.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | export function HelpBox() {
 4 |   return (
 5 |     <div className="rounded-md bg-blue-50 border-blue-200 border px-4 py-2">
 6 |       <div className="flex items-center">
 7 |         <div className="flex-shrink-0">
 8 |           <svg
 9 |             className="h-5 w-5 text-blue-400"
10 |             xmlns="http://www.w3.org/2000/svg"
11 |             viewBox="0 0 20 20"
12 |             fill="currentColor"
13 |             aria-hidden="true"
14 |           >
15 |             <path
16 |               fillRule="evenodd"
17 |               d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
18 |               clipRule="evenodd"
19 |             />
20 |           </svg>
21 |         </div>
22 |         <div className="ml-3 flex-1 md:flex md:justify-between">
23 |           <p className="text-sm text-blue-600">
24 |             In case something is unclear, feel free to open an issue at{" "}
25 |             <a href="https://github.com/happykit/flags">
26 |               github.com/happykit/flags
27 |             </a>{" "}
28 |             or send a DM to{" "}
29 |             <a href="https://twitter.com/happykitdev">@happykitdev</a>.
30 |           </p>
31 |         </div>
32 |       </div>
33 |     </div>
34 |   );
35 | }
36 | 


--------------------------------------------------------------------------------
/example/components/Nav.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | export function Nav({
 4 |   NavLink,
 5 | }: {
 6 |   NavLink: (props: {
 7 |     href: string;
 8 |     children: React.ReactNode;
 9 |     indent?: boolean | undefined;
10 |   }) => JSX.Element;
11 | }) {
12 |   return (
13 |     <React.Fragment>
14 |       <div>
15 |         <NavLink href="/">Introduction</NavLink>
16 |       </div>
17 |       <div>
18 |         <NavLink href="/demo/basic-usage">Basic Usage</NavLink>
19 |       </div>
20 |       <div className="bg-white text-gray-600 group w-full flex items-center pl-7 pr-2 py-2 text-sm font-medium rounded-md">
21 |         App Router
22 |       </div>
23 |       <div className="space-y-1 list-disc">
24 |         <NavLink indent href="/demo/client-components">
25 |           Client Components
26 |         </NavLink>
27 |         <NavLink indent href="/demo/server-components">
28 |           Server Components
29 |         </NavLink>
30 |       </div>
31 |       <div className="bg-white text-gray-600 group w-full flex items-center pl-7 pr-2 py-2 text-sm font-medium rounded-md">
32 |         Pages Router
33 |       </div>
34 |       <div className="space-y-1 list-disc">
35 |         <NavLink indent href="/demo/client-side-rendering">
36 |           Client-Side Rendering
37 |         </NavLink>
38 |         <NavLink indent href="/demo/server-side-rendering-pure">
39 |           Server-Side Rendering (Pure)
40 |         </NavLink>
41 |         <NavLink indent href="/demo/server-side-rendering-hybrid">
42 |           Server-Side Rendering (Hybrid)
43 |         </NavLink>
44 |         <NavLink indent href="/demo/static-site-generation-pure">
45 |           Static Site Generation (Pure)
46 |         </NavLink>
47 |         <NavLink indent href="/demo/static-site-generation-hybrid">
48 |           Static Site Generation (Hybrid)
49 |         </NavLink>
50 |         <NavLink indent href="/demo/middleware">
51 |           Middleware
52 |         </NavLink>
53 |       </div>
54 |       <div className="bg-white text-gray-600 group w-full flex items-center pl-7 pr-2 py-2 text-sm font-medium rounded-md">
55 |         Targeting and Rules
56 |       </div>
57 |       <div className="space-y-1">
58 |         <NavLink indent href="/demo/targeting-by-visitor-key">
59 |           Targeting by Visitor Key
60 |         </NavLink>
61 |         <NavLink indent href="/demo/targeting-by-user">
62 |           Targeting by User
63 |         </NavLink>
64 |         <NavLink indent href="/demo/targeting-by-traits">
65 |           Targeting by Traits
66 |         </NavLink>
67 |       </div>
68 |       <div>
69 |         <NavLink href="/demo/rollouts">Rollouts</NavLink>
70 |       </div>
71 |       <div className="bg-white text-gray-600 group w-full flex items-center pl-7 pr-2 py-2 text-sm font-medium rounded-md">
72 |         Options and Patterns
73 |       </div>
74 |       <div className="space-y-1">
75 |         <NavLink indent href="/demo/disabled-revalidation">
76 |           Disabled Revalidation
77 |         </NavLink>
78 |         <NavLink indent href="/demo/context">
79 |           Context
80 |         </NavLink>
81 |       </div>
82 |       <div>
83 |         <NavLink href="/demo/dynamics">Dynamics</NavLink>
84 |       </div>
85 |       <div>
86 |         <NavLink href="/docs/public-api">Public API</NavLink>
87 |       </div>
88 |     </React.Fragment>
89 |   );
90 | }
91 | 


--------------------------------------------------------------------------------
/example/components/NavLink-12.tsx:
--------------------------------------------------------------------------------
 1 | // This component is duplicated as NavLink-12 and NavLink-13
 2 | //   - NavLink-12 is used by the pages directory
 3 | //   - NavLink-13 is used by the app directory
 4 | // This is necessary as the router is accessed differently.
 5 | import * as React from "react";
 6 | import Link from "next/link";
 7 | import { useRouter } from "next/dist/client/router";
 8 | 
 9 | export function NavLink(props: {
10 |   href: string;
11 |   children: React.ReactNode;
12 |   indent?: boolean;
13 | }) {
14 |   const router = useRouter();
15 |   const active =
16 |     router.asPath === props.href ||
17 |     // TODO this is a workaround since the _middeware's rewrite messes with asPath
18 |     (props.href === "/demo/middleware" && router.asPath.startsWith(props.href));
19 |   return (
20 |     <Link
21 |       href={props.href}
22 |       className={[
23 |         "group w-full flex items-center pr-2 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50",
24 |         props.indent ? "pl-10" : "pl-7",
25 |         active
26 |           ? "bg-gray-100 text-gray-900"
27 |           : "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
28 |       ].join(" ")}
29 |     >
30 |       {props.children}
31 |     </Link>
32 |   );
33 | }
34 | 


--------------------------------------------------------------------------------
/example/components/NavLink-13.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | // This component is duplicated as NavLink-12 and NavLink-13
 3 | //   - NavLink-12 is used by the pages directory
 4 | //   - NavLink-13 is used by the app directory
 5 | // This is necessary as the router is accessed differently.
 6 | import * as React from "react";
 7 | import Link from "next/link";
 8 | import { useSelectedLayoutSegment } from "next/navigation";
 9 | 
10 | export function NavLink(props: {
11 |   href: string;
12 |   children: React.ReactNode;
13 |   indent?: boolean;
14 | }) {
15 |   const activeSegment = useSelectedLayoutSegment();
16 |   const active = activeSegment ? props.href.endsWith(activeSegment) : false;
17 | 
18 |   return (
19 |     <Link
20 |       href={props.href}
21 |       className={[
22 |         "group w-full flex items-center pr-2 py-2 text-sm font-medium text-gray-600 rounded-md hover:text-gray-900 hover:bg-gray-50",
23 |         props.indent ? "pl-10" : "pl-7",
24 |         active
25 |           ? "bg-gray-100 text-gray-900"
26 |           : "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
27 |       ].join(" ")}
28 |     >
29 |       {props.children}
30 |     </Link>
31 |   );
32 | }
33 | 


--------------------------------------------------------------------------------
/example/components/Performance.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | import * as React from "react";
 3 | import type { FlagBag } from "@happykit/flags/client";
 4 | 
 5 | export function Performance(props: { flagBag: FlagBag | null }) {
 6 |   // has to be done this way to avoid differing output in client and server render
 7 |   const [supportsPerformanceMetrics, setSupportsPerformanceMetrics] =
 8 |     React.useState<boolean>(false);
 9 | 
10 |   React.useEffect(
11 |     () => setSupportsPerformanceMetrics(typeof performance !== "undefined"),
12 |     []
13 |   );
14 | 
15 |   const [performanceEntry, setPerformanceEntry] =
16 |     React.useState<null | PerformanceResourceTiming>(null);
17 | 
18 |   React.useEffect(() => {
19 |     if (typeof performance === "undefined") return;
20 |     if (props.flagBag && props.flagBag.settled) {
21 |       const entries = performance
22 |         .getEntriesByType("resource")
23 |         .filter((entry) => {
24 |           return entry.name.endsWith(
25 |             [
26 |               process.env.NEXT_PUBLIC_FLAGS_ENDPOINT!,
27 |               process.env.NEXT_PUBLIC_FLAGS_ENV_KEY!,
28 |             ].join("/")
29 |           );
30 |         });
31 | 
32 |       if (entries.length > 0) {
33 |         setPerformanceEntry(
34 |           entries[entries.length - 1] as PerformanceResourceTiming
35 |         );
36 |       }
37 |     }
38 |   }, [props.flagBag]);
39 | 
40 |   // clear timings so the next page doesn't accidentally load timings
41 |   // of the current page
42 |   React.useEffect(() => {
43 |     if (typeof performance === "undefined") return;
44 | 
45 |     return () => {
46 |       performance.clearResourceTimings();
47 |       performance.clearMeasures();
48 |       performance.clearMarks();
49 |     };
50 |   }, []);
51 | 
52 |   if (!supportsPerformanceMetrics) return null;
53 |   return (
54 |     <div className="bg-gray-100">
55 |       <hr />
56 |       <div className="pt-3 pb-1 font-semibold max-w-7xl mx-auto px-4 sm:px-6 md:px-8 text-gray-500 uppercase tracking-wide text-sm">
57 |         Performance
58 |       </div>
59 |       {performanceEntry ? (
60 |         <div className="pb-3 max-w-7xl mx-auto px-4 sm:px-6 md:px-8 text-gray-500 text-sm">
61 |           The last flag evaluation request took{" "}
62 |           {Math.floor(performanceEntry.duration)}ms.{" "}
63 |           {Math.floor(performanceEntry.duration) < 100 && (
64 |             <React.Fragment>
65 |               For comparison:{" "}
66 |               <a
67 |                 href="https://en.wikipedia.org/wiki/Blinking"
68 |                 className="hover:underline"
69 |                 rel="noopener noreferrer"
70 |                 target="_blank"
71 |               >
72 |                 The blink of a human eye takes 100ms
73 |               </a>
74 |               .
75 |             </React.Fragment>
76 |           )}
77 |         </div>
78 |       ) : (
79 |         <div className="pb-3 max-w-7xl mx-auto px-4 sm:px-6 md:px-8 text-gray-500 text-sm">
80 |           No feature flags loaded by the browser so far.
81 |         </div>
82 |       )}
83 |     </div>
84 |   );
85 | }
86 | 


--------------------------------------------------------------------------------
/example/components/Result.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | import * as React from "react";
 3 | 
 4 | export function has<X extends {}, Y extends PropertyKey>(
 5 |   obj: X,
 6 |   prop: Y
 7 | ): obj is X & Record<Y, unknown> {
 8 |   return Object.prototype.hasOwnProperty.call(obj, prop);
 9 | }
10 | 
11 | // source: https://github.com/lukeed/dequal/blob/master/src/lite.js
12 | export function deepEqual(objA: any, objB: any) {
13 |   var ctor, len;
14 |   if (objA === objB) return true;
15 | 
16 |   if (objA && objB && (ctor = objA.constructor) === objB.constructor) {
17 |     if (ctor === Date) return objA.getTime() === objB.getTime();
18 |     if (ctor === RegExp) return objA.toString() === objB.toString();
19 | 
20 |     if (ctor === Array) {
21 |       if ((len = objA.length) === objB.length) {
22 |         while (len-- && deepEqual(objA[len], objB[len]));
23 |       }
24 |       return len === -1;
25 |     }
26 | 
27 |     if (!ctor || typeof objA === "object") {
28 |       len = 0;
29 |       for (ctor in objA) {
30 |         if (has(objA, ctor) && ++len && !has(objB, ctor)) return false;
31 |         if (!(ctor in objB) || !deepEqual(objA[ctor], objB[ctor])) return false;
32 |       }
33 |       return Object.keys(objB).length === len;
34 |     }
35 |   }
36 | 
37 |   return objA !== objA && objB !== objB;
38 | }
39 | 
40 | const quotelessJson = (obj: any) => {
41 |   const json = JSON.stringify(obj, null, 2);
42 |   return json.replace(/"([^"]+)":/g, "$1:");
43 | };
44 | 
45 | export function Result(props: { value: any; label?: string }) {
46 |   const [results, setResults] = React.useState<{ key: number; value: any }[]>([
47 |     { key: 0, value: props.value },
48 |   ]);
49 |   const [currentResult, ...previousResults] = results;
50 | 
51 |   React.useEffect(() => {
52 |     setResults((prev) => {
53 |       if (deepEqual(props.value, prev[0].value)) return prev;
54 |       return [{ key: prev[0].key + 1, value: props.value }, ...prev];
55 |     });
56 |   }, [props.value]);
57 | 
58 |   return (
59 |     <div className="mt-4">
60 |       <pre className="font-mono rounded p-2">
61 |         <div className="text-gray-400 text-xs pb-1">
62 |           {props.label || `Render #${results.length} (Current render)`}
63 |         </div>
64 |         {quotelessJson(currentResult.value)}
65 |       </pre>
66 |       {previousResults.length > 0 && (
67 |         <details className="my-1">
68 |           <summary className="p-1">History ({previousResults.length})</summary>
69 | 
70 |           <div className="p-2 text-md">
71 |             Previous return values of the <code>useFlags()</code> hook.
72 |           </div>
73 | 
74 |           {previousResults.map((result, index) => (
75 |             <pre key={result.key} className="font-mono rounded p-2">
76 |               <div className="text-gray-400 text-xs pb-1">
77 |                 Render #{previousResults.length - index}
78 |               </div>
79 |               {quotelessJson(result.value)}
80 |             </pre>
81 |           ))}
82 |         </details>
83 |       )}
84 |     </div>
85 |   );
86 | }
87 | 


--------------------------------------------------------------------------------
/example/components/Switch.tsx:
--------------------------------------------------------------------------------
 1 | import clsx from "clsx";
 2 | 
 3 | export function Switch(props: {
 4 |   id: string;
 5 |   label: string;
 6 |   active?: boolean;
 7 |   onClick: React.DOMAttributes<HTMLButtonElement>["onClick"];
 8 | }) {
 9 |   return (
10 |     <div className="flex items-center">
11 |       <button
12 |         type="button"
13 |         aria-pressed={props.active}
14 |         aria-labelledby={props.id}
15 |         onClick={props.onClick}
16 |         className={clsx(
17 |           "bg-gray-200 relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500",
18 |           props.active ? "bg-indigo-600" : "bg-gray-200"
19 |         )}
20 |       >
21 |         <span className="sr-only">{props.label}</span>
22 |         <span
23 |           aria-hidden="true"
24 |           className={clsx(
25 |             "translate-x-0 pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200",
26 |             props.active ? "translate-x-5" : "translate-x-0"
27 |           )}
28 |         ></span>
29 |       </button>
30 |       <span className="ml-3 flex items-center" id={props.id}>
31 |         <span className="text-sm font-medium text-gray-900">{props.label}</span>
32 |       </span>
33 |     </div>
34 |   );
35 | }
36 | 


--------------------------------------------------------------------------------
/example/flags/client.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   createUseFlags,
 3 |   type InitialFlagState as GenericInitialFlagState,
 4 | } from "@happykit/flags/client";
 5 | import { createUseFlagBag } from "@happykit/flags/context";
 6 | import { type AppFlags, config } from "./config";
 7 | 
 8 | export type InitialFlagState = GenericInitialFlagState<AppFlags>;
 9 | export const useFlags = createUseFlags<AppFlags>(config);
10 | export const useFlagBag = createUseFlagBag<AppFlags>();
11 | 


--------------------------------------------------------------------------------
/example/flags/config.ts:
--------------------------------------------------------------------------------
 1 | import type { Configuration } from "@happykit/flags/config";
 2 | 
 3 | export type AppFlags = {
 4 |   ads: boolean;
 5 |   checkout: "short" | "medium" | "full";
 6 |   discount: 5 | 10 | 15;
 7 |   purchaseButtonLabel: string;
 8 | };
 9 | 
10 | export const config: Configuration<AppFlags> = {
11 |   envKey: process.env.NEXT_PUBLIC_FLAGS_ENV_KEY!,
12 | 
13 |   // You can just delete this line in your own application.
14 |   // It's only here because we use it while working on @happykit/flags itself.
15 |   endpoint: process.env.NEXT_PUBLIC_FLAGS_ENDPOINT,
16 | 
17 |   // You can uncomment this if you do not want to set the visitorKey cookie
18 |   // serializeVisitorKeyCookie: () => null,
19 | };
20 | 


--------------------------------------------------------------------------------
/example/flags/edge.ts:
--------------------------------------------------------------------------------
1 | import { createGetEdgeFlags } from "@happykit/flags/edge";
2 | import { type AppFlags, config } from "./config";
3 | 
4 | export const getEdgeFlags = createGetEdgeFlags<AppFlags>(config);
5 | export { ensureVisitorKeyCookie } from "@happykit/flags/edge";
6 | 


--------------------------------------------------------------------------------
/example/flags/server.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   createGetFlags,
 3 |   type GenericEvaluationResponseBody,
 4 | } from "@happykit/flags/server";
 5 | import { type AppFlags, config } from "./config";
 6 | 
 7 | export type EvaluationResponseBody = GenericEvaluationResponseBody<AppFlags>;
 8 | 
 9 | export const getFlags = createGetFlags<AppFlags>(config);
10 | 


--------------------------------------------------------------------------------
/example/middleware.ts:
--------------------------------------------------------------------------------
 1 | import { NextRequest, NextResponse } from "next/server";
 2 | import { getEdgeFlags, ensureVisitorKeyCookie } from "flags/edge";
 3 | 
 4 | export const config = {
 5 |   matcher: ["/demo/middleware", "/demo/server-components"],
 6 | };
 7 | 
 8 | export async function middleware(request: NextRequest) {
 9 |   switch (request.nextUrl.pathname) {
10 |     // Server Components Demo
11 |     case "/demo/server-components": {
12 |       if (request.cookies.has("hkvk")) return;
13 | 
14 |       const response = NextResponse.next();
15 |       response.cookies.set;
16 |       ensureVisitorKeyCookie(response);
17 |       return response;
18 |     }
19 | 
20 |     // Middleware Demo
21 |     case "/demo/middleware": {
22 |       const flagBag = await getEdgeFlags({ request });
23 | 
24 |       const nextUrl = request.nextUrl.clone();
25 |       nextUrl.pathname = `/demo/middleware/${
26 |         flagBag.flags?.checkout || "full"
27 |       }`;
28 |       const response = NextResponse.rewrite(nextUrl);
29 | 
30 |       if (flagBag.cookie) response.cookies.set(...flagBag.cookie.args);
31 | 
32 |       return response;
33 |     }
34 |   }
35 | }
36 | 


--------------------------------------------------------------------------------
/example/next-env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="next" />
2 | /// <reference types="next/image-types/global" />
3 | /// <reference types="next/navigation-types/compat/navigation" />
4 | 
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 | 


--------------------------------------------------------------------------------
/example/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 |   // Config options here
4 | };
5 | 
6 | const withVercelToolbar = require("@vercel/toolbar/plugins/next")();
7 | // Instead of module.exports = nextConfig, do this:
8 | module.exports = withVercelToolbar(nextConfig);
9 | 


--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "example",
 3 |   "version": "0.1.7",
 4 |   "private": true,
 5 |   "scripts": {
 6 |     "dev": "next dev",
 7 |     "build": "next build",
 8 |     "start": "next start"
 9 |   },
10 |   "dependencies": {
11 |     "@happykit/flags": "workspace:*",
12 |     "@headlessui/react": "1.4.3",
13 |     "@tailwindcss/typography": "0.5.1",
14 |     "@tailwindui/react": "0.1.1",
15 |     "@types/dedent": "0.7.0",
16 |     "@types/node": "17.0.15",
17 |     "@types/react": "18.0.18",
18 |     "@vercel/edge-config": "0.1.0-canary.14",
19 |     "@vercel/flags": "2.4.0-bde40fe0-20240402110149",
20 |     "@vercel/toolbar": "^0.1.13",
21 |     "autoprefixer": "10.4.2",
22 |     "clsx": "1.1.1",
23 |     "dedent": "0.7.0",
24 |     "next": "14.1.4",
25 |     "postcss": "8.4.6",
26 |     "react": "18.2.0",
27 |     "react-dom": "18.2.0",
28 |     "tailwindcss": "3.0.18",
29 |     "typescript": "4.5.5"
30 |   }
31 | }
32 | 


--------------------------------------------------------------------------------
/example/pages/_app.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import Head from "next/head";
 3 | import type { AppProps } from "next/app";
 4 | import "tailwindcss/tailwind.css";
 5 | import Script from "next/script";
 6 | import { VercelToolbar } from "@vercel/toolbar/next";
 7 | 
 8 | function MyApp({ Component, pageProps }: AppProps) {
 9 |   return (
10 |     <React.Fragment>
11 |       <Head>
12 |         <link rel="icon" href="/favicon.png" />
13 |       </Head>
14 |       {process.env.NODE_ENV === "production" && (
15 |         <Script
16 |           src="https://plausible.io/js/plausible.js"
17 |           data-domain="flags.happykit.dev"
18 |         />
19 |       )}
20 |       <Component {...pageProps} />
21 |       {
22 |         // shows the toolbar when developing locally or in preview, but not in production
23 |         process.env.NEXT_PUBLIC_FLAGS_ENV_KEY &&
24 |         /^flags_pub_(development|preview)_/.test(
25 |           process.env.NEXT_PUBLIC_FLAGS_ENV_KEY
26 |         ) ? (
27 |           <VercelToolbar />
28 |         ) : null
29 |       }
30 |     </React.Fragment>
31 |   );
32 | }
33 | 
34 | export default MyApp;
35 | 


--------------------------------------------------------------------------------
/example/pages/demo/basic-usage.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { Layout } from "components/Layout";
 3 | import { Result } from "components/Result";
 4 | import { useFlags } from "flags/client";
 5 | 
 6 | export default function Page() {
 7 |   const flagBag = useFlags();
 8 |   return (
 9 |     <Layout
10 |       title="Basic Usage"
11 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/basic-usage.tsx`}
12 |       flagBag={flagBag}
13 |     >
14 |       <article className="py-4 prose max-w-prose">
15 |         <p>
16 |           This shows the basics of how to use <code>@happykit/flags</code>. You
17 |           can find more detailed examples for the different use cases in the
18 |           navigation.
19 |         </p>
20 |         <p>
21 |           In this example where <code>@happykit/flags</code> is used for a
22 |           static site, you'll notice three different renders
23 |         </p>
24 |         <ul>
25 |           <li>
26 |             the earliest one (Render #1) is the initial render, using the
27 |             fallback flags (no fallback is configured in this demo)
28 |           </li>
29 |           <li>
30 |             the second one (Render #2) is the rehydration from the cache, whose
31 |             outcome depends on whether you have visited the demo page before
32 |           </li>
33 |           <li>
34 |             the last one (Render #3) is the final settlement with the flags
35 |             loaded from the server
36 |           </li>
37 |         </ul>
38 |         <p>
39 |           Notice that the <code>settled</code> flag only switches to{" "}
40 |           <code>true</code> after the flags were loaded from the server and are
41 |           thus guaranteed to be up to date.
42 |         </p>
43 |         <p>
44 |           If you are doing capturing important information or causing heavy work
45 |           like code splitting depending on feature flags, it's best to wait
46 |           until <code>settled</code> turns <code>true</code>. You can then kick
47 |           the work of confidently.
48 |         </p>
49 |         <Result key="basic-usage" value={flagBag} />
50 |       </article>
51 |     </Layout>
52 |   );
53 | }
54 | 


--------------------------------------------------------------------------------
/example/pages/demo/client-side-rendering.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { Layout } from "components/Layout";
 3 | import { Result } from "components/Result";
 4 | import { useFlags } from "flags/client";
 5 | 
 6 | export default function Page() {
 7 |   const flagBag = useFlags();
 8 | 
 9 |   return (
10 |     <Layout
11 |       title="Client-Side Rendering"
12 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/client-side-rendering.tsx`}
13 |       flagBag={flagBag}
14 |     >
15 |       <article className="py-4 prose max-w-prose">
16 |         <p>
17 |           This demo shows how to use <code>@happykit/flags</code> for regular
18 |           pages.
19 |         </p>
20 |         <p>
21 |           In this configuration, the feature flags will be loaded when the
22 |           client renders the page. They will not be loaded at build time. Check
23 |           the Static Site Generation examples if you need that, or the
24 |           Server-Side Rendering examples.
25 |         </p>
26 |         <p>
27 |           When the page mounts, <code>@happykit/flags</code> will fetch the
28 |           latest flags from the server and render the page accordingly.
29 |         </p>
30 |         <p>
31 |           The <code>settled</code> value will then flip to true after the
32 |           evaluation on the client finishes.
33 |         </p>
34 |         <Result key="static-site-generation-hybrid" value={flagBag} />
35 |       </article>
36 |     </Layout>
37 |   );
38 | }
39 | 


--------------------------------------------------------------------------------
/example/pages/demo/context.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { FlagBagProvider } from "@happykit/flags/context";
 6 | import { getFlags } from "flags/server";
 7 | import { useFlags, useFlagBag, type InitialFlagState } from "flags/client";
 8 | 
 9 | type ServerSideProps = {
10 |   initialFlagState: InitialFlagState;
11 | };
12 | 
13 | function SomeNestedComponent() {
14 |   // The nested component has access to the flagBag using context
15 |   const flagBag = useFlagBag();
16 |   return <Result key="context" value={flagBag} />;
17 | }
18 | 
19 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
20 |   context
21 | ) => {
22 |   const { initialFlagState } = await getFlags({ context });
23 |   return { props: { initialFlagState } };
24 | };
25 | 
26 | export default function Page(props: ServerSideProps) {
27 |   const flagBag = useFlags({ initialState: props.initialFlagState });
28 | 
29 |   return (
30 |     // The FlagBagProvider is intended to be set on every page
31 |     <FlagBagProvider value={flagBag}>
32 |       <Layout
33 |         title="Context"
34 |         source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/context.tsx`}
35 |         flagBag={flagBag}
36 |       >
37 |         <article className="py-4 prose max-w-prose">
38 |           <p>
39 |             This demo shows how to propagate the flag bag through the context.
40 |           </p>
41 |           <p>
42 |             The <code>useFlags()</code> hook of HappyKit should only be used
43 |             once per Next.js route, at the top level of the default exported
44 |             page. You should then pass the feature flags down to each component
45 |             that needs access to them using props.
46 |           </p>
47 |           <p>
48 |             But some developers might prefer being able to use context instead.
49 |             This demo shows how to use <code>FlagBagProvider</code> and the{" "}
50 |             <code>useFlagBag()</code>
51 |             hook to put the flagBag into context and how to access it from a
52 |             nested component.
53 |           </p>
54 |           <SomeNestedComponent />
55 |         </article>
56 |       </Layout>
57 |     </FlagBagProvider>
58 |   );
59 | }
60 | 


--------------------------------------------------------------------------------
/example/pages/demo/disabled-revalidation.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { Layout } from "components/Layout";
 3 | import { Result } from "components/Result";
 4 | import { useFlags } from "flags/client";
 5 | 
 6 | export default function Page() {
 7 |   const flagBag = useFlags({ revalidateOnFocus: false });
 8 |   return (
 9 |     <Layout
10 |       title="Disabled Revalidation"
11 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/disabled-revalidation.tsx`}
12 |       flagBag={flagBag}
13 |     >
14 |       <article className="py-4 prose max-w-prose">
15 |         <p>
16 |           This demo shows how to configure <code>@happykit/flags</code> to not
17 |           refetch the flags when the window regains focus.
18 |         </p>
19 |         <p>
20 |           When you leave the window and come back to it later, HappyKit will
21 |           reevaluate the feature flags by default. A new request is sent and the
22 |           browser's flags are reevaluated. We use the{" "}
23 |           <a
24 |             href="https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event"
25 |             target="_blank"
26 |             rel="noopener noreferrer"
27 |             className="underline hover:text-gray-800"
28 |           >
29 |             visibility change
30 |           </a>{" "}
31 |           API to detect when the window regained focus. But this behavior can be
32 |           turned off. This demo shows how to prevent reevaluations when the
33 |           window regains focus.
34 |         </p>
35 |         <Result key="disabled-validation" value={flagBag} />
36 |       </article>
37 |     </Layout>
38 |   );
39 | }
40 | 


--------------------------------------------------------------------------------
/example/pages/demo/dynamics.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import { Layout } from "components/Layout";
  3 | import { Result } from "components/Result";
  4 | import { RadioGroup } from "@headlessui/react";
  5 | import clsx from "clsx";
  6 | import { useFlags } from "flags/client";
  7 | 
  8 | export default function Page() {
  9 |   const users = React.useMemo(
 10 |     () => [
 11 |       {
 12 |         name: "No user",
 13 |         description: "Passes no user",
 14 |         value: null,
 15 |       },
 16 |       {
 17 |         name: "George",
 18 |         description: "Passes a user with the key george",
 19 |         value: { key: "george" },
 20 |       },
 21 |       {
 22 |         name: "Linda",
 23 |         description: "Passes a user with the key linda",
 24 |         value: { key: "linda" },
 25 |       },
 26 |     ],
 27 |     []
 28 |   );
 29 | 
 30 |   const traits = React.useMemo(
 31 |     () => [
 32 |       {
 33 |         name: "No traits",
 34 |         description: "Passes no traits",
 35 |         value: null,
 36 |       },
 37 |       {
 38 |         name: "Team Member (yes)",
 39 |         description: "Passes teamMember: true as a trait",
 40 |         value: { teamMember: true },
 41 |       },
 42 |       {
 43 |         name: "Team Member (no)",
 44 |         description: "Passes teamMember: false as a trait",
 45 |         value: { teamMember: false },
 46 |       },
 47 |     ],
 48 |     []
 49 |   );
 50 | 
 51 |   const [user, setUser] = React.useState(users[0]);
 52 |   const [trait, setTraits] = React.useState(traits[0]);
 53 | 
 54 |   const flagBag = useFlags({
 55 |     user: user.value,
 56 |     traits: trait.value,
 57 |   });
 58 | 
 59 |   return (
 60 |     <Layout
 61 |       title="Dynamic"
 62 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/dynamics.tsx`}
 63 |       flagBag={flagBag}
 64 |     >
 65 |       <article className="py-4 prose max-w-prose">
 66 |         <p>
 67 |           This demo shows how the <code>@happykit/flags</code> client behaves
 68 |           when the passed in user attributes or traits change.
 69 |         </p>
 70 |         <p>
 71 |           Simulate changing user attributes or traits using the buttons below.
 72 |           Explore how the loading state changes throughout the lifecycle.
 73 |         </p>
 74 |         <p>Use the buttons below to modify the passed in values</p>
 75 | 
 76 |         <RadioGroup value={user} onChange={setUser}>
 77 |           <RadioGroup.Label className="text-sm font-medium text-gray-900">
 78 |             User
 79 |           </RadioGroup.Label>
 80 | 
 81 |           <div className="mt-1 bg-white rounded-md shadow-sm -space-y-px">
 82 |             {users.map((setting, settingIdx) => (
 83 |               <RadioGroup.Option
 84 |                 key={setting.name}
 85 |                 value={setting}
 86 |                 className={({ checked }) =>
 87 |                   clsx(
 88 |                     settingIdx === 0 ? "rounded-tl-md rounded-tr-md" : "",
 89 |                     settingIdx === users.length - 1
 90 |                       ? "rounded-bl-md rounded-br-md"
 91 |                       : "",
 92 |                     checked
 93 |                       ? "bg-blue-50 border-blue-200 z-10"
 94 |                       : "border-gray-200",
 95 |                     "relative border p-4 flex cursor-pointer focus:outline-none"
 96 |                   )
 97 |                 }
 98 |               >
 99 |                 {({ active, checked }) => (
100 |                   <React.Fragment>
101 |                     <span
102 |                       className={clsx(
103 |                         checked
104 |                           ? "bg-blue-600 border-transparent"
105 |                           : "bg-white border-gray-300",
106 |                         active ? "ring-2 ring-offset-2 ring-blue-500" : "",
107 |                         "h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
108 |                       )}
109 |                       aria-hidden="true"
110 |                     >
111 |                       <span className="rounded-full bg-white w-1.5 h-1.5" />
112 |                     </span>
113 |                     <div className="ml-3 flex flex-col">
114 |                       <RadioGroup.Label
115 |                         as="span"
116 |                         className={clsx(
117 |                           checked ? "text-blue-900" : "text-gray-900",
118 |                           "block text-sm font-medium"
119 |                         )}
120 |                       >
121 |                         {setting.name}
122 |                       </RadioGroup.Label>
123 |                       <RadioGroup.Description
124 |                         as="span"
125 |                         className={clsx(
126 |                           checked ? "text-blue-700" : "text-gray-500",
127 |                           "block text-sm"
128 |                         )}
129 |                       >
130 |                         {setting.description}
131 |                       </RadioGroup.Description>
132 |                     </div>
133 |                   </React.Fragment>
134 |                 )}
135 |               </RadioGroup.Option>
136 |             ))}
137 |           </div>
138 |         </RadioGroup>
139 | 
140 |         <RadioGroup value={trait} onChange={setTraits} className="mt-3">
141 |           <RadioGroup.Label className="text-sm font-medium text-gray-900">
142 |             Traits
143 |           </RadioGroup.Label>
144 | 
145 |           <div className="mt-1 bg-white rounded-md shadow-sm -space-y-px">
146 |             {traits.map((setting, settingIdx) => (
147 |               <RadioGroup.Option
148 |                 key={setting.name}
149 |                 value={setting}
150 |                 className={({ checked }) =>
151 |                   clsx(
152 |                     settingIdx === 0 ? "rounded-tl-md rounded-tr-md" : "",
153 |                     settingIdx === traits.length - 1
154 |                       ? "rounded-bl-md rounded-br-md"
155 |                       : "",
156 |                     checked
157 |                       ? "bg-blue-50 border-blue-200 z-10"
158 |                       : "border-gray-200",
159 |                     "relative border p-4 flex cursor-pointer focus:outline-none"
160 |                   )
161 |                 }
162 |               >
163 |                 {({ active, checked }) => (
164 |                   <React.Fragment>
165 |                     <span
166 |                       className={clsx(
167 |                         checked
168 |                           ? "bg-blue-600 border-transparent"
169 |                           : "bg-white border-gray-300",
170 |                         active ? "ring-2 ring-offset-2 ring-blue-500" : "",
171 |                         "h-4 w-4 mt-0.5 cursor-pointer rounded-full border flex items-center justify-center"
172 |                       )}
173 |                       aria-hidden="true"
174 |                     >
175 |                       <span className="rounded-full bg-white w-1.5 h-1.5" />
176 |                     </span>
177 |                     <div className="ml-3 flex flex-col">
178 |                       <RadioGroup.Label
179 |                         as="span"
180 |                         className={clsx(
181 |                           checked ? "text-blue-900" : "text-gray-900",
182 |                           "block text-sm font-medium"
183 |                         )}
184 |                       >
185 |                         {setting.name}
186 |                       </RadioGroup.Label>
187 |                       <RadioGroup.Description
188 |                         as="span"
189 |                         className={clsx(
190 |                           checked ? "text-blue-700" : "text-gray-500",
191 |                           "block text-sm"
192 |                         )}
193 |                       >
194 |                         {setting.description}
195 |                       </RadioGroup.Description>
196 |                     </div>
197 |                   </React.Fragment>
198 |                 )}
199 |               </RadioGroup.Option>
200 |             ))}
201 |           </div>
202 |         </RadioGroup>
203 | 
204 |         <div className="text-sm font-medium text-gray-900 mt-8">
205 |           Generated Input
206 |         </div>
207 |         <pre className="font-mono rounded bg-gray-200 p-2 text-gray-800">
208 |           useFlags(
209 |           {JSON.stringify({ user: user.value, traits: trait.value }, null, 2)})
210 |         </pre>
211 |         <div className="text-sm font-medium text-gray-900 mt-8">
212 |           Value returned from hook
213 |         </div>
214 |         <Result key="static-site-generation-hybrid" value={flagBag} />
215 |       </article>
216 |     </Layout>
217 |   );
218 | }
219 | 


--------------------------------------------------------------------------------
/example/pages/demo/middleware/[variant].tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetStaticPaths, GetStaticProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { EdgeFunctionContent } from "components/EdgeFunctionContent";
 5 | import type { AppFlags } from "flags/config";
 6 | 
 7 | // ℹ️ Check out the /middleware.ts file as well.
 8 | // That middlware.ts file routes requests to /demo/middlware
 9 | // to any of the variants under /demo/middleware/:variant.
10 | //
11 | // It is the second piece aside from this [variant].tsx file.
12 | 
13 | // This generates a static page for each variant (short, medium, full).
14 | // You could also create these files manually instead of generating them.
15 | // But we use the path generating approach for this demo.
16 | export const getStaticPaths: GetStaticPaths = () => ({
17 |   paths: [
18 |     { params: { variant: "full" } },
19 |     { params: { variant: "medium" } },
20 |     { params: { variant: "short" } },
21 |   ],
22 |   fallback: false,
23 | });
24 | 
25 | export const getStaticProps: GetStaticProps<{ variant: string }> = (
26 |   context
27 | ) => ({ props: { variant: context.params!.variant as string } });
28 | 
29 | export default function Page(props: { variant: AppFlags["checkout"] }) {
30 |   return (
31 |     <Layout
32 |       title="Middleware"
33 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/middleware`}
34 |       flagBag={null}
35 |     >
36 |       <article className="py-4 prose max-w-prose">
37 |         <EdgeFunctionContent checkoutVariant={props.variant} />
38 |       </article>
39 |     </Layout>
40 |   );
41 | }
42 | 


--------------------------------------------------------------------------------
/example/pages/demo/rollouts.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react";
  2 | import { GetServerSideProps } from "next";
  3 | import { Layout } from "components/Layout";
  4 | import { Result } from "components/Result";
  5 | import { getFlags } from "flags/server";
  6 | import { useFlags, type InitialFlagState } from "flags/client";
  7 | 
  8 | type ServerSideProps = { initialFlagState: InitialFlagState };
  9 | 
 10 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
 11 |   context
 12 | ) => {
 13 |   const { initialFlagState } = await getFlags({ context });
 14 |   return { props: { initialFlagState } };
 15 | };
 16 | 
 17 | export default function Page(props: ServerSideProps) {
 18 |   const flagBag = useFlags({ initialState: props.initialFlagState });
 19 |   return (
 20 |     <Layout
 21 |       title="Rollouts"
 22 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/rollouts.tsx`}
 23 |       flagBag={flagBag}
 24 |     >
 25 |       <article className="py-4 prose max-w-prose">
 26 |         <p>
 27 |           This demo shows how to use <code>@happykit/flags</code> for rollouts.
 28 |         </p>
 29 |         <p>
 30 |           You can use flags to roll out features to a certain percentage of your
 31 |           users. Or you can use flags as the basis for your A/B-testing. This is
 32 |           all enabled by having the option to serve a variant to a certain
 33 |           percentage of incoming requests. We refer to this as percentage-based
 34 |           rollouts, or rollouts for short.
 35 |         </p>
 36 |         <p>
 37 |           Rollouts work with any rendering strategy and you don't need to do
 38 |           anything special inside of <code>@happykit/flags</code>.
 39 |         </p>
 40 |         <h3>Setup</h3>
 41 |         <p>
 42 |           You can configure rollouts in rules or in the <code>On</code> default
 43 |           of your flag. Simply use the UI to specify which percentage of flag
 44 |           evaluations should be answered by which variant.
 45 |         </p>
 46 |         <p>
 47 |           <i>
 48 |             Note that configuring rollouts is only possible in the flag details
 49 |             after you created the flag. It is not possible when you first create
 50 |             a flag.
 51 |           </i>
 52 |         </p>
 53 |         <h3>Splitting based on attributes</h3>
 54 |         <p>
 55 |           The percentage-based rollouts happen based on an attribute you specify
 56 |           when setting the rollout. You can use the <code>visitorKey</code>, any
 57 |           user attribute or any trait as the basis of the rollout.
 58 |         </p>
 59 |         <p>
 60 |           Splitting on the same attribute value will always resolve to the same
 61 |           flag variant. A flag will resolve to <code>null</code> in case the
 62 |           attribute used as the base of splitting was not present on the
 63 |           request.
 64 |         </p>
 65 |         <h3>Example</h3>
 66 |         <p>
 67 |           When you first loaded this page, <code>@happykit/flags</code>{" "}
 68 |           generated a visitor key for you during server-side rendering. This key
 69 |           was then stored as a cookie in your browser by <code>getFlags()</code>
 70 |           .
 71 |         </p>
 72 |         <p>
 73 |           The <code>purchaseButtonLabel</code> flag is configured to do a
 74 |           rollout of <i>"Purchase"</i>, <i>"Buy now"</i>, <i>"Add to cart"</i>{" "}
 75 |           or <i>"Get it"</i> split evenly with 25% weight each.
 76 |         </p>
 77 |         <p>
 78 |           If you refresh the page, you'll consistently see the same value. But
 79 |           if you repeatedly open the same page in a new Incognito Window, you'll
 80 |           see the four possible values alternate.
 81 |         </p>
 82 |         <pre>
 83 |           purchaseButtonLabel: "{flagBag.flags?.purchaseButtonLabel}"
 84 |           <br />
 85 |           visitorKey: "{flagBag.visitorKey}"
 86 |         </pre>
 87 |         <p>
 88 |           Your <code>purchaseButtonLabel</code> is currently resolving to{" "}
 89 |           <span className="italic font-semibold">
 90 |             "{flagBag.flags?.purchaseButtonLabel}"
 91 |           </span>{" "}
 92 |           based on your <code>visitorKey</code>, which is set to{" "}
 93 |           <code>{flagBag.visitorKey}</code>.
 94 |         </p>
 95 |         <Result key="rollout" value={flagBag} />
 96 |       </article>
 97 |     </Layout>
 98 |   );
 99 | }
100 | 


--------------------------------------------------------------------------------
/example/pages/demo/server-side-rendering-hybrid.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import { type InitialFlagState, useFlags } from "flags/client";
 7 | 
 8 | type ServerSideProps = { initialFlagState: InitialFlagState };
 9 | 
10 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
11 |   context
12 | ) => {
13 |   const { initialFlagState } = await getFlags({ context });
14 |   return { props: { initialFlagState } };
15 | };
16 | 
17 | export default function Page(props: ServerSideProps) {
18 |   const flagBag = useFlags({ initialState: props.initialFlagState });
19 |   return (
20 |     <Layout
21 |       title="Server Side Rendering (Hybrid)"
22 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/server-side-rendering-hybrid.tsx`}
23 |       flagBag={flagBag}
24 |     >
25 |       <article className="py-4 prose max-w-prose">
26 |         <p>
27 |           This demo shows how to use <code>@happykit/flags</code> for
28 |           server-rendered pages.
29 |         </p>
30 |         <p>
31 |           The server preloads the initial values and the client then rehydrates
32 |           them.
33 |         </p>
34 |         <p>
35 |           This means the client does not need to reload the flags. It simply
36 |           rehydrates the result of the server-side rendering.
37 |         </p>
38 |         <p>
39 |           However, when you leave the window and come back to it, a new request
40 |           is sent and the browser's flags are reevaluated. We use the{" "}
41 |           <a
42 |             href="https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event"
43 |             target="_blank"
44 |             rel="noopener noreferrer"
45 |           >
46 |             visibility change
47 |           </a>{" "}
48 |           API for that. You can try this by switching to another tab and then
49 |           coming back to this one.
50 |         </p>
51 |         <Result key="server-side-rendering-hybrid" value={flagBag} />
52 |       </article>
53 |     </Layout>
54 |   );
55 | }
56 | 


--------------------------------------------------------------------------------
/example/pages/demo/server-side-rendering-pure.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags, type EvaluationResponseBody } from "flags/server";
 6 | import type { AppFlags } from "flags/config";
 7 | 
 8 | type ServerSideProps = {
 9 |   flags: AppFlags | null;
10 |   data: EvaluationResponseBody | null;
11 | };
12 | 
13 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
14 |   context
15 | ) => {
16 |   const { flags, data } = await getFlags({ context });
17 |   return { props: { flags, data } };
18 | };
19 | 
20 | export default function Page(props: ServerSideProps) {
21 |   return (
22 |     <Layout
23 |       title="Server Side Rendering (Pure)"
24 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/server-side-rendering-pure.tsx`}
25 |       flagBag={null}
26 |     >
27 |       <article className="py-4 prose max-w-prose">
28 |         <p>
29 |           This demo shows how to use <code>@happykit/flags</code> for
30 |           server-rendered pages.
31 |         </p>
32 |         <p>
33 |           Since this page is rendered on the server only, there is no{" "}
34 |           <code>flagBag</code>. Instead, the values are shown directly.
35 |         </p>
36 |         <Result
37 |           key="server-side-rendering-pure"
38 |           label="Flags"
39 |           value={props.flags}
40 |         />
41 |         <p>
42 |           Aside from the flags, we have access to the loaded flags as well.
43 |           These are the flags without any fallback values.
44 |         </p>
45 |         <Result
46 |           key="server-side-rendering-pure-with-fallback-values"
47 |           label="Loaded data (without fallback values)"
48 |           value={props.data}
49 |         />
50 |       </article>
51 |     </Layout>
52 |   );
53 | }
54 | 


--------------------------------------------------------------------------------
/example/pages/demo/static-site-generation-hybrid.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetStaticProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import { type InitialFlagState, useFlags } from "flags/client";
 7 | 
 8 | type StaticProps = { initialFlagState: InitialFlagState };
 9 | 
10 | export const getStaticProps: GetStaticProps<StaticProps> = async (context) => {
11 |   const { initialFlagState } = await getFlags({ context });
12 |   return { props: { initialFlagState } };
13 | };
14 | 
15 | export default function Page(props: StaticProps) {
16 |   const flagBag = useFlags({ initialState: props.initialFlagState });
17 | 
18 |   return (
19 |     <Layout
20 |       title="Static Site Generation (Hybrid)"
21 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/static-site-generation-hybrid.tsx`}
22 |       flagBag={flagBag}
23 |     >
24 |       <article className="py-4 prose max-w-prose">
25 |         <p>
26 |           This demo shows how to use <code>@happykit/flags</code> for static
27 |           pages.
28 |         </p>
29 |         <p>
30 |           This page is rendered statically at first. The first rendering pass
31 |           will use no visitor key. This is necessary as the concept of a visitor
32 |           does not exist during static site generation. Thus all rules and
33 |           percentage-based rollouts targeting a visitor resolve to{" "}
34 |           <code>null</code>. If provided, the fallback values will be used for
35 |           those.
36 |         </p>
37 |         <p>
38 |           The client reuses the statically evaluated feature flags for the first
39 |           rendering pass. Then it reevaluates the flag with the visitor
40 |           information. Some flags might change as a result of this.
41 |         </p>
42 |         <p>
43 |           The <code>settled</code> value will then flip to true after the
44 |           reevaluation on the client finishes.
45 |         </p>
46 |         <Result key="static-site-generation-hybrid" value={flagBag} />
47 |       </article>
48 |     </Layout>
49 |   );
50 | }
51 | 


--------------------------------------------------------------------------------
/example/pages/demo/static-site-generation-pure.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetStaticProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import type { AppFlags } from "flags/config";
 7 | 
 8 | type StaticProps = { flags: AppFlags | null };
 9 | 
10 | export const getStaticProps: GetStaticProps<StaticProps> = async (context) => {
11 |   const { flags } = await getFlags({ context });
12 |   return { props: { flags } };
13 | };
14 | 
15 | export default function Page(props: StaticProps) {
16 |   return (
17 |     <Layout
18 |       title="Static Site Generation (Pure)"
19 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/static-site-generation-pure.tsx`}
20 |       flagBag={null}
21 |     >
22 |       <article className="py-4 prose max-w-prose">
23 |         <p>
24 |           This demo shows how to use <code>@happykit/flags</code> for static
25 |           pages.
26 |         </p>
27 |         <p>
28 |           Since this page is only rendered statically, the rendering will use no
29 |           visitor key. This is necessary as the concept of a visitor does not
30 |           exist during static site generation. Thus all rules and
31 |           percentage-based rollouts targeting a visitor resolve to{" "}
32 |           <code>null</code>.
33 |         </p>
34 |         <Result key="static-site-generation-pure" value={props.flags} />
35 |       </article>
36 |     </Layout>
37 |   );
38 | }
39 | 


--------------------------------------------------------------------------------
/example/pages/demo/targeting-by-traits.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import { type InitialFlagState, useFlags } from "flags/client";
 7 | 
 8 | type Traits = { teamMember: boolean };
 9 | type ServerSideProps = {
10 |   initialFlagState: InitialFlagState;
11 |   traits: Traits;
12 | };
13 | 
14 | // This demo uses server-side rendering, but this works just as well with
15 | // static site generation or client-only rendering.
16 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
17 |   context
18 | ) => {
19 |   // These could be loaded from anywhere
20 |   const traits: Traits = { teamMember: true };
21 | 
22 |   const { initialFlagState } = await getFlags({ context, traits });
23 |   return { props: { initialFlagState, traits } };
24 | };
25 | 
26 | export default function Page(props: ServerSideProps) {
27 |   const flagBag = useFlags({
28 |     initialState: props.initialFlagState,
29 |     traits: props.traits,
30 |   });
31 |   return (
32 |     <Layout
33 |       title="Targeting by Traits"
34 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/targeting-by-traits.tsx`}
35 |       flagBag={flagBag}
36 |     >
37 |       <article className="py-4 prose max-w-prose">
38 |         <p>
39 |           This demo shows how to use <code>@happykit/flags</code> for targeting
40 |           by traits.
41 |         </p>
42 |         <p>
43 |           You can pass any traits into the flag evaluation. These traits can
44 |           then be used by the rules defined in your flags. This allows you to
45 |           resolve flags differently based on the provided traits.
46 |         </p>
47 |         <p>
48 |           Traits can be related to the visitor, to the authenticated user or to
49 |           anything else. You can pass any traits you want. Use the traits in
50 |           your HappyKit flag rules to resolve the flag to different variants
51 |           based on the passed traits.
52 |         </p>
53 |         <Result key="targeting-by-traits" value={flagBag} />
54 |         <p>
55 |           Note that aside from traits, HappyKit also has the concepts of a
56 |           visitor and a user. These three concepts are all independent of each
57 |           other.
58 |         </p>
59 |       </article>
60 |     </Layout>
61 |   );
62 | }
63 | 


--------------------------------------------------------------------------------
/example/pages/demo/targeting-by-user.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import { type InitialFlagState, useFlags } from "flags/client";
 7 | 
 8 | type User = { key: string; name: string };
 9 | 
10 | type ServerSideProps = {
11 |   initialFlagState: InitialFlagState;
12 |   user: User;
13 | };
14 | 
15 | // This demo uses server-side rendering, but this works just as well with
16 | // static site generation or client-only rendering.
17 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
18 |   context
19 | ) => {
20 |   // These could be loaded from anywhere
21 |   const user: User = { key: "fake-user-key-1", name: "Jon" };
22 | 
23 |   const { initialFlagState } = await getFlags({ context, user });
24 |   return { props: { initialFlagState, user } };
25 | };
26 | 
27 | export default function Page(props: ServerSideProps) {
28 |   const flagBag = useFlags({
29 |     initialState: props.initialFlagState,
30 |     user: props.user,
31 |   });
32 |   return (
33 |     <Layout
34 |       title="Targeting by User"
35 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/targeting-by-user.tsx`}
36 |       flagBag={flagBag}
37 |     >
38 |       <article className="py-4 prose max-w-prose">
39 |         <p>
40 |           This demo shows how to use <code>@happykit/flags</code> for targeting
41 |           users.
42 |         </p>
43 |         <p>
44 |           HappyKit allows you to do pass in a user. You can use that user and
45 |           the provided uesr profile for rules or percentage-based rollouts. The
46 |           fields supported in the user profile are defined in the README and in
47 |           the TypeScript types.
48 |         </p>
49 |         <Result key="targeting-by-user" value={flagBag} />
50 |         <p>
51 |           Note that aside from users, HappyKit also has the concepts of a
52 |           visitor and traits. These three concepts are all independent of each
53 |           other.
54 |         </p>
55 |       </article>
56 |     </Layout>
57 |   );
58 | }
59 | 


--------------------------------------------------------------------------------
/example/pages/demo/targeting-by-visitor-key.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { GetServerSideProps } from "next";
 3 | import { Layout } from "components/Layout";
 4 | import { Result } from "components/Result";
 5 | import { getFlags } from "flags/server";
 6 | import { type InitialFlagState, useFlags } from "flags/client";
 7 | 
 8 | type ServerSideProps = {
 9 |   initialFlagState: InitialFlagState;
10 | };
11 | 
12 | // This demo uses server-side rendering, but this works just as well with
13 | // static site generation or client-only rendering.
14 | export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (
15 |   context
16 | ) => {
17 |   const { initialFlagState } = await getFlags({ context });
18 |   return { props: { initialFlagState } };
19 | };
20 | 
21 | export default function Page(props: ServerSideProps) {
22 |   // This demo shows that you never need to deal with the visitor key yourself
23 |   const flagBag = useFlags({ initialState: props.initialFlagState });
24 | 
25 |   return (
26 |     <Layout
27 |       title="Targeting by Visitor Key"
28 |       source={`https://github.com/happykit/flags/blob/${process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_REF}/example/pages/demo/targeting-by-visitor-key.tsx`}
29 |       flagBag={flagBag}
30 |     >
31 |       <article className="py-4 prose max-w-prose">
32 |         <p>
33 |           This demo shows how to use <code>@happykit/flags</code> for targeting
34 |           visitors.
35 |         </p>
36 |         <p>
37 |           HappyKit allows you to do percentage-based rollouts or A/B testing. To
38 |           do this, HappyKit automatically creates a unique visitor key for every
39 |           visitor and saves it as a cookie. You don't need to manage it
40 |           yourself. The visitor key can not be passed in at all.
41 |         </p>
42 |         <p>
43 |           If you know more about the visitor, you can use configure your
44 |           HappyKit Flags to use percentage-based rollouts or A/B testing based
45 |           on the passed in user instead. HappyKit makes distinguishes users and
46 |           visitors. You have control over the user HappyKit sees, but HappyKit
47 |           controls the visitor.
48 |         </p>
49 |         <Result key="targeting-by-visitor-key" value={flagBag} />
50 |         <p>
51 |           Note that aside from visitors, HappyKit also has the concepts of a
52 |           user and traits. These three concepts are all independent of each
53 |           other.
54 |         </p>
55 |       </article>
56 |     </Layout>
57 |   );
58 | }
59 | 


--------------------------------------------------------------------------------
/example/pages/index.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { Layout } from "components/Layout";
 3 | import { Result } from "components/Result";
 4 | 
 5 | // defined outside to guarantee a consistent ref
 6 | const exampleFlagBag = {
 7 |   flags: {
 8 |     ads: true,
 9 |     checkout: "medium",
10 |     discount: 5,
11 |     purchaseButtonLabel: "Get it",
12 |   },
13 |   data: {
14 |     flags: {
15 |       ads: true,
16 |       checkout: "medium",
17 |       discount: 5,
18 |       purchaseButtonLabel: "Get it",
19 |     },
20 |     visitor: {
21 |       key: "jy9jlWoINT6RF80ckkVVA",
22 |     },
23 |   },
24 |   error: null,
25 |   fetching: false,
26 |   settled: true,
27 |   visitorKey: "jy9jlWoINT6RF80ckkVVA",
28 | };
29 | 
30 | export default function Index() {
31 |   return (
32 |     <Layout title="Introduction" flagBag={null}>
33 |       {/* Replace with your content */}
34 |       <article className="py-4 prose max-w-prose">
35 |         <p>
36 |           This site contains usage examples for{" "}
37 |           <a href="https://github.com/happykit/flags" target="_blank">
38 |             <code>@happykit/flags</code>
39 |           </a>
40 |           .
41 |         </p>
42 |         <p>
43 |           Read the <a href="https://github.com/happykit/flags">README.md</a>{" "}
44 |           file for a full technical setup before you jump into these examples.
45 |           The examples are intended as reference implementations for the
46 |           different ways in which the <code>@happykit/flags</code> client can be
47 |           used.
48 |         </p>
49 |         <p>
50 |           Make sure you have set up <code>configure()</code> inside of{" "}
51 |           <code>_app.js</code> before continuing with these examples.
52 |         </p>
53 | 
54 |         <hr />
55 | 
56 |         <p className="max-w-prose text-gray-600">
57 |           Boxes like the one below show the return value of the{" "}
58 |           <code className="text-sm font-mono font-thin">useFlags()</code> hook.
59 |           If you do this in your code
60 |         </p>
61 |         <pre>const flagBag = useFlags()</pre>
62 |         <p>
63 |           then the value of <code>flagBag</code> would be something like
64 |         </p>
65 |         <Result key="index" value={exampleFlagBag} />
66 |         <p>
67 |           We usually call this value the <code>flagBag</code>, as it contains
68 |           the evaluated feature flags and a bunch of other things you might
69 |           need.
70 |         </p>
71 |       </article>
72 |     </Layout>
73 |   );
74 | }
75 | 


--------------------------------------------------------------------------------
/example/pages/notes/simultaneous-invocations-of-use-flags-detected.tsx:
--------------------------------------------------------------------------------
 1 | import Link from "next/link";
 2 | import * as React from "react";
 3 | import { Layout } from "components/Layout";
 4 | 
 5 | export default function Page() {
 6 |   return (
 7 |     <Layout
 8 |       title="Simultaneous invocations of useFlags() detected"
 9 |       flagBag={null}
10 |     >
11 |       <article className="py-4 prose max-w-prose">
12 |         <p className="max-w-prose text-gray-600">
13 |           This page describes what to do about the{" "}
14 |           <code>
15 |             @happykit/flags: Simultaneous invocations of useFlags() detected
16 |           </code>{" "}
17 |           warning.
18 |         </p>
19 |         <h2>Why you see this warning</h2>
20 |         <p>
21 |           You are calling <code>useFlags()</code> multiple times on the same
22 |           page.
23 |         </p>
24 |         <h2>Philosophy behind this warning</h2>
25 |         <p>
26 |           <code>useFlags()</code> is supposed to be called exactly once per
27 |           page. Usually from within the page component. This has multiple
28 |           benefits
29 |         </p>
30 |         <ul>
31 |           <li>
32 |             <code>useFlags()</code> invocations send requests to evaluate your
33 |             feature flags. Calling <code>useFlag()</code> exactly once per page
34 |             guarantees no extraneous requests are made.
35 |           </li>
36 |           <li>
37 |             When you invoke <code>useFlags()</code> more than once per page your
38 |             inputs might differ on each invocation. And so your flags might
39 |             evaluate to different values.
40 |           </li>
41 |         </ul>
42 |         <h2>How to resolve this warning</h2>
43 |         <p>
44 |           Call <code>useFlags()</code> only in your page components. If child
45 |           components need access to the flags, pass the return value down to
46 |           them using props.
47 |         </p>
48 |         <p>
49 |           You can alternatively pass the return value via{" "}
50 |           <Link href="/demo/context">Context</Link> if you dislike passing
51 |           props. I would personally recommend passing it via props instead
52 |           though.
53 |         </p>
54 |       </article>
55 |     </Layout>
56 |   );
57 | }
58 | 


--------------------------------------------------------------------------------
/example/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   plugins: {
3 |     tailwindcss: {},
4 |     autoprefixer: {},
5 |   },
6 | }
7 | 


--------------------------------------------------------------------------------
/example/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/happykit/flags/854dd725f403f06ebb55fa0c548445c5d1c88981/example/public/favicon.png


--------------------------------------------------------------------------------
/example/public/github.svg:
--------------------------------------------------------------------------------
1 | <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>


--------------------------------------------------------------------------------
/example/public/logo.svg:
--------------------------------------------------------------------------------
1 | <svg viewBox="0 0 260 80" version="1.1" xmlns="http://www.w3.org/2000/svg"><g stroke="none" stroke-width="1" fill="currentColor" fill-rule="evenodd"><g fill="currentColor"><path d="M59.4759997,54 L59.4759997,42.012 L65.4879997,42.012 L65.4879997,54 L71.4639997,54 L71.4639997,27 L65.4879997,27 L65.4879997,38.988 L59.4759997,38.988 L59.4759997,27 L53.4639997,27 L53.4639997,54 L59.4759997,54 Z M85.1902855,54 C86.9662855,54 87.9742855,53.004 88.2142855,51.012 L88.2142855,51.012 L88.2142855,54 L94.1902855,54 L94.1902855,38.988 C94.1902855,37.356 93.6022855,35.952 92.4262855,34.776 C91.2502855,33.6 89.8462855,33.012 88.2142855,33.012 L88.2142855,33.012 L82.2022855,33.012 C78.6742855,33.012 76.6702855,35.004 76.1902855,38.988 L76.1902855,38.988 L79.2142855,38.988 C79.4302855,36.996 80.4262855,36 82.2022855,36 L82.2022855,36 L85.1902855,36 C86.0302855,36 86.7442855,36.294 87.3322855,36.882 C87.9202855,37.47 88.2142855,38.172 88.2142855,38.988 L88.2142855,38.988 L88.2142855,42.012 L82.2022855,42.012 C80.5702855,42.012 79.1602855,42.6 77.9722855,43.776 C76.7842855,44.952 76.1902855,46.356 76.1902855,47.988 C76.1902855,49.644 76.7842855,51.06 77.9722855,52.236 C79.1602855,53.412 80.5702855,54 82.2022855,54 L82.2022855,54 L85.1902855,54 Z M85.1902855,51.012 C84.3742855,51.012 83.6722855,50.712 83.0842855,50.112 C82.4962855,49.512 82.2022855,48.804 82.2022855,47.988 C82.2022855,47.172 82.4962855,46.47 83.0842855,45.882 C83.6722855,45.294 84.3742855,45 85.1902855,45 L85.1902855,45 L88.2142855,45 L88.2142855,47.988 C88.2142855,48.804 87.9202855,49.512 87.3322855,50.112 C86.7442855,50.712 86.0302855,51.012 85.1902855,51.012 Z M104.928571,60.012 L104.928571,54 L110.940571,54 C112.572571,54 113.976571,53.412 115.152571,52.236 C116.328571,51.06 116.916571,49.644 116.916571,47.988 L116.916571,47.988 L116.916571,38.988 C116.916571,37.356 116.328571,35.952 115.152571,34.776 C113.976571,33.6 112.572571,33.012 110.940571,33.012 L110.940571,33.012 L107.916571,33.012 C106.932571,33.012 106.188571,33.258 105.684571,33.75 C105.180571,34.242 104.928571,34.992 104.928571,36 L104.928571,36 L104.928571,33.012 L98.9165713,33.012 L98.9165713,60.012 L104.928571,60.012 Z M107.916571,51.012 L104.928571,51.012 L104.928571,38.988 C104.928571,38.172 105.222571,37.47 105.810571,36.882 C106.398571,36.294 107.100571,36 107.916571,36 C108.756571,36 109.470571,36.294 110.058571,36.882 C110.646571,37.47 110.940571,38.172 110.940571,38.988 L110.940571,38.988 L110.940571,47.988 C110.940571,48.828 110.646571,49.542 110.058571,50.13 C109.470571,50.718 108.756571,51.012 107.916571,51.012 L107.916571,51.012 Z M127.654857,60.012 L127.654857,54 L133.666857,54 C135.298857,54 136.702857,53.412 137.878857,52.236 C139.054857,51.06 139.642857,49.644 139.642857,47.988 L139.642857,47.988 L139.642857,38.988 C139.642857,37.356 139.054857,35.952 137.878857,34.776 C136.702857,33.6 135.298857,33.012 133.666857,33.012 L133.666857,33.012 L130.642857,33.012 C129.658857,33.012 128.914857,33.258 128.410857,33.75 C127.906857,34.242 127.654857,34.992 127.654857,36 L127.654857,36 L127.654857,33.012 L121.642857,33.012 L121.642857,60.012 L127.654857,60.012 Z M130.642857,51.012 L127.654857,51.012 L127.654857,38.988 C127.654857,38.172 127.948857,37.47 128.536857,36.882 C129.124857,36.294 129.826857,36 130.642857,36 C131.482857,36 132.196857,36.294 132.784857,36.882 C133.372857,37.47 133.666857,38.172 133.666857,38.988 L133.666857,38.988 L133.666857,47.988 C133.666857,48.828 133.372857,49.542 132.784857,50.13 C132.196857,50.718 131.482857,51.012 130.642857,51.012 L130.642857,51.012 Z M156.393143,63 C158.025143,63 159.429143,62.406 160.605143,61.218 C161.781143,60.03 162.369143,58.62 162.369143,56.988 L162.369143,56.988 L162.369143,33.012 L156.393143,33.012 L156.393143,47.988 C156.393143,48.804 156.099143,49.512 155.511143,50.112 C154.923143,50.712 154.209143,51.012 153.369143,51.012 C152.553143,51.012 151.851143,50.712 151.263143,50.112 C150.675143,49.512 150.381143,48.804 150.381143,47.988 L150.381143,47.988 L150.381143,33.012 L144.369143,33.012 L144.369143,47.988 C144.369143,49.644 144.963143,51.06 146.151143,52.236 C147.339143,53.412 148.749143,54 150.381143,54 L150.381143,54 L153.369143,54 C155.145143,54 156.153143,53.004 156.393143,51.012 L156.393143,51.012 L156.393143,56.988 C156.393143,57.828 156.099143,58.542 155.511143,59.13 C154.923143,59.718 154.209143,60.012 153.369143,60.012 L153.369143,60.012 L150.381143,60.012 C148.533143,60.012 147.537143,59.004 147.393143,56.988 L147.393143,56.988 L144.369143,56.988 C144.849143,60.996 146.853143,63 150.381143,63 L150.381143,63 L156.393143,63 Z M173.107429,54 L173.107429,43.488 L182.107429,54 L188.119429,54 L176.095429,40.5 L188.119429,27 L182.827429,27 L173.107429,38.016 L173.107429,27 L167.095429,27 L167.095429,54 L173.107429,54 Z M198.821715,29.988 L198.821715,27 L192.809715,27 L192.809715,29.988 L198.821715,29.988 Z M198.821715,54 L198.821715,33.012 L192.809715,33.012 L192.809715,54 L198.821715,54 Z M215.536,54 L215.536,51.012 L215.248,51.012 C214.72,51.012 214.132,50.754 213.484,50.238 C212.836,49.722 212.512,48.972 212.512,47.988 L212.512,47.988 L212.512,36 L215.536,36 L215.536,33.012 L212.512,33.012 L212.512,27 L206.536,27 L206.536,33.012 L203.512,33.012 L203.512,36 L206.536,36 L206.536,47.988 C206.536,49.644 207.124,51.06 208.3,52.236 C209.476,53.412 210.88,54 212.512,54 L212.512,54 L215.536,54 Z" fill-rule="nonzero"></path><g transform="translate(25, 40) translate(-29.5, -40) translate(19, 11)"><g><path d="M15.2727273,42.8246842 L15.2727273,20.5201415 C18.7720909,18.6043813 21,14.7437102 21,10.5030579 C21,6.03114368 18.5269636,1.9853404 14.6999045,0.19630879 C14.4053318,0.0575107861 14.0646545,0.088911661 13.7951864,0.278237457 C13.5258136,0.467051839 13.3636364,0.788731811 13.3636364,1.13383459 L13.3636364,9.316473 L7.63636364,9.316473 L7.63636364,1.13383459 C7.63636364,0.788731811 7.47418636,0.467051839 7.20481364,0.27833974 C6.93544091,0.089013944 6.59466818,0.057613069 6.30009545,0.196411073 C2.47303636,1.9853404 0,6.03124596 0,10.5031601 C0,14.7438125 2.22790909,18.6043813 5.72727273,20.5202438 L5.72727273,42.8246842 C3.92537727,44.373453 2.86363636,46.7207452 2.86363636,49.2068353 C2.86363636,53.7186398 6.28940455,57.3894737 10.5,57.3894737 C14.7105955,57.3894737 18.1363636,53.7186398 18.1363636,49.2068353 C18.1363636,46.7206429 17.0746227,44.3733508 15.2727273,42.8246842 Z M10.5,55.3438141 C7.34179091,55.3438141 4.77272727,52.59097 4.77272727,49.2068353 C4.77272727,47.2060779 5.69090455,45.324787 7.22858182,44.1741035 C7.48392273,43.9828343 7.63636364,43.6696439 7.63636364,43.3355876 L7.63636364,19.8734062 C7.63636364,19.4668314 7.41166364,19.0993286 7.06449545,18.9364941 C3.93282273,17.4676083 1.90909091,14.1574242 1.90909091,10.5031601 C1.90909091,7.39570092 3.36887727,4.53996011 5.72727273,2.85341605 L5.72727273,10.3393028 C5.72727273,10.9041094 6.15471818,11.3621326 6.68181818,11.3621326 L14.3181818,11.3621326 C14.8452818,11.3621326 15.2727273,10.9041094 15.2727273,10.3393028 L15.2727273,2.85341605 C17.6311227,4.53996011 19.0909091,7.39570092 19.0909091,10.5031601 C19.0909091,14.1575265 17.0671773,17.4677105 13.9355045,18.9364941 C13.5883364,19.0993286 13.3636364,19.4668314 13.3636364,19.8734062 L13.3636364,43.3355876 C13.3636364,43.6697461 13.5160773,43.9828343 13.7714182,44.1741035 C15.3090955,45.324787 16.2272727,47.2060779 16.2272727,49.2068353 C16.2272727,52.59097 13.6582091,55.3438141 10.5,55.3438141 Z" fill-rule="nonzero"></path><path d="M10.5,45.1155161 C8.39465455,45.1155161 6.68181818,46.9508819 6.68181818,49.2068353 C6.68181818,51.4627887 8.39465455,53.2981545 10.5,53.2981545 C12.6053455,53.2981545 14.3181818,51.4627887 14.3181818,49.2068353 C14.3181818,46.9508819 12.6053455,45.1155161 10.5,45.1155161 Z M10.5,51.2524949 C9.44713636,51.2524949 8.59090909,50.3350165 8.59090909,49.2068353 C8.59090909,48.078654 9.44713636,47.1611757 10.5,47.1611757 C11.5528636,47.1611757 12.4090909,48.078654 12.4090909,49.2068353 C12.4090909,50.3350165 11.5528636,51.2524949 10.5,51.2524949 Z" fill-rule="nonzero"></path><path d="M10.5,42.0470267 C11.0271,42.0470267 11.4545455,41.5890035 11.4545455,41.0241969 L11.4545455,23.6360902 C11.4545455,23.0712836 11.0271,22.6132604 10.5,22.6132604 C9.9729,22.6132604 9.54545455,23.0712836 9.54545455,23.6360902 L9.54545455,41.0241969 C9.54545455,41.5890035 9.9729,42.0470267 10.5,42.0470267 Z"></path></g></g></g></g></svg>
2 | 


--------------------------------------------------------------------------------
/example/public/sitemap.xml:
--------------------------------------------------------------------------------
  1 | <?xml version="1.0" encoding="UTF-8"?>
  2 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  3 |   <url>
  4 |     <loc>https://flags.happykit.dev/</loc>
  5 |     <lastmod>2022-07-15</lastmod>
  6 |     <changefreq>monthly</changefreq>
  7 |      <priority>1.0</priority>
  8 |   </url>
  9 |   <url>
 10 |     <loc>https://flags.happykit.dev/demo/basic-usage</loc>
 11 |     <lastmod>2022-07-15</lastmod>
 12 |     <changefreq>monthly</changefreq>
 13 |     <priority>0.9</priority>
 14 |   </url>
 15 |   <url>
 16 |     <loc>https://flags.happykit.dev/demo/client-components</loc>
 17 |     <lastmod>2023-07-24</lastmod>
 18 |     <changefreq>monthly</changefreq>
 19 |     <priority>0.8</priority>
 20 |   </url>
 21 |   <url>
 22 |     <loc>https://flags.happykit.dev/demo/server-components</loc>
 23 |     <lastmod>2023-07-24</lastmod>
 24 |     <changefreq>monthly</changefreq>
 25 |     <priority>0.8</priority>
 26 |   </url>
 27 |   <url>
 28 |     <loc>https://flags.happykit.dev/demo/client-side-rendering</loc>
 29 |     <lastmod>2022-07-15</lastmod>
 30 |     <changefreq>monthly</changefreq>
 31 |     <priority>0.8</priority>
 32 |   </url>
 33 |   <url>
 34 |     <loc>https://flags.happykit.dev/demo/server-side-rendering-pure</loc>
 35 |     <lastmod>2022-07-15</lastmod>
 36 |     <changefreq>monthly</changefreq>
 37 |     <priority>0.8</priority>
 38 |   </url>
 39 |   <url>
 40 |     <loc>https://flags.happykit.dev/demo/server-side-rendering-hybrid</loc>
 41 |     <lastmod>2022-07-15</lastmod>
 42 |     <changefreq>monthly</changefreq>
 43 |     <priority>0.8</priority>
 44 |   </url>
 45 |   <url>
 46 |     <loc>https://flags.happykit.dev/demo/static-site-generation-pure</loc>
 47 |     <lastmod>2022-07-15</lastmod>
 48 |     <changefreq>monthly</changefreq>
 49 |     <priority>0.8</priority>
 50 |   </url>
 51 |   <url>
 52 |     <loc>https://flags.happykit.dev/demo/static-site-generation-hybrid</loc>
 53 |     <lastmod>2022-07-15</lastmod>
 54 |     <changefreq>monthly</changefreq>
 55 |     <priority>0.8</priority>
 56 |   </url>
 57 |   <url>
 58 |     <loc>https://flags.happykit.dev/demo/middleware</loc>
 59 |     <lastmod>2022-07-15</lastmod>
 60 |     <changefreq>monthly</changefreq>
 61 |     <priority>0.8</priority>
 62 |   </url>
 63 |   <url>
 64 |     <loc>https://flags.happykit.dev/demo/targeting-by-visitor-key</loc>
 65 |     <lastmod>2022-07-15</lastmod>
 66 |     <changefreq>monthly</changefreq>
 67 |     <priority>0.8</priority>
 68 |   </url>
 69 |   <url>
 70 |     <loc>https://flags.happykit.dev/demo/targeting-by-user</loc>
 71 |     <lastmod>2022-07-15</lastmod>
 72 |     <changefreq>monthly</changefreq>
 73 |     <priority>0.8</priority>
 74 |   </url>
 75 |   <url>
 76 |     <loc>https://flags.happykit.dev/demo/targeting-by-traits</loc>
 77 |     <lastmod>2022-07-15</lastmod>
 78 |     <changefreq>monthly</changefreq>
 79 |     <priority>0.8</priority>
 80 |   </url>
 81 |   <url>
 82 |     <loc>https://flags.happykit.dev/demo/rollouts</loc>
 83 |     <lastmod>2022-07-15</lastmod>
 84 |     <changefreq>monthly</changefreq>
 85 |     <priority>0.8</priority>
 86 |   </url>
 87 |   <url>
 88 |     <loc>https://flags.happykit.dev/demo/disabled-revalidation</loc>
 89 |     <lastmod>2022-07-15</lastmod>
 90 |     <changefreq>monthly</changefreq>
 91 |     <priority>0.7</priority>
 92 |   </url>
 93 |   <url>
 94 |     <loc>https://flags.happykit.dev/demo/context</loc>
 95 |     <lastmod>2022-07-15</lastmod>
 96 |     <changefreq>monthly</changefreq>
 97 |     <priority>0.7</priority>
 98 |   </url>
 99 |   <url>
100 |     <loc>https://flags.happykit.dev/demo/dynamics</loc>
101 |     <lastmod>2022-07-15</lastmod>
102 |     <changefreq>monthly</changefreq>
103 |     <priority>0.7</priority>
104 |   </url>
105 |   <url>
106 |     <loc>https://flags.happykit.dev/docs/public-api</loc>
107 |     <lastmod>2022-07-15</lastmod>
108 |     <changefreq>monthly</changefreq>
109 |     <priority>0.9</priority>
110 |   </url>
111 | </urlset>
112 | 


--------------------------------------------------------------------------------
/example/tailwind.config.js:
--------------------------------------------------------------------------------
 1 | module.exports = {
 2 |   content: [
 3 |     "./app/**/*.{js,ts,jsx,tsx}",
 4 |     "./pages/**/*.{js,ts,jsx,tsx}",
 5 |     "./components/**/*.{js,ts,jsx,tsx}",
 6 |   ],
 7 |   theme: {
 8 |     extend: {},
 9 |   },
10 |   variants: {
11 |     extend: {},
12 |   },
13 |   plugins: [require("@tailwindcss/typography")],
14 | };
15 | 


--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "baseUrl": ".",
 4 |     "target": "es5",
 5 |     "lib": [
 6 |       "dom",
 7 |       "dom.iterable",
 8 |       "esnext"
 9 |     ],
10 |     "allowJs": true,
11 |     "skipLibCheck": true,
12 |     "strict": false,
13 |     "forceConsistentCasingInFileNames": true,
14 |     "noEmit": true,
15 |     "esModuleInterop": true,
16 |     "module": "esnext",
17 |     "moduleResolution": "node",
18 |     "resolveJsonModule": true,
19 |     "isolatedModules": true,
20 |     "strictNullChecks": true,
21 |     "noImplicitAny": true,
22 |     "jsx": "preserve",
23 |     "incremental": true,
24 |     "plugins": [
25 |       {
26 |         "name": "next"
27 |       }
28 |     ]
29 |   },
30 |   "include": [
31 |     "next-env.d.ts",
32 |     "**/*.ts",
33 |     "**/*.tsx",
34 |     ".next/types/**/*.ts"
35 |   ],
36 |   "exclude": [
37 |     "node_modules"
38 |   ]
39 | }
40 | 


--------------------------------------------------------------------------------
/example/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 |   "$schema": "https://openapi.vercel.sh/vercel.json",
3 |   "buildCommand": "cd .. && npx turbo run build --filter=example",
4 |   "ignoreCommand": "npx turbo-ignore"
5 | }
6 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "happykit",
 3 |   "private": true,
 4 |   "license": "MIT",
 5 |   "author": "Dominik Ferber",
 6 |   "scripts": {
 7 |     "dev": "pnpm turbo run dev",
 8 |     "test": "pnpm turbo run test",
 9 |     "build": "pnpm turbo run build",
10 |     "release": "pnpm build && changeset publish",
11 |     "version-packages": "changeset version && pnpm i --no-frozen-lockfile && git add ."
12 |   },
13 |   "workspaces": [
14 |     "package",
15 |     "example"
16 |   ],
17 |   "dependencies": {
18 |     "next": "14.1.4",
19 |     "react": "18.2.0",
20 |     "react-dom": "18.2.0"
21 |   },
22 |   "devDependencies": {
23 |     "@changesets/cli": "^2.27.1",
24 |     "turbo": "1.9.4"
25 |   }
26 | }
27 | 


--------------------------------------------------------------------------------
/package/ARCHITECTURE.md:
--------------------------------------------------------------------------------
 1 | # Architecture
 2 | 
 3 | We are using turborepo so you don't need to start any compilation.
 4 | 
 5 | This project uses two pnpm workspaces: `package` and `example`.
 6 | 
 7 | ## `package`
 8 | 
 9 | The `@happykit/flags` library lives inside `package`.
10 | 
11 | ## `example`
12 | 
13 | A Next.js example project lives inside `example`. The Next.js example project uses turborepo to load in `@happykit/flags` via the workspace.
14 | 
15 | # Publishing
16 | 
17 | ## Publishing for the `next` dist-tag
18 | 
19 | To publish a `next` version from the `package` folder run `pnpm test && pnpm build` and then `pnpm publish --tag next`.
20 | 
21 | This will ask you for the next version, automatically change it in `package.json` and commit it.
22 | 
23 | Don't forget to `git push` after publishing!
24 | 
25 | Here are the commands in a simple order:
26 | 
27 | ```bash
28 | git checkout next # ensure you are on "next"
29 | git status # ensure you can push and are up to date
30 | 
31 | cd package
32 | pnpm test
33 | pnpm build
34 | pnpm publish --tag next
35 | git push --follow-tags
36 | ```
37 | 
38 | ## Moving the `latest` dist-tag
39 | 
40 | Run this command to release a published version under the `latest` dist-tag:
41 | 
42 | ```
43 | npm dist-tag add @happykit/flags@<version> latest
44 | ```
45 | 


--------------------------------------------------------------------------------
/package/CHANGELOG.md:
--------------------------------------------------------------------------------
  1 | # @happykit/flags
  2 | 
  3 | ## 3.3.0
  4 | 
  5 | ### Minor Changes
  6 | 
  7 | - 79d13e9: allow disabling visitor key cookie
  8 | 
  9 |   You can now configure or disable the visitor key cookie `@happykit/flags` sets by default. You can pass a `serializeVisitorKeyCookie` function to the options when calling `createUseFlags` and `createGetFlags`.
 10 | 
 11 | ## 3.2.0
 12 | 
 13 | ### Minor Changes
 14 | 
 15 | - e473304: add Next.js [App Router](https://nextjs.org/blog/next-13-4#nextjs-app-router) support
 16 | 
 17 |   HappyKit has a feature called `visitorKey`, you can learn more about it [here](https://flags.happykit.dev/demo/targeting-by-visitor-key). If you want to use this feature with App Router you need to set the cookie from middleware using the `ensureVisitorKeyCookie` from `@happykit/flags/edge`. See the `example/middleware.ts` file in this repository for an example of how to do this. This is necessary as App Router pages can not set any cookies when they render, so we have to fall back to setting the cookie from middleware instead. If you do not need the `visitorKey` for your custom evaluation rules or rollouts then you do not need to set the cookie from middleware.
 18 | 
 19 | ## 3.1.3
 20 | 
 21 | ### Patch Changes
 22 | 
 23 | - 21a540c: add cache: no-store to all fetch requests
 24 | 
 25 | ## 3.1.2
 26 | 
 27 | ### Patch Changes
 28 | 
 29 | - c53e69a: ensure getEdgeFlags is compatible with Next.js 13.4
 30 | 
 31 | ## 3.1.1
 32 | 
 33 | ### Patch Changes
 34 | 
 35 | - d509435: Update the cookie logic to work with Next.js 13.
 36 | 
 37 | ## 3.1.0
 38 | 
 39 | ### Minor Changes
 40 | 
 41 | - b3c53da: Add custom storage capabilities
 42 | 
 43 | ## 3.0.0
 44 | 
 45 | ### Major Changes
 46 | 
 47 | - 1822587: BREAKING CHANGE: Configuration overhaul
 48 | 
 49 |   ### What
 50 | 
 51 |   This release changes HappyKit's configuration approach.
 52 | 
 53 |   Previously you had to create a `flags.config.js` file and import it into your `pages/_app.js` and into every middleware that wanted to use feature flags. If you were using your own `AppFlags` type, you also had to pass this type every time you invoked `getFlags()`, `useFlags()` or `getEdgeFlags()`. And the configuration options for client-, server- and edge were mixed together into a single `flags.config.js` file.
 54 | 
 55 |   ### Why
 56 | 
 57 |   This release replaces the existing configuration approach with a new one. This new approach configuration prepares happykit for upcoming features.
 58 | 
 59 |   ### How
 60 | 
 61 |   #### 1. Add `flags` folder
 62 | 
 63 |   Follow the updated [Setup](https://github.com/happykit/flags/tree/master/package#setup) instructions to create the `flags` folder in your own application, and fill it with.
 64 | 
 65 |   After this step, you should have
 66 | 
 67 |   - `./flags/config.ts` which exports a configuration
 68 |   - `./flags/client.ts` which exports a `useFlags` function
 69 |   - `./flags/server.ts` which exports a `getFlags` function
 70 |   - `./flags/edge.ts` which exports a `getEdgeFlags` function
 71 | 
 72 |   #### 2. Set up absolute imports
 73 | 
 74 |   Enable Absolute Imports as described [here](https://github.com/happykit/flags/tree/master/package#absolute-imports).
 75 | 
 76 |   #### 3. Adapt your imports
 77 | 
 78 |   Then change the application code in your `pages/` folder to use these functions from your `flags/` folder instead of from `@happykit/flags`:
 79 | 
 80 |   ```diff
 81 |   - import { useFlags } from "@happykit/flags/client"
 82 |   + import { useFlags } from "flags/client"
 83 |   ```
 84 | 
 85 |   ```diff
 86 |   - import { getFlags } from "@happykit/flags/server"
 87 |   + import { getFlags } from "flags/server"
 88 |   ```
 89 | 
 90 |   ```diff
 91 |   - import { getEdgeFlags } from "@happykit/flags/edge"
 92 |   + import { getEdgeFlags } from "flags/edge"
 93 |   ```
 94 | 
 95 |   _Note that because of the absolute imports we configured in step 2, all imports from `"flags/_"` will use the local flags folder you created in step 1.\*
 96 | 
 97 |   #### 4. Delete your old setup
 98 | 
 99 |   We can now delete the old setup since we no longer need it
100 | 
101 |   - delete `flags.config.js`
102 |   - remove the `flags.config` import from your `pages/_app` file
103 |     - you might be able to delete the `pages/_app` file if it's not doing anything else anymore
104 |   - remove the import of `flags.config` from your middleware
105 | 


--------------------------------------------------------------------------------
/package/api-route/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/api-route.js",
3 |   "module": "../dist/api-route.mjs",
4 |   "types": "../dist/api-route.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   presets: ["@babel/preset-env", "@babel/preset-typescript"],
3 | };
4 | 


--------------------------------------------------------------------------------
/package/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/client.js",
3 |   "module": "../dist/client.mjs",
4 |   "types": "../dist/client.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/config/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/config.js",
3 |   "module": "../dist/config.mjs",
4 |   "types": "../dist/config.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/context/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/context.js",
3 |   "module": "../dist/context.mjs",
4 |   "types": "../dist/context.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/edge/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/edge.js",
3 |   "module": "../dist/edge.mjs",
4 |   "types": "../dist/edge.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/evaluate/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/evaluate.js",
3 |   "module": "../dist/evaluate.mjs",
4 |   "types": "../dist/evaluate.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @typedef {import('ts-jest/dist/types')} */
2 | /** @type {import('@jest/types').Config.InitialOptions} */
3 | module.exports = {
4 |   preset: "ts-jest",
5 |   testEnvironment: "node",
6 |   // automock: false,
7 |   setupFiles: ["./jest/mutation-observer.js"],
8 | };
9 | 


--------------------------------------------------------------------------------
/package/jest/delete-all-cookies.ts:
--------------------------------------------------------------------------------
 1 | // Based heavily on various answers given in
 2 | // http://stackoverflow.com/questions/179355/clearing-all-cookies-with-javascript
 3 | 
 4 | function clearAllPaths(window: Window, cookieBase: string) {
 5 |   var p = window.location.pathname.split("/");
 6 |   window.document.cookie = cookieBase + "; path=/";
 7 |   while (p.length > 0) {
 8 |     window.document.cookie = cookieBase + "; path=" + p.join("/");
 9 |     p.pop();
10 |   }
11 | }
12 | 
13 | export function deleteAllCookies(window: Window) {
14 |   var cookies = window.document.cookie.split("; ");
15 |   for (var c = 0; c < cookies.length; c++) {
16 |     var encodedCookieName = encodeURIComponent(
17 |       cookies[c].split(";")[0].split("=")[0]
18 |     );
19 |     var cookieBase =
20 |       encodedCookieName + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT";
21 |     clearAllPaths(window, cookieBase);
22 | 
23 |     var d = window.location.hostname.split(".");
24 |     while (d.length > 0) {
25 |       clearAllPaths(window, cookieBase + "; domain=" + d.join("."));
26 |       d.shift();
27 |     }
28 |   }
29 | }
30 | 


--------------------------------------------------------------------------------
/package/jest/mutation-observer.js:
--------------------------------------------------------------------------------
1 | // This polyfill is necessary until tsdx upgrades to the latest jest and jsdom
2 | // https://github.com/testing-library/dom-testing-library/releases/tag/v7.0.0
3 | if (typeof window !== "undefined")
4 |   window.MutationObserver = require("@sheerun/mutationobserver-shim");
5 | 


--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "@happykit/flags",
 3 |   "version": "3.3.0",
 4 |   "description": "Feature Flags for Next.js",
 5 |   "author": "Dominik Ferber <dominik.ferber+npm@gmail.com> (http://dferber.de/)",
 6 |   "license": "MIT",
 7 |   "scripts": {
 8 |     "dev": "tsup --watch",
 9 |     "prebuild": "jest",
10 |     "build": "tsup --minify",
11 |     "test": "jest"
12 |   },
13 |   "keywords": [
14 |     "next",
15 |     "react",
16 |     "feature flags",
17 |     "feature toggling",
18 |     "flags"
19 |   ],
20 |   "homepage": "https://happykit.dev/",
21 |   "repository": {
22 |     "type": "git",
23 |     "url": "https://github.com/happykit/flags"
24 |   },
25 |   "peerDependencies": {
26 |     "next": ">=12.0.2",
27 |     "react": ">=16.13.1"
28 |   },
29 |   "dependencies": {
30 |     "nanoid": "3.2.0"
31 |   },
32 |   "devDependencies": {
33 |     "@babel/core": "7.17.0",
34 |     "@babel/preset-env": "7.16.11",
35 |     "@babel/preset-typescript": "7.16.7",
36 |     "@sheerun/mutationobserver-shim": "0.3.3",
37 |     "@testing-library/jest-dom": "5.16.5",
38 |     "@testing-library/react": "13.3.0",
39 |     "@testing-library/react-hooks": "8.0.1",
40 |     "@types/cookie": "0.4.1",
41 |     "@types/jest": "27.4.0",
42 |     "@types/node": "17.0.15",
43 |     "@types/react": "18.0.18",
44 |     "fetch-mock-jest": "1.5.1",
45 |     "jest": "27.5.0",
46 |     "next": "14.1.4",
47 |     "node-fetch": "^2.6.7",
48 |     "react": "18.2.0",
49 |     "react-dom": "18.2.0",
50 |     "ts-jest": "27.1.3",
51 |     "tsup": "^6.2.3",
52 |     "typescript": "4.5.5",
53 |     "whatwg-fetch": "3.6.2"
54 |   },
55 |   "files": [
56 |     "dist",
57 |     "client",
58 |     "config",
59 |     "context",
60 |     "edge",
61 |     "evaluate",
62 |     "server",
63 |     "api-route"
64 |   ]
65 | }
66 | 


--------------------------------------------------------------------------------
/package/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 |   "main": "../dist/server.js",
3 |   "module": "../dist/server.mjs",
4 |   "types": "../dist/server.d.ts"
5 | }
6 | 


--------------------------------------------------------------------------------
/package/src/api-route.ts:
--------------------------------------------------------------------------------
  1 | import type { Environment, Flag } from "./evaluation-types";
  2 | import type { NextFetchEvent, NextRequest } from "next/server";
  3 | import {
  4 |   evaluate,
  5 |   toTraits,
  6 |   toUser,
  7 |   toVariantValues,
  8 |   toVisitor,
  9 | } from "./evaluate";
 10 | 
 11 | // function toVariantIds(
 12 | //   input: Record<string, FlagVariant | null>
 13 | // ): Record<string, FlagVariant["id"] | null> {
 14 | //   return Object.entries(input).reduce<Record<string, FlagVariant["id"] | null>>(
 15 | //     (acc, [key, variant]) => {
 16 | //       acc[key] = variant ? variant.id : null;
 17 | //       return acc;
 18 | //     },
 19 | //     {}
 20 | //   );
 21 | // }
 22 | 
 23 | export type Definitions = {
 24 |   projectId: string;
 25 |   format: "v1";
 26 |   revision: string;
 27 |   flags: Flag[];
 28 | };
 29 | 
 30 | export type GetDefinitions = (
 31 |   projectId: string,
 32 |   envKey: string,
 33 |   environment: Environment
 34 | ) => Promise<Definitions | null>;
 35 | 
 36 | // We support the GET, POST, HEAD, and OPTIONS methods from any origin,
 37 | // and allow any header on requests. These headers must be present
 38 | // on all responses to all CORS preflight requests. In practice, this means
 39 | // all responses to OPTIONS requests.
 40 | const defaultCorsHeaders = {
 41 |   "Access-Control-Allow-Origin": "*",
 42 |   "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
 43 |   "Access-Control-Max-Age": "86400",
 44 |   "Access-Control-Expose-Headers": "*",
 45 | };
 46 | 
 47 | export function createApiRoute({
 48 |   /**
 49 |    * Load feature flag definitions from your data source.
 50 |    * Called when feature flags are evaluated.
 51 |    */
 52 |   getDefinitions,
 53 |   corsHeaders = defaultCorsHeaders,
 54 |   serverTiming = false,
 55 | }: {
 56 |   getDefinitions: GetDefinitions;
 57 |   corsHeaders?: Record<string, string>;
 58 |   serverTiming?: boolean;
 59 | }) {
 60 |   const headers = { ...corsHeaders, "content-type": "application/json" };
 61 | 
 62 |   return async function readHandler(
 63 |     request: NextRequest,
 64 |     event: NextFetchEvent
 65 |   ) {
 66 |     // to avoid handling additional requests during development
 67 |     if (request.url.endsWith("favicon.ico")) {
 68 |       return new Response(null, { status: 404, headers });
 69 |     }
 70 | 
 71 |     const body =
 72 |       request.method === "POST" ? await request.json().catch(() => ({})) : {};
 73 | 
 74 |     const visitorKeyFromRequestBody: string | null =
 75 |       body && typeof body.visitorKey === "string" ? body.visitorKey : null;
 76 | 
 77 |     // get visitor key from request body
 78 |     // or use "null" and assume that this is a static render
 79 |     //
 80 |     // this also means that it's the client's job to generate a visitorKey before
 81 |     // sending the request in case no key exists yet (e.g. for ssr)
 82 |     const visitorKey = visitorKeyFromRequestBody
 83 |       ? visitorKeyFromRequestBody
 84 |       : null;
 85 | 
 86 |     const user = toUser(body ? body.user : null);
 87 | 
 88 |     // visitor might be null when feature flags are requested for a static site
 89 |     const visitor = visitorKey ? toVisitor(visitorKey) : null;
 90 |     const traits = toTraits(body ? body.traits : null);
 91 | 
 92 |     // parse the environment from /api/flags/:environment instead
 93 |     // determine environment based on key to avoid additional request
 94 |     //
 95 |     // the worker route is defined as /api/flags* so that it matches these:
 96 |     // - /api/flags
 97 |     // - /api/flags/flags_pub_xxxxxxx
 98 |     // - /api/flags/flags_pub_preview_xxxxxxx
 99 |     // - /api/flags/flags_pub_development_xxxxxxx
100 |     //
101 |     // See https://developers.cloudflare.com/workers/platform/routes
102 |     // currently done in wrangler.toml, but not sure it's correct
103 |     const envKey = (() => {
104 |       const match = new URL(request.url).pathname.match(
105 |         /^\/api\/flags\/([_a-z0-9]+)$/
106 |       );
107 |       // try to use env key from url param at /api/flags/:envKey
108 |       return match && match.length === 2 ? match[1] : null;
109 |     })();
110 | 
111 |     if (typeof envKey !== "string")
112 |       // The response body is for developers only, it is not used by @happykit/flags
113 |       return new Response(JSON.stringify({ reason: "Missing envKey" }), {
114 |         status: 422,
115 |         headers,
116 |       });
117 | 
118 |     const match = envKey.match(
119 |       /^flags_pub_(?:(development|preview|production)_)?([a-z0-9]+)$/
120 |     );
121 | 
122 |     if (!match || match.length < 2)
123 |       // The response body is for developers only, it is not used by @happykit/flags
124 |       return new Response(JSON.stringify({ reason: "Invalid envKey" }), {
125 |         status: 422,
126 |         headers,
127 |       });
128 | 
129 |     const environment: Environment = (match[1] as Environment) || "production";
130 |     const projectId = match[2];
131 | 
132 |     let flags: Flag[];
133 | 
134 |     // read definitions from storage based on projectId
135 |     const originStart = Date.now();
136 |     const definitions = await getDefinitions(projectId, envKey, environment);
137 |     const originStop = Date.now();
138 | 
139 |     if (!definitions)
140 |       return new Response(null, {
141 |         status: 500,
142 |         statusText: "Internal Server Error",
143 |         headers,
144 |       });
145 | 
146 |     flags = definitions.flags;
147 | 
148 |     const evaluatedVariants = evaluate({
149 |       flags,
150 |       environment,
151 |       user,
152 |       visitor,
153 |       traits,
154 |     });
155 | 
156 |     const serverTimingHeader = serverTiming
157 |       ? {
158 |           "server-timing": [
159 |             originStop !== null && originStart !== null
160 |               ? `definitions;dur=${originStop - originStart}`
161 |               : null,
162 |           ]
163 |             .filter(Boolean)
164 |             .join(", "),
165 |         }
166 |       : null;
167 | 
168 |     return new Response(
169 |       JSON.stringify({
170 |         flags: toVariantValues(evaluatedVariants),
171 |         // resolvedVariantIds: toVariantIds(evaluatedVariants),
172 |         visitor: visitorKey ? { key: visitorKey } : null,
173 |       }),
174 |       { headers: { ...headers, ...serverTimingHeader } }
175 |     );
176 |   };
177 | }
178 | 


--------------------------------------------------------------------------------
/package/src/client.csr.spec.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @jest-environment jsdom
  3 |  */
  4 | import "@testing-library/jest-dom/extend-expect";
  5 | import "@testing-library/jest-dom";
  6 | import { renderHook } from "@testing-library/react-hooks";
  7 | import { createUseFlags, cache, UseFlagsOptions } from "./client";
  8 | import * as fetchMock from "fetch-mock-jest";
  9 | import { deleteAllCookies } from "../jest/delete-all-cookies";
 10 | import { nanoid } from "nanoid";
 11 | import { FlagBag, Flags } from "./internal/types";
 12 | 
 13 | let useFlags: ReturnType<typeof createUseFlags>;
 14 | 
 15 | beforeEach(() => {
 16 |   useFlags = createUseFlags({ envKey: "flags_pub_000000" });
 17 |   fetchMock.reset();
 18 |   deleteAllCookies(window);
 19 |   cache.clear();
 20 | });
 21 | 
 22 | describe("when cookie is not set", () => {
 23 |   it("generates a visitor key", async () => {
 24 |     let visitorKey;
 25 |     // mock incoming post request...
 26 |     fetchMock.post(
 27 |       // ...when it matches this...
 28 |       {
 29 |         url: "https://happykit.dev/api/flags/flags_pub_000000",
 30 |         body: {
 31 |           /* body is checked in the response function */
 32 |         },
 33 |         matchPartialBody: true,
 34 |       },
 35 |       // ... and respond with that...
 36 |       (url: string, options: RequestInit, request: Request) => {
 37 |         // parse visitorKey so we can mirror it back
 38 |         const body = JSON.parse(options.body as string);
 39 |         visitorKey = body.visitorKey;
 40 | 
 41 |         expect(body).toEqual({
 42 |           user: null,
 43 |           traits: null,
 44 |           visitorKey: expect.any(String),
 45 |         });
 46 | 
 47 |         return {
 48 |           flags: {
 49 |             ads: true,
 50 |             checkout: "medium",
 51 |             discount: 5,
 52 |             purchaseButtonLabel: "Purchase",
 53 |           },
 54 |           visitor: { key: visitorKey },
 55 |         };
 56 |       }
 57 |     );
 58 | 
 59 |     expect(document.cookie).toEqual("");
 60 | 
 61 |     const { result, waitForNextUpdate } = renderHook(() => useFlags());
 62 | 
 63 |     expect(result.all).toHaveLength(2);
 64 | 
 65 |     await waitForNextUpdate();
 66 | 
 67 |     expect(result.all).toEqual([
 68 |       {
 69 |         flags: null,
 70 |         data: null,
 71 |         error: null,
 72 |         fetching: false,
 73 |         settled: false,
 74 |         visitorKey: null,
 75 |         revalidate: expect.any(Function),
 76 |       },
 77 |       {
 78 |         flags: null,
 79 |         data: null,
 80 |         error: null,
 81 |         fetching: true,
 82 |         settled: false,
 83 |         visitorKey: visitorKey,
 84 |         revalidate: expect.any(Function),
 85 |       },
 86 |       {
 87 |         flags: {
 88 |           ads: true,
 89 |           checkout: "medium",
 90 |           discount: 5,
 91 |           purchaseButtonLabel: "Purchase",
 92 |         },
 93 |         data: {
 94 |           flags: {
 95 |             ads: true,
 96 |             checkout: "medium",
 97 |             discount: 5,
 98 |             purchaseButtonLabel: "Purchase",
 99 |           },
100 |           visitor: { key: visitorKey },
101 |         },
102 |         error: null,
103 |         fetching: false,
104 |         settled: true,
105 |         visitorKey: visitorKey,
106 |         revalidate: expect.any(Function),
107 |       },
108 |     ]);
109 | 
110 |     // visitor key may not change
111 |     expect((result.all[1] as FlagBag<any>).visitorKey).toEqual(
112 |       (result.all[2] as FlagBag<any>).visitorKey
113 |     );
114 |   });
115 | });
116 | 
117 | describe("when cookie is set", () => {
118 |   it("reuses the visitor key", async () => {
119 |     // prepare cookie before test
120 |     const visitorKeyInCookie = nanoid();
121 |     document.cookie = `hkvk=${visitorKeyInCookie}`;
122 | 
123 |     // prepare response before test
124 |     fetchMock.post(
125 |       {
126 |         url: "https://happykit.dev/api/flags/flags_pub_000000",
127 |         body: {
128 |           visitorKey: visitorKeyInCookie,
129 |           user: null,
130 |           traits: null,
131 |         },
132 |       },
133 |       {
134 |         headers: { "content-type": "application/json" },
135 |         body: {
136 |           flags: {
137 |             ads: true,
138 |             checkout: "medium",
139 |             discount: 5,
140 |             purchaseButtonLabel: "Purchase",
141 |           },
142 |           visitor: { key: visitorKeyInCookie },
143 |         },
144 |       }
145 |     );
146 | 
147 |     expect(document.cookie).toEqual(`hkvk=${visitorKeyInCookie}`);
148 | 
149 |     // start actual testing
150 |     const { result, waitForNextUpdate } = renderHook(() => useFlags());
151 | 
152 |     expect(result.all).toHaveLength(2);
153 | 
154 |     await waitForNextUpdate();
155 | 
156 |     expect(result.all).toEqual([
157 |       {
158 |         flags: null,
159 |         data: null,
160 |         error: null,
161 |         fetching: false,
162 |         settled: false,
163 |         visitorKey: null,
164 |         revalidate: expect.any(Function),
165 |       },
166 |       {
167 |         flags: null,
168 |         data: null,
169 |         error: null,
170 |         fetching: true,
171 |         settled: false,
172 |         visitorKey: visitorKeyInCookie,
173 |         revalidate: expect.any(Function),
174 |       },
175 |       {
176 |         flags: {
177 |           ads: true,
178 |           checkout: "medium",
179 |           discount: 5,
180 |           purchaseButtonLabel: "Purchase",
181 |         },
182 |         data: {
183 |           flags: {
184 |             ads: true,
185 |             checkout: "medium",
186 |             discount: 5,
187 |             purchaseButtonLabel: "Purchase",
188 |           },
189 |           visitor: { key: visitorKeyInCookie },
190 |         },
191 |         error: null,
192 |         fetching: false,
193 |         settled: true,
194 |         visitorKey: visitorKeyInCookie,
195 |         revalidate: expect.any(Function),
196 |       },
197 |     ]);
198 |   });
199 | });
200 | 
201 | describe("stories", () => {
202 |   describe("client-side rendering", () => {
203 |     it("works", async () => {
204 |       let generatedVisitorKey = null;
205 |       fetchMock.post(
206 |         {
207 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
208 |           body: {
209 |             /* checked in response function */
210 |           },
211 |           matchPartialBody: true,
212 |         },
213 |         (url: string, options: RequestInit, request: Request) => {
214 |           // parse visitorKey so we can mirror it back
215 |           const body = JSON.parse(options.body as string);
216 |           generatedVisitorKey = body.visitorKey;
217 | 
218 |           expect(body).toEqual({
219 |             user: null,
220 |             traits: null,
221 |             visitorKey: expect.any(String),
222 |           });
223 | 
224 |           return {
225 |             flags: {
226 |               ads: true,
227 |               checkout: "medium",
228 |               discount: 5,
229 |               purchaseButtonLabel: "Purchase",
230 |             },
231 |             visitor: { key: generatedVisitorKey },
232 |           };
233 |         }
234 |       );
235 | 
236 |       expect(document.cookie).toEqual("");
237 | 
238 |       const { result, waitForNextUpdate, rerender } = renderHook<
239 |         UseFlagsOptions,
240 |         FlagBag<Flags>
241 |       >((options) => useFlags(options), { initialProps: undefined });
242 | 
243 |       expect(result.all).toHaveLength(2);
244 | 
245 |       await waitForNextUpdate();
246 | 
247 |       expect(result.all).toHaveLength(3);
248 | 
249 |       fetchMock.post(
250 |         {
251 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
252 |           body: {
253 |             visitorKey: generatedVisitorKey,
254 |             user: { key: "george" },
255 |             traits: null,
256 |           },
257 |         },
258 |         {
259 |           flags: {
260 |             ads: true,
261 |             checkout: "medium",
262 |             discount: 10,
263 |             purchaseButtonLabel: "Purchase",
264 |           },
265 |           visitor: { key: generatedVisitorKey },
266 |         },
267 |         { overwriteRoutes: true }
268 |       );
269 | 
270 |       rerender({ user: { key: "george" } });
271 | 
272 |       expect(result.all).toHaveLength(5);
273 | 
274 |       await waitForNextUpdate();
275 | 
276 |       expect(result.all).toEqual([
277 |         {
278 |           flags: null,
279 |           data: null,
280 |           error: null,
281 |           fetching: false,
282 |           settled: false,
283 |           visitorKey: null,
284 |           revalidate: expect.any(Function),
285 |         },
286 |         {
287 |           flags: null,
288 |           data: null,
289 |           error: null,
290 |           fetching: true,
291 |           settled: false,
292 |           visitorKey: generatedVisitorKey,
293 |           revalidate: expect.any(Function),
294 |         },
295 |         {
296 |           flags: {
297 |             ads: true,
298 |             checkout: "medium",
299 |             discount: 5,
300 |             purchaseButtonLabel: "Purchase",
301 |           },
302 |           data: {
303 |             flags: {
304 |               ads: true,
305 |               checkout: "medium",
306 |               discount: 5,
307 |               purchaseButtonLabel: "Purchase",
308 |             },
309 |             visitor: {
310 |               key: generatedVisitorKey,
311 |             },
312 |           },
313 |           error: null,
314 |           fetching: false,
315 |           settled: true,
316 |           visitorKey: generatedVisitorKey,
317 |           revalidate: expect.any(Function),
318 |         },
319 |         {
320 |           flags: {
321 |             ads: true,
322 |             checkout: "medium",
323 |             discount: 5,
324 |             purchaseButtonLabel: "Purchase",
325 |           },
326 |           data: {
327 |             flags: {
328 |               ads: true,
329 |               checkout: "medium",
330 |               discount: 5,
331 |               purchaseButtonLabel: "Purchase",
332 |             },
333 |             visitor: {
334 |               key: generatedVisitorKey,
335 |             },
336 |           },
337 |           error: null,
338 |           fetching: false,
339 |           settled: true,
340 |           visitorKey: generatedVisitorKey,
341 |           revalidate: expect.any(Function),
342 |         },
343 |         {
344 |           flags: null,
345 |           data: null,
346 |           error: null,
347 |           fetching: true,
348 |           settled: false,
349 |           visitorKey: generatedVisitorKey,
350 |           revalidate: expect.any(Function),
351 |         },
352 |         {
353 |           flags: {
354 |             ads: true,
355 |             checkout: "medium",
356 |             discount: 10,
357 |             purchaseButtonLabel: "Purchase",
358 |           },
359 |           data: {
360 |             flags: {
361 |               ads: true,
362 |               checkout: "medium",
363 |               discount: 10,
364 |               purchaseButtonLabel: "Purchase",
365 |             },
366 |             visitor: {
367 |               key: generatedVisitorKey,
368 |             },
369 |           },
370 |           error: null,
371 |           fetching: false,
372 |           settled: true,
373 |           visitorKey: generatedVisitorKey,
374 |           revalidate: expect.any(Function),
375 |         },
376 |       ]);
377 |     });
378 |   });
379 | });
380 | 


--------------------------------------------------------------------------------
/package/src/client.spec.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * @jest-environment jsdom
 3 |  */
 4 | import "@testing-library/jest-dom/extend-expect";
 5 | import "@testing-library/jest-dom";
 6 | import { createUseFlags, cache } from "./client";
 7 | import * as fetchMock from "fetch-mock-jest";
 8 | import { deleteAllCookies } from "../jest/delete-all-cookies";
 9 | 
10 | let useFlags: ReturnType<typeof createUseFlags>;
11 | 
12 | beforeEach(() => {
13 |   useFlags = createUseFlags({ envKey: "flags_pub_000000" });
14 |   fetchMock.reset();
15 |   deleteAllCookies(window);
16 |   cache.clear();
17 | });
18 | 
19 | it("exports a useFlags hook", () => {
20 |   expect(typeof useFlags).toBe("function");
21 | });
22 | 


--------------------------------------------------------------------------------
/package/src/client.ssg.spec.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @jest-environment jsdom
  3 |  */
  4 | import "@testing-library/jest-dom/extend-expect";
  5 | import "@testing-library/jest-dom";
  6 | import { renderHook } from "@testing-library/react-hooks";
  7 | import { cache, createUseFlags } from "./client";
  8 | import * as fetchMock from "fetch-mock-jest";
  9 | import { deleteAllCookies } from "../jest/delete-all-cookies";
 10 | import { nanoid } from "nanoid";
 11 | import { Flags, InitialFlagState } from "./internal/types";
 12 | 
 13 | let useFlags: ReturnType<typeof createUseFlags>;
 14 | 
 15 | beforeEach(() => {
 16 |   useFlags = createUseFlags({ envKey: "flags_pub_000000" });
 17 |   fetchMock.reset();
 18 |   deleteAllCookies(window);
 19 |   cache.clear();
 20 | });
 21 | 
 22 | describe("when cookie is not set", () => {
 23 |   it("generates a visitorKey", async () => {
 24 |     let generatedVisitorKey;
 25 | 
 26 |     fetchMock.post(
 27 |       {
 28 |         url: "https://happykit.dev/api/flags/flags_pub_000000",
 29 |         body: {
 30 |           /* checked in response function */
 31 |         },
 32 |         matchPartialBody: true,
 33 |       },
 34 |       (url: string, options: RequestInit, request: Request) => {
 35 |         // parse visitorKey so we can mirror it back
 36 |         const body = JSON.parse(options.body as string);
 37 |         generatedVisitorKey = body.visitorKey;
 38 | 
 39 |         expect(body).toEqual({
 40 |           traits: null,
 41 |           user: null,
 42 |           visitorKey: expect.any(String),
 43 |         });
 44 | 
 45 |         expect(body.visitorKey).toHaveLength(21);
 46 | 
 47 |         return {
 48 |           flags: {
 49 |             ads: true,
 50 |             checkout: "short",
 51 |             discount: 5,
 52 |             purchaseButtonLabel: "Purchase",
 53 |           },
 54 |           visitor: { key: generatedVisitorKey },
 55 |         };
 56 |       }
 57 |     );
 58 | 
 59 |     expect(document.cookie).toEqual("");
 60 | 
 61 |     const initialStateFromProps: InitialFlagState<Flags> = {
 62 |       input: {
 63 |         endpoint: "https://happykit.dev/api/flags",
 64 |         envKey: "flags_pub_000000",
 65 |         requestBody: {
 66 |           visitorKey: null,
 67 |           user: null,
 68 |           traits: null,
 69 |         },
 70 |       },
 71 |       outcome: {
 72 |         data: {
 73 |           flags: {
 74 |             ads: true,
 75 |             checkout: null,
 76 |             discount: 5,
 77 |             purchaseButtonLabel: null,
 78 |           },
 79 |           visitor: null,
 80 |         },
 81 |       },
 82 |     };
 83 | 
 84 |     const { result, waitForNextUpdate } = renderHook(() =>
 85 |       useFlags({ initialState: initialStateFromProps })
 86 |     );
 87 | 
 88 |     expect(result.all).toHaveLength(2);
 89 |     expect(document.cookie).toEqual("");
 90 |     await waitForNextUpdate();
 91 |     expect(document.cookie).toEqual(`hkvk=${generatedVisitorKey}`);
 92 | 
 93 |     // the worker never sets cookies now, so the client has to deal
 94 |     // with generating a visitor key and storing it as a cookie
 95 |     //
 96 |     // we need this to work nicely with ssr and ssg, so the expected behaviour
 97 |     // of useFlags depends on which initial state is passed in
 98 |     //
 99 |     // rendered with ssr? => server should have generated visitorKey if not present, and it should be set in response
100 |     // rendered with ssg? => server sets visitorKey: null in request, and useFlags needs to reevaluate the flags after the initial render
101 | 
102 |     expect(result.all).toEqual([
103 |       {
104 |         flags: {
105 |           ads: true,
106 |           checkout: null,
107 |           discount: 5,
108 |           purchaseButtonLabel: null,
109 |         },
110 |         data: {
111 |           flags: {
112 |             ads: true,
113 |             checkout: null,
114 |             discount: 5,
115 |             purchaseButtonLabel: null,
116 |           },
117 |           visitor: null,
118 |         },
119 |         error: null,
120 |         fetching: false,
121 |         settled: false,
122 |         visitorKey: null,
123 |         revalidate: expect.any(Function),
124 |       },
125 |       {
126 |         flags: {
127 |           ads: true,
128 |           checkout: null,
129 |           discount: 5,
130 |           purchaseButtonLabel: null,
131 |         },
132 |         data: {
133 |           flags: {
134 |             ads: true,
135 |             checkout: null,
136 |             discount: 5,
137 |             purchaseButtonLabel: null,
138 |           },
139 |           visitor: null,
140 |         },
141 |         error: null,
142 |         fetching: true,
143 |         settled: false,
144 |         visitorKey: null,
145 |         revalidate: expect.any(Function),
146 |       },
147 |       {
148 |         flags: {
149 |           ads: true,
150 |           checkout: "short",
151 |           discount: 5,
152 |           purchaseButtonLabel: "Purchase",
153 |         },
154 |         data: {
155 |           flags: {
156 |             ads: true,
157 |             checkout: "short",
158 |             discount: 5,
159 |             purchaseButtonLabel: "Purchase",
160 |           },
161 |           visitor: { key: generatedVisitorKey },
162 |         },
163 |         error: null,
164 |         fetching: false,
165 |         settled: true,
166 |         visitorKey: generatedVisitorKey,
167 |         revalidate: expect.any(Function),
168 |       },
169 |     ]);
170 | 
171 |     // ensure there are no further updates
172 |     await expect(waitForNextUpdate({ timeout: 500 })).rejects.toThrow(
173 |       "Timed out"
174 |     );
175 |   });
176 | });
177 | 
178 | describe("when cookie is set", () => {
179 |   it("reuses the visitorKey", async () => {
180 |     // prepare cookie before test
181 |     const visitorKeyInCookie = nanoid();
182 |     document.cookie = `hkvk=${visitorKeyInCookie}`;
183 | 
184 |     fetchMock.post(
185 |       {
186 |         url: "https://happykit.dev/api/flags/flags_pub_000000",
187 |         body: {
188 |           traits: null,
189 |           user: null,
190 |           // not static because this is the request of the client afer hydration,
191 |           // not the one during static site generation
192 |           visitorKey: visitorKeyInCookie,
193 |         },
194 |       },
195 |       {
196 |         flags: {
197 |           ads: true,
198 |           checkout: "short",
199 |           discount: 5,
200 |           purchaseButtonLabel: "Purchase",
201 |         },
202 |         visitor: { key: visitorKeyInCookie },
203 |       }
204 |     );
205 | 
206 |     const initialStateFromProps: InitialFlagState<Flags> = {
207 |       input: {
208 |         endpoint: "https://happykit.dev/api/flags",
209 |         envKey: "flags_pub_000000",
210 |         requestBody: {
211 |           visitorKey: null,
212 |           user: null,
213 |           traits: null,
214 |         },
215 |       },
216 |       outcome: {
217 |         data: {
218 |           flags: {
219 |             ads: true,
220 |             checkout: null,
221 |             discount: 5,
222 |             purchaseButtonLabel: null,
223 |           },
224 |           visitor: null,
225 |         },
226 |       },
227 |     };
228 | 
229 |     const { result, waitForNextUpdate } = renderHook(() =>
230 |       useFlags({ initialState: initialStateFromProps })
231 |     );
232 | 
233 |     expect(result.all).toHaveLength(2);
234 |     expect(document.cookie).toEqual(`hkvk=${visitorKeyInCookie}`);
235 |     await waitForNextUpdate();
236 |     expect(document.cookie).toEqual(`hkvk=${visitorKeyInCookie}`);
237 | 
238 |     expect(result.all).toEqual([
239 |       {
240 |         flags: {
241 |           ads: true,
242 |           checkout: null,
243 |           discount: 5,
244 |           purchaseButtonLabel: null,
245 |         },
246 |         data: {
247 |           flags: {
248 |             ads: true,
249 |             checkout: null,
250 |             discount: 5,
251 |             purchaseButtonLabel: null,
252 |           },
253 |           visitor: null,
254 |         },
255 |         error: null,
256 |         fetching: false,
257 |         settled: false,
258 |         visitorKey: null,
259 |         revalidate: expect.any(Function),
260 |       },
261 |       {
262 |         flags: {
263 |           ads: true,
264 |           checkout: null,
265 |           discount: 5,
266 |           purchaseButtonLabel: null,
267 |         },
268 |         data: {
269 |           flags: {
270 |             ads: true,
271 |             checkout: null,
272 |             discount: 5,
273 |             purchaseButtonLabel: null,
274 |           },
275 |           visitor: null,
276 |         },
277 |         error: null,
278 |         fetching: true,
279 |         settled: false,
280 |         visitorKey: null,
281 |         revalidate: expect.any(Function),
282 |       },
283 |       {
284 |         flags: {
285 |           ads: true,
286 |           checkout: "short",
287 |           discount: 5,
288 |           purchaseButtonLabel: "Purchase",
289 |         },
290 |         data: {
291 |           flags: {
292 |             ads: true,
293 |             checkout: "short",
294 |             discount: 5,
295 |             purchaseButtonLabel: "Purchase",
296 |           },
297 |           visitor: { key: visitorKeyInCookie },
298 |         },
299 |         error: null,
300 |         fetching: false,
301 |         settled: true,
302 |         visitorKey: visitorKeyInCookie,
303 |         revalidate: expect.any(Function),
304 |       },
305 |     ]);
306 | 
307 |     // ensure there are no further updates
308 |     await expect(waitForNextUpdate({ timeout: 500 })).rejects.toThrow(
309 |       "Timed out"
310 |     );
311 |   });
312 | });
313 | 


--------------------------------------------------------------------------------
/package/src/client.ssr.spec.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @jest-environment jsdom
  3 |  */
  4 | import "@testing-library/jest-dom/extend-expect";
  5 | import "@testing-library/jest-dom";
  6 | import { renderHook } from "@testing-library/react-hooks";
  7 | import { createUseFlags, cache } from "./client";
  8 | import * as fetchMock from "fetch-mock-jest";
  9 | import { deleteAllCookies } from "../jest/delete-all-cookies";
 10 | import { nanoid } from "nanoid";
 11 | import { Flags, InitialFlagState } from "./internal/types";
 12 | 
 13 | let useFlags: ReturnType<typeof createUseFlags>;
 14 | 
 15 | beforeEach(() => {
 16 |   useFlags = createUseFlags({ envKey: "flags_pub_000000" });
 17 |   fetchMock.reset();
 18 |   deleteAllCookies(window);
 19 |   cache.clear();
 20 | });
 21 | 
 22 | describe("when visitorKey is not set in cookie", () => {
 23 |   it("uses the visitor key generated on the server", async () => {
 24 |     expect(document.cookie).toBe("");
 25 |     const generatedVisitorKey = nanoid();
 26 | 
 27 |     expect(document.cookie).toEqual("");
 28 | 
 29 |     const initialStateFromProps: InitialFlagState<Flags> = {
 30 |       input: {
 31 |         endpoint: "https://happykit.dev/api/flags",
 32 |         envKey: "flags_pub_000000",
 33 |         requestBody: {
 34 |           visitorKey: generatedVisitorKey,
 35 |           user: null,
 36 |           traits: null,
 37 |         },
 38 |       },
 39 |       outcome: {
 40 |         data: {
 41 |           flags: {
 42 |             ads: true,
 43 |             checkout: "short",
 44 |             discount: 5,
 45 |             purchaseButtonLabel: "Buy now",
 46 |           },
 47 |           visitor: { key: generatedVisitorKey },
 48 |         },
 49 |       },
 50 |     };
 51 | 
 52 |     const { result } = renderHook(() =>
 53 |       useFlags({ initialState: initialStateFromProps })
 54 |     );
 55 | 
 56 |     expect(result.all).toEqual([
 57 |       {
 58 |         flags: {
 59 |           ads: true,
 60 |           checkout: "short",
 61 |           discount: 5,
 62 |           purchaseButtonLabel: "Buy now",
 63 |         },
 64 |         data: {
 65 |           flags: {
 66 |             ads: true,
 67 |             checkout: "short",
 68 |             discount: 5,
 69 |             purchaseButtonLabel: "Buy now",
 70 |           },
 71 |           visitor: { key: generatedVisitorKey },
 72 |         },
 73 |         error: null,
 74 |         fetching: false,
 75 |         settled: true,
 76 |         visitorKey: generatedVisitorKey,
 77 |         revalidate: expect.any(Function),
 78 |       },
 79 |     ]);
 80 | 
 81 |     // should not fetch at all
 82 |     expect(fetchMock.calls()).toHaveLength(0);
 83 | 
 84 |     // getFlags() would set the cookies in the response to the page itself,
 85 |     // but that server function is mocked in this example.
 86 |     //
 87 |     // The header sent in response to the page load would look like
 88 |     // Set-Cookie: hkvk=generatedVisitorKey; Path=/; Max-Age=15552000; SameSite=Lax
 89 |     //
 90 |     // We expect an empty cookie instead, since this is a unit, in which
 91 |     // the getFlags() on the server does not run, and thus has no chance to
 92 |     // set the cookie.
 93 |     expect(document.cookie).toBe("");
 94 |   });
 95 | });
 96 | 
 97 | describe("when visitorKey is set in cookie", () => {
 98 |   it("reuses the visitor key", async () => {
 99 |     // prepare cookie before test
100 |     const visitorKeyInCookie = nanoid();
101 |     document.cookie = `hkvk=${visitorKeyInCookie}`;
102 | 
103 |     expect(document.cookie).toEqual(`hkvk=${visitorKeyInCookie}`);
104 | 
105 |     const initialStateFromProps: InitialFlagState<Flags> = {
106 |       input: {
107 |         endpoint: "https://happykit.dev/api/flags",
108 |         envKey: "flags_pub_000000",
109 |         requestBody: {
110 |           visitorKey: visitorKeyInCookie,
111 |           user: null,
112 |           traits: null,
113 |         },
114 |       },
115 |       outcome: {
116 |         data: {
117 |           flags: {
118 |             ads: true,
119 |             checkout: "short",
120 |             discount: 5,
121 |             purchaseButtonLabel: "Buy now",
122 |           },
123 |           visitor: { key: visitorKeyInCookie },
124 |         },
125 |       },
126 |     };
127 | 
128 |     // start actual testing
129 |     const { result } = renderHook(() =>
130 |       useFlags({ initialState: initialStateFromProps })
131 |     );
132 | 
133 |     // should not fetch at all
134 |     expect(fetchMock.calls()).toHaveLength(0);
135 | 
136 |     expect(result.all).toEqual([
137 |       {
138 |         fetching: false,
139 |         flags: {
140 |           ads: true,
141 |           checkout: "short",
142 |           discount: 5,
143 |           purchaseButtonLabel: "Buy now",
144 |         },
145 |         data: {
146 |           flags: {
147 |             ads: true,
148 |             checkout: "short",
149 |             discount: 5,
150 |             purchaseButtonLabel: "Buy now",
151 |           },
152 |           visitor: { key: visitorKeyInCookie },
153 |         },
154 |         error: null,
155 |         settled: true,
156 |         visitorKey: visitorKeyInCookie,
157 |         revalidate: expect.any(Function),
158 |       },
159 |     ]);
160 |   });
161 | });
162 | 


--------------------------------------------------------------------------------
/package/src/config.ts:
--------------------------------------------------------------------------------
 1 | import type { Flags } from "./internal/types";
 2 | 
 3 | /**
 4 |  * Configuration thing
 5 |  */
 6 | export type Configuration<F extends Flags> = {
 7 |   /**
 8 |    * Find this key in your happykit.dev project settings.
 9 |    *
10 |    * It specifies the project and environment your flags will be loaded for.
11 |    *
12 |    * There are three different keys per project, one for each of these
13 |    * environments: development, preview and production.
14 |    *
15 |    * It's recommeneded to stor eyour `envKey` in an environment variable like
16 |    * `NEXT_PUBLIC_FLAGS_ENV_KEY`. That way you can pass in a different env key
17 |    * for each environment easily.
18 |    */
19 |   envKey: string;
20 |   /**
21 |    * A flags object that will be used as the default.
22 |    *
23 |    * This default kicks in when the flags could not be loaded from the server
24 |    * for whatever reason.
25 |    *
26 |    * The default is also used to extend the loaded flags. When a flag was deleted
27 |    * in happykit, but you have a default set up for it, the default will be served.
28 |    *
29 |    * This is most useful to gracefully deal with loading errors of feature flags.
30 |    * It also keeps the number of possible states a flag can be in small, as
31 |    * you'll have the guarantee that all flags will always have a value when you set this.
32 |    *
33 |    * This can be useful while you're developing in case you haven't created a new
34 |    * flag yet, but want to program as if it already exists.
35 |    *
36 |    * @default `{}`
37 |    */
38 |   defaultFlags?: F;
39 |   /**
40 |    * Where the environment variables will be fetched from.
41 |    *
42 |    * This gets combined with your `envKey` into something like
43 |    * `https://happykit.dev/api/flags/flags_pub_000000000`.
44 |    *
45 |    * It is rare that you need to pass this in. It is mostly used for development
46 |    * of this library itself, but it might be useful when you have to proxy the
47 |    * feature flag requests for whatever reason.
48 |    *
49 |    * @default "https://happykit.dev/api/flags"
50 |    */
51 |   endpoint?: string;
52 | 
53 |   /**
54 |    * Gets called with a visitorKey and must return a string which can be assigned to `document.cookie` or null.
55 |    *
56 |    * The name of the cookie you set within that string must be `hkvk` (short for "happykit visitor key").
57 |    *
58 |    * If the option is undefined, it will use the default implementation which sets the cookie for 180 days.
59 |    * You can override this with your own behavior or write a function which returns null to set no cookie at all.
60 |    *
61 |    * @param visitorKey the randomly assigned key of the visitor
62 |    * @returns null or cookie to set, for example `hkvk=${encodeURIComponent(visitorKey)}; Path=/; Max-Age=15552000; SameSite=Lax`
63 |    */
64 |   serializeVisitorKeyCookie?: (visitorKey: string) => string | null;
65 | };
66 | 


--------------------------------------------------------------------------------
/package/src/context.ts:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | import { FlagBag, Flags } from "./internal/types";
 3 | 
 4 | /**
 5 |  * Allows you to access the flags from context
 6 |  */
 7 | const FlagBagContext = React.createContext<FlagBag<Flags> | null>(null);
 8 | 
 9 | /**
10 |  * This function accesses the `flagBag` from the context. You have to put it there
11 |  * first by rendering a `<FlagBagProvider value={flagBag} />`.
12 |  *
13 |  * _Note that it's generally better to explicitly pass your flags down as props,
14 |  * so you might not need this at all._
15 |  */
16 | export function createUseFlagBag<F extends Flags = Flags>() {
17 |   /**
18 |    * Accesses the evaluated flags from context.
19 |    *
20 |    * You need to render a <FlagBagProvider /> further up to be able to use
21 |    * this component.
22 |    */
23 |   return function useFlagBag() {
24 |     const flagBagContext = React.useContext(FlagBagContext);
25 |     if (flagBagContext === null)
26 |       throw new Error("Error: useFlagBag was used outside of FlagBagProvider.");
27 |     return flagBagContext as FlagBag<F>;
28 |   };
29 | }
30 | 
31 | /**
32 |  * If you want to be able to access the flags from context using `useFlagBag()`,
33 |  * you can render the FlagBagProvider at the top of your Next.js pages, like so:
34 |  *
35 |  * ```js
36 |  * import { useFlags } from "@happykit/flags/client"
37 |  * import { FlagBagProvider, useFlagBag } from "@happykit/flags/context"
38 |  *
39 |  * export default function YourPage () {
40 |  *   const flagBag = useFlags()
41 |  *
42 |  *   return (
43 |  *     <FlagBagProvider value={flagBag}>
44 |  *       <YourOwnComponent />
45 |  *     </FlagBagProvider>
46 |  *   )
47 |  * }
48 |  * ```
49 |  *
50 |  * You can then call `useFlagBag()` to access your `flagBag` from within
51 |  * `YourOwnComponent` or further down.
52 |  *
53 |  * _Note that it's generally better to explicitly pass your flags down as props,
54 |  * so you might not need this at all._
55 |  */
56 | export function FlagBagProvider<F extends Flags>(props: {
57 |   value: FlagBag<F>;
58 |   children: React.ReactNode;
59 | }) {
60 |   return React.createElement(
61 |     FlagBagContext.Provider,
62 |     { value: props.value },
63 |     props.children
64 |   );
65 | }
66 | 


--------------------------------------------------------------------------------
/package/src/edge.spec.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * @jest-environment node
  3 |  */
  4 | // whatwg-fetch defines HeadersInit globally which our tests need
  5 | import "whatwg-fetch";
  6 | import "@testing-library/jest-dom/extend-expect";
  7 | import * as fetchMock from "fetch-mock-jest";
  8 | import { createGetEdgeFlags } from "./edge";
  9 | import { nanoid } from "nanoid";
 10 | import { RequestCookies } from "next/dist/compiled/@edge-runtime/cookies";
 11 | 
 12 | jest.mock("nanoid", () => {
 13 |   return { nanoid: jest.fn() };
 14 | });
 15 | 
 16 | let getEdgeFlags: ReturnType<typeof createGetEdgeFlags>;
 17 | 
 18 | beforeEach(() => {
 19 |   getEdgeFlags = createGetEdgeFlags({ envKey: "flags_pub_000000" });
 20 |   fetchMock.reset();
 21 | });
 22 | 
 23 | // const { NextRequest } = await import(
 24 | //   "next/dist/server/web/spec-extension/request"
 25 | // );
 26 | function createNextRequest(options: { headers?: HeadersInit }) {
 27 |   const headers = new Headers(options.headers || []);
 28 | 
 29 |   return {
 30 |     headers,
 31 |     cookies: new RequestCookies(headers),
 32 |   };
 33 | }
 34 | 
 35 | describe("createGetEdgeFlags", () => {
 36 |   it("should throw when called without options", async () => {
 37 |     // @ts-ignore this is the situation we want to test
 38 |     expect(() => createGetEdgeFlags()).toThrowError(
 39 |       "@happykit/flags: config missing"
 40 |     );
 41 |   });
 42 | 
 43 |   it("should throw with missing envKey", async () => {
 44 |     // @ts-ignore this is the situation we want to test
 45 |     expect(() => createGetEdgeFlags({})).toThrowError(
 46 |       "@happykit/flags: envKey missing"
 47 |     );
 48 |   });
 49 | });
 50 | 
 51 | describe("middleware", () => {
 52 |   describe("when traits are passed in", () => {
 53 |     it("forwards the passed in traits", async () => {
 54 |       fetchMock.post(
 55 |         {
 56 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
 57 |           body: {
 58 |             traits: { teamMember: true },
 59 |             user: null,
 60 |             visitorKey: "V1StGXR8_Z5jdHi6B-myT",
 61 |           },
 62 |         },
 63 |         {
 64 |           headers: { "content-type": "application/json" },
 65 |           body: {
 66 |             flags: { meal: "large" },
 67 |             visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
 68 |           },
 69 |         }
 70 |       );
 71 | 
 72 |       const request = createNextRequest({
 73 |         headers: new Headers({ Cookie: "hkvk=V1StGXR8_Z5jdHi6B-myT" }),
 74 |       });
 75 | 
 76 |       expect(
 77 |         await getEdgeFlags({ request, traits: { teamMember: true } })
 78 |       ).toEqual({
 79 |         flags: { meal: "large" },
 80 |         data: {
 81 |           flags: { meal: "large" },
 82 |           visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
 83 |         },
 84 |         error: null,
 85 |         initialFlagState: {
 86 |           input: {
 87 |             endpoint: "https://happykit.dev/api/flags",
 88 |             envKey: "flags_pub_000000",
 89 |             requestBody: {
 90 |               traits: { teamMember: true },
 91 |               user: null,
 92 |               visitorKey: "V1StGXR8_Z5jdHi6B-myT",
 93 |             },
 94 |           },
 95 |           outcome: {
 96 |             data: {
 97 |               flags: { meal: "large" },
 98 |               visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
 99 |             },
100 |           },
101 |         },
102 |         cookie: {
103 |           args: [
104 |             "hkvk",
105 |             "V1StGXR8_Z5jdHi6B-myT",
106 |             { maxAge: 15552000, path: "/", sameSite: "lax" },
107 |           ],
108 |           name: "hkvk",
109 |           options: { maxAge: 15552000, path: "/", sameSite: "lax" },
110 |           value: "V1StGXR8_Z5jdHi6B-myT",
111 |         },
112 |       });
113 |     });
114 |   });
115 | 
116 |   describe("when user is passed in", () => {
117 |     it("forwards the passed in user", async () => {
118 |       fetchMock.post(
119 |         {
120 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
121 |           body: {
122 |             traits: null,
123 |             user: { key: "random-user-key", name: "joe" },
124 |             visitorKey: "V1StGXR8_Z5jdHi6B-myT",
125 |           },
126 |         },
127 |         {
128 |           headers: { "content-type": "application/json" },
129 |           body: {
130 |             flags: { meal: "large" },
131 |             visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
132 |           },
133 |         }
134 |       );
135 | 
136 |       const request = createNextRequest({
137 |         headers: { Cookie: "hkvk=V1StGXR8_Z5jdHi6B-myT" },
138 |       });
139 | 
140 |       expect(
141 |         await getEdgeFlags({
142 |           request,
143 |           user: { key: "random-user-key", name: "joe" },
144 |         })
145 |       ).toEqual({
146 |         flags: { meal: "large" },
147 |         data: {
148 |           flags: { meal: "large" },
149 |           visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
150 |         },
151 |         error: null,
152 |         initialFlagState: {
153 |           input: {
154 |             endpoint: "https://happykit.dev/api/flags",
155 |             envKey: "flags_pub_000000",
156 |             requestBody: {
157 |               traits: null,
158 |               user: { key: "random-user-key", name: "joe" },
159 |               visitorKey: "V1StGXR8_Z5jdHi6B-myT",
160 |             },
161 |           },
162 |           outcome: {
163 |             data: {
164 |               flags: { meal: "large" },
165 |               visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
166 |             },
167 |           },
168 |         },
169 |         cookie: {
170 |           args: [
171 |             "hkvk",
172 |             "V1StGXR8_Z5jdHi6B-myT",
173 |             {
174 |               maxAge: 15552000,
175 |               path: "/",
176 |               sameSite: "lax",
177 |             },
178 |           ],
179 |           name: "hkvk",
180 |           options: {
181 |             maxAge: 15552000,
182 |             path: "/",
183 |             sameSite: "lax",
184 |           },
185 |           value: "V1StGXR8_Z5jdHi6B-myT",
186 |         },
187 |       });
188 |     });
189 |   });
190 | 
191 |   describe("when no cookie exists on initial request", () => {
192 |     it("generates and sets the hkvk cookie", async () => {
193 |       // @ts-ignore
194 |       nanoid.mockReturnValueOnce("V1StGXR8_Z5jdHi6B-myT");
195 | 
196 |       fetchMock.post(
197 |         {
198 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
199 |           body: {
200 |             traits: null,
201 |             user: null,
202 |             // nanoid is mocked to return "V1StGXR8_Z5jdHi6B-myT",
203 |             // so the generated id is always this one
204 |             visitorKey: "V1StGXR8_Z5jdHi6B-myT",
205 |           },
206 |         },
207 |         {
208 |           headers: { "content-type": "application/json" },
209 |           body: {
210 |             flags: { meal: "large" },
211 |             visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
212 |           },
213 |         }
214 |       );
215 | 
216 |       const request = createNextRequest({ headers: { Cookie: "foo=bar" } });
217 | 
218 |       expect(await getEdgeFlags({ request })).toEqual({
219 |         flags: { meal: "large" },
220 |         data: {
221 |           flags: { meal: "large" },
222 |           visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
223 |         },
224 |         error: null,
225 |         initialFlagState: {
226 |           input: {
227 |             endpoint: "https://happykit.dev/api/flags",
228 |             envKey: "flags_pub_000000",
229 |             requestBody: {
230 |               traits: null,
231 |               user: null,
232 |               visitorKey: "V1StGXR8_Z5jdHi6B-myT",
233 |             },
234 |           },
235 |           outcome: {
236 |             data: {
237 |               flags: { meal: "large" },
238 |               visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
239 |             },
240 |           },
241 |         },
242 |         cookie: {
243 |           args: [
244 |             "hkvk",
245 |             "V1StGXR8_Z5jdHi6B-myT",
246 |             { maxAge: 15552000, path: "/", sameSite: "lax" },
247 |           ],
248 |           name: "hkvk",
249 |           options: { maxAge: 15552000, path: "/", sameSite: "lax" },
250 |           value: "V1StGXR8_Z5jdHi6B-myT",
251 |         },
252 |       });
253 |     });
254 |   });
255 | 
256 |   describe("when request cookies are an object", () => {
257 |     it("gets the cookie value", async () => {
258 |       fetchMock.post(
259 |         {
260 |           url: "https://happykit.dev/api/flags/flags_pub_000000",
261 |           body: {
262 |             traits: null,
263 |             user: null,
264 |             visitorKey: "V1StGXR8_Z5jdHi6B-myT",
265 |           },
266 |         },
267 |         {
268 |           headers: { "content-type": "application/json" },
269 |           body: {
270 |             flags: { meal: "large" },
271 |             visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
272 |           },
273 |         }
274 |       );
275 | 
276 |       const request = createNextRequest({
277 |         headers: { Cookie: "hkvk=V1StGXR8_Z5jdHi6B-myT" },
278 |       });
279 | 
280 |       expect(await getEdgeFlags({ request })).toEqual({
281 |         flags: { meal: "large" },
282 |         data: {
283 |           flags: { meal: "large" },
284 |           visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
285 |         },
286 |         error: null,
287 |         initialFlagState: {
288 |           input: {
289 |             endpoint: "https://happykit.dev/api/flags",
290 |             envKey: "flags_pub_000000",
291 |             requestBody: {
292 |               traits: null,
293 |               user: null,
294 |               visitorKey: "V1StGXR8_Z5jdHi6B-myT",
295 |             },
296 |           },
297 |           outcome: {
298 |             data: {
299 |               flags: { meal: "large" },
300 |               visitor: { key: "V1StGXR8_Z5jdHi6B-myT" },
301 |             },
302 |           },
303 |         },
304 |         cookie: {
305 |           args: [
306 |             "hkvk",
307 |             "V1StGXR8_Z5jdHi6B-myT",
308 |             { maxAge: 15552000, path: "/", sameSite: "lax" },
309 |           ],
310 |           name: "hkvk",
311 |           options: { maxAge: 15552000, path: "/", sameSite: "lax" },
312 |           value: "V1StGXR8_Z5jdHi6B-myT",
313 |         },
314 |       });
315 |     });
316 |   });
317 | });
318 | 


--------------------------------------------------------------------------------
/package/src/edge.ts:
--------------------------------------------------------------------------------
  1 | /** global: fetch */
  2 | import type { Configuration } from "./config";
  3 | import type { CookieSerializeOptions } from "cookie";
  4 | import { nanoid } from "nanoid";
  5 | import type {
  6 |   FlagUser,
  7 |   Traits,
  8 |   Flags,
  9 |   GenericEvaluationResponseBody,
 10 |   Input,
 11 |   GetFlagsSuccessBag,
 12 |   GetFlagsErrorBag,
 13 | } from "./internal/types";
 14 | import { combineRawFlagsWithDefaultFlags, has } from "./internal/utils";
 15 | import { applyConfigurationDefaults } from "./internal/apply-configuration-defaults";
 16 | import type { GetDefinitions, Definitions } from "./api-route";
 17 | import {
 18 |   evaluate,
 19 |   toTraits,
 20 |   toUser,
 21 |   toVariantValues,
 22 |   toVisitor,
 23 | } from "./evaluate";
 24 | import { resolvingErrorBag } from "./internal/errors";
 25 | 
 26 | export type { GenericEvaluationResponseBody } from "./internal/types";
 27 | 
 28 | function getRequestingIp(req: { headers: Headers }): null | string {
 29 |   const key = "x-forwarded-for";
 30 |   const xForwardedFor = req.headers.get(key);
 31 |   if (typeof xForwardedFor === "string") return xForwardedFor;
 32 |   return null;
 33 | }
 34 | 
 35 | interface FactoryGetEdgeFlagsOptions {
 36 |   getDefinitions?: GetDefinitions;
 37 | }
 38 | 
 39 | /**
 40 |  * Creates the getEdgeFlags() function your application should use when
 41 |  * loading flags from Middleware or Edge API Routes.
 42 |  */
 43 | export function createGetEdgeFlags<F extends Flags>(
 44 |   configuration: Configuration<F>,
 45 |   { getDefinitions: factoryGetDefinitions }: FactoryGetEdgeFlagsOptions = {}
 46 | ) {
 47 |   const cookieOptions: CookieSerializeOptions = {
 48 |     path: "/",
 49 |     maxAge: 60 * 60 * 24 * 180,
 50 |     sameSite: "lax",
 51 |   };
 52 | 
 53 |   const config = applyConfigurationDefaults(configuration);
 54 |   return async function getEdgeFlags(
 55 |     options: {
 56 |       request: {
 57 |         cookies?: any; // using any to be compatible with Next.js 12 and 13
 58 |         headers: Headers;
 59 |       };
 60 |       user?: FlagUser;
 61 |       traits?: Traits;
 62 |     } & FactoryGetEdgeFlagsOptions
 63 |   ): Promise<GetFlagsSuccessBag<F> | GetFlagsErrorBag<F>> {
 64 |     const currentGetDefinitions = has(options, "getDefinitions,")
 65 |       ? options.getDefinitions
 66 |       : factoryGetDefinitions;
 67 | 
 68 |     // determine visitor key
 69 |     let visitorKeyFromCookie;
 70 |     if (typeof options.request.cookies.get === "function") {
 71 |       const fromCookiesGet = options.request.cookies.get("hkvk");
 72 | 
 73 |       // In Next.js 13, the value returned from cookies.get() is an object with the type: { name: string, value: string }
 74 |       visitorKeyFromCookie =
 75 |         typeof fromCookiesGet === "string"
 76 |           ? fromCookiesGet
 77 |           : fromCookiesGet?.value;
 78 |     } else {
 79 |       // backwards compatible for when cookies was { [key: string]: string; }
 80 |       // in Next.js
 81 |       visitorKeyFromCookie = (options.request.cookies as any).hkvk || null;
 82 |     }
 83 | 
 84 |     // When using server-side rendering and there was no visitor key cookie,
 85 |     // we generate a visitor key
 86 |     // When using static rendering, we never set any visitor key
 87 |     const visitorKey = visitorKeyFromCookie ? visitorKeyFromCookie : nanoid();
 88 | 
 89 |     const input: Input = {
 90 |       endpoint: config.endpoint,
 91 |       envKey: config.envKey,
 92 |       requestBody: {
 93 |         visitorKey,
 94 |         user: options.user || null,
 95 |         traits: options.traits || null,
 96 |       },
 97 |     };
 98 | 
 99 |     const requestingIp = getRequestingIp(options.request);
100 | 
101 |     const xForwardedForHeader: { "x-forwarded-for": string } | {} = requestingIp
102 |       ? // add x-forwarded-for header so the service worker gets
103 |         // access to the real client ip
104 |         { "x-forwarded-for": requestingIp }
105 |       : {};
106 | 
107 |     // new logic
108 |     if (currentGetDefinitions) {
109 |       let definitions: Definitions | null;
110 |       try {
111 |         definitions = await currentGetDefinitions(
112 |           config.projectId,
113 |           config.envKey,
114 |           config.environment
115 |         );
116 |       } catch {
117 |         return resolvingErrorBag<F>({
118 |           error: "network-error",
119 |           flags: config.defaultFlags,
120 |           input,
121 |           cookie: null,
122 |         });
123 |       }
124 | 
125 |       // if definitions don't contain what we expect them to
126 |       if (
127 |         !definitions ||
128 |         definitions.format !== "v1" ||
129 |         definitions.projectId !== config.projectId ||
130 |         !Array.isArray(definitions.flags)
131 |       ) {
132 |         return resolvingErrorBag<F>({
133 |           error: "response-not-ok",
134 |           flags: config.defaultFlags,
135 |           input,
136 |           cookie: null,
137 |         });
138 |       }
139 | 
140 |       const evaluated = evaluate({
141 |         flags: definitions.flags,
142 |         environment: config.environment,
143 |         traits: options.traits ? toTraits(options.traits) : null,
144 |         user: options.user ? toUser(options.user) : null,
145 |         visitor: visitorKey ? toVisitor(visitorKey) : null,
146 |       });
147 | 
148 |       // not actually a response, as we evaluated inline
149 |       const outcomeData: GenericEvaluationResponseBody<F> = {
150 |         flags: toVariantValues(evaluated) as F,
151 |         visitor: visitorKey ? toVisitor(visitorKey) : null,
152 |       };
153 | 
154 |       // add defaults to flags here, but not in initialFlagState
155 |       const flags = outcomeData.flags ? outcomeData.flags : null;
156 |       const flagsWithDefaults = combineRawFlagsWithDefaultFlags<F>(
157 |         flags as F | null,
158 |         config.defaultFlags
159 |       );
160 | 
161 |       return {
162 |         flags: flagsWithDefaults,
163 |         data: outcomeData,
164 |         error: null,
165 |         initialFlagState: {
166 |           input,
167 |           outcome: { data: outcomeData },
168 |         },
169 |         cookie: outcomeData.visitor?.key
170 |           ? {
171 |               name: "hkvk",
172 |               value: outcomeData.visitor.key,
173 |               options: cookieOptions,
174 |               args: ["hkvk", outcomeData.visitor.key, cookieOptions],
175 |             }
176 |           : null,
177 |       };
178 |     }
179 |     // end new logic
180 | 
181 |     return fetch([input.endpoint, input.envKey].join("/"), {
182 |       method: "POST",
183 |       headers: Object.assign(
184 |         { "content-type": "application/json" },
185 |         xForwardedForHeader
186 |       ),
187 |       body: JSON.stringify(input.requestBody),
188 |       cache: "no-store",
189 |     }).then(
190 |       (
191 |         workerResponse
192 |       ):
193 |         | Promise<GetFlagsSuccessBag<F> | GetFlagsErrorBag<F>>
194 |         | GetFlagsErrorBag<F> => {
195 |         if (!workerResponse.ok /* status not 200-299 */) {
196 |           return resolvingErrorBag<F>({
197 |             error: "response-not-ok",
198 |             flags: config.defaultFlags,
199 |             input,
200 |             cookie: null,
201 |           });
202 |         }
203 | 
204 |         return workerResponse.json().then(
205 |           (outcomeData: GenericEvaluationResponseBody<F>) => {
206 |             // add defaults to flags here, but not in initialFlagState
207 |             const flags = outcomeData.flags ? outcomeData.flags : null;
208 |             const flagsWithDefaults = combineRawFlagsWithDefaultFlags<F>(
209 |               flags,
210 |               config.defaultFlags
211 |             );
212 | 
213 |             return {
214 |               flags: flagsWithDefaults,
215 |               data: outcomeData,
216 |               error: null,
217 |               initialFlagState: {
218 |                 input,
219 |                 outcome: { data: outcomeData },
220 |               },
221 |               cookie: outcomeData.visitor?.key
222 |                 ? {
223 |                     name: "hkvk",
224 |                     value: outcomeData.visitor.key,
225 |                     options: cookieOptions,
226 |                     args: ["hkvk", outcomeData.visitor.key, cookieOptions],
227 |                   }
228 |                 : null,
229 |             };
230 |           },
231 |           () => {
232 |             return resolvingErrorBag<F>({
233 |               error: "invalid-response-body",
234 |               flags: config.defaultFlags,
235 |               input,
236 |               cookie: null,
237 |             });
238 |           }
239 |         );
240 |       },
241 |       () => {
242 |         return resolvingErrorBag<F>({
243 |           error: "network-error",
244 |           input,
245 |           flags: config.defaultFlags,
246 |           cookie: null,
247 |         });
248 |       }
249 |     );
250 |   };
251 | }
252 | 
253 | /**
254 |  * Sets a cookie called hkvk on the response, which contains the generated visitorKey.
255 |  *
256 |  * @returns The generated visitorKey
257 |  */
258 | export function ensureVisitorKeyCookie(response: {
259 |   cookies: { set: (name: string, value: string, options: any) => any };
260 | }) {
261 |   const visitorKey = nanoid();
262 |   response.cookies.set("hkvk", visitorKey, {
263 |     path: "/",
264 |     maxAge: 60 * 60 * 24 * 180,
265 |     sameSite: "lax",
266 |   });
267 |   return visitorKey;
268 | }
269 | 


--------------------------------------------------------------------------------
/package/src/evaluate.spec.ts:
--------------------------------------------------------------------------------
 1 | import { Flag } from "./evaluation-types";
 2 | import { evaluate } from "./evaluate";
 3 | 
 4 | const fakeVariant1 = {
 5 |   id: "fake-variant-id-1",
 6 |   description: "Fake Description 1",
 7 |   name: "Fake Variant 1",
 8 |   value: "fake-variant-value-1",
 9 | };
10 | 
11 | const fakeVariant2 = {
12 |   id: "fake-variant-id-2",
13 |   description: "Fake Description 2",
14 |   name: "Fake Variant 2",
15 |   value: "fake-variant-value-2",
16 | };
17 | 
18 | const fakeVariant3 = {
19 |   id: "fake-variant-id-3",
20 |   description: "Fake Description 3",
21 |   name: "Fake Variant 3",
22 |   value: "fake-variant-value-3",
23 | };
24 | 
25 | const flag: Flag = {
26 |   id: "fake-id",
27 |   kind: "string",
28 |   slug: "fake-slug",
29 |   projectId: "fake-project-id",
30 |   variants: [fakeVariant1, fakeVariant2, fakeVariant3],
31 |   production: {
32 |     active: false,
33 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
34 |     offVariation: "fake-variant-id-2",
35 |     rules: [],
36 |     targets: {
37 |       "fake-variant-id-1": { values: [] },
38 |       "fake-variant-id-2": { values: [] },
39 |       "fake-variant-id-3": { values: [] },
40 |     },
41 |   },
42 |   development: {
43 |     active: false,
44 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
45 |     offVariation: "fake-variant-id-2",
46 |     rules: [],
47 |     targets: {
48 |       "fake-variant-id-1": { values: [] },
49 |       "fake-variant-id-2": { values: [] },
50 |       "fake-variant-id-3": { values: [] },
51 |     },
52 |   },
53 |   preview: {
54 |     active: false,
55 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
56 |     offVariation: "fake-variant-id-2",
57 |     rules: [],
58 |     targets: {
59 |       "fake-variant-id-1": { values: [] },
60 |       "fake-variant-id-2": { values: [] },
61 |       "fake-variant-id-3": { values: [] },
62 |     },
63 |   },
64 | };
65 | 
66 | // more in-depth tests are in resolve-flag-to-variant.spec.ts
67 | describe("evaluate", () => {
68 |   it("should be a function", () => {
69 |     expect(typeof evaluate).toEqual("function");
70 |   });
71 | 
72 |   it("evaluates flags", () => {
73 |     expect(
74 |       evaluate({
75 |         flags: [flag],
76 |         environment: "production",
77 |         user: null,
78 |         visitor: null,
79 |         traits: null,
80 |       })
81 |     ).toEqual({
82 |       "fake-slug": {
83 |         description: "Fake Description 2",
84 |         id: "fake-variant-id-2",
85 |         name: "Fake Variant 2",
86 |         value: "fake-variant-value-2",
87 |       },
88 |     });
89 |   });
90 | });
91 | 


--------------------------------------------------------------------------------
/package/src/evaluate.ts:
--------------------------------------------------------------------------------
  1 | import type {
  2 |   Environment,
  3 |   Flag,
  4 |   FlagUserAttributes,
  5 |   FlagVariant,
  6 |   FlagVisitor,
  7 | } from "./evaluation-types";
  8 | import { resolveFlagToVariant } from "./internal/resolve-flag-to-variant";
  9 | import type { FlagUser, Traits } from "./internal/types";
 10 | 
 11 | export function toUser(incomingUser: {
 12 |   key: string;
 13 |   email?: unknown;
 14 |   name?: unknown;
 15 |   avatar?: unknown;
 16 |   language?: unknown;
 17 |   country?: unknown;
 18 |   timeZone?: unknown;
 19 | }) {
 20 |   if (!incomingUser) return null;
 21 |   if (typeof incomingUser !== "object") return null;
 22 |   if (typeof incomingUser.key !== "string") return null;
 23 |   if (incomingUser.key.trim().length === 0) return null;
 24 | 
 25 |   const user: FlagUserAttributes = {
 26 |     key: incomingUser.key.trim().substring(0, 516),
 27 |   };
 28 | 
 29 |   if (typeof incomingUser.email === "string")
 30 |     user.email = incomingUser.email.trim().substring(0, 516);
 31 |   if (typeof incomingUser.name === "string")
 32 |     user.name = incomingUser.name.trim().substring(0, 516);
 33 |   if (typeof incomingUser.avatar === "string")
 34 |     user.avatar = incomingUser.avatar.trim().substring(0, 1024);
 35 |   if (typeof incomingUser.language === "string")
 36 |     user.language = incomingUser.language.trim().substring(0, 1024);
 37 |   if (typeof incomingUser.timeZone === "string")
 38 |     user.timeZone = incomingUser.timeZone.trim().substring(0, 128);
 39 |   if (typeof incomingUser.country === "string")
 40 |     user.country = incomingUser.country.trim().substring(0, 2).toUpperCase();
 41 | 
 42 |   return user;
 43 | }
 44 | 
 45 | // the visitor attributes are always collected by the worker
 46 | export function toVisitor(visitorKey: string): FlagVisitor {
 47 |   return { key: visitorKey };
 48 | }
 49 | 
 50 | /**
 51 |  * Traits must be JSON.stringify-able and the result may at most be 4096 chars.
 52 |  * Keys may at most be 1024 chars.
 53 |  * otherwise the trait gets left out.
 54 |  */
 55 | export function toTraits(
 56 |   incomingTraits?: {
 57 |     [key: string]: any;
 58 |   } | null
 59 | ): { [key: string]: any } | null {
 60 |   if (!incomingTraits) return null;
 61 |   if (typeof incomingTraits !== "object") return null;
 62 | 
 63 |   return Object.entries(incomingTraits).reduce<{ [key: string]: any }>(
 64 |     (acc, [key, value]) => {
 65 |       if (String(key).length > 1024) return acc;
 66 |       try {
 67 |         if (JSON.stringify(value).length > 4096) return acc;
 68 |       } catch (e) {
 69 |         return acc;
 70 |       }
 71 |       acc[key] = value;
 72 |       return acc;
 73 |     },
 74 |     {}
 75 |   );
 76 | }
 77 | 
 78 | export function toVariantValues(
 79 |   input: Record<string, FlagVariant | null>
 80 | ): Record<string, FlagVariant["value"] | null> {
 81 |   return Object.entries(input).reduce<
 82 |     Record<string, FlagVariant["value"] | null>
 83 |   >((acc, [key, variant]) => {
 84 |     acc[key] = variant ? variant.value : null;
 85 |     return acc;
 86 |   }, {});
 87 | }
 88 | 
 89 | /**
 90 |  * Evaluates feature flags to their variant values given some inputs.
 91 |  */
 92 | export function evaluate({
 93 |   flags,
 94 |   environment,
 95 |   user,
 96 |   visitor,
 97 |   traits,
 98 | }: {
 99 |   flags: Flag[];
100 |   environment: Environment;
101 |   user: FlagUser | null;
102 |   visitor: FlagVisitor | null;
103 |   traits: Traits | null;
104 | }) {
105 |   return flags.reduce<Record<string, FlagVariant | null>>((acc, flag) => {
106 |     const variant: FlagVariant | null = resolveFlagToVariant({
107 |       flag,
108 |       environment,
109 |       user,
110 |       visitor,
111 |       traits,
112 |     });
113 | 
114 |     acc[flag.slug] = variant ? variant : null;
115 |     return acc;
116 |   }, {});
117 | }
118 | 


--------------------------------------------------------------------------------
/package/src/evaluation-types.ts:
--------------------------------------------------------------------------------
  1 | export type VariantFlagResolution = {
  2 |   mode: "variant";
  3 |   variant: string;
  4 | };
  5 | 
  6 | export type Condition =
  7 |   | {
  8 |       id: string;
  9 |       group: "visitor";
 10 |       lhs: "key";
 11 |       operator: "equal-to" | "not-equal-to" | "starts-with" | "ends-with";
 12 |       rhs: string;
 13 |     }
 14 |   | {
 15 |       id: string;
 16 |       group: "visitor";
 17 |       lhs: "key" | "language" | "country" | "referrer" | "timeZone";
 18 |       operator: "set" | "not-set";
 19 |     }
 20 |   | {
 21 |       id: string;
 22 |       group: "user";
 23 |       lhs: "key" | "email" | "name" | "language" | "country";
 24 |       operator: "set" | "not-set";
 25 |     }
 26 |   | {
 27 |       id: string;
 28 |       group: "user";
 29 |       lhs: "key" | "email" | "name" | "language" | "country";
 30 |       operator: "equal-to" | "not-equal-to" | "starts-with" | "ends-with";
 31 |       rhs: string;
 32 |     }
 33 |   | {
 34 |       id: string;
 35 |       group: "user";
 36 |       lhs: "authentication";
 37 |       operator: "authenticated" | "not-authenticated";
 38 |     }
 39 |   | {
 40 |       id: string;
 41 |       group: "traits";
 42 |       lhs: string;
 43 |       operator: "set" | "not-set";
 44 |     }
 45 |   | {
 46 |       id: string;
 47 |       group: "traits";
 48 |       lhs: string;
 49 |       operator: "equal-to" | "not-equal-to" | "starts-with" | "ends-with";
 50 |       rhs: string;
 51 |     };
 52 | 
 53 | export type FlagRule = {
 54 |   id: string;
 55 |   conditions: Condition[];
 56 |   resolution: FlagResolution;
 57 | };
 58 | 
 59 | export interface BaseRolloutFlagResolution {
 60 |   /**
 61 |    * This property does not do anything on rollouts.
 62 |    * It might still be around due to inconsistent data, which
 63 |    * can happen due to the form submitting wrong data.
 64 |    */
 65 |   variant?: string;
 66 |   mode: "rollout";
 67 |   variants: Record<string, { weight: number }>;
 68 | }
 69 | 
 70 | export interface VisitorRolloutFlagResolution
 71 |   extends BaseRolloutFlagResolution {
 72 |   bucketByCategory: "visitor";
 73 |   bucketByUserAttribute?: never;
 74 |   bucketByTrait?: never;
 75 | }
 76 | 
 77 | export interface UserRolloutFlagResolution extends BaseRolloutFlagResolution {
 78 |   bucketByCategory: "user";
 79 |   bucketByUserAttribute: "key" | "email" | "name" | "country";
 80 |   bucketByTrait?: never;
 81 | }
 82 | 
 83 | export interface TraitRolloutFlagResolution extends BaseRolloutFlagResolution {
 84 |   bucketByCategory: "trait";
 85 |   bucketByUserAttribute?: never;
 86 |   bucketByTrait: string;
 87 | }
 88 | 
 89 | export type RolloutFlagResolution =
 90 |   | UserRolloutFlagResolution
 91 |   | VisitorRolloutFlagResolution
 92 |   | TraitRolloutFlagResolution;
 93 | 
 94 | export type FlagResolution = VariantFlagResolution | RolloutFlagResolution;
 95 | 
 96 | export type EnvironmentConfiguration = {
 97 |   active: boolean;
 98 |   fallthrough: FlagResolution;
 99 |   offVariation: string;
100 |   /**
101 |    * Keys are variantIds, values are keys of users targeted by that flag
102 |    */
103 |   targets: Record<string, { values: string[] }>;
104 |   rules: FlagRule[];
105 | };
106 | 
107 | // import * as apiFlag from "../../app/types/api/flag";
108 | export interface BaseFlag {
109 |   kind: "boolean" | "number" | "string";
110 |   id: string;
111 |   projectId: string;
112 |   slug: string;
113 |   production: EnvironmentConfiguration;
114 |   preview: EnvironmentConfiguration;
115 |   development: EnvironmentConfiguration;
116 |   variants: BooleanVariant[] | NumberVariant[] | StringVariant[];
117 | }
118 | 
119 | export interface BaseVariant {
120 |   id: string;
121 |   value: string | number | boolean;
122 | }
123 | 
124 | export interface BooleanVariant extends BaseVariant {
125 |   value: boolean;
126 | }
127 | export interface NumberVariant extends BaseVariant {
128 |   value: number;
129 | }
130 | export interface StringVariant extends BaseVariant {
131 |   value: string;
132 | }
133 | 
134 | export interface BooleanFlag extends BaseFlag {
135 |   kind: "boolean";
136 |   variants: BooleanVariant[];
137 | }
138 | 
139 | export interface NumberFlag extends BaseFlag {
140 |   kind: "number";
141 |   variants: NumberVariant[];
142 | }
143 | 
144 | export interface StringFlag extends BaseFlag {
145 |   kind: "string";
146 |   variants: StringVariant[];
147 | }
148 | 
149 | export type Flag = BooleanFlag | NumberFlag | StringFlag;
150 | 
151 | // from Models.Fauna
152 | export type FlagUserAttributes = {
153 |   key: string;
154 |   email?: string;
155 |   name?: string;
156 |   avatar?: string;
157 |   language?: string;
158 |   timeZone?: string;
159 |   country?: string;
160 | };
161 | 
162 | // from Models.Fauna
163 | export type FlagVisitor = {
164 |   key: string;
165 | };
166 | 
167 | export type FlagVariant = {
168 |   id: string;
169 |   value: string | number | boolean;
170 | };
171 | 
172 | export type Environment = "development" | "preview" | "production";
173 | 


--------------------------------------------------------------------------------
/package/src/internal/apply-configuration-defaults.ts:
--------------------------------------------------------------------------------
 1 | import type { Flags, FullConfiguration } from "./types";
 2 | import type { Configuration } from "../config";
 3 | import type { Environment } from "../evaluation-types";
 4 | 
 5 | function serializeVisitorKeyCookie(visitorKey: string) {
 6 |   // Max-Age 15552000 seconds equals 180 days
 7 |   return `hkvk=${encodeURIComponent(
 8 |     visitorKey
 9 |   )}; Path=/; Max-Age=15552000; SameSite=Lax`;
10 | }
11 | 
12 | export function applyConfigurationDefaults<F extends Flags>(
13 |   incomingConfig: Configuration<F>
14 | ) {
15 |   if (!incomingConfig) throw new Error("@happykit/flags: config missing");
16 |   if (!incomingConfig.envKey || incomingConfig.envKey.length === 0)
17 |     throw new Error("@happykit/flags: envKey missing");
18 | 
19 |   const defaults: Partial<Configuration<F>> = {
20 |     endpoint: "https://happykit.dev/api/flags",
21 |     defaultFlags: {} as F,
22 |     serializeVisitorKeyCookie,
23 |   };
24 | 
25 |   const match = incomingConfig.envKey.match(
26 |     /^flags_pub_(?:(development|preview|production)_)?([a-z0-9]+)$/
27 |   );
28 |   if (!match) throw new Error("@happykit/flags: invalid envKey");
29 |   const projectId = match[2];
30 |   const environment: Environment = (match[1] as Environment) || "production";
31 | 
32 |   if (!projectId)
33 |     throw new Error("@happykit/flags: could not parse projectId from envKey");
34 |   if (!environment)
35 |     throw new Error("@happykit/flags: could not parse environment from envKey");
36 | 
37 |   return Object.assign({}, defaults, incomingConfig, {
38 |     projectId,
39 |     environment,
40 |   }) as FullConfiguration<F>;
41 | }
42 | 


--------------------------------------------------------------------------------
/package/src/internal/errors.ts:
--------------------------------------------------------------------------------
 1 | import { Flags, Input, GetFlagsErrorBag, ResolvingError } from "./types";
 2 | 
 3 | export function resolvingErrorBag<F extends Flags>(options: {
 4 |   error: ResolvingError;
 5 |   input: Input;
 6 |   flags: F;
 7 |   cookie?: GetFlagsErrorBag<F>["cookie"];
 8 | }): GetFlagsErrorBag<F> {
 9 |   return {
10 |     flags: options.flags as F,
11 |     data: null,
12 |     error: options.error,
13 |     initialFlagState: {
14 |       input: options.input,
15 |       outcome: { error: options.error },
16 |     },
17 |     cookie: options.cookie,
18 |   };
19 | }
20 | 


--------------------------------------------------------------------------------
/package/src/internal/resolve-flag-to-variant.spec.ts:
--------------------------------------------------------------------------------
  1 | import { resolveFlagToVariant } from "./resolve-flag-to-variant";
  2 | import type { Flag } from "../evaluation-types";
  3 | 
  4 | const fakeVariant1 = {
  5 |   id: "fake-variant-id-1",
  6 |   description: "Fake Description 1",
  7 |   name: "Fake Variant 1",
  8 |   value: "fake-variant-value-1",
  9 | };
 10 | 
 11 | const fakeVariant2 = {
 12 |   id: "fake-variant-id-2",
 13 |   description: "Fake Description 2",
 14 |   name: "Fake Variant 2",
 15 |   value: "fake-variant-value-2",
 16 | };
 17 | 
 18 | const fakeVariant3 = {
 19 |   id: "fake-variant-id-3",
 20 |   description: "Fake Description 3",
 21 |   name: "Fake Variant 3",
 22 |   value: "fake-variant-value-3",
 23 | };
 24 | 
 25 | const flag: Flag = {
 26 |   id: "fake-id",
 27 |   kind: "string",
 28 |   slug: "fake-slug",
 29 |   projectId: "fake-project-id",
 30 |   variants: [fakeVariant1, fakeVariant2, fakeVariant3],
 31 |   production: {
 32 |     active: false,
 33 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
 34 |     offVariation: "fake-variant-id-2",
 35 |     rules: [],
 36 |     targets: {
 37 |       "fake-variant-id-1": { values: [] },
 38 |       "fake-variant-id-2": { values: [] },
 39 |       "fake-variant-id-3": { values: [] },
 40 |     },
 41 |   },
 42 |   development: {
 43 |     active: false,
 44 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
 45 |     offVariation: "fake-variant-id-2",
 46 |     rules: [],
 47 |     targets: {
 48 |       "fake-variant-id-1": { values: [] },
 49 |       "fake-variant-id-2": { values: [] },
 50 |       "fake-variant-id-3": { values: [] },
 51 |     },
 52 |   },
 53 |   preview: {
 54 |     active: false,
 55 |     fallthrough: { mode: "variant", variant: "fake-variant-id-1" },
 56 |     offVariation: "fake-variant-id-2",
 57 |     rules: [],
 58 |     targets: {
 59 |       "fake-variant-id-1": { values: [] },
 60 |       "fake-variant-id-2": { values: [] },
 61 |       "fake-variant-id-3": { values: [] },
 62 |     },
 63 |   },
 64 | };
 65 | 
 66 | describe("requests from client", () => {
 67 |   it("serves the offVariation when flag is turned off", () => {
 68 |     expect(
 69 |       resolveFlagToVariant({
 70 |         visitor: { key: "fake-visitor-id" },
 71 |         flag,
 72 |         environment: "production",
 73 |       })
 74 |     ).toEqual({
 75 |       id: "fake-variant-id-2",
 76 |       description: "Fake Description 2",
 77 |       name: "Fake Variant 2",
 78 |       value: "fake-variant-value-2",
 79 |     });
 80 |   });
 81 | 
 82 |   it("serves the fallthrough when flag is turned on", () => {
 83 |     expect(
 84 |       resolveFlagToVariant({
 85 |         visitor: { key: "fake-visitor-id" },
 86 |         flag: { ...flag, production: { ...flag.production, active: true } },
 87 |         environment: "production",
 88 |       })
 89 |     ).toEqual({
 90 |       id: "fake-variant-id-1",
 91 |       description: "Fake Description 1",
 92 |       name: "Fake Variant 1",
 93 |       value: "fake-variant-value-1",
 94 |     });
 95 |   });
 96 | 
 97 |   it("serves a specific variant for a targeted user", () => {
 98 |     expect(
 99 |       resolveFlagToVariant({
100 |         visitor: { key: "fake-visitor-id" },
101 |         flag: {
102 |           ...flag,
103 |           production: {
104 |             ...flag.production,
105 |             active: true,
106 |             targets: {
107 |               "fake-variant-id-1": { values: [] },
108 |               "fake-variant-id-2": { values: [] },
109 |               "fake-variant-id-3": { values: ["foo", "fake-user-id"] },
110 |             },
111 |           },
112 |         },
113 |         environment: "production",
114 |         user: { key: "fake-user-id" },
115 |       })
116 |     ).toEqual(fakeVariant3);
117 |   });
118 | 
119 |   describe("rules", () => {
120 |     it("serves a specific variant by rule", () => {
121 |       expect(
122 |         resolveFlagToVariant({
123 |           visitor: { key: "fake-visitor-id" },
124 |           flag: {
125 |             ...flag,
126 |             production: {
127 |               ...flag.production,
128 |               active: true,
129 |               rules: [
130 |                 {
131 |                   id: "fake-rule-id-1",
132 |                   conditions: [
133 |                     {
134 |                       id: "fake-condition-id-1",
135 |                       group: "user",
136 |                       lhs: "key",
137 |                       operator: "equal-to",
138 |                       rhs: "fake-user-id",
139 |                     },
140 |                   ],
141 |                   resolution: { mode: "variant", variant: "fake-variant-id-3" },
142 |                 },
143 |               ],
144 |             },
145 |           },
146 |           environment: "production",
147 |           user: { key: "fake-user-id" },
148 |         })
149 |       ).toEqual(fakeVariant3);
150 |     });
151 | 
152 |     it("serves a specific variant by user email rule", () => {
153 |       expect(
154 |         resolveFlagToVariant({
155 |           visitor: { key: "fake-visitor-id" },
156 |           user: {
157 |             key: "fake-user-id",
158 |             email: "test@foo.com",
159 |           },
160 |           environment: "production",
161 |           flag: {
162 |             ...flag,
163 |             production: {
164 |               ...flag.production,
165 |               active: true,
166 |               rules: [
167 |                 {
168 |                   id: "fake-rule-id-1",
169 |                   conditions: [
170 |                     {
171 |                       id: "fake-condition-id-1",
172 |                       group: "user",
173 |                       lhs: "email",
174 |                       operator: "ends-with",
175 |                       rhs: "foo.com",
176 |                     },
177 |                   ],
178 |                   resolution: {
179 |                     mode: "variant",
180 |                     variant: "fake-variant-id-3",
181 |                   },
182 |                 },
183 |               ],
184 |             },
185 |           },
186 |         })
187 |       ).toEqual(fakeVariant3);
188 |     });
189 |   });
190 | 
191 |   describe("fallthrough percentage rollouts", () => {
192 |     it("does a rollout by visitor key", () => {
193 |       expect(
194 |         resolveFlagToVariant({
195 |           visitor: { key: "fake-visitor-id" },
196 |           flag: {
197 |             ...flag,
198 |             production: {
199 |               ...flag.production,
200 |               active: true,
201 |               fallthrough: {
202 |                 mode: "rollout",
203 |                 bucketByCategory: "visitor",
204 |                 variants: {
205 |                   // murmur number for this visitor key + flag id is 6161
206 |                   // which is 61.61, so we make variant 1 go to 61.60
207 |                   // and expect it to get bucketed into the tiny variant 2
208 |                   [fakeVariant1.id]: { weight: 61.61 },
209 |                   [fakeVariant2.id]: { weight: 0.01 },
210 |                   [fakeVariant3.id]: { weight: 38.38 },
211 |                 },
212 |               },
213 |             },
214 |           },
215 |           environment: "production",
216 |           user: { key: "fake-user-id" },
217 |         })
218 |       ).toEqual(fakeVariant2);
219 |     });
220 |   });
221 | });
222 | 


--------------------------------------------------------------------------------
/package/src/internal/resolve-flag-to-variant.ts:
--------------------------------------------------------------------------------
  1 | import { murmur } from "./murmur";
  2 | import {
  3 |   Condition,
  4 |   FlagVisitor,
  5 |   FlagUserAttributes,
  6 |   Flag,
  7 |   RolloutFlagResolution,
  8 |   FlagVariant,
  9 |   FlagResolution,
 10 |   Environment,
 11 | } from "../evaluation-types";
 12 | 
 13 | function hasOwnProperty<X extends {}, Y extends PropertyKey>(
 14 |   obj: X,
 15 |   prop: Y
 16 | ): obj is X & Record<Y, unknown> {
 17 |   return obj.hasOwnProperty(prop);
 18 | }
 19 | 
 20 | type Traits = Record<string, string | number | null | undefined>;
 21 | 
 22 | function matchCondition(
 23 |   condition: Condition,
 24 |   visitor: FlagVisitor | null | undefined,
 25 |   user: FlagUserAttributes | null | undefined,
 26 |   traits: Traits | null | undefined
 27 | ): boolean {
 28 |   if (condition.group === "user") {
 29 |     // when a user is missing the user rules can't match, except when the
 30 |     // operator is "not-authenticated" in which case the rule matches
 31 |     if (!user || !user.key) return condition.operator === "not-authenticated";
 32 | 
 33 |     switch (condition.operator) {
 34 |       case "equal-to":
 35 |         return String(user[condition.lhs]) === condition.rhs;
 36 |       case "not-equal-to":
 37 |         return String(user[condition.lhs]) !== condition.rhs;
 38 |       case "authenticated":
 39 |         return Boolean(user.key);
 40 |       case "not-authenticated":
 41 |         return !user || !user.key;
 42 |       case "set":
 43 |         return hasOwnProperty(user, condition.lhs);
 44 |       case "not-set":
 45 |         return !hasOwnProperty(user, condition.lhs);
 46 |       case "starts-with": {
 47 |         const lhs = user[condition.lhs];
 48 |         const rhs = condition.rhs;
 49 |         return (
 50 |           typeof lhs === "string" &&
 51 |           typeof rhs === "string" &&
 52 |           lhs.toLowerCase().startsWith(rhs.toLowerCase())
 53 |         );
 54 |       }
 55 |       case "ends-with": {
 56 |         const lhs = user[condition.lhs];
 57 |         const rhs = condition.rhs;
 58 |         return (
 59 |           typeof lhs === "string" &&
 60 |           typeof rhs === "string" &&
 61 |           lhs.toLowerCase().endsWith(rhs.toLowerCase())
 62 |         );
 63 |       }
 64 |     }
 65 |   } else if (condition.group === "visitor") {
 66 |     switch (condition.operator) {
 67 |       case "equal-to":
 68 |         return Boolean(
 69 |           visitor && String(visitor[condition.lhs]) === condition.rhs
 70 |         );
 71 |       case "not-equal-to":
 72 |         return Boolean(
 73 |           visitor && String(visitor[condition.lhs]) !== condition.rhs
 74 |         );
 75 |       case "set":
 76 |         return Boolean(visitor && hasOwnProperty(visitor, condition.lhs));
 77 |       case "not-set":
 78 |         return !visitor || !hasOwnProperty(visitor, condition.lhs);
 79 |       case "starts-with": {
 80 |         if (!visitor) return false;
 81 |         const lhs = visitor[condition.lhs];
 82 |         const rhs = condition.lhs;
 83 |         return (
 84 |           typeof lhs === "string" &&
 85 |           typeof rhs === "string" &&
 86 |           lhs.toLowerCase().startsWith(rhs.toLowerCase())
 87 |         );
 88 |       }
 89 |       case "ends-with": {
 90 |         if (!visitor) return false;
 91 |         const lhs = visitor[condition.lhs];
 92 |         const rhs = condition.rhs;
 93 |         return (
 94 |           typeof lhs === "string" &&
 95 |           typeof rhs === "string" &&
 96 |           lhs.toLowerCase().endsWith(rhs.toLowerCase())
 97 |         );
 98 |       }
 99 |     }
100 |   } else if (condition.group === "traits") {
101 |     switch (condition.operator) {
102 |       case "equal-to":
103 |         return Boolean(
104 |           traits && String(traits[condition.lhs]) === condition.rhs
105 |         );
106 |       case "not-equal-to":
107 |         return Boolean(
108 |           traits && String(traits[condition.lhs]) !== condition.rhs
109 |         );
110 |       case "set":
111 |         return Boolean(traits && hasOwnProperty(traits, condition.lhs));
112 |       case "not-set":
113 |         return !traits || !hasOwnProperty(traits, condition.lhs);
114 |       case "starts-with": {
115 |         if (!traits) return false;
116 |         const lhs = traits[condition.lhs];
117 |         const rhs = condition.rhs;
118 |         return (
119 |           typeof lhs === "string" &&
120 |           typeof rhs === "string" &&
121 |           lhs.toLowerCase().startsWith(rhs.toLowerCase())
122 |         );
123 |       }
124 |       case "ends-with": {
125 |         if (!traits) return false;
126 |         const lhs = traits[condition.lhs];
127 |         const rhs = condition.rhs;
128 |         return (
129 |           typeof lhs === "string" &&
130 |           typeof rhs === "string" &&
131 |           lhs.toLowerCase().endsWith(rhs.toLowerCase())
132 |         );
133 |       }
134 |     }
135 |   } else {
136 |     return false;
137 |   }
138 | }
139 | 
140 | function resolvePercentageRolloutToVariant(
141 |   splitValue: string | number,
142 |   flag: Pick<Flag, "id" | "variants">,
143 |   resolution: RolloutFlagResolution
144 | ) {
145 |   // First we need to sort the buckets by the variant order for consistency,
146 |   // otherwise the same murmur number might end up in different buckets each
147 |   // time
148 |   const buckets = (flag.variants as FlagVariant[])
149 |     .map((variant) => {
150 |       return { variant, weight: resolution.variants[variant.id].weight };
151 |     })
152 |     // strip out any buckets with no weight
153 |     .filter((bucket) => bucket.weight > 0);
154 | 
155 |   // mix flag id into hash for more diverse results
156 |   const murmurNumber = murmur(String(splitValue) + flag.id);
157 | 
158 |   const scaledMurmurNumber = murmurNumber % 10000;
159 |   let upperLimit = 0;
160 |   const bucket = buckets.find((bucketCandidate) => {
161 |     // Weights go from 0-100 with two decimals, so we multiply them by
162 |     // 100 to turn them into integers from 0-10000, but we also need to
163 |     // use parseInt() to cut off any floating point decimal rounding errors,
164 |     // otherwise 1.09 * 100 would turn into 109.00000000000001 instead of 109.
165 |     upperLimit += parseInt(String(bucketCandidate.weight * 100), 10);
166 |     return scaledMurmurNumber < upperLimit;
167 |   });
168 | 
169 |   if (bucket) return bucket.variant;
170 | 
171 |   return null;
172 | }
173 | 
174 | function getResolution({
175 |   flag,
176 |   environment,
177 |   visitor,
178 |   user,
179 |   traits,
180 | }: Options): FlagResolution {
181 |   const envConfig = flag[environment];
182 |   if (!envConfig.active)
183 |     return { mode: "variant", variant: envConfig.offVariation };
184 | 
185 |   if (user) {
186 |     const variantIdTargetingUserKey = Object.keys(envConfig.targets).find(
187 |       (variantId) => envConfig.targets[variantId].values.includes(user.key)
188 |     );
189 | 
190 |     if (variantIdTargetingUserKey)
191 |       return {
192 |         mode: "variant",
193 |         variant: variantIdTargetingUserKey,
194 |       };
195 |   }
196 | 
197 |   if (Array.isArray(envConfig.rules)) {
198 |     const matchedRule = envConfig.rules.find((rule) =>
199 |       rule.conditions.every((condition) =>
200 |         matchCondition(condition, visitor, user, traits)
201 |       )
202 |     );
203 | 
204 |     if (matchedRule) return matchedRule.resolution;
205 |   }
206 | 
207 |   return envConfig.fallthrough;
208 | }
209 | 
210 | interface Options {
211 |   flag: {
212 |     id: Flag["id"];
213 |     production: Flag["production"];
214 |     preview: Flag["preview"];
215 |     development: Flag["development"];
216 |     variants: Flag["variants"];
217 |   };
218 |   environment: Environment;
219 |   visitor?: FlagVisitor | null;
220 |   user?: FlagUserAttributes | null;
221 |   traits?: Traits | null;
222 | }
223 | 
224 | function resolveResolution(
225 |   { flag, environment, visitor, user, traits }: Options,
226 |   resolution: FlagResolution
227 | ) {
228 |   // This part serves the determined resolution
229 |   if (resolution.mode === "variant") {
230 |     const variant = (flag.variants as FlagVariant[]).find(
231 |       (variant) => variant.id === resolution.variant
232 |     )!;
233 |     return variant ? variant : null;
234 |   }
235 | 
236 |   switch (resolution.bucketByCategory) {
237 |     case "visitor": {
238 |       return visitor && visitor.key
239 |         ? resolvePercentageRolloutToVariant(visitor.key, flag, resolution)
240 |         : // we were attempting to do a percentage rollout based on a visitor key,
241 |           // but the client didn't send a visitor key
242 |           null;
243 |     }
244 |     case "user": {
245 |       // when no user exists but we're bucketing by user
246 |       if (!user) return null;
247 | 
248 |       const attributeValue = user[resolution.bucketByUserAttribute];
249 | 
250 |       // when the user doesn't have the specified attribute
251 |       if (!attributeValue) return null;
252 | 
253 |       return resolvePercentageRolloutToVariant(
254 |         attributeValue,
255 |         flag,
256 |         resolution
257 |       );
258 |     }
259 |     case "trait": {
260 |       // when no traits were sent but we're bucketing by traits
261 |       if (!traits) return null;
262 | 
263 |       const traitValue = traits[resolution.bucketByTrait];
264 | 
265 |       // when the user doesn't have the specified trait
266 |       if (!traitValue) return null;
267 | 
268 |       return resolvePercentageRolloutToVariant(traitValue, flag, resolution);
269 |     }
270 |     default:
271 |       // serve offVariation as a sane fallback just in case
272 |       return null;
273 |   }
274 | }
275 | 
276 | /**
277 |  * Resolves a flag to a variant, based on input.
278 |  *
279 |  * Returns null when something unexpected happens.
280 |  */
281 | export function resolveFlagToVariant(options: Options): FlagVariant | null {
282 |   // This part determines the resolution to use
283 |   const resolution = getResolution(options);
284 |   // This maps the determined resolution to a variant
285 |   return resolveResolution(options, resolution);
286 | }
287 | 


--------------------------------------------------------------------------------
/package/src/internal/types.ts:
--------------------------------------------------------------------------------
  1 | import { CookieSerializeOptions } from "cookie";
  2 | import { Configuration } from "../config";
  3 | import type { Environment } from "../evaluation-types";
  4 | 
  5 | /**
  6 |  * A user to load the flags for. A user must at least have a `key`. See the
  7 |  * supported user attributes [here](#supported-user-attributes).
  8 |  * The user information you pass can be used for individual targeting or rules.
  9 |  */
 10 | export type FlagUser = {
 11 |   key: string;
 12 |   email?: string;
 13 |   name?: string;
 14 |   avatar?: string;
 15 |   country?: string;
 16 | };
 17 | 
 18 | /**
 19 |  * Traits
 20 |  *
 21 |  * An object which you have access to in the flag's rules.
 22 |  * You can target users based on traits.
 23 |  */
 24 | export type Traits = { [key: string]: any };
 25 | 
 26 | /**
 27 |  * Generic Feature Flags
 28 |  *
 29 |  * Entries consist of the feature flag name as the key and the resolved variant's value as the value.
 30 |  */
 31 | export type Flags = {
 32 |   // A flag can resolve to null when a percentage based rollout is set based
 33 |   // on a criteria not present on the user, e.g. when bucketing by trait,
 34 |   // but no such trait was sent
 35 |   [key: string]: boolean | number | string | null;
 36 | };
 37 | 
 38 | export type FullConfiguration<F extends Flags> = Required<Configuration<F>> & {
 39 |   projectId: string;
 40 |   environment: Environment;
 41 | };
 42 | 
 43 | /**
 44 |  * The inputs to a flag evaluation.
 45 |  *
 46 |  * Given a flag has not changed, the same inputs will always lead to the same variants when evaluating a flag.
 47 |  */
 48 | export type Input = {
 49 |   endpoint: string;
 50 |   envKey: string;
 51 |   requestBody: EvaluationRequestBody;
 52 | };
 53 | 
 54 | export type SuccessOutcome<F extends Flags> = {
 55 |   data: GenericEvaluationResponseBody<F>;
 56 |   error?: never;
 57 | };
 58 | 
 59 | export type ErrorOutcome = {
 60 |   data?: never;
 61 |   error: ResolvingError;
 62 | };
 63 | 
 64 | /**
 65 |  * The result of a flag evaluation
 66 |  */
 67 | export type Outcome<F extends Flags> = SuccessOutcome<F> | ErrorOutcome;
 68 | 
 69 | /**
 70 |  * The fetch() request failed due to a network error (fetch itself threw).
 71 |  */
 72 | type NetworkError = "network-error";
 73 | /**
 74 |  * The response body could not be parsed into the expected JSON structure.
 75 |  */
 76 | type InvalidResponseBodyError = "invalid-response-body";
 77 | /**
 78 |  * The HTTP Status Code was not 200-299, so the response was not ok.
 79 |  */
 80 | type ResponseNotOkError = "response-not-ok";
 81 | /**
 82 |  * The request was aborted because it reached any of the loadingTimeouts.
 83 |  */
 84 | type RequestTimeoutError = "request-timed-out";
 85 | 
 86 | export type ResolvingError =
 87 |   | NetworkError
 88 |   | InvalidResponseBodyError
 89 |   | ResponseNotOkError
 90 |   | RequestTimeoutError;
 91 | 
 92 | export type EvaluationRequestBody = {
 93 |   visitorKey: string | null;
 94 |   user: FlagUser | null;
 95 |   traits: Traits | null;
 96 | };
 97 | 
 98 | /**
 99 |  * The HappyKit API response to a feature flag evaluation request.
100 |  */
101 | export type GenericEvaluationResponseBody<F extends Flags> = {
102 |   visitor: { key: string } | null;
103 |   flags: F;
104 | };
105 | 
106 | export type SuccessInitialFlagState<F extends Flags> = {
107 |   input: Input;
108 |   outcome: SuccessOutcome<F>;
109 |   error?: never;
110 | };
111 | 
112 | export type ErrorInitialFlagState = {
113 |   input: Input;
114 |   outcome: ErrorOutcome;
115 | };
116 | 
117 | /**
118 |  * The initial flag state.
119 |  *
120 |  * In case you preloaded your flags during server-side rendering using `getFlags()`, provide the returned state as `initialState`. The client will then skip the first request whenever possible and use the provided flags instead. This allows you to get rid of loading states and on the client.
121 |  */
122 | export type InitialFlagState<F extends Flags> =
123 |   | SuccessInitialFlagState<F>
124 |   | ErrorInitialFlagState;
125 | 
126 | type Revalidate = () => void;
127 | 
128 | export type EmptyFlagBag = {
129 |   flags: null;
130 |   data: null;
131 |   error: null;
132 |   fetching: false;
133 |   settled: false;
134 |   revalidate: Revalidate;
135 |   visitorKey: null;
136 | };
137 | 
138 | export type EvaluatingFlagBag<F extends Flags> = {
139 |   flags: null | F;
140 |   data: null;
141 |   error: null;
142 |   fetching: true;
143 |   settled: false;
144 |   revalidate: Revalidate;
145 |   visitorKey: string;
146 | };
147 | 
148 | export type SucceededFlagBag<F extends Flags> = {
149 |   flags: F;
150 |   data: GenericEvaluationResponseBody<F>;
151 |   error: null;
152 |   fetching: false;
153 |   // true, unless input is for a static page (has no visitorKey)
154 |   settled: boolean;
155 |   revalidate: Revalidate;
156 |   visitorKey: string;
157 | };
158 | 
159 | export type RevalidatingAfterSuccessFlagBag<F extends Flags> = {
160 |   flags: F;
161 |   data: GenericEvaluationResponseBody<F>;
162 |   error: null;
163 |   fetching: true;
164 |   settled: boolean;
165 |   revalidate: Revalidate;
166 |   visitorKey: string;
167 | };
168 | 
169 | export type FailedFlagBag<F extends Flags> = {
170 |   flags: F | null; // cached or default or null
171 |   data: null;
172 |   error: ResolvingError;
173 |   fetching: false;
174 |   // true, unless input is for a static page (has no visitorKey)
175 |   settled: boolean;
176 |   revalidate: Revalidate;
177 |   visitorKey: string;
178 | };
179 | 
180 | export type RevalidatingAfterErrorFlagBag<F extends Flags> = {
181 |   flags: F | null; // cached or default or null
182 |   data: null;
183 |   error: ResolvingError;
184 |   fetching: true;
185 |   settled: false;
186 |   revalidate: Revalidate;
187 |   visitorKey: string;
188 | };
189 | 
190 | /**
191 |  * A bag of feature flag related data.
192 |  */
193 | export type FlagBag<F extends Flags = Flags> =
194 |   | EmptyFlagBag
195 |   | EvaluatingFlagBag<F>
196 |   | SucceededFlagBag<F>
197 |   | RevalidatingAfterSuccessFlagBag<F>
198 |   | FailedFlagBag<F>
199 |   | RevalidatingAfterErrorFlagBag<F>;
200 | 
201 | export type GetFlagsSuccessBag<F extends Flags> = {
202 |   /**
203 |    * The resolved flags
204 |    *
205 |    * In case the default flags contain flags not present in the loaded flags,
206 |    * the missing flags will get added to the returned flags.
207 |    */
208 |   flags: F;
209 |   /**
210 |    * The actually loaded data without any defaults applied, or null when
211 |    * the flags could not be loaded.
212 |    */
213 |   data: GenericEvaluationResponseBody<F> | null;
214 |   error: null;
215 |   initialFlagState: SuccessInitialFlagState<F>;
216 |   /**
217 |    * The cookie options you should forward using
218 |    *
219 |    * Only used for edge, not for server.
220 |    *
221 |    * ```
222 |    * response.cookie(
223 |    *   flagBag.cookie.name,
224 |    *   flagBag.cookie.value,
225 |    *   flagBag.cookie.options
226 |    * );
227 |    * ```
228 |    *
229 |    * or using
230 |    *
231 |    * ```
232 |    * response.cookie(...flagBag.cookie.args)
233 |    * ```
234 |    */
235 |   cookie?: {
236 |     name: string;
237 |     value: string;
238 |     options: CookieSerializeOptions;
239 |     /**
240 |      * Arguments for response.cookie()
241 |      */
242 |     args: [string, string, CookieSerializeOptions];
243 |   } | null;
244 | };
245 | 
246 | export type GetFlagsErrorBag<F extends Flags> = {
247 |   /**
248 |    * The resolved flags
249 |    *
250 |    * In case the flags could not be loaded, you will see the default
251 |    * flags here (from config.defaultFlags)
252 |    */
253 |   flags: F | null;
254 |   /**
255 |    * The actually loaded data without any defaults applied, or null when
256 |    * the flags could not be loaded.
257 |    */
258 |   data: null;
259 |   error: ResolvingError;
260 |   /**
261 |    * The initial flag state that you can use to initialize useFlags()
262 |    */
263 |   initialFlagState: ErrorInitialFlagState;
264 |   /**
265 |    * Only used for edge, not for server.
266 |    */
267 |   cookie?: null;
268 | };
269 | 


--------------------------------------------------------------------------------
/package/src/internal/utils.ts:
--------------------------------------------------------------------------------
  1 | import type { Flags } from "./types";
  2 | 
  3 | export function has<X extends {}, Y extends PropertyKey>(
  4 |   obj: X,
  5 |   prop: Y
  6 | ): obj is X & Record<Y, unknown> {
  7 |   return Object.prototype.hasOwnProperty.call(obj, prop);
  8 | }
  9 | 
 10 | function omitNullValues<O extends object, T = Partial<O>>(obj: O): T {
 11 |   return Object.entries(obj).reduce((acc, [key, value]) => {
 12 |     if (value !== null) acc[key as keyof T] = value;
 13 |     return acc;
 14 |   }, {} as T);
 15 | }
 16 | 
 17 | /**
 18 |  * Returns a combination of the loaded flags and the default flags.
 19 |  *
 20 |  * Tries to return the loaded flags directly in case the they contain all defaults
 21 |  * to avoid changing object references in the caller.
 22 |  *
 23 |  * @param rawFlags
 24 |  * @param defaultFlags
 25 |  */
 26 | export function combineRawFlagsWithDefaultFlags<F extends Flags>(
 27 |   rawFlags: F | null,
 28 |   defaultFlags: Flags
 29 | ): F {
 30 |   if (!rawFlags) return defaultFlags as F;
 31 | 
 32 |   const rawFlagsContainAllDefaultFlags = Object.keys(defaultFlags).every(
 33 |     (key) => has(rawFlags, key) && rawFlags[key] !== null
 34 |   );
 35 | 
 36 |   return rawFlagsContainAllDefaultFlags
 37 |     ? (rawFlags as F)
 38 |     : ({
 39 |         // this triple ordering ensures that null-ish loaded values are
 40 |         // overwritten by the defaults:
 41 |         //   - loaded null & default exists => default value
 42 |         //   - loaded null & no default => null
 43 |         //   - loaded value & default => loaded value
 44 |         //   - loaded value & no default => loaded value
 45 |         ...rawFlags,
 46 |         ...defaultFlags,
 47 |         ...omitNullValues(rawFlags),
 48 |       } as F);
 49 | }
 50 | 
 51 | /**
 52 |  * Gets the cookie by the name
 53 |  *
 54 |  * From: https://developers.cloudflare.com/workers/examples/extract-cookie-value
 55 |  */
 56 | export function getCookie(
 57 |   cookieString: string | null | undefined,
 58 |   name: string
 59 | ) {
 60 |   if (cookieString) {
 61 |     const cookies = cookieString.split(";");
 62 |     for (let cookie of cookies) {
 63 |       const cookiePair = cookie.split("=", 2);
 64 |       const cookieName = cookiePair[0].trim();
 65 |       if (cookieName === name) return cookiePair[1];
 66 |     }
 67 |   }
 68 |   return null;
 69 | }
 70 | 
 71 | // source: https://github.com/lukeed/dequal/blob/master/src/lite.js
 72 | export function deepEqual(objA: any, objB: any) {
 73 |   var ctor, len;
 74 |   if (objA === objB) return true;
 75 | 
 76 |   if (objA && objB && (ctor = objA.constructor) === objB.constructor) {
 77 |     if (ctor === Date) return objA.getTime() === objB.getTime();
 78 |     if (ctor === RegExp) return objA.toString() === objB.toString();
 79 | 
 80 |     if (ctor === Array) {
 81 |       if ((len = objA.length) === objB.length) {
 82 |         while (len-- && deepEqual(objA[len], objB[len]));
 83 |       }
 84 |       return len === -1;
 85 |     }
 86 | 
 87 |     if (!ctor || typeof objA === "object") {
 88 |       len = 0;
 89 |       for (ctor in objA) {
 90 |         if (has(objA, ctor) && ++len && !has(objB, ctor)) return false;
 91 |         if (!(ctor in objB) || !deepEqual(objA[ctor], objB[ctor])) return false;
 92 |       }
 93 |       return Object.keys(objB).length === len;
 94 |     }
 95 |   }
 96 | 
 97 |   return objA !== objA && objB !== objB;
 98 | }
 99 | 
100 | export class ObjectMap<Key extends any, Value extends any> {
101 |   keys: Key[];
102 |   values: Value[];
103 | 
104 |   constructor() {
105 |     this.keys = [];
106 |     this.values = [];
107 |   }
108 | 
109 |   private _getIndex(key: Key) {
110 |     return this.keys.findIndex((storedKey) => deepEqual(key, storedKey));
111 |   }
112 | 
113 |   set(key: Key, value: Value) {
114 |     const index = this._getIndex(key);
115 |     if (index == -1) {
116 |       // "push" to front of arrays as more recently values are more likely
117 |       // to be looked up again, and we want them to match early
118 |       this.keys = [key, ...this.keys];
119 |       this.values = [value, ...this.values];
120 |     } else {
121 |       this.values[index] = value;
122 |     }
123 |   }
124 | 
125 |   get<ReturnValue>(key: Key): ReturnValue | null {
126 |     const index = this._getIndex(key);
127 |     return index === -1 ? null : (this.values[index] as unknown as ReturnValue);
128 |   }
129 | 
130 |   // exists(key: Key) {
131 |   //   return this.keys.some((storedKey) => deepEqual(storedKey, key));
132 |   // }
133 | 
134 |   /** Resets the cache. For testing purposes. */
135 |   clear() {
136 |     this.keys = [];
137 |     this.values = [];
138 |   }
139 | }
140 | 


--------------------------------------------------------------------------------
/package/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "allowSyntheticDefaultImports": false,
 4 |     "target": "es5",
 5 |     "module": "commonjs",
 6 |     "jsx": "react",
 7 |     "moduleResolution": "node",
 8 |     "noImplicitAny": true,
 9 |     "noUnusedLocals": false,
10 |     "noUnusedParameters": false,
11 |     "removeComments": false,
12 |     "strictNullChecks": true,
13 |     "preserveConstEnums": true,
14 |     "sourceMap": true,
15 |     "isolatedModules": true,
16 |     "lib": ["es2015", "es2016", "dom"],
17 |     "types": ["node", "jest"]
18 |   },
19 |   "include": ["./src"],
20 |   "exclude": [
21 |     "./client",
22 |     "./config",
23 |     "./context",
24 |     "./edge",
25 |     "./evaluate",
26 |     "./server",
27 |     "./context",
28 |     "./dist"
29 |   ]
30 | }
31 | 


--------------------------------------------------------------------------------
/package/tsup.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from "tsup";
 2 | 
 3 | export default defineConfig((options) => ({
 4 |   entry: [
 5 |     "src/config.ts",
 6 |     "src/server.ts",
 7 |     "src/client.ts",
 8 |     "src/edge.ts",
 9 |     "src/evaluate.ts",
10 |     "src/context.ts",
11 |     "src/api-route.ts",
12 |   ],
13 |   format: ["esm", "cjs"],
14 |   splitting: true,
15 |   sourcemap: false,
16 |   minify: true,
17 |   clean: true,
18 |   skipNodeModulesBundle: true,
19 |   dts: true,
20 |   external: [
21 |     "node_modules",
22 |     "@happykit/flags/config",
23 |     "@happykit/flags/server",
24 |     "@happykit/flags/client",
25 |     "@happykit/flags/edge",
26 |     "@happykit/flags/evaluate",
27 |     "@happykit/flags/context",
28 |     "@happykit/flags/api-route",
29 |   ],
30 | }));
31 | 


--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 |   - package
3 |   - example
4 | 


--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://turborepo.org/schema.json",
 3 |   "pipeline": {
 4 |     "build": {
 5 |       "dependsOn": ["^build"],
 6 |       "outputs": [".next/**", "dist/**"]
 7 |     },
 8 |     "test": {
 9 |       "dependsOn": ["^test"]
10 |     },
11 |     "dev": {
12 |       "cache": false
13 |     }
14 |   }
15 | }
16 | 


--------------------------------------------------------------------------------