flag-engine 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ name: "Monorepo setup"
2
+ description: "Install deps + dotenv setup"
3
+ runs:
4
+ using: "composite"
5
+ steps:
6
+ - uses: actions/setup-node@v2
7
+ name: Install Node 20
8
+ with:
9
+ node-version: "20"
10
+
11
+ - uses: ./.github/actions/pnpm
@@ -0,0 +1,29 @@
1
+ name: "Pnpm setup"
2
+ description: "Handle caching and pnpm resolution"
3
+ runs:
4
+ using: "composite"
5
+ steps:
6
+ - uses: actions/setup-node@v2
7
+ name: Install Node 20
8
+ with:
9
+ node-version: "20"
10
+
11
+ - uses: pnpm/action-setup@v4
12
+ name: Install pnpm
13
+ id: pnpm-install
14
+ with:
15
+ run_install: false
16
+
17
+ - name: Get pnpm store directory
18
+ id: pnpm-cache
19
+ shell: bash
20
+ run: |
21
+ echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
22
+
23
+ - uses: actions/cache@v3
24
+ name: Setup pnpm cache
25
+ with:
26
+ path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
27
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
28
+ restore-keys: |
29
+ ${{ runner.os }}-pnpm-store-
@@ -0,0 +1,27 @@
1
+ name: Core
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+
11
+ jobs:
12
+ shared:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: "20"
19
+
20
+ - uses: ./.github/actions/monorepo
21
+
22
+ - name: Install dependencies
23
+ shell: bash
24
+ run: pnpm install
25
+
26
+ - name: Shared CI checks
27
+ run: pnpm run ci
package/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ v20.18.0
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ <div align="center">⛵ A feature flags evaluation engine, runtime agnostic with no remote services.
2
+ <br/>
3
+ <br/>
4
+
5
+ Demo [NextJs](https://codesandbox.io/p/devbox/7tjw9s) | [React Native (Snack)](https://snack.expo.dev/@mfrachet/flag-engine-example) | [Client Side](https://stackblitz.com/edit/vitejs-vite-a8kiur?file=main.js) | [Server Side](https://stackblitz.com/edit/stackblitz-starters-jfhzjq?file=index.js)
6
+
7
+ </div>
8
+ <br/>
9
+
10
+ ![20 pixelized boats on the sea](https://github.com/user-attachments/assets/5628ad4c-6e77-4f5c-9e81-2bc5f14b5d51)
11
+
12
+ ---
13
+
14
+ ## What is Flag Engine?
15
+
16
+ Flag Engine is a runtime agnostic and source agnostic feature flags evaluation engine. You give it a configuration and a context, and it will evaluate the flags for you.
17
+
18
+ **What Flag Engine is not:**
19
+
20
+ - A feature flag management platform
21
+ - A feature flag dashboard
22
+ - An analytics platform, you have to send your events to your analytics platform
23
+ - A way to store your feature flags
24
+ - A drop-in replacement for [OpenFeature](https://openfeature.dev). (a provider for Flag Engine will be created to be compliant with the OpenFeature API)
25
+
26
+ ## Usage
27
+
28
+ 1. Install the package:
29
+
30
+ ```bash
31
+ $ pnpm add @flag-engine/core
32
+ ```
33
+
34
+ 2. Create a configuration (or build it from where it makes sense for you like a DB or a static file, or whatever)
35
+
36
+ ```typescript
37
+ import {
38
+ createFlagEngine,
39
+ FlagsConfiguration,
40
+ UserConfiguration,
41
+ } from "@flag-engine/core";
42
+
43
+ const flagsConfig: FlagsConfiguration = [
44
+ {
45
+ key: "feature-flag-key",
46
+ status: "enabled", // the status of the flag, can be "enabled" or "disabled"
47
+ strategies: [], // a set of condition for customization purpose
48
+ },
49
+ ];
50
+
51
+ // This is useful to create conditions based on the user's attributes.
52
+ // The __id is mandatory and a special one that will be used to compute % based variants.
53
+ const userConfiguration: UserConfiguration = {
54
+ __id: "73a56693-0f83-4ffc-a61d-7c95fdf68693", // a unique identifier for the user or an empty string if the users are not connected.
55
+ };
56
+
57
+ const engine = createFlagEngine(flagsConfig);
58
+ const userCtx = engine.createUserContext(userConfiguration);
59
+
60
+ // Evaluate one specific feature flag
61
+ const isFlagEnabled = userCtx.evaluate("feature-flag-key"); // true
62
+
63
+ // Evaluate all the feature flags at once
64
+ const allFlags = userCtx.evaluateAll(); // { "feature-flag-key": true }
65
+ ```
66
+
67
+ ## Concepts
68
+
69
+ ### Flag configuration
70
+
71
+ It's a descriptive object that contains guidance on how the feature flag should be evaluated. It's composed of a list of feature flags with their **status** (`enabled` or `disabled`) and a list of **strategies**.
72
+
73
+ ### User configuration
74
+
75
+ This is an object that holds details about the current user. It includes a unique identifier (`__id`, which is mandatory) and other custom attributes (defined by you) that can be used to evaluate feature flags. These attributes can be utilized within strategies to specify the conditions necessary to determine a computed feature flag variant.
76
+
77
+ This is useful if you want your QA team to test the feature behind the flag: you can create a strategy that targets users with your domain address (e.g., `@gmail.com`), ensuring that only they will see the flag enabled.
78
+
79
+ > Another example: the current user has a `country` attribute with a value of `France`. I have defined a strategy with a condition on the `country` attribute with a value of `France`. This user is eligible to resolve a computed variant. If the user sends a `US` country, they will resolve a `false` variant.
80
+
81
+ **Notes**:
82
+
83
+ - the `__id` is mandatory and should be your user uniquer id OR an empty string if the users are not connected.
84
+
85
+ ### Flag status
86
+
87
+ - `enabled`: The feature flag is enabled. (returns true or the computed variant)
88
+ - `disabled`: The feature flag is disabled. (returns false every time)
89
+
90
+ ### Strategies
91
+
92
+ **Strategies** is where all the customization stands. In a strategy, you can define:
93
+
94
+ - **a set of rules** that are needed to be eligible for the feature flag evaluation (using the user configuration/context)
95
+ - a list of variants with the percentage of the population that should see each variant. Those are computed against the `__id` of the user (this is why it's mandatory).
96
+
97
+ **It's important to understand that:**
98
+
99
+ - Each **strategy** is an `or`. It means that if the user matches **at least one strategy**, they will be eligible for the feature flag evaluation.
100
+
101
+ - Each **rule** is an `and`. It means that **all the rules in one strategy must be true** for the user for the strategy to be eligible.
102
+
103
+ This is convenient for combining **and** and **or** logic and create complex conditional feature flags.
104
+
105
+ ## An exhaustive example
106
+
107
+ I want to show my audience **2 variants** of a feature. Only the people living in `France` and `Spain` should see the feature.
108
+
109
+ Here is how I can do that:
110
+
111
+ ```typescript
112
+ const flagsConfig: FlagsConfiguration = [
113
+ {
114
+ key: "feature-flag-key",
115
+ status: "enabled", // the status of the flag, can be "enabled" or "disabled"
116
+ strategies: [
117
+ {
118
+ name: "only-france-and-spain",
119
+ rules: [
120
+ {
121
+ field: "country",
122
+ operator: "in",
123
+ value: ["France", "Spain"],
124
+ },
125
+ ],
126
+ variants: [
127
+ {
128
+ name: "A",
129
+ percent: 50,
130
+ },
131
+ {
132
+ name: "B",
133
+ percent: 50,
134
+ },
135
+ ],
136
+ },
137
+ ],
138
+ },
139
+ ];
140
+
141
+ const engine = createFlagEngine(flagsConfig);
142
+ const userCtx = engine.createUserContext({
143
+ __id: "b",
144
+ country: "France",
145
+ });
146
+
147
+ const variant = userCtx.evaluate("feature-flag-key"); // gives back B
148
+ ```
149
+
150
+ Now, I suggest you give it a try, build your config object the way you prefer and start building stuff!
151
+
152
+ ❤️❤️❤️
153
+
154
+ ---
155
+
156
+ Built by [@mfrachet](https://twitter.com/mfrachet)
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "flag-engine",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "packageManager": "pnpm@10.12.2",
6
+ "scripts": {
7
+ "dev": "turbo dev",
8
+ "build": "turbo build",
9
+ "lint": "turbo lint",
10
+ "test": "turbo test",
11
+ "bundlesize": "turbo bundlesize",
12
+ "ci": "CI=true turbo run lint test bundlesize"
13
+ },
14
+ "keywords": [],
15
+ "author": "",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "turbo": "^2.5.4"
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ // @ts-check
2
+
3
+ import eslint from "@eslint/js";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ export default tseslint.config(
7
+ eslint.configs.recommended,
8
+ ...tseslint.configs.recommended,
9
+ {
10
+ ignores: ["dist"],
11
+ }
12
+ );
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@flag-engine/core",
3
+ "private": false,
4
+ "version": "0.0.9",
5
+ "description": "Feature flags evaluation engine, runtime agnostic",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs.js",
8
+ "module": "./dist/index.mjs",
9
+ "exports": {
10
+ ".": {
11
+ "require": "./dist/index.cjs.js",
12
+ "import": "./dist/index.mjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "types": "./dist/index.d.ts",
17
+ "scripts": {
18
+ "build": "rm -rf dist .turbo && rollup -c rollup.config.mjs",
19
+ "start": "tsx src/index.ts",
20
+ "test": "vitest",
21
+ "coverage": "vitest run --coverage",
22
+ "lint": "eslint .",
23
+ "size": "bundlesize"
24
+ },
25
+ "keywords": [],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "devDependencies": {
29
+ "@eslint/js": "^9.29.0",
30
+ "@rollup/plugin-node-resolve": "^16.0.1",
31
+ "@rollup/plugin-terser": "^0.4.4",
32
+ "@rollup/plugin-typescript": "^12.1.3",
33
+ "@types/eslint__js": "^9.14.0",
34
+ "@types/murmurhash-js": "^1.0.6",
35
+ "@vitest/coverage-v8": "3.2.4",
36
+ "bundlesize": "^0.18.2",
37
+ "eslint": "^9.29.0",
38
+ "rollup": "^4.44.0",
39
+ "tslib": "^2.8.1",
40
+ "tsx": "^4.20.3",
41
+ "typescript": "^5.8.3",
42
+ "typescript-eslint": "^8.34.1",
43
+ "vitest": "^3.2.4"
44
+ },
45
+ "dependencies": {
46
+ "murmurhash-js": "^1.0.0"
47
+ },
48
+ "bundlesize": [
49
+ {
50
+ "path": "./dist/index.mjs",
51
+ "maxSize": "1.3 kB"
52
+ },
53
+ {
54
+ "path": "./dist/index.cjs.js",
55
+ "maxSize": "1.3 kB"
56
+ }
57
+ ]
58
+ }
@@ -0,0 +1,33 @@
1
+ import typescript from "@rollup/plugin-typescript";
2
+ import terser from "@rollup/plugin-terser";
3
+ import { nodeResolve } from "@rollup/plugin-node-resolve";
4
+
5
+ const external = ["murmurhash-js"];
6
+ const globals = { "murmurhash-js": "murmurhash-js" };
7
+
8
+ export default () => {
9
+ return {
10
+ input: "src/index.ts",
11
+ output: [
12
+ {
13
+ file: "dist/index.cjs.js",
14
+ format: "cjs",
15
+ name: "ff-engine",
16
+ globals,
17
+ },
18
+ {
19
+ file: "dist/index.mjs",
20
+ format: "es",
21
+ },
22
+ ],
23
+ plugins: [
24
+ nodeResolve(),
25
+ typescript({
26
+ tsconfig: "./tsconfig.json",
27
+ sourceMap: true,
28
+ }),
29
+ terser(),
30
+ ],
31
+ external,
32
+ };
33
+ };
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext", // Target the latest ECMAScript standard for optimal ESM support.
4
+ "module": "ESNext", // Output ES Modules.
5
+ "moduleResolution": "Node", // Use Node module resolution strategy.
6
+ "strict": true, // Enable strict type-checking.
7
+ "esModuleInterop": true, // Enable interoperability between CommonJS and ES modules.
8
+ "skipLibCheck": true, // Skip type checking of all declaration files for faster builds.
9
+ "declaration": true, // Generate declaration files (.d.ts).
10
+ "declarationMap": true, // Generate sourcemaps for declaration files.
11
+ "sourceMap": true, // Enable sourcemaps for debugging.
12
+ "outDir": "dist", // Output directory for compiled files.
13
+ "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export.
14
+ "forceConsistentCasingInFileNames": true, // Enforce consistent file casing.
15
+ "noEmit": false
16
+ },
17
+ "exclude": ["node_modules", "dist"] // Exclude node_modules and dist from compilation.
18
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ testTimeout: 15000,
6
+ coverage: {
7
+ provider: "v8",
8
+ reporter: ["text", "html", "lcov"],
9
+ exclude: ["**/__tests__/**", "**/*.test.ts", "vitest.config.ts"],
10
+ },
11
+ },
12
+ });
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - "packages/*"
package/turbo.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "tasks": {
4
+ "lint": {},
5
+ "test": {},
6
+ "build": {
7
+ "dependsOn": ["^build"],
8
+ "outputs": ["dist/**"]
9
+ },
10
+ "bundlesize": {
11
+ "dependsOn": ["^build"]
12
+ }
13
+ }
14
+ }