@nexusts/feature-flag 0.8.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.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # `@nexusts/feature-flag`
2
+
3
+ Feature flags, canary deployments, and A/B testing for NexusTS.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @nexusts/feature-flag
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import { FeatureFlagModule, FeatureFlagService, FeatureFlag } from '@nexusts/feature-flag';
15
+
16
+ @Module({
17
+ imports: [
18
+ FeatureFlagModule.forRoot({
19
+ flags: {
20
+ 'new-dashboard': { enabled: true, rollout: 0.5 },
21
+ 'beta-api': false,
22
+ },
23
+ }),
24
+ ],
25
+ })
26
+ class AppModule {}
27
+
28
+ // In a controller:
29
+ const showBeta = await flags.isEnabled('new-checkout', { userId: 'u-1' });
30
+
31
+ // Or as a decorator:
32
+ @FeatureFlag('new-dashboard')
33
+ async index() { ... }
34
+ ```
35
+
36
+ ## API
37
+
38
+ | Method | Description |
39
+ | ------ | ----------- |
40
+ | `isEnabled(flag, context?)` | `Promise<boolean>` — `true` if the flag is active |
41
+ | `setFlag(name, definition)` | Add or update a flag at runtime |
42
+ | `getFlag(name)` | Return the current definition |
43
+
44
+ Flag evaluation order: `denylist` → `allowlist` → `enabled: false` →
45
+ `rollout` (djb2 hash) → default.
46
+
47
+ ## Links
48
+
49
+ - [User guide](../../docs/user-guide/feature-flags.md)
50
+ - [NestJS comparison — feature flags](../../docs/analysis/nestjs-comparison.md)
@@ -0,0 +1 @@
1
+ export { MemoryFlagBackend } from "./memory.js";
@@ -0,0 +1,9 @@
1
+ import type { FlagContext, FlagDefinition, FeatureFlagBackend } from "../types.js";
2
+ /** In-process feature flag backend — no external dependencies. */
3
+ export declare class MemoryFlagBackend implements FeatureFlagBackend {
4
+ #private;
5
+ constructor(initial?: Record<string, FlagDefinition | boolean>);
6
+ setFlag(flagName: string, definition: FlagDefinition | boolean): void;
7
+ getFlag(flagName: string): FlagDefinition | undefined;
8
+ isEnabled(flagName: string, context?: FlagContext): Promise<boolean>;
9
+ }
@@ -0,0 +1,28 @@
1
+ import "reflect-metadata";
2
+ import type { FlagContext } from "../types.js";
3
+ export interface FeatureFlagOptions {
4
+ /** Extract a `FlagContext` from the Hono `Context` object (first arg of the handler). */
5
+ contextFn?: (c: any) => FlagContext;
6
+ /** Custom response when the flag is disabled. Defaults to 404 JSON. */
7
+ onDisabled?: (c: any) => Response | Promise<Response>;
8
+ }
9
+ export interface FlagSpec {
10
+ propertyKey: string | symbol;
11
+ flagName: string;
12
+ contextFn?: (c: any) => FlagContext;
13
+ onDisabled?: (c: any) => Response | Promise<Response>;
14
+ original: (...args: any[]) => any;
15
+ }
16
+ /**
17
+ * Mark a route handler so `FeatureFlagService.applyDecorators()` will gate it.
18
+ *
19
+ * @Get('/')
20
+ * @FeatureFlag('new-dashboard')
21
+ * async index(c: Context) { ... }
22
+ *
23
+ * When the flag is disabled the handler returns a 404 JSON response.
24
+ * Pass `onDisabled` to customise the response, or `contextFn` to extract
25
+ * a `FlagContext` (userId etc.) from the Hono `Context`.
26
+ */
27
+ export declare function FeatureFlag(flagName: string, options?: FeatureFlagOptions): MethodDecorator;
28
+ export declare function getFlagSpecs(target: any): FlagSpec[];
@@ -0,0 +1,2 @@
1
+ export { FeatureFlag, getFlagSpecs } from "./feature-flag.decorator.js";
2
+ export type { FlagSpec, FeatureFlagOptions } from "./feature-flag.decorator.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `FeatureFlagModule` — drop-in feature flags.
3
+ *
4
+ * @Module({
5
+ * imports: [
6
+ * FeatureFlagModule.forRoot({
7
+ * flags: {
8
+ * 'new-dashboard': { enabled: true, rollout: 0.5 },
9
+ * 'beta-api': false,
10
+ * },
11
+ * }),
12
+ * ],
13
+ * })
14
+ * export class AppModule {}
15
+ */
16
+ import "reflect-metadata";
17
+ import type { FeatureFlagConfig } from "./types.js";
18
+ export declare class FeatureFlagModule {
19
+ static forRoot(config?: FeatureFlagConfig): {
20
+ new (): {};
21
+ };
22
+ }
@@ -0,0 +1,22 @@
1
+ import type { FlagContext, FlagDefinition, FeatureFlagConfig } from "./types.js";
2
+ export declare class FeatureFlagService {
3
+ #private;
4
+ /** DI token. */
5
+ static readonly TOKEN: unique symbol;
6
+ constructor(config?: FeatureFlagConfig);
7
+ /** Returns `true` if the flag is enabled for the given context. */
8
+ isEnabled(flagName: string, context?: FlagContext): Promise<boolean>;
9
+ /** Create or update a flag at runtime. */
10
+ setFlag(flagName: string, definition: FlagDefinition | boolean): void;
11
+ /** Return the raw definition (or `undefined` if unknown). */
12
+ getFlag(flagName: string): FlagDefinition | undefined;
13
+ /**
14
+ * Wire `@FeatureFlag` decorators onto a controller or service instance.
15
+ * Each decorated route handler is wrapped so it returns a 404 JSON
16
+ * response when the flag is disabled.
17
+ *
18
+ * The DI container calls this automatically when FeatureFlagModule is
19
+ * imported; you can also call it manually in tests.
20
+ */
21
+ applyDecorators(target: any): void;
22
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public entry point for `@nexusts/feature-flag`.
3
+ */
4
+ export { FeatureFlagModule } from "./feature-flag.module.js";
5
+ export { FeatureFlagService } from "./feature-flag.service.js";
6
+ export { MemoryFlagBackend } from "./backends/index.js";
7
+ export { FeatureFlag, getFlagSpecs } from "./decorators/index.js";
8
+ export type { FlagSpec, FeatureFlagOptions } from "./decorators/index.js";
9
+ export type { FlagContext, FlagDefinition, FeatureFlagConfig, FeatureFlagBackend, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,167 @@
1
+ // @bun
2
+ var __legacyDecorateClassTS = function(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
5
+ r = Reflect.decorate(decorators, target, key, desc);
6
+ else
7
+ for (var i = decorators.length - 1;i >= 0; i--)
8
+ if (d = decorators[i])
9
+ r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
10
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
11
+ };
12
+ var __legacyDecorateParamTS = (index, decorator) => (target, key) => decorator(target, key, index);
13
+ var __legacyMetadataTS = (k, v) => {
14
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function")
15
+ return Reflect.metadata(k, v);
16
+ };
17
+
18
+ // packages/feature-flag/src/feature-flag.module.ts
19
+ import"reflect-metadata";
20
+ import { Module } from "@nexusts/core";
21
+
22
+ // packages/feature-flag/src/feature-flag.service.ts
23
+ import { Inject, Injectable } from "@nexusts/core";
24
+
25
+ // packages/feature-flag/src/backends/memory.ts
26
+ function hashFloat(s) {
27
+ let h = 5381;
28
+ for (let i = 0;i < s.length; i++) {
29
+ h = Math.imul(h, 33) ^ s.charCodeAt(i);
30
+ }
31
+ return (h >>> 0) / 4294967295;
32
+ }
33
+
34
+ class MemoryFlagBackend {
35
+ #flags = new Map;
36
+ constructor(initial = {}) {
37
+ for (const [k, v] of Object.entries(initial)) {
38
+ this.#flags.set(k, typeof v === "boolean" ? { enabled: v } : v);
39
+ }
40
+ }
41
+ setFlag(flagName, definition) {
42
+ this.#flags.set(flagName, typeof definition === "boolean" ? { enabled: definition } : definition);
43
+ }
44
+ getFlag(flagName) {
45
+ return this.#flags.get(flagName);
46
+ }
47
+ async isEnabled(flagName, context) {
48
+ const def = this.#flags.get(flagName);
49
+ if (!def)
50
+ return false;
51
+ const id = context?.userId ?? context?.tenantId ?? context?.key ?? "";
52
+ if (id && def.denylist?.includes(id))
53
+ return false;
54
+ if (id && def.allowlist?.includes(id))
55
+ return true;
56
+ if (def.enabled === false)
57
+ return false;
58
+ if (def.rollout !== undefined && def.rollout < 1) {
59
+ if (!id)
60
+ return def.rollout > 0;
61
+ return hashFloat(`${flagName}:${id}`) < def.rollout;
62
+ }
63
+ return def.enabled !== false;
64
+ }
65
+ }
66
+
67
+ // packages/feature-flag/src/decorators/feature-flag.decorator.ts
68
+ import"reflect-metadata";
69
+ var FLAG_META = Symbol.for("nexus:FeatureFlag");
70
+ function FeatureFlag(flagName, options = {}) {
71
+ return (target, propertyKey, descriptor) => {
72
+ const specs = Reflect.getMetadata(FLAG_META, target.constructor) ?? [];
73
+ specs.push({
74
+ propertyKey,
75
+ flagName,
76
+ contextFn: options.contextFn,
77
+ onDisabled: options.onDisabled,
78
+ original: descriptor.value
79
+ });
80
+ Reflect.defineMetadata(FLAG_META, specs, target.constructor);
81
+ };
82
+ }
83
+ function getFlagSpecs(target) {
84
+ return Reflect.getMetadata(FLAG_META, target) ?? [];
85
+ }
86
+
87
+ // packages/feature-flag/src/feature-flag.service.ts
88
+ class FeatureFlagService {
89
+ static TOKEN = Symbol.for("nexus:FeatureFlagService");
90
+ #backend;
91
+ constructor(config = {}) {
92
+ this.#backend = new MemoryFlagBackend(config.flags ?? {});
93
+ }
94
+ async isEnabled(flagName, context) {
95
+ return this.#backend.isEnabled(flagName, context);
96
+ }
97
+ setFlag(flagName, definition) {
98
+ this.#backend.setFlag(flagName, definition);
99
+ }
100
+ getFlag(flagName) {
101
+ return this.#backend.getFlag(flagName);
102
+ }
103
+ applyDecorators(target) {
104
+ const specs = getFlagSpecs(target.constructor);
105
+ for (const spec of specs) {
106
+ const original = spec.original;
107
+ target[spec.propertyKey] = async (c, ...rest) => {
108
+ const ctx = spec.contextFn ? spec.contextFn(c) : undefined;
109
+ const enabled = await this.isEnabled(spec.flagName, ctx);
110
+ if (!enabled) {
111
+ if (spec.onDisabled)
112
+ return spec.onDisabled(c);
113
+ return c.json({ message: "Feature not available", code: "FEATURE_DISABLED" }, 404);
114
+ }
115
+ return original.apply(target, [c, ...rest]);
116
+ };
117
+ }
118
+ }
119
+ }
120
+ FeatureFlagService = __legacyDecorateClassTS([
121
+ Injectable(),
122
+ __legacyDecorateParamTS(0, Inject("FEATURE_FLAG_CONFIG")),
123
+ __legacyMetadataTS("design:paramtypes", [
124
+ typeof FeatureFlagConfig === "undefined" ? Object : FeatureFlagConfig
125
+ ])
126
+ ], FeatureFlagService);
127
+
128
+ // packages/feature-flag/src/feature-flag.module.ts
129
+ class FeatureFlagModule {
130
+ static forRoot(config = {}) {
131
+ class ConfiguredFeatureFlagModule {
132
+ }
133
+ ConfiguredFeatureFlagModule = __legacyDecorateClassTS([
134
+ Module({
135
+ providers: [
136
+ FeatureFlagService,
137
+ { provide: FeatureFlagService.TOKEN, useExisting: FeatureFlagService },
138
+ { provide: "FEATURE_FLAG_CONFIG", useValue: config }
139
+ ],
140
+ exports: [FeatureFlagService, FeatureFlagService.TOKEN]
141
+ })
142
+ ], ConfiguredFeatureFlagModule);
143
+ Object.defineProperty(ConfiguredFeatureFlagModule, "name", {
144
+ value: "ConfiguredFeatureFlagModule"
145
+ });
146
+ return ConfiguredFeatureFlagModule;
147
+ }
148
+ }
149
+ FeatureFlagModule = __legacyDecorateClassTS([
150
+ Module({
151
+ providers: [
152
+ FeatureFlagService,
153
+ { provide: FeatureFlagService.TOKEN, useExisting: FeatureFlagService }
154
+ ],
155
+ exports: [FeatureFlagService, FeatureFlagService.TOKEN]
156
+ })
157
+ ], FeatureFlagModule);
158
+ export {
159
+ getFlagSpecs,
160
+ MemoryFlagBackend,
161
+ FeatureFlagService,
162
+ FeatureFlagModule,
163
+ FeatureFlag
164
+ };
165
+
166
+ //# debugId=4B9056D725A21D8664756E2164756E21
167
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/feature-flag.module.ts", "../src/feature-flag.service.ts", "../src/backends/memory.ts", "../src/decorators/feature-flag.decorator.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * `FeatureFlagModule` — drop-in feature flags.\n *\n * @Module({\n * imports: [\n * FeatureFlagModule.forRoot({\n * flags: {\n * 'new-dashboard': { enabled: true, rollout: 0.5 },\n * 'beta-api': false,\n * },\n * }),\n * ],\n * })\n * export class AppModule {}\n */\nimport \"reflect-metadata\";\nimport { Module } from \"@nexusts/core\";\nimport { FeatureFlagService } from \"./feature-flag.service.js\";\nimport type { FeatureFlagConfig } from \"./types.js\";\n\n@Module({\n\tproviders: [\n\t\tFeatureFlagService,\n\t\t{ provide: FeatureFlagService.TOKEN, useExisting: FeatureFlagService },\n\t],\n\texports: [FeatureFlagService, FeatureFlagService.TOKEN],\n})\nexport class FeatureFlagModule {\n\tstatic forRoot(config: FeatureFlagConfig = {}) {\n\t\t@Module({\n\t\t\tproviders: [\n\t\t\t\tFeatureFlagService,\n\t\t\t\t{ provide: FeatureFlagService.TOKEN, useExisting: FeatureFlagService },\n\t\t\t\t{ provide: \"FEATURE_FLAG_CONFIG\", useValue: config },\n\t\t\t],\n\t\t\texports: [FeatureFlagService, FeatureFlagService.TOKEN],\n\t\t})\n\t\tclass ConfiguredFeatureFlagModule {}\n\t\tObject.defineProperty(ConfiguredFeatureFlagModule, \"name\", {\n\t\t\tvalue: \"ConfiguredFeatureFlagModule\",\n\t\t});\n\t\treturn ConfiguredFeatureFlagModule;\n\t}\n}\n",
6
+ "/**\n * `FeatureFlagService` — isEnabled / setFlag / getFlag + decorator wiring.\n *\n * const flags = new FeatureFlagService({\n * flags: {\n * 'new-ui': { enabled: true, rollout: 0.5 },\n * 'beta-api': { enabled: false },\n * },\n * });\n *\n * await flags.isEnabled('new-ui', { userId: 'u1' }); // true or false by hash\n */\nimport { Inject, Injectable } from \"@nexusts/core\";\nimport type { FlagContext, FlagDefinition, FeatureFlagBackend, FeatureFlagConfig } from \"./types.js\";\nimport { MemoryFlagBackend } from \"./backends/memory.js\";\nimport { getFlagSpecs } from \"./decorators/feature-flag.decorator.js\";\n\n@Injectable()\nexport class FeatureFlagService {\n\t/** DI token. */\n\tstatic readonly TOKEN = Symbol.for(\"nexus:FeatureFlagService\");\n\n\t#backend: FeatureFlagBackend;\n\n\tconstructor(@Inject(\"FEATURE_FLAG_CONFIG\") config: FeatureFlagConfig = {}) {\n\t\tthis.#backend = new MemoryFlagBackend(config.flags ?? {});\n\t}\n\n\t/** Returns `true` if the flag is enabled for the given context. */\n\tasync isEnabled(flagName: string, context?: FlagContext): Promise<boolean> {\n\t\treturn this.#backend.isEnabled(flagName, context);\n\t}\n\n\t/** Create or update a flag at runtime. */\n\tsetFlag(flagName: string, definition: FlagDefinition | boolean): void {\n\t\tthis.#backend.setFlag(flagName, definition);\n\t}\n\n\t/** Return the raw definition (or `undefined` if unknown). */\n\tgetFlag(flagName: string): FlagDefinition | undefined {\n\t\treturn this.#backend.getFlag(flagName);\n\t}\n\n\t/**\n\t * Wire `@FeatureFlag` decorators onto a controller or service instance.\n\t * Each decorated route handler is wrapped so it returns a 404 JSON\n\t * response when the flag is disabled.\n\t *\n\t * The DI container calls this automatically when FeatureFlagModule is\n\t * imported; you can also call it manually in tests.\n\t */\n\tapplyDecorators(target: any): void {\n\t\tconst specs = getFlagSpecs(target.constructor);\n\t\tfor (const spec of specs) {\n\t\t\tconst original = spec.original;\n\t\t\t(target as any)[spec.propertyKey] = async (c: any, ...rest: any[]) => {\n\t\t\t\tconst ctx = spec.contextFn ? spec.contextFn(c) : undefined;\n\t\t\t\tconst enabled = await this.isEnabled(spec.flagName, ctx);\n\t\t\t\tif (!enabled) {\n\t\t\t\t\tif (spec.onDisabled) return spec.onDisabled(c);\n\t\t\t\t\treturn c.json({ message: \"Feature not available\", code: \"FEATURE_DISABLED\" }, 404);\n\t\t\t\t}\n\t\t\t\treturn original.apply(target, [c, ...rest]);\n\t\t\t};\n\t\t}\n\t}\n}\n",
7
+ "import type { FlagContext, FlagDefinition, FeatureFlagBackend } from \"../types.js\";\n\n/** djb2 hash → deterministic 0-1 float for rollout bucketing. */\nfunction hashFloat(s: string): number {\n\tlet h = 5381;\n\tfor (let i = 0; i < s.length; i++) {\n\t\th = Math.imul(h, 33) ^ s.charCodeAt(i);\n\t}\n\treturn (h >>> 0) / 0xffffffff;\n}\n\n/** In-process feature flag backend — no external dependencies. */\nexport class MemoryFlagBackend implements FeatureFlagBackend {\n\t#flags = new Map<string, FlagDefinition>();\n\n\tconstructor(initial: Record<string, FlagDefinition | boolean> = {}) {\n\t\tfor (const [k, v] of Object.entries(initial)) {\n\t\t\tthis.#flags.set(k, typeof v === \"boolean\" ? { enabled: v } : v);\n\t\t}\n\t}\n\n\tsetFlag(flagName: string, definition: FlagDefinition | boolean): void {\n\t\tthis.#flags.set(\n\t\t\tflagName,\n\t\t\ttypeof definition === \"boolean\" ? { enabled: definition } : definition,\n\t\t);\n\t}\n\n\tgetFlag(flagName: string): FlagDefinition | undefined {\n\t\treturn this.#flags.get(flagName);\n\t}\n\n\tasync isEnabled(flagName: string, context?: FlagContext): Promise<boolean> {\n\t\tconst def = this.#flags.get(flagName);\n\t\tif (!def) return false;\n\n\t\tconst id = context?.userId ?? context?.tenantId ?? context?.key ?? \"\";\n\n\t\t// Denylist has highest priority\n\t\tif (id && def.denylist?.includes(id)) return false;\n\n\t\t// Allowlist always wins after denylist\n\t\tif (id && def.allowlist?.includes(id)) return true;\n\n\t\t// Base enabled gate\n\t\tif (def.enabled === false) return false;\n\n\t\t// Rollout: deterministic hash bucketing\n\t\tif (def.rollout !== undefined && def.rollout < 1) {\n\t\t\tif (!id) return def.rollout > 0;\n\t\t\treturn hashFloat(`${flagName}:${id}`) < def.rollout;\n\t\t}\n\n\t\treturn def.enabled !== false;\n\t}\n}\n",
8
+ "import \"reflect-metadata\";\nimport type { FlagContext } from \"../types.js\";\n\nconst FLAG_META = Symbol.for(\"nexus:FeatureFlag\");\n\nexport interface FeatureFlagOptions {\n\t/** Extract a `FlagContext` from the Hono `Context` object (first arg of the handler). */\n\tcontextFn?: (c: any) => FlagContext;\n\t/** Custom response when the flag is disabled. Defaults to 404 JSON. */\n\tonDisabled?: (c: any) => Response | Promise<Response>;\n}\n\nexport interface FlagSpec {\n\tpropertyKey: string | symbol;\n\tflagName: string;\n\tcontextFn?: (c: any) => FlagContext;\n\tonDisabled?: (c: any) => Response | Promise<Response>;\n\toriginal: (...args: any[]) => any;\n}\n\n/**\n * Mark a route handler so `FeatureFlagService.applyDecorators()` will gate it.\n *\n * @Get('/')\n * @FeatureFlag('new-dashboard')\n * async index(c: Context) { ... }\n *\n * When the flag is disabled the handler returns a 404 JSON response.\n * Pass `onDisabled` to customise the response, or `contextFn` to extract\n * a `FlagContext` (userId etc.) from the Hono `Context`.\n */\nexport function FeatureFlag(\n\tflagName: string,\n\toptions: FeatureFlagOptions = {},\n): MethodDecorator {\n\treturn (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {\n\t\tconst specs: FlagSpec[] = Reflect.getMetadata(FLAG_META, target.constructor) ?? [];\n\t\tspecs.push({\n\t\t\tpropertyKey,\n\t\t\tflagName,\n\t\t\tcontextFn: options.contextFn,\n\t\t\tonDisabled: options.onDisabled,\n\t\t\toriginal: descriptor.value,\n\t\t});\n\t\tReflect.defineMetadata(FLAG_META, specs, target.constructor);\n\t};\n}\n\nexport function getFlagSpecs(target: any): FlagSpec[] {\n\treturn Reflect.getMetadata(FLAG_META, target) ?? [];\n}\n"
9
+ ],
10
+ "mappings": "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",
11
+ "debugId": "4B9056D725A21D8664756E2164756E21",
12
+ "names": []
13
+ }
@@ -0,0 +1,35 @@
1
+ /** Targeting context passed to `isEnabled()`. */
2
+ export interface FlagContext {
3
+ /** User identifier — used for allowlist/denylist and rollout hash. */
4
+ userId?: string;
5
+ /** Tenant identifier — fallback when userId is absent. */
6
+ tenantId?: string;
7
+ /** Arbitrary key used as the rollout hash seed when userId/tenantId are absent. */
8
+ key?: string;
9
+ /** Additional attributes available to custom backends. */
10
+ attributes?: Record<string, unknown>;
11
+ }
12
+ /** Per-flag definition stored in the backend. */
13
+ export interface FlagDefinition {
14
+ /** Whether the flag is on. Defaults to `false`. */
15
+ enabled?: boolean;
16
+ /** Fractional rollout: 0 = nobody, 1 = everybody, 0.5 = 50% of traffic. */
17
+ rollout?: number;
18
+ /** User/tenant IDs that are always enabled regardless of `rollout`. */
19
+ allowlist?: string[];
20
+ /** User/tenant IDs that are always disabled regardless of other rules. */
21
+ denylist?: string[];
22
+ }
23
+ /** Config passed to `FeatureFlagModule.forRoot()`. */
24
+ export interface FeatureFlagConfig {
25
+ /** Storage backend. Default: `'memory'`. */
26
+ backend?: "memory";
27
+ /** Initial flag definitions. Values may be `true/false` shorthand or full `FlagDefinition`. */
28
+ flags?: Record<string, FlagDefinition | boolean>;
29
+ }
30
+ /** Interface that any backend must implement. */
31
+ export interface FeatureFlagBackend {
32
+ isEnabled(flagName: string, context?: FlagContext): Promise<boolean>;
33
+ setFlag(flagName: string, definition: FlagDefinition | boolean): void;
34
+ getFlag(flagName: string): FlagDefinition | undefined;
35
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@nexusts/feature-flag",
3
+ "version": "0.8.0",
4
+ "description": "Feature flags, canary deployments, and A/B testing for NexusTS",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun run ../../build.ts"
21
+ },
22
+ "keywords": [
23
+ "nexusts",
24
+ "framework",
25
+ "bun",
26
+ "feature-flag",
27
+ "ab-testing",
28
+ "canary"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@nexusts/core": "file:../core"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/nexus-ts/nexusts.git",
37
+ "directory": "packages/feature-flag"
38
+ },
39
+ "homepage": "https://github.com/nexus-ts/nexusts#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/nexus-ts/nexusts/issues"
42
+ }
43
+ }