├── .github └── workflows │ └── test.yaml ├── .gitignore ├── README.md ├── build └── make-bundle.cjs ├── index.d.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.spec.ts └── index.ts └── tsconfig.json /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - '**.md' 8 | - '.gitignore' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 16.13.1 19 | - run: npm ci 20 | - run: npm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /build/compiled 3 | /next-feature-flags.d.ts 4 | /next-feature-flags.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-feature-flags 2 | 3 | Add support for feature flags on Next.js based on cookies + environment variables. 4 | 5 | # How it works 6 | 7 | It reads from cookies and Next.js's runtime config and provides an interface to enable and disable feature flags. 8 | 9 | An interface to toggle feature flags on the browser is also provided via `window.FEATURES`. 10 | 11 | It prioritizes feature flags from cookies over Next.js runtime config, meaning that if the same feature flag is set on both sides, the **cookie** one prevails. 12 | 13 | # Setup 14 | 15 | ```js 16 | import { configure } from 'next-feature-flags'; 17 | 18 | configure({ 19 | featureFlags: ['SHOW_LOGIN_BANNER', 'USER_ESCALATE_PRIVILEGES'], 20 | allowCookieOverride: ['SHOW_LOGIN_BANNER'] 21 | }) 22 | ``` 23 | 24 | `featureFlags` - lists the feature flags available 25 | 26 | `allowCookieOverride` - lists the features flags which can be overriden by setting a cookie. This enables for some safety and not letting users override critical feature flags 27 | 28 | # Usage 29 | 30 | ```js 31 | import { configure } from 'next-feature-flags'; 32 | 33 | const { getFeatureFlag } = configure({ 34 | featureFlags: ['SHOW_LOGIN_BANNER', 'USER_ESCALATE_PRIVILEGES'], 35 | allowCookieOverride: ['SHOW_LOGIN_BANNER'] 36 | }) 37 | 38 | const shouldShowLoginBanner = () => getFeatureFlag('SHOW_LOGIN_BANNER') 39 | ``` 40 | 41 | **Note:** `next-feature-flags` uses the `FEATURE` key both to write/read from cookies and from the environment, meaning the if you send `SHOW_LOGIN_BANNER` `next-feature-flags` will try to read from `FEATURE_SHOW_LOGIN_BANNER` and will read/write from that same cookie. 42 | 43 | Possible improvement: customize this key. 44 | 45 | ## Overriding feature flags on the browser 46 | 47 | To override feature flags on the client, use the `window.FEATURES` interface. 48 | 49 | On the console: 50 | 51 | ```js 52 | > window.FEATURES 53 | > {SHOW_LOGIN_BANNER: {enable: fn, disable: fn}} 54 | > window.FEATURES.SHOW_LOGIN_BANNER.enable() 55 | > 'FEATURES_SHOW_LOGIN_BANNER=true' 56 | ``` 57 | 58 | Using this interface will set the cookie value (FEATURES_SHOW_LOGIN_BANNER), if you're using the feature flags on the server side (like `getServerSideProps`) make sure you reload your browser. 59 | 60 | Whenever you want to turn it off, you can use the `disable` method the same way. 61 | 62 | ## Using feature flags from the environment 63 | 64 | `next-feature-flags` gets environment variables from Next.js runtime config (https://nextjs.org/docs/api-reference/next.config.js/runtime-configuration). 65 | 66 | To expose these variables there (and make them available both on the client and server side), use them the following way: 67 | 68 | ```js 69 | // next.config.js 70 | module.exports = { 71 | serverRuntimeConfig: { 72 | SHOW_LOGIN_BANNER: process.env.FEATURE_SHOW_LOGIN_BANNER, // Pass through env variables 73 | }, 74 | publicRuntimeConfig: { 75 | SHOW_LOGIN_BANNER: process.env.NEXT_PUBLIC_FEATURE_SHOW_LOGIN_BANNER 76 | }, 77 | } 78 | ``` 79 | 80 | By having the variables with the same name (even though one is getting from the public namespace and the other not) makes it possible for `next-feature-flags` to get them. It will prioritize the value in `serverRuntimeConfig` over `publicRuntimeConfig`. 81 | 82 | -------------------------------------------------------------------------------- /build/make-bundle.cjs: -------------------------------------------------------------------------------- 1 | const { promisify } = require("util") 2 | const fs = require("fs") 3 | const path = require("path") 4 | const rollup = require("rollup") 5 | const readFile = promisify(fs.readFile) 6 | const srcPath = path.join(__dirname, "..", "src") 7 | const compiledPath = path.join(__dirname, "compiled") 8 | async function build() { 9 | let bundle = await rollup.rollup({ 10 | input: path.join(compiledPath, "index.js") 11 | }) 12 | let { code } = await bundle.generate({ 13 | format: "cjs", 14 | sourcemap: false, 15 | }) 16 | } 17 | async function makeDefinitionsCode() { 18 | let defs = [ 19 | "// -- Usage definitions --", 20 | removeLocalImportsExports((await readFile(path.join(srcPath, "exported-definitions.d.ts"), "utf-8")).trim()), 21 | "// -- Driver definitions --", 22 | removeLocalImportsExports((await readFile(path.join(srcPath, "driver-definitions.d.ts"), "utf-8")).trim()), 23 | "// -- Entry point definition --", 24 | removeSemicolons( 25 | removeLocalImportsExports((await readFile(path.join(compiledPath, "index.d.ts"), "utf-8")).trim()), 26 | ) 27 | ] 28 | return defs.join("\n\n") 29 | } 30 | function removeLocalImportsExports(code) { 31 | let localImportExport = /^\s*(import|export) .* from "\.\/.*"\s*;?\s*$/ 32 | return code.split("\n").filter(line => { 33 | return !localImportExport.test(line) 34 | }).join("\n").trim() 35 | } 36 | function removeSemicolons(code) { 37 | return code.replace(/;/g, "") 38 | } 39 | build().then(() => { 40 | console.log("done") 41 | }, err => console.log(err.message, err.stack)) -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export {}; 3 | 4 | declare global { 5 | interface Window { 6 | FEATURES: Record< 7 | string, 8 | { enable: () => void; disable: () => void, state: () => boolean } 9 | >; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-feature-flags", 3 | "version": "0.6.5", 4 | "description": "A package to enable feature-flag support on Next.js via cookies and environment variables", 5 | "scripts": { 6 | "preversion": "npm test", 7 | "test": "jest", 8 | "_clear": "rimraf build/compiled/*", 9 | "_tsc": "tsc", 10 | "_make-bundle": "node build/make-bundle.cjs", 11 | "build": "run-s _clear _tsc _make-bundle", 12 | "watch": "tsc --watch", 13 | "version": "npm run build", 14 | "postversion": "git push && git push --tags" 15 | }, 16 | "keywords": [ 17 | "nextjs", 18 | "feature-flags", 19 | "react", 20 | "cookies" 21 | ], 22 | "author": "asantos00", 23 | "license": "MIT", 24 | "repository": { 25 | "url": "https://github.com/asantos00/next-feature-flags", 26 | "type": "git" 27 | }, 28 | "main": "build/compiled/index.js", 29 | "types": "build/compiled/index.d.ts", 30 | "devDependencies": { 31 | "@types/jest": "^27.5.0", 32 | "@types/node": "^17.0.31", 33 | "@types/react": "^18.0.9", 34 | "@types/react-dom": "^18.0.3", 35 | "jest": "^28.1.0", 36 | "jsdom": "^19.0.0", 37 | "next": "^12.1.6", 38 | "npm-run-all": "^4.1.5", 39 | "react": "^18.1.0", 40 | "react-dom": "^18.1.0", 41 | "rimraf": "^3.0.2", 42 | "rollup": "^2.72.1", 43 | "ts-jest": "^28.0.2", 44 | "typescript": "^4.6.4" 45 | }, 46 | "files": [ 47 | "build/compiled/*", 48 | "index.d.ts" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { configure } from "./index"; 2 | import { JSDOM } from "jsdom"; 3 | import { GetServerSidePropsContext } from "next"; 4 | import getConfig from "next/config"; 5 | 6 | const utilMockGetConfigForEnvVariable = (obj: Record) => { 7 | (getConfig as jest.Mock).mockReturnValue({ 8 | serverRuntimeConfig: { ...obj }, 9 | publicRuntimeConfig: { ...obj }, 10 | }); 11 | }; 12 | 13 | jest.mock("next/config"); 14 | 15 | describe("basic configuration", () => { 16 | beforeEach(() => { 17 | jest.resetAllMocks(); 18 | 19 | utilMockGetConfigForEnvVariable({}); 20 | }); 21 | 22 | it("runs without throwing", () => { 23 | expect(() => configure()).not.toThrow(); 24 | }); 25 | 26 | it("returns false by default for any feature flag", () => { 27 | const { getFeatureFlag } = configure(); 28 | 29 | expect(getFeatureFlag("abcd")).toBe(false); 30 | }); 31 | 32 | it("warns when using a feature flag that doesnt exist", () => { 33 | const { getFeatureFlag } = configure({ 34 | debug: true, 35 | allowCookieOverride: [], 36 | featureFlags: ["abcd"], 37 | }); 38 | 39 | console.warn = jest.fn(); 40 | 41 | getFeatureFlag("abcd"); 42 | 43 | expect(console.warn).toHaveBeenCalledWith( 44 | 'Feature flag "abcd" does not exist on next-feature-flags' 45 | ); 46 | }); 47 | }); 48 | 49 | describe("get feature flag from cookie", () => { 50 | beforeEach(() => { 51 | jest.resetAllMocks(); 52 | 53 | utilMockGetConfigForEnvVariable({}); 54 | const dom = new JSDOM(); 55 | global.document = dom.window.document; 56 | global.window = dom.window; 57 | }); 58 | it("returns true for feature flag defined on cookie", () => { 59 | const { getFeatureFlag } = configure({ 60 | featureFlags: ["FOO"], 61 | allowCookieOverride: ["FOO"], 62 | }); 63 | 64 | document.cookie = "FEATURE_FOO=true"; 65 | 66 | expect(getFeatureFlag("FOO")).toBeTruthy(); 67 | }); 68 | 69 | it("returns false for feature flag defined on cookie as false", () => { 70 | const { getFeatureFlag } = configure({ 71 | featureFlags: ["FOO"], 72 | allowCookieOverride: ["FOO"], 73 | }); 74 | 75 | document.cookie = "STUFF=true;abcd=123;auth=asbcd;FEATURE_FOO=false"; 76 | 77 | expect(getFeatureFlag("FOO")).toBeFalsy(); 78 | }); 79 | 80 | it("returns false for feature flag defined on cookie as random value", () => { 81 | const { getFeatureFlag } = configure({ 82 | featureFlags: ["FOO"], 83 | allowCookieOverride: ["FOO"], 84 | }); 85 | 86 | document.cookie = "STUFF=true;abcd=123;auth=asbcd;FEATURE_FOO=4125214"; 87 | 88 | expect(getFeatureFlag("FOO")).toBeFalsy(); 89 | }); 90 | 91 | it("returns false for feature flag not defined on cookie", () => { 92 | const { getFeatureFlag } = configure({ 93 | featureFlags: ["FOO"], 94 | allowCookieOverride: ["FOO"], 95 | }); 96 | 97 | document.cookie = "STUFF=true;abcd=123;auth=asbcd;"; 98 | 99 | expect(getFeatureFlag("FOO")).toBeFalsy(); 100 | }); 101 | 102 | it("server side - cookie as true", () => { 103 | //@ts-ignore 104 | window = undefined; 105 | 106 | const { getFeatureFlag } = configure({ 107 | featureFlags: ["FOO"], 108 | allowCookieOverride: ["FOO"], 109 | }); 110 | 111 | const contextMock = { 112 | req: { 113 | cookies: { FEATURE_FOO: "true" }, 114 | } as unknown as GetServerSidePropsContext["req"], 115 | } as unknown as GetServerSidePropsContext; 116 | 117 | expect(getFeatureFlag("FOO", contextMock)).toBeTruthy(); 118 | }); 119 | 120 | it("server side - cookie as empty", () => { 121 | //@ts-ignore 122 | window = undefined; 123 | 124 | const { getFeatureFlag } = configure({ 125 | featureFlags: ["FOO"], 126 | allowCookieOverride: ["FOO"], 127 | }); 128 | 129 | const contextMock = { 130 | req: { 131 | cookies: { FEATURE_FOO: "" }, 132 | } as unknown as GetServerSidePropsContext["req"], 133 | } as unknown as GetServerSidePropsContext; 134 | 135 | expect(getFeatureFlag("FOO", contextMock)).toBeFalsy(); 136 | }); 137 | 138 | it("server side - cookie not present", () => { 139 | //@ts-ignore 140 | window = undefined; 141 | 142 | const { getFeatureFlag } = configure({ 143 | featureFlags: ["FOO"], 144 | allowCookieOverride: ["FOO"], 145 | }); 146 | 147 | const contextMock = { 148 | req: { 149 | cookies: { FEATURE_STUFF: "true" }, 150 | } as unknown as GetServerSidePropsContext["req"], 151 | } as unknown as GetServerSidePropsContext; 152 | 153 | expect(getFeatureFlag("FOO", contextMock)).toBeFalsy(); 154 | }); 155 | 156 | it("server side - no cookies", () => { 157 | //@ts-ignore 158 | window = undefined; 159 | 160 | const { getFeatureFlag } = configure({ 161 | featureFlags: ["FOO"], 162 | allowCookieOverride: ["FOO"], 163 | }); 164 | 165 | const contextMock = { 166 | req: { 167 | cookies: {}, 168 | } as unknown as GetServerSidePropsContext["req"], 169 | } as unknown as GetServerSidePropsContext; 170 | 171 | expect(getFeatureFlag("FOO", contextMock)).toBeFalsy(); 172 | }); 173 | }); 174 | 175 | describe("feature flag from env variable", () => { 176 | beforeEach(() => { 177 | jest.resetAllMocks(); 178 | utilMockGetConfigForEnvVariable({}); 179 | }); 180 | 181 | it("returns true for feature flag defined on env variable", () => { 182 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "true" }); 183 | 184 | const { getFeatureFlag } = configure({ 185 | featureFlags: ["FOO"], 186 | allowCookieOverride: ["FOO"], 187 | }); 188 | 189 | expect(getFeatureFlag("FOO")).toBeTruthy(); 190 | }); 191 | 192 | it("returns false for feature flag defined on env as false", () => { 193 | utilMockGetConfigForEnvVariable({ 194 | FEATURE_FOO: "false", 195 | FEATURE_BLA: "true", 196 | }); 197 | 198 | const { getFeatureFlag } = configure({ 199 | featureFlags: ["FOO", "BLA"], 200 | allowCookieOverride: ["FOO"], 201 | }); 202 | 203 | expect(getFeatureFlag("FOO")).toBeFalsy(); 204 | expect(getFeatureFlag("BLA")).toBeTruthy(); 205 | }); 206 | 207 | it("returns false for feature flag defined on env as random value", () => { 208 | utilMockGetConfigForEnvVariable({ 209 | FEATURE_FOO: "false", 210 | FEATURE_BLA: "true", 211 | }); 212 | 213 | const { getFeatureFlag } = configure({ 214 | featureFlags: ["FOO"], 215 | allowCookieOverride: ["FOO"], 216 | }); 217 | 218 | expect(getFeatureFlag("FOO")).toBeFalsy(); 219 | }); 220 | 221 | it("returns false for feature flag not defined on env", () => { 222 | utilMockGetConfigForEnvVariable({}); 223 | 224 | const { getFeatureFlag } = configure({ 225 | featureFlags: ["FOO"], 226 | allowCookieOverride: ["FOO"], 227 | }); 228 | 229 | expect(getFeatureFlag("FOO")).toBeFalsy(); 230 | }); 231 | 232 | it("returns true for feature flag defined as true", () => { 233 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "true" }); 234 | 235 | const { getFeatureFlag } = configure({ 236 | featureFlags: ["FOO"], 237 | allowCookieOverride: ["FOO"], 238 | }); 239 | 240 | process.env.FEATURE_FOO = "true"; 241 | 242 | expect(getFeatureFlag("FOO")).toBeTruthy(); 243 | }); 244 | 245 | it("returns true for feature flag defined as 1", () => { 246 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "1" }); 247 | 248 | const { getFeatureFlag } = configure({ 249 | featureFlags: ["FOO"], 250 | allowCookieOverride: ["FOO"], 251 | }); 252 | 253 | expect(getFeatureFlag("FOO")).toBeTruthy(); 254 | }); 255 | 256 | it("returns false when empty", () => { 257 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "" }); 258 | 259 | const { getFeatureFlag } = configure({ 260 | featureFlags: ["FOO"], 261 | allowCookieOverride: ["FOO"], 262 | }); 263 | 264 | expect(getFeatureFlag("FOO")).toBeFalsy(); 265 | }); 266 | 267 | it("returns false when variable not present", () => { 268 | utilMockGetConfigForEnvVariable({}); 269 | 270 | const { getFeatureFlag } = configure({ 271 | featureFlags: ["FOO"], 272 | allowCookieOverride: ["FOO"], 273 | }); 274 | 275 | expect(getFeatureFlag("FOO")).toBeFalsy(); 276 | }); 277 | 278 | it("returns false on no env defined", () => { 279 | utilMockGetConfigForEnvVariable({}); 280 | 281 | const { getFeatureFlag } = configure({ 282 | featureFlags: ["FOO"], 283 | allowCookieOverride: ["FOO"], 284 | }); 285 | 286 | expect(getFeatureFlag("FOO")).toBeFalsy(); 287 | }); 288 | }); 289 | 290 | describe("cookie overriding env on allowed variables", () => { 291 | beforeEach(() => { 292 | jest.resetAllMocks(); 293 | 294 | const dom = new JSDOM(); 295 | global.document = dom.window.document; 296 | global.window = dom.window; 297 | utilMockGetConfigForEnvVariable({}); 298 | }); 299 | 300 | it("prefers cookie false over env when allowCookieOverride enabled", () => { 301 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "true" }); 302 | 303 | const { getFeatureFlag } = configure({ 304 | featureFlags: ["FOO"], 305 | allowCookieOverride: ["FOO"], 306 | }); 307 | 308 | document.cookie = "FEATURE_FOO=false;something=wrong"; 309 | 310 | expect(getFeatureFlag("FOO")).toBeFalsy(); 311 | }); 312 | 313 | it("prefers cookie true over env when allowCookieOverride enabled", () => { 314 | utilMockGetConfigForEnvVariable({ FEATURE_FOO: "false" }); 315 | 316 | const { getFeatureFlag } = configure({ 317 | featureFlags: ["FOO"], 318 | allowCookieOverride: ["FOO"], 319 | }); 320 | 321 | document.cookie = "FEATURE_FOO=true;something=wrong"; 322 | 323 | expect(getFeatureFlag("FOO")).toBeTruthy(); 324 | }); 325 | 326 | it("gets env value when variable is not in allowCookieOVerride", () => { 327 | utilMockGetConfigForEnvVariable({ 328 | FEATURE_FOO: "false", 329 | FEATURE_BAR: "true", 330 | }); 331 | 332 | const { getFeatureFlag } = configure({ 333 | featureFlags: ["FOO", "BAR"], 334 | allowCookieOverride: ["FOO"], 335 | }); 336 | 337 | document.cookie = "FEATURE_FOO=true;FEATURE_BAR=false"; 338 | 339 | expect(getFeatureFlag("BAR")).toBeTruthy(); 340 | expect(getFeatureFlag("FOO")).toBeTruthy(); 341 | }); 342 | }); 343 | 344 | describe("window interface", () => { 345 | beforeEach(() => { 346 | jest.resetAllMocks(); 347 | 348 | const dom = new JSDOM(); 349 | global.document = dom.window.document; 350 | global.window = dom.window; 351 | utilMockGetConfigForEnvVariable({}); 352 | }); 353 | 354 | it("makes it possible to enable a variable via window", () => { 355 | document.cookie = "FEATURE_FOO=true"; 356 | document.cookie = "FEATURE_BAR=false"; 357 | 358 | const { getFeatureFlag } = configure({ 359 | featureFlags: ["FOO", "BAR"], 360 | allowCookieOverride: ["FOO", "BAR"], 361 | }); 362 | 363 | expect(getFeatureFlag("BAR")).toBeFalsy(); 364 | window.FEATURES.BAR.enable(); 365 | 366 | expect(getFeatureFlag("FOO")).toBeTruthy(); 367 | window.FEATURES.FOO.disable(); 368 | 369 | expect(getFeatureFlag("BAR")).toBeTruthy(); 370 | expect(getFeatureFlag("FOO")).toBeFalsy(); 371 | }); 372 | 373 | it("shows state of the feature from cookie", () => { 374 | document.cookie = "FEATURE_FOO=true"; 375 | document.cookie = "FEATURE_BAR=false"; 376 | 377 | configure({ 378 | featureFlags: ["FOO", "BAR"], 379 | allowCookieOverride: ["FOO", "BAR"], 380 | }); 381 | 382 | expect(window.FEATURES.BAR.state()).toBeFalsy(); 383 | expect(window.FEATURES.FOO.state()).toBeTruthy(); 384 | }); 385 | 386 | it("only provide interface to overridable variables", () => { 387 | document.cookie = "FEATURE_FOO=true"; 388 | document.cookie = "FEATURE_BAR=false"; 389 | 390 | const { getFeatureFlag } = configure({ 391 | featureFlags: ["FOO", "BAR"], 392 | allowCookieOverride: ["FOO"], 393 | }); 394 | 395 | expect(getFeatureFlag("FOO")).toBeTruthy(); 396 | expect(getFeatureFlag("BAR")).toBeFalsy(); 397 | 398 | expect(window.FEATURES.BAR).toBeFalsy(); 399 | }); 400 | }); 401 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import * as Next from "next"; 3 | import getConfig from "next/config"; 4 | 5 | export const isServer = () => { 6 | return typeof window === "undefined"; 7 | }; 8 | 9 | const _parseCookie = (cookie: string) => { 10 | // FEATURE_HIDE_PLP=true;FEATURE_NO_LOGIN=false; 11 | return cookie 12 | .split(";") 13 | .map((cookieKeyVal) => { 14 | const [key, val] = cookieKeyVal.split("="); 15 | return { [key.trim()]: decodeURI(val).trim() }; 16 | }) 17 | .reduce((cookieParsed, keyValPair) => ({ ...cookieParsed, ...keyValPair })); 18 | }; 19 | 20 | const _stringToBool = (boolString: string) => { 21 | if (boolString.toLowerCase() === "true" || boolString === "1") { 22 | return true; 23 | } 24 | 25 | return false; 26 | }; 27 | 28 | const getFeatureFromCookie = ( 29 | cookieName: string, 30 | { 31 | context, 32 | htmlDocument, 33 | }: { 34 | context?: Next.GetServerSidePropsContext; 35 | htmlDocument: { cookie: any }; 36 | } 37 | ) => { 38 | const fromCookie = isServer() 39 | ? context?.req.cookies[cookieName] 40 | : _parseCookie(htmlDocument.cookie)[cookieName]; 41 | 42 | if (fromCookie) { 43 | return _stringToBool(fromCookie); 44 | } 45 | 46 | return false; 47 | }; 48 | 49 | export const configure = ( 50 | { 51 | featureFlags, 52 | allowCookieOverride, 53 | debug, 54 | }: { 55 | featureFlags: KeysType[]; 56 | allowCookieOverride: KeysType[]; 57 | debug?: boolean; 58 | } = { featureFlags: [], allowCookieOverride: [] } 59 | ) => { 60 | defineWindowInterface(featureFlags, allowCookieOverride); 61 | 62 | const { serverRuntimeConfig, publicRuntimeConfig } = getConfig(); 63 | 64 | return { 65 | getFeatureFlag: ( 66 | key: KeysType, 67 | context?: Next.GetServerSidePropsContext 68 | ) => { 69 | const index = `FEATURE_${key}`; 70 | 71 | if (allowCookieOverride.includes(key)) { 72 | const fromCookie = isServer() 73 | ? context?.req.cookies[index] 74 | : _parseCookie(document.cookie)[index]; 75 | 76 | if (fromCookie) { 77 | return _stringToBool(fromCookie); 78 | } 79 | } 80 | 81 | const fromEnv = serverRuntimeConfig[index] || publicRuntimeConfig[index]; 82 | if (fromEnv) { 83 | return _stringToBool(fromEnv); 84 | } 85 | 86 | debug 87 | ? console.warn( 88 | `Feature flag "${key}" does not exist on next-feature-flags` 89 | ) 90 | : null; 91 | return false; 92 | }, 93 | }; 94 | }; 95 | 96 | // Sets an interface on window to enable and disable feature flags 97 | function defineWindowInterface( 98 | featureFlags: string[], 99 | allowCookieOverride: string[] 100 | ) { 101 | if (!isServer()) { 102 | window.FEATURES = 103 | window.FEATURES || 104 | featureFlags.reduce((features, key) => { 105 | // Only create interface for features with override 106 | if (!allowCookieOverride.includes(key)) { 107 | return features; 108 | 109 | } 110 | 111 | const cookieName = `FEATURE_${key}`; 112 | return { 113 | ...features, 114 | [key]: { 115 | enable: () => (document.cookie = `${cookieName}=true;`), 116 | disable: () => (document.cookie = `${cookieName}=false;`), 117 | state: () => getFeatureFromCookie(cookieName, { htmlDocument: document }), 118 | }, 119 | }; 120 | }, {}); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "strict": false, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "checkJs": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | "stripInternal": true, 14 | "skipDefaultLibCheck": true, 15 | "allowSyntheticDefaultImports": true, 16 | "outDir": "build/compiled", 17 | "declaration": true, 18 | "target": "es2017" 19 | }, 20 | "include": ["src", "index.ts", "index.d.ts"], 21 | "exclude": ["node_modules", "compiled", "**/*.spec.ts"] 22 | } 23 | --------------------------------------------------------------------------------