├── .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 |
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 | 
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 | }
--------------------------------------------------------------------------------