├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE.md ├── README.md ├── assets ├── antiny.png ├── extension-example.png └── tiny-flags.png ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── constants.ts ├── create-flags-context.tsx ├── create-tiny-flags.tsx ├── helpers.ts ├── index.ts ├── public-types.d.ts └── types.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["matiasbontempo"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.tabSize": 2, 4 | "editor.insertSpaces": true, 5 | "editor.detectIndentation": false, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll": true, 8 | }, 9 | "eslint.format.enable": true, 10 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Matías Bontempo 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tiny Flags 2 | 3 | ## 🚩 What is this? Feature flags for ants? 4 | 5 | Well, yes! [Antiny 🐜](./assets/antiny.png) looks super happy with it. But also it is a simple way to add client-side feature flags that can be updated at runtime using a companion browser extension. 6 | 7 | Let PMs, designers, fellow developers or even clients try your awesome new features without worrying about waiting for the whole thing to be finished or blocking a release. 8 | 9 | Tiny Flags is a great option when you don't want to pay for a third-party provider. Every user of your application can update the flags' status without the need of re-deploying, allowing them to test in a real environment. 10 | 11 | This project also provides full TypeScript support when using the `useFlags` hook. 12 | 13 | ## 🪄 Demo 14 | 15 | Want to see it in action? Check out the [demo](https://wm3il4.csb.app/). 16 | 17 | Also, you can check this [CodeSandbox](https://codesandbox.io/s/tiny-flags-demo-wm3il4) to play with the code. 18 | 19 | ## 📦 Installation 20 | 21 | ```sh 22 | npm i tiny-flags 23 | ``` 24 | 25 | ## 🧑‍💻 Usage 26 | 27 | ### Setup 28 | 29 | First, you'll need a configuration: 30 | 31 | ```js 32 | // tiny-flags.ts 33 | 34 | import { createTinyFlags } from 'tiny-flags'; 35 | 36 | const flags = { 37 |   newFeature: { 38 |     label: 'New Feature', 39 |     value: false, // value is not required 40 |   }, 41 |   anotherFlag: { 42 |     label: 'This is another feature enabled by default', 43 |     value: true, 44 |   }, 45 | }; 46 | 47 | export const { FlagsProvider, useFlags } = createTinyFlags(flags); 48 | ``` 49 | 50 | Then you can wrap your application with FlagsProvider. 51 | 52 | ```js 53 | import ReactDOM from 'react-dom/client'; 54 | import App from './App'; 55 | 56 | import { FlagsProvider } from './tiny-flags'; 57 | 58 | ReactDOM.render( 59 |   60 |     61 |   , 62 |   document.getElementById('root') 63 | ) 64 | ``` 65 | 66 | ### Hook 67 | 68 | Import `useFlags` in your components to check your flag's status. 69 | 70 | ```js 71 | // component.ts 72 | 73 | import { useFlags } from './tiny-flags'; 74 | 75 | const App = () => { 76 |   const flags = useFlags(); 77 | 78 |   return ( 79 |    
80 |       This will show if 81 |       { flags.newFeature &&
Ta-da! 🎉
} 82 |    
83 |   ); 84 | }; 85 | 86 | export default App; 87 | ``` 88 | 89 | ### Component 90 | 91 | You can also use the `FlagsWrapper` component to wrap your components and check the flag's status. 92 | 93 | The `FlagsWrapper` component receives a `condition` prop that can be a string, an array of strings or a function. 94 | - If the condition is a string, it will check if the flag is enabled. 95 | - If the condition is an array of strings, it will check if all the flags are enabled. 96 | - If the condition is a function, it will check if the function returns `true`. 97 | 98 | ```js 99 | // component.ts 100 | 101 | import { FlagsWrapper } from './tiny-flags'; 102 | 103 | const Component = () => { 104 |   return ( 105 | 106 |
Ta-da! 🎉
107 |
108 | ); 109 | }; 110 | 111 | export default Component; 112 | ``` 113 | 114 | > Make sure to import `FlagsProvider`, `useFlags` and `FlagsWrapper` from the `tiny-flags` configuration file and not the `tiny-flags` package. 115 | 116 | ## 🧩 Extension 117 | 118 | This library establishes a two-way communication with the Tiny Flags Extension so you can see the available flags and also toggle their state. 119 | 120 | ![Extension](/assets/extension-example.png) 121 | 122 | ## ⚠️ When not to use? 123 | - You need to remotely update your flags 124 | - You need complex rules or different audiences for your flags 125 | - You don't want your flags to be exposed 126 | -------------------------------------------------------------------------------- /assets/antiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiasbontempo/tiny-flags/9d468b8f621d49da389eecff60bb8130986c75a5/assets/antiny.png -------------------------------------------------------------------------------- /assets/extension-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiasbontempo/tiny-flags/9d468b8f621d49da389eecff60bb8130986c75a5/assets/extension-example.png -------------------------------------------------------------------------------- /assets/tiny-flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matiasbontempo/tiny-flags/9d468b8f621d49da389eecff60bb8130986c75a5/assets/tiny-flags.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-flags", 3 | "version": "1.3.0", 4 | "description": "What is this? Feature flags for ants? Well yes! But also Tiny Flags is simple way to add client-side feature flags that can be updated at runtime.", 5 | "author": "Matias Bontempo (https://matiasbontempo.com)", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/index.js", 9 | "module": "dist/index.mjs", 10 | "types": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "lint": "npm run lint:eslint & npm run lint:ts", 16 | "lint:eslint": "eslint --ext .ts,.tsx ./src", 17 | "lint:ts": "tsc --noemit", 18 | "build": "yarn cleanup & rollup -c --bundleConfigAsCjs", 19 | "cleanup": "rimraf ./dist" 20 | }, 21 | "peerDependencies": { 22 | "react": ">=16.9.0" 23 | }, 24 | "devDependencies": { 25 | "@rollup/plugin-typescript": "^9.0.2", 26 | "@types/chrome": "^0.0.198", 27 | "@types/react": "^18.0.21", 28 | "eslint": "^8.26.0", 29 | "eslint-config-matiasbontempo": "github:matiasbontempo/eslint-config-matiasbontempo", 30 | "react": "^18.0.0", 31 | "react-dom": "^18.2.0", 32 | "rimraf": "^3.0.2", 33 | "rollup": "^3.2.5", 34 | "rollup-plugin-copy": "^3.4.0", 35 | "tslib": "^2.4.1", 36 | "typescript": "^4.8.4" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git+https://github.com/matiasbontempo/tiny-flags.git" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/matiasbontempo/tiny-flags/issues" 44 | }, 45 | "homepage": "https://github.com/matiasbontempo/tiny-flags#readme", 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "keywords": [ 50 | "feature", 51 | "flags", 52 | "toggles", 53 | "toggle", 54 | "react", 55 | "tiny-flags", 56 | "tiny flags", 57 | "feature flag", 58 | "feature flags", 59 | "feature toggle", 60 | "feature toggles", 61 | "feature flagging", 62 | "feature toggling", 63 | "feature management", 64 | "react hooks", 65 | "react hook", 66 | "react components", 67 | "react component", 68 | "react library", 69 | "react feature flag", 70 | "react feature flags", 71 | "react feature toggle", 72 | "react feature toggles", 73 | "react feature flagging", 74 | "react feature toggling", 75 | "react feature management", 76 | "react tiny flags", 77 | "react tiny-flags", 78 | "chrome extension", 79 | "web extension", 80 | "extension companion" 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "@rollup/plugin-typescript"; 2 | import copy from 'rollup-plugin-copy'; 3 | 4 | const packageJson = require("./package.json"); 5 | 6 | export default [ 7 | { 8 | input: 'src/index.ts', 9 | output: [ 10 | { 11 | file: packageJson.main, 12 | format: 'cjs' 13 | }, 14 | { 15 | file: packageJson.module, 16 | format: "esm", 17 | }, 18 | ], 19 | plugins: [ 20 | typescript({ tsconfig: './tsconfig.json' }), 21 | copy({ 22 | targets: [{ src: './src/public-types.d.ts', dest: './dist', rename: 'index.d.ts' }] 23 | }), 24 | ], 25 | external: ['react'], 26 | }, 27 | ] 28 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_STORAGE_KEY = 'tiny-flags'; 2 | -------------------------------------------------------------------------------- /src/create-flags-context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, useEffect, useMemo, useState, 3 | } from 'react'; 4 | 5 | import { syncFlags } from './helpers'; 6 | import { LOCAL_STORAGE_KEY } from './constants'; 7 | 8 | import type { ContextProps, Flags, ProviderProps } from './types'; 9 | 10 | const createFlagsContext = (defaultFlags: Flags) => { 11 | const FlagsContext = createContext>({ 12 | flags: defaultFlags, 13 | updateFlag: () => {}, 14 | }); 15 | 16 | const FlagsContextProvider = ( 17 | { children }: ProviderProps, 18 | ) => { 19 | const [flags, setFlags] = useState>(syncFlags(defaultFlags)); 20 | 21 | const updateFlag = (key: T, value: boolean): void => { 22 | const flag = flags[key]; 23 | if (!flag) return; 24 | 25 | flag.value = value; 26 | setFlags({ 27 | ...flags, 28 | [key]: flag, 29 | }); 30 | 31 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(flags)); 32 | }; 33 | 34 | useEffect(() => { 35 | const handleGetFlagsEvent = () => { 36 | const event = new CustomEvent('TF_FLAGS', { detail: flags }); 37 | document.dispatchEvent(event); 38 | }; 39 | 40 | const handleSetFlagEvent = (e: Event) => { 41 | const { detail } = e as CustomEvent<{ key: T, value: boolean }>; 42 | updateFlag(detail.key, detail.value); 43 | }; 44 | 45 | document.addEventListener('TF_GET_FLAGS', handleGetFlagsEvent); 46 | document.addEventListener('TF_SET_FLAG', handleSetFlagEvent); 47 | 48 | return () => { 49 | document.removeEventListener('TF_GET_FLAGS', handleGetFlagsEvent); 50 | document.removeEventListener('TF_SET_FLAG', handleSetFlagEvent); 51 | }; 52 | }, []); 53 | 54 | const contextValue = useMemo(() => ({ 55 | flags, 56 | updateFlag, 57 | }), [flags, updateFlag]); 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | }; 65 | 66 | return { context: FlagsContext, Provider: FlagsContextProvider }; 67 | }; 68 | 69 | export default createFlagsContext; 70 | -------------------------------------------------------------------------------- /src/create-tiny-flags.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import createFlagsContext from './create-flags-context'; 4 | 5 | import type { Flag, FlagsDictionary, WrapperProps } from './types'; 6 | 7 | let initialized = false; 8 | 9 | const createTinyFlags = (defaultFlags: Record) => { 10 | if (initialized) throw new Error('Tiny Flags already initialized'); 11 | initialized = true; 12 | 13 | const { context, Provider } = createFlagsContext(defaultFlags); 14 | 15 | const useFlags = () => { 16 | const { flags } = useContext(context); 17 | const flagsDictionary: Partial> = {}; 18 | const flagsKeys = Object.keys(flags) as T[]; 19 | 20 | flagsKeys.forEach((key) => { flagsDictionary[key] = flags[key].value || false; }); 21 | 22 | return flagsDictionary as FlagsDictionary; 23 | }; 24 | 25 | const FlagsWrapper = ({ children, condition }: WrapperProps) => { 26 | const tinyFlags = useFlags(); 27 | 28 | let isActive = false; 29 | 30 | if (typeof condition === 'string') isActive = !!tinyFlags[condition]; 31 | else if (Array.isArray(condition)) isActive = condition.every((flag) => !!tinyFlags[flag]); 32 | else if (typeof condition === 'function') isActive = condition(tinyFlags); 33 | 34 | if (!isActive) return null; 35 | return children; 36 | }; 37 | 38 | return { FlagsProvider: Provider, useFlags, FlagsWrapper }; 39 | }; 40 | 41 | export default createTinyFlags; 42 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { LOCAL_STORAGE_KEY } from './constants'; 2 | import { Flags } from './types'; 3 | 4 | export const syncFlags = (defaultFlags: Flags): Flags => { 5 | const rawFlags = localStorage.getItem(LOCAL_STORAGE_KEY); 6 | if (!rawFlags) return defaultFlags; 7 | 8 | const flagsToReturn = { ...defaultFlags }; 9 | 10 | try { 11 | const savedFlags = JSON.parse(rawFlags) as Flags; 12 | const keys = Object.keys(savedFlags) as T[]; 13 | keys.forEach((key) => { 14 | if (!flagsToReturn[key]) return; 15 | if (!savedFlags[key]) return; 16 | flagsToReturn[key].value = savedFlags[key].value; 17 | }); 18 | 19 | return flagsToReturn; 20 | } catch (err) { 21 | return defaultFlags; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createTinyFlags } from './create-tiny-flags'; 2 | export { Flag } from './types'; 3 | -------------------------------------------------------------------------------- /src/public-types.d.ts: -------------------------------------------------------------------------------- 1 | import type { FC, ReactNode } from 'react'; 2 | 3 | export type Flag = { 4 | label: string; 5 | value?: boolean; 6 | }; 7 | 8 | type Flags = Record; 9 | 10 | export const createTinyFlags: (flags: Flags) => { 11 | FlagsProvider: FC<{ 12 | children: ReactNode; 13 | }>, 14 | useFlags: () => Record, 15 | FlagsWrapper: FC<{ 16 | condition: T | T[] | ((flags: Record) => boolean); 17 | children: ReactNode; 18 | }> 19 | }; 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | 3 | export type Flag = { 4 | label: string; 5 | value?: boolean; 6 | }; 7 | 8 | export type Flags = Record; 9 | 10 | export type FlagsDictionary = Record; 11 | 12 | export type ContextProps = { 13 | flags: Flags; 14 | updateFlag: (key: T, value: boolean) => void; 15 | }; 16 | 17 | export type ProviderProps = { 18 | flags: Flags; 19 | children: ReactNode; 20 | }; 21 | 22 | export type WrapperProps = { 23 | condition: T | T[] | ((flags: FlagsDictionary) => boolean); 24 | children: ReactNode; 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["DOM"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "jsx": "react-jsx", 8 | "baseUrl": "./src", 9 | "outDir": "./dist", 10 | "declaration": false, 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "removeComments": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "strictPropertyInitialization": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "forceConsistentCasingInFileNames": true, 24 | }, 25 | "exclude": [ 26 | "node_modules" 27 | ], 28 | "include": ["src/**/*"], 29 | } --------------------------------------------------------------------------------