@velajs/feature-flags 0.1.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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/decorators/feature-flag.decorator.d.ts +35 -0
- package/dist/decorators/feature-flag.decorator.js +26 -0
- package/dist/drivers/driver.d.ts +21 -0
- package/dist/drivers/driver.js +13 -0
- package/dist/drivers/memory.driver.d.ts +35 -0
- package/dist/drivers/memory.driver.js +50 -0
- package/dist/drivers/registry.d.ts +24 -0
- package/dist/drivers/registry.js +51 -0
- package/dist/feature-flags.error.d.ts +14 -0
- package/dist/feature-flags.error.js +15 -0
- package/dist/feature-flags.module.d.ts +21 -0
- package/dist/feature-flags.module.js +63 -0
- package/dist/feature-flags.service.d.ts +83 -0
- package/dist/feature-flags.service.js +208 -0
- package/dist/feature-flags.tokens.d.ts +16 -0
- package/dist/feature-flags.tokens.js +10 -0
- package/dist/feature-flags.types.d.ts +71 -0
- package/dist/feature-flags.types.js +3 -0
- package/dist/guards/feature-flag.guard.d.ts +24 -0
- package/dist/guards/feature-flag.guard.js +52 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +15 -0
- package/dist/testing/index.d.ts +25 -0
- package/dist/testing/index.js +36 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- Driver-based feature flags for Vela, authored purely on the public API
|
|
8
|
+
(`defineModule`, `Reflector`, guards) — edge-pure, no `node:*`/`Buffer`/`process`.
|
|
9
|
+
- `FeatureFlagDriver` contract + in-package `MemoryFlagDriver` (default driver and test fake).
|
|
10
|
+
- `FeatureFlagsService`: manifest defaults, never-throw evaluation (`getBoolean/String/Number/Object`
|
|
11
|
+
Value/Details), immutable `use(driver)`, `all()`, and a per-request `context` merge.
|
|
12
|
+
- `FeatureFlagsModule` (`defineModule`, `lazy: true`) with `forRoot`/`forRootAsync` and an
|
|
13
|
+
`isGlobal` app-wide guard option.
|
|
14
|
+
- `@FeatureFlag(key, { onDisabled })` + `FeatureFlagGuard` to hide routes (404) or forbid them (403).
|
|
15
|
+
- `@velajs/feature-flags/testing` subpath: memory driver helpers + `createTestFeatureFlags()`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kauan Guesser
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @velajs/feature-flags
|
|
2
|
+
|
|
3
|
+
Edge-first, driver-based feature flags for the [Vela](https://github.com/velajs/vela) framework.
|
|
4
|
+
|
|
5
|
+
Runs on Cloudflare Workers, Deno, Bun, Node 20+, and Vercel Edge — the package source is
|
|
6
|
+
edge-pure (no `node:*`, `Buffer`, or `process`). Backends plug in through one small
|
|
7
|
+
`FeatureFlagDriver` contract; an in-memory driver ships in the box and doubles as the test fake.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @velajs/feature-flags
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { FeatureFlagsModule, FeatureFlagsService, memoryFlagDriver } from '@velajs/feature-flags';
|
|
19
|
+
|
|
20
|
+
@Module({
|
|
21
|
+
imports: [
|
|
22
|
+
FeatureFlagsModule.forRoot({
|
|
23
|
+
drivers: [memoryFlagDriver({ values: { 'new-checkout': false } })],
|
|
24
|
+
manifest: { 'new-checkout': false, layout: 'v1' },
|
|
25
|
+
context: (ctx) => ({ userId: ctx.get('userId') }), // merged into every evaluation
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
})
|
|
29
|
+
class AppModule {}
|
|
30
|
+
|
|
31
|
+
// Inject and evaluate — evaluation never throws.
|
|
32
|
+
class CheckoutService {
|
|
33
|
+
constructor(@Inject(FEATURE_FLAG_TOKENS.Service) private readonly flags: FeatureFlagsService) {}
|
|
34
|
+
|
|
35
|
+
async run() {
|
|
36
|
+
if (await this.flags.getBooleanValue('new-checkout')) { /* … */ }
|
|
37
|
+
const layout = await this.flags.getStringValue('layout'); // manifest default when unset
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`forRootAsync({ inject, useFactory })` is also generated for you.
|
|
43
|
+
|
|
44
|
+
## Hiding routes behind a flag
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { FeatureFlag, FeatureFlagGuard } from '@velajs/feature-flags';
|
|
48
|
+
|
|
49
|
+
@UseGuards(FeatureFlagGuard)
|
|
50
|
+
@Controller('/checkout')
|
|
51
|
+
class CheckoutController {
|
|
52
|
+
@FeatureFlag('new-checkout') // 404 when off (route looks hidden)
|
|
53
|
+
@Get('/v2') v2() { /* … */ }
|
|
54
|
+
|
|
55
|
+
@FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
56
|
+
@Get('/beta') beta() { /* … */ }
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Or gate every `@FeatureFlag()` route app-wide with `FeatureFlagsModule.forRoot({ isGlobal: true })`.
|
|
61
|
+
|
|
62
|
+
## Drivers
|
|
63
|
+
|
|
64
|
+
The in-memory driver ships here; runtime-specific drivers live in their platform packages
|
|
65
|
+
(`@velajs/cloudflare` ships Flagship-binding and KV drivers), all implementing the same contract:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export interface FeatureFlagDriver {
|
|
69
|
+
readonly name: string;
|
|
70
|
+
getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
|
|
71
|
+
getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
|
|
72
|
+
getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
|
|
73
|
+
getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A driver returns the caller's `fallback` (never throws) when it can't resolve a key; the service
|
|
78
|
+
additionally absorbs any thrown error into the same fallback and logs a warning.
|
|
79
|
+
|
|
80
|
+
## Typed flag keys
|
|
81
|
+
|
|
82
|
+
Augment `FeatureFlagRegistry` to type the service methods and the `@FeatureFlag()` decorator:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
declare module '@velajs/feature-flags' {
|
|
86
|
+
interface FeatureFlagRegistry {
|
|
87
|
+
'new-checkout': boolean;
|
|
88
|
+
layout: string;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Testing
|
|
94
|
+
|
|
95
|
+
`@velajs/feature-flags/testing` re-exports the memory driver (the fake) plus a zero-DI helper:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { createTestFeatureFlags } from '@velajs/feature-flags/testing';
|
|
99
|
+
|
|
100
|
+
const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
|
|
101
|
+
expect(await service.getBooleanValue('new-checkout')).toBe(true);
|
|
102
|
+
driver.set('new-checkout', false);
|
|
103
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FlagKey } from '../feature-flags.types';
|
|
2
|
+
/** What the {@link FeatureFlagGuard} does when a gated flag is off. */
|
|
3
|
+
export type FeatureFlagDisabledBehavior = 'notFound' | 'forbidden';
|
|
4
|
+
export interface FeatureFlagOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Response when the flag is off. `'notFound'` (default) hides the route
|
|
7
|
+
* entirely (404); `'forbidden'` reveals it exists but denies access (403).
|
|
8
|
+
*/
|
|
9
|
+
onDisabled?: FeatureFlagDisabledBehavior;
|
|
10
|
+
}
|
|
11
|
+
/** The metadata `@FeatureFlag()` attaches, read by {@link FeatureFlagGuard}. */
|
|
12
|
+
export interface FeatureFlagMetadata {
|
|
13
|
+
key: string;
|
|
14
|
+
onDisabled: FeatureFlagDisabledBehavior;
|
|
15
|
+
}
|
|
16
|
+
export declare const FEATURE_FLAG_METADATA = "vela:feature-flags:flag";
|
|
17
|
+
/**
|
|
18
|
+
* Gate a route (handler) or controller behind a boolean feature flag. Pair
|
|
19
|
+
* with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
|
|
20
|
+
* app-wide registration).
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* @UseGuards(FeatureFlagGuard)
|
|
25
|
+
* @Controller('/checkout')
|
|
26
|
+
* class CheckoutController {
|
|
27
|
+
* @FeatureFlag('new-checkout') // 404 when off
|
|
28
|
+
* @Get('/v2') v2() { ... }
|
|
29
|
+
*
|
|
30
|
+
* @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
31
|
+
* @Get('/beta') beta() { ... }
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function FeatureFlag(key: FlagKey, options?: FeatureFlagOptions): (target: object, propertyKey?: string | symbol) => void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SetMetadata } from "@velajs/vela";
|
|
2
|
+
export const FEATURE_FLAG_METADATA = 'vela:feature-flags:flag';
|
|
3
|
+
/**
|
|
4
|
+
* Gate a route (handler) or controller behind a boolean feature flag. Pair
|
|
5
|
+
* with {@link FeatureFlagGuard} (via `@UseGuards` or the module's `isGlobal`
|
|
6
|
+
* app-wide registration).
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* @UseGuards(FeatureFlagGuard)
|
|
11
|
+
* @Controller('/checkout')
|
|
12
|
+
* class CheckoutController {
|
|
13
|
+
* @FeatureFlag('new-checkout') // 404 when off
|
|
14
|
+
* @Get('/v2') v2() { ... }
|
|
15
|
+
*
|
|
16
|
+
* @FeatureFlag('beta', { onDisabled: 'forbidden' }) // 403 when off
|
|
17
|
+
* @Get('/beta') beta() { ... }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/ export function FeatureFlag(key, options = {}) {
|
|
21
|
+
const meta = {
|
|
22
|
+
key,
|
|
23
|
+
onDisabled: options.onDisabled ?? 'notFound'
|
|
24
|
+
};
|
|
25
|
+
return SetMetadata(FEATURE_FLAG_METADATA, meta);
|
|
26
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FlagContext } from '../feature-flags.types';
|
|
2
|
+
/**
|
|
3
|
+
* The cross-package contract every feature-flag backend implements.
|
|
4
|
+
*
|
|
5
|
+
* This is the seam that platform packages plug into: `@velajs/cloudflare`
|
|
6
|
+
* ships a Flagship-binding driver and a KV-backed driver against this exact
|
|
7
|
+
* interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
|
|
8
|
+
* maps a key + fallback (+ optional targeting context) onto a typed value and
|
|
9
|
+
* MUST return the `fallback` — never throw — when it cannot resolve the key.
|
|
10
|
+
*
|
|
11
|
+
* Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
|
|
12
|
+
* `FeatureFlagsService` around these four value methods; drivers may
|
|
13
|
+
* optionally override that synthesis, but the value methods are the contract.
|
|
14
|
+
*/
|
|
15
|
+
export interface FeatureFlagDriver {
|
|
16
|
+
readonly name: string;
|
|
17
|
+
getBoolean(key: string, fallback: boolean, ctx?: FlagContext): Promise<boolean>;
|
|
18
|
+
getString(key: string, fallback: string, ctx?: FlagContext): Promise<string>;
|
|
19
|
+
getNumber(key: string, fallback: number, ctx?: FlagContext): Promise<number>;
|
|
20
|
+
getObject<T extends object>(key: string, fallback: T, ctx?: FlagContext): Promise<T>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cross-package contract every feature-flag backend implements.
|
|
3
|
+
*
|
|
4
|
+
* This is the seam that platform packages plug into: `@velajs/cloudflare`
|
|
5
|
+
* ships a Flagship-binding driver and a KV-backed driver against this exact
|
|
6
|
+
* interface, `@velajs/feature-flags` ships {@link MemoryFlagDriver}. A driver
|
|
7
|
+
* maps a key + fallback (+ optional targeting context) onto a typed value and
|
|
8
|
+
* MUST return the `fallback` — never throw — when it cannot resolve the key.
|
|
9
|
+
*
|
|
10
|
+
* Evaluation *details* (`FlagEvaluationDetails`) are synthesized by
|
|
11
|
+
* `FeatureFlagsService` around these four value methods; drivers may
|
|
12
|
+
* optionally override that synthesis, but the value methods are the contract.
|
|
13
|
+
*/ export { };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FeatureFlagDriver } from './driver';
|
|
2
|
+
import type { FlagContext, FlagManifest, FlagValue } from '../feature-flags.types';
|
|
3
|
+
export interface MemoryFlagDriverOptions {
|
|
4
|
+
/** Driver name used for `use(name)` / the default-driver selection. Default `"memory"`. */
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Initial flag values. */
|
|
7
|
+
values?: FlagManifest;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
|
|
11
|
+
* and the test fake in one. Reads from an internal `Map` seeded from options
|
|
12
|
+
* or mutated with {@link set}/{@link reset} — the memory driver ignores the
|
|
13
|
+
* evaluation context (it does no targeting). An unknown key returns the
|
|
14
|
+
* caller's `fallback`, exactly like a remote driver that can't resolve it.
|
|
15
|
+
*/
|
|
16
|
+
export declare class MemoryFlagDriver implements FeatureFlagDriver {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
private readonly store;
|
|
19
|
+
constructor(options?: MemoryFlagDriverOptions);
|
|
20
|
+
/** Set a flag value. Chainable. */
|
|
21
|
+
set(key: string, value: FlagValue): this;
|
|
22
|
+
/** Remove a flag (subsequent reads return the caller's fallback). Chainable. */
|
|
23
|
+
delete(key: string): this;
|
|
24
|
+
/** True when `key` has a stored value. */
|
|
25
|
+
has(key: string): boolean;
|
|
26
|
+
/** Clear all flags, then optionally seed a fresh set. Chainable. */
|
|
27
|
+
reset(values?: FlagManifest): this;
|
|
28
|
+
getBoolean(key: string, fallback: boolean, _ctx?: FlagContext): Promise<boolean>;
|
|
29
|
+
getString(key: string, fallback: string, _ctx?: FlagContext): Promise<string>;
|
|
30
|
+
getNumber(key: string, fallback: number, _ctx?: FlagContext): Promise<number>;
|
|
31
|
+
getObject<T extends object>(key: string, fallback: T, _ctx?: FlagContext): Promise<T>;
|
|
32
|
+
private read;
|
|
33
|
+
}
|
|
34
|
+
/** Convenience factory for {@link MemoryFlagDriver}. */
|
|
35
|
+
export declare function memoryFlagDriver(options?: MemoryFlagDriverOptions): MemoryFlagDriver;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory {@link FeatureFlagDriver}: the default driver shipped in-package
|
|
3
|
+
* and the test fake in one. Reads from an internal `Map` seeded from options
|
|
4
|
+
* or mutated with {@link set}/{@link reset} — the memory driver ignores the
|
|
5
|
+
* evaluation context (it does no targeting). An unknown key returns the
|
|
6
|
+
* caller's `fallback`, exactly like a remote driver that can't resolve it.
|
|
7
|
+
*/ export class MemoryFlagDriver {
|
|
8
|
+
name;
|
|
9
|
+
store;
|
|
10
|
+
constructor(options = {}){
|
|
11
|
+
this.name = options.name ?? 'memory';
|
|
12
|
+
this.store = new Map(Object.entries(options.values ?? {}));
|
|
13
|
+
}
|
|
14
|
+
/** Set a flag value. Chainable. */ set(key, value) {
|
|
15
|
+
this.store.set(key, value);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
/** Remove a flag (subsequent reads return the caller's fallback). Chainable. */ delete(key) {
|
|
19
|
+
this.store.delete(key);
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
/** True when `key` has a stored value. */ has(key) {
|
|
23
|
+
return this.store.has(key);
|
|
24
|
+
}
|
|
25
|
+
/** Clear all flags, then optionally seed a fresh set. Chainable. */ reset(values) {
|
|
26
|
+
this.store.clear();
|
|
27
|
+
if (values) {
|
|
28
|
+
for (const [key, value] of Object.entries(values))this.store.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
getBoolean(key, fallback, _ctx) {
|
|
33
|
+
return Promise.resolve(this.read(key, fallback));
|
|
34
|
+
}
|
|
35
|
+
getString(key, fallback, _ctx) {
|
|
36
|
+
return Promise.resolve(this.read(key, fallback));
|
|
37
|
+
}
|
|
38
|
+
getNumber(key, fallback, _ctx) {
|
|
39
|
+
return Promise.resolve(this.read(key, fallback));
|
|
40
|
+
}
|
|
41
|
+
getObject(key, fallback, _ctx) {
|
|
42
|
+
return Promise.resolve(this.read(key, fallback));
|
|
43
|
+
}
|
|
44
|
+
read(key, fallback) {
|
|
45
|
+
return this.store.has(key) ? this.store.get(key) : fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Convenience factory for {@link MemoryFlagDriver}. */ export function memoryFlagDriver(options) {
|
|
49
|
+
return new MemoryFlagDriver(options);
|
|
50
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { FeatureFlagsOptions } from '../feature-flags.types';
|
|
2
|
+
import type { FeatureFlagDriver } from './driver';
|
|
3
|
+
/**
|
|
4
|
+
* The set of registered {@link FeatureFlagDriver}s plus the default selection.
|
|
5
|
+
* Built once per module instance ({@link buildDriverRegistry}) and injected
|
|
6
|
+
* into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
|
|
7
|
+
*/
|
|
8
|
+
export declare class FeatureFlagDriverRegistry {
|
|
9
|
+
private readonly drivers;
|
|
10
|
+
readonly defaultName: string;
|
|
11
|
+
constructor(drivers: readonly FeatureFlagDriver[], defaultName?: string);
|
|
12
|
+
/** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */
|
|
13
|
+
get(name: string): FeatureFlagDriver;
|
|
14
|
+
/** Resolve a named driver, or the default when `name` is omitted. */
|
|
15
|
+
resolve(name?: string): FeatureFlagDriver;
|
|
16
|
+
/** All registered driver names. */
|
|
17
|
+
names(): string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the driver registry from module options. Falls back to a single
|
|
21
|
+
* in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
|
|
22
|
+
* yields a working (all-defaults) flags service for local development.
|
|
23
|
+
*/
|
|
24
|
+
export declare function buildDriverRegistry(options: FeatureFlagsOptions): FeatureFlagDriverRegistry;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { FeatureFlagError } from "../feature-flags.error.js";
|
|
2
|
+
import { MemoryFlagDriver } from "./memory.driver.js";
|
|
3
|
+
/**
|
|
4
|
+
* The set of registered {@link FeatureFlagDriver}s plus the default selection.
|
|
5
|
+
* Built once per module instance ({@link buildDriverRegistry}) and injected
|
|
6
|
+
* into {@link FeatureFlagsService}; `use(name)` looks a driver up here.
|
|
7
|
+
*/ export class FeatureFlagDriverRegistry {
|
|
8
|
+
drivers = new Map();
|
|
9
|
+
defaultName;
|
|
10
|
+
constructor(drivers, defaultName){
|
|
11
|
+
if (drivers.length === 0) {
|
|
12
|
+
throw new FeatureFlagError('No feature flag drivers registered. Provide at least one driver to FeatureFlagsModule.forRoot({ drivers: [...] }).');
|
|
13
|
+
}
|
|
14
|
+
for (const driver of drivers){
|
|
15
|
+
if (this.drivers.has(driver.name)) {
|
|
16
|
+
throw new FeatureFlagError(`Duplicate feature flag driver "${driver.name}".`);
|
|
17
|
+
}
|
|
18
|
+
this.drivers.set(driver.name, driver);
|
|
19
|
+
}
|
|
20
|
+
const requested = defaultName ?? drivers[0].name;
|
|
21
|
+
if (!this.drivers.has(requested)) {
|
|
22
|
+
throw new FeatureFlagError(`Default feature flag driver "${requested}" is not registered.`);
|
|
23
|
+
}
|
|
24
|
+
this.defaultName = requested;
|
|
25
|
+
}
|
|
26
|
+
/** Look a driver up by name; throws {@link FeatureFlagError} when unknown. */ get(name) {
|
|
27
|
+
const driver = this.drivers.get(name);
|
|
28
|
+
if (!driver) {
|
|
29
|
+
throw new FeatureFlagError(`Feature flag driver "${name}" is not registered.`);
|
|
30
|
+
}
|
|
31
|
+
return driver;
|
|
32
|
+
}
|
|
33
|
+
/** Resolve a named driver, or the default when `name` is omitted. */ resolve(name) {
|
|
34
|
+
return this.get(name ?? this.defaultName);
|
|
35
|
+
}
|
|
36
|
+
/** All registered driver names. */ names() {
|
|
37
|
+
return [
|
|
38
|
+
...this.drivers.keys()
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the driver registry from module options. Falls back to a single
|
|
44
|
+
* in-memory driver when none are configured, so `FeatureFlagsModule.forRoot({})`
|
|
45
|
+
* yields a working (all-defaults) flags service for local development.
|
|
46
|
+
*/ export function buildDriverRegistry(options) {
|
|
47
|
+
const drivers = options.drivers && options.drivers.length > 0 ? options.drivers : [
|
|
48
|
+
new MemoryFlagDriver()
|
|
49
|
+
];
|
|
50
|
+
return new FeatureFlagDriverRegistry(drivers, options.default);
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single error type for `@velajs/feature-flags`.
|
|
3
|
+
*
|
|
4
|
+
* Note: flag *evaluation* never throws — the service absorbs driver failures
|
|
5
|
+
* into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
|
|
6
|
+
* is reserved for *configuration* faults surfaced at wiring time: an unknown
|
|
7
|
+
* driver name, a duplicate driver, or an empty driver set.
|
|
8
|
+
*/
|
|
9
|
+
export declare class FeatureFlagError extends Error {
|
|
10
|
+
name: string;
|
|
11
|
+
constructor(message: string, options?: {
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single error type for `@velajs/feature-flags`.
|
|
3
|
+
*
|
|
4
|
+
* Note: flag *evaluation* never throws — the service absorbs driver failures
|
|
5
|
+
* into the fallback value (see {@link FeatureFlagsService}). `FeatureFlagError`
|
|
6
|
+
* is reserved for *configuration* faults surfaced at wiring time: an unknown
|
|
7
|
+
* driver name, a duplicate driver, or an empty driver set.
|
|
8
|
+
*/ export class FeatureFlagError extends Error {
|
|
9
|
+
name = 'FeatureFlagError';
|
|
10
|
+
constructor(message, options){
|
|
11
|
+
super(message, options?.cause !== undefined ? {
|
|
12
|
+
cause: options.cause
|
|
13
|
+
} : undefined);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FeatureFlagsOptions } from './feature-flags.types';
|
|
2
|
+
/**
|
|
3
|
+
* The feature-flags module. Authored on vela's public `defineModule`, so
|
|
4
|
+
* `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
|
|
5
|
+
* matching `forRootAsync({ inject, useFactory, ... })` come for free.
|
|
6
|
+
*
|
|
7
|
+
* `lazy: true` is valid here: every provider is sync-constructible (a factory
|
|
8
|
+
* for the driver registry, a sync-constructor service, a guard) and there are
|
|
9
|
+
* no async lifecycle hooks — so the module defers to first use without
|
|
10
|
+
* violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
|
|
11
|
+
*
|
|
12
|
+
* `isGlobal: true` makes the module globally visible AND registers
|
|
13
|
+
* {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
|
|
14
|
+
* route is gated without a per-controller `@UseGuards`.
|
|
15
|
+
*/
|
|
16
|
+
declare const ConfigurableModuleClass: import("@velajs/vela").ConfigurableModuleClassType<FeatureFlagsOptions, "forRoot", "create", {
|
|
17
|
+
isGlobal?: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
export declare class FeatureFlagsModule extends ConfigurableModuleClass {
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { APP_GUARD, Scope, defineModule } from "@velajs/vela";
|
|
2
|
+
import { buildDriverRegistry } from "./drivers/registry.js";
|
|
3
|
+
import { FeatureFlagGuard } from "./guards/feature-flag.guard.js";
|
|
4
|
+
import { FeatureFlagsService } from "./feature-flags.service.js";
|
|
5
|
+
import { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
|
|
6
|
+
/**
|
|
7
|
+
* The feature-flags module. Authored on vela's public `defineModule`, so
|
|
8
|
+
* `forRoot({ drivers, default?, manifest?, context?, isGlobal? })` and the
|
|
9
|
+
* matching `forRootAsync({ inject, useFactory, ... })` come for free.
|
|
10
|
+
*
|
|
11
|
+
* `lazy: true` is valid here: every provider is sync-constructible (a factory
|
|
12
|
+
* for the driver registry, a sync-constructor service, a guard) and there are
|
|
13
|
+
* no async lifecycle hooks — so the module defers to first use without
|
|
14
|
+
* violating the sync-seam rule (see vela `MODULE_AUTHORING.md`).
|
|
15
|
+
*
|
|
16
|
+
* `isGlobal: true` makes the module globally visible AND registers
|
|
17
|
+
* {@link FeatureFlagGuard} app-wide (`APP_GUARD`) so every `@FeatureFlag()`
|
|
18
|
+
* route is gated without a per-controller `@UseGuards`.
|
|
19
|
+
*/ const { ConfigurableModuleClass } = defineModule({
|
|
20
|
+
name: 'FeatureFlags',
|
|
21
|
+
optionsToken: FEATURE_FLAG_TOKENS.Options,
|
|
22
|
+
lazy: true,
|
|
23
|
+
transform: (definition, extras)=>extras.isGlobal ? {
|
|
24
|
+
...definition,
|
|
25
|
+
global: true,
|
|
26
|
+
providers: [
|
|
27
|
+
...definition.providers ?? [],
|
|
28
|
+
{
|
|
29
|
+
provide: APP_GUARD,
|
|
30
|
+
useExisting: FeatureFlagGuard
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
} : definition,
|
|
34
|
+
setup: ({ OPTIONS })=>({
|
|
35
|
+
providers: [
|
|
36
|
+
{
|
|
37
|
+
provide: FEATURE_FLAG_TOKENS.DriverRegistry,
|
|
38
|
+
useFactory: (options)=>buildDriverRegistry(options),
|
|
39
|
+
inject: [
|
|
40
|
+
OPTIONS
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
// Transient: a fresh service per injection, so `use()`/`forRequest()`
|
|
44
|
+
// clones and per-request context stay isolated. The service never injects
|
|
45
|
+
// REQUEST_CONTEXT, so it does not bubble to request scope and remains
|
|
46
|
+
// resolvable in queue / scheduled / global scope.
|
|
47
|
+
{
|
|
48
|
+
provide: FEATURE_FLAG_TOKENS.Service,
|
|
49
|
+
useClass: FeatureFlagsService,
|
|
50
|
+
scope: Scope.TRANSIENT
|
|
51
|
+
},
|
|
52
|
+
FeatureFlagGuard
|
|
53
|
+
],
|
|
54
|
+
exports: [
|
|
55
|
+
FEATURE_FLAG_TOKENS.Service,
|
|
56
|
+
FEATURE_FLAG_TOKENS.DriverRegistry,
|
|
57
|
+
FeatureFlagGuard,
|
|
58
|
+
OPTIONS
|
|
59
|
+
]
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
export class FeatureFlagsModule extends ConfigurableModuleClass {
|
|
63
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type LoggerService, type RequestContext } from '@velajs/vela';
|
|
2
|
+
import type { FeatureFlagDriver } from './drivers/driver';
|
|
3
|
+
import type { FeatureFlagDriverRegistry } from './drivers/registry';
|
|
4
|
+
import type { FeatureFlagsOptions, FlagContext, FlagEvaluationDetails, FlagKey, FlagValue } from './feature-flags.types';
|
|
5
|
+
/**
|
|
6
|
+
* The injectable consumers reach for. A thin, type-safe, never-throw wrapper
|
|
7
|
+
* over a {@link FeatureFlagDriver}, with two ergonomic additions:
|
|
8
|
+
*
|
|
9
|
+
* - **Manifest defaults** — omit a default and the value declared in the app's
|
|
10
|
+
* `manifest` is used (an explicit argument always wins).
|
|
11
|
+
* - **Default context** — the module's `context` resolver is merged into every
|
|
12
|
+
* evaluation (per-call context overrides it), resolved from the current
|
|
13
|
+
* request and skipped automatically outside request scope.
|
|
14
|
+
*
|
|
15
|
+
* Switch drivers with {@link use}; bind a request with {@link forRequest} (the
|
|
16
|
+
* guard does this). Evaluation NEVER throws — the driver returns the fallback
|
|
17
|
+
* on evaluation errors, and the service additionally absorbs any thrown error
|
|
18
|
+
* into the same fallback with a logged warning.
|
|
19
|
+
*
|
|
20
|
+
* `@Transient`: constructed synchronously (lazy-module-safe), a fresh instance
|
|
21
|
+
* per injection, and resolvable in and out of request scope — it never
|
|
22
|
+
* DI-injects `REQUEST_CONTEXT`, so it does not bubble to request scope.
|
|
23
|
+
*/
|
|
24
|
+
export declare class FeatureFlagsService {
|
|
25
|
+
private readonly registry;
|
|
26
|
+
private readonly options;
|
|
27
|
+
private readonly boundContext?;
|
|
28
|
+
private readonly logger;
|
|
29
|
+
private readonly manifest;
|
|
30
|
+
private readonly driver;
|
|
31
|
+
constructor(registry: FeatureFlagDriverRegistry, options: FeatureFlagsOptions, logger?: LoggerService, boundContext?: RequestContext | undefined, boundDriver?: FeatureFlagDriver);
|
|
32
|
+
/** The name of the driver this instance targets. */
|
|
33
|
+
get driverName(): string;
|
|
34
|
+
/**
|
|
35
|
+
* Switch to a different registered driver. Returns a NEW immutable instance
|
|
36
|
+
* bound to `name`; the original is unchanged. Throws if `name` is unknown.
|
|
37
|
+
*/
|
|
38
|
+
use(name: string): FeatureFlagsService;
|
|
39
|
+
/**
|
|
40
|
+
* Bind a request context. Returns a NEW immutable instance whose evaluations
|
|
41
|
+
* merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
|
|
42
|
+
* resolved in request scope can also call it explicitly.
|
|
43
|
+
*/
|
|
44
|
+
forRequest(ctx: RequestContext): FeatureFlagsService;
|
|
45
|
+
/** Evaluate a flag as a `boolean`. */
|
|
46
|
+
getBooleanValue(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<boolean>;
|
|
47
|
+
/** Evaluate a flag as a `string`. */
|
|
48
|
+
getStringValue(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<string>;
|
|
49
|
+
/** Evaluate a flag as a `number`. */
|
|
50
|
+
getNumberValue(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<number>;
|
|
51
|
+
/** Evaluate a flag as a typed object. */
|
|
52
|
+
getObjectValue<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<T>;
|
|
53
|
+
/** Evaluate a `boolean` flag with synthesized evaluation metadata. */
|
|
54
|
+
getBooleanDetails(flagKey: FlagKey, defaultValue?: boolean, context?: FlagContext): Promise<FlagEvaluationDetails<boolean>>;
|
|
55
|
+
/** Evaluate a `string` flag with synthesized evaluation metadata. */
|
|
56
|
+
getStringDetails(flagKey: FlagKey, defaultValue?: string, context?: FlagContext): Promise<FlagEvaluationDetails<string>>;
|
|
57
|
+
/** Evaluate a `number` flag with synthesized evaluation metadata. */
|
|
58
|
+
getNumberDetails(flagKey: FlagKey, defaultValue?: number, context?: FlagContext): Promise<FlagEvaluationDetails<number>>;
|
|
59
|
+
/** Evaluate a typed object flag with synthesized evaluation metadata. */
|
|
60
|
+
getObjectDetails<T extends object>(flagKey: FlagKey, defaultValue?: T, context?: FlagContext): Promise<FlagEvaluationDetails<T>>;
|
|
61
|
+
/**
|
|
62
|
+
* Evaluate every flag declared in the manifest and return a `{ key: value }`
|
|
63
|
+
* map. The evaluation method is chosen from each declared default's type.
|
|
64
|
+
* A throwing context resolver falls back to the manifest defaults rather than
|
|
65
|
+
* taking the batch down.
|
|
66
|
+
*/
|
|
67
|
+
all(context?: FlagContext): Promise<Record<string, FlagValue>>;
|
|
68
|
+
/** Immutable clone with a different driver and/or bound context. */
|
|
69
|
+
private clone;
|
|
70
|
+
/** Resolve the merged evaluation context (default context + per-call override). */
|
|
71
|
+
private context;
|
|
72
|
+
/** Pick the default: explicit arg, then manifest, then the type's zero value. */
|
|
73
|
+
private fallback;
|
|
74
|
+
/** Evaluate a single flag, choosing the method from the declared default's type. */
|
|
75
|
+
private evaluate;
|
|
76
|
+
/**
|
|
77
|
+
* Run an evaluation and absorb any failure into the fallback. A flag lookup
|
|
78
|
+
* must never take the caller down with it.
|
|
79
|
+
*/
|
|
80
|
+
private safe;
|
|
81
|
+
private details;
|
|
82
|
+
private errorDetails;
|
|
83
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Never-throw + manifest-default ergonomics adapted from
|
|
16
|
+
// @stratal/feature-flags (MIT, © Temitayo Fadojutimi), reshaped from a single
|
|
17
|
+
// Cloudflare-binding service into this driver-based, edge-pure form.
|
|
18
|
+
import { Inject, Injectable, Logger, Optional, Scope, getCurrentRequestContext } from "@velajs/vela";
|
|
19
|
+
import { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
|
|
20
|
+
/**
|
|
21
|
+
* Safely read the current request context. Returns `undefined` outside a
|
|
22
|
+
* request or when ambient access isn't enabled — never throws — so the service
|
|
23
|
+
* resolves and evaluates in queue / scheduled / global scope too.
|
|
24
|
+
*/ function ambientRequestContext() {
|
|
25
|
+
try {
|
|
26
|
+
return getCurrentRequestContext();
|
|
27
|
+
} catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class FeatureFlagsService {
|
|
32
|
+
registry;
|
|
33
|
+
options;
|
|
34
|
+
boundContext;
|
|
35
|
+
logger;
|
|
36
|
+
manifest;
|
|
37
|
+
driver;
|
|
38
|
+
constructor(registry, options, logger, // The next two are never provided by DI (they carry `@Optional()` so the
|
|
39
|
+
// container leaves them `undefined`). `forRequest()` and `use()` set them
|
|
40
|
+
// when cloning; tests may pass them directly.
|
|
41
|
+
boundContext, boundDriver){
|
|
42
|
+
this.registry = registry;
|
|
43
|
+
this.options = options;
|
|
44
|
+
this.boundContext = boundContext;
|
|
45
|
+
this.logger = logger ?? new Logger('FeatureFlags');
|
|
46
|
+
this.manifest = options.manifest ?? {};
|
|
47
|
+
this.driver = boundDriver ?? registry.resolve(options.default);
|
|
48
|
+
}
|
|
49
|
+
/** The name of the driver this instance targets. */ get driverName() {
|
|
50
|
+
return this.driver.name;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Switch to a different registered driver. Returns a NEW immutable instance
|
|
54
|
+
* bound to `name`; the original is unchanged. Throws if `name` is unknown.
|
|
55
|
+
*/ use(name) {
|
|
56
|
+
if (name === this.driver.name) return this;
|
|
57
|
+
return this.clone({
|
|
58
|
+
driver: this.registry.get(name)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Bind a request context. Returns a NEW immutable instance whose evaluations
|
|
63
|
+
* merge `options.context(ctx)`. Used by {@link FeatureFlagGuard}; consumers
|
|
64
|
+
* resolved in request scope can also call it explicitly.
|
|
65
|
+
*/ forRequest(ctx) {
|
|
66
|
+
return this.clone({
|
|
67
|
+
context: ctx
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// ==================== EVALUATION ====================
|
|
71
|
+
/** Evaluate a flag as a `boolean`. */ async getBooleanValue(flagKey, defaultValue, context) {
|
|
72
|
+
const fallback = this.fallback(flagKey, defaultValue, false);
|
|
73
|
+
return this.safe(flagKey, async ()=>this.driver.getBoolean(flagKey, fallback, await this.context(context)), ()=>fallback);
|
|
74
|
+
}
|
|
75
|
+
/** Evaluate a flag as a `string`. */ async getStringValue(flagKey, defaultValue, context) {
|
|
76
|
+
const fallback = this.fallback(flagKey, defaultValue, '');
|
|
77
|
+
return this.safe(flagKey, async ()=>this.driver.getString(flagKey, fallback, await this.context(context)), ()=>fallback);
|
|
78
|
+
}
|
|
79
|
+
/** Evaluate a flag as a `number`. */ async getNumberValue(flagKey, defaultValue, context) {
|
|
80
|
+
const fallback = this.fallback(flagKey, defaultValue, 0);
|
|
81
|
+
return this.safe(flagKey, async ()=>this.driver.getNumber(flagKey, fallback, await this.context(context)), ()=>fallback);
|
|
82
|
+
}
|
|
83
|
+
/** Evaluate a flag as a typed object. */ async getObjectValue(flagKey, defaultValue, context) {
|
|
84
|
+
const fallback = this.fallback(flagKey, defaultValue, {});
|
|
85
|
+
return this.safe(flagKey, async ()=>this.driver.getObject(flagKey, fallback, await this.context(context)), ()=>fallback);
|
|
86
|
+
}
|
|
87
|
+
/** Evaluate a `boolean` flag with synthesized evaluation metadata. */ async getBooleanDetails(flagKey, defaultValue, context) {
|
|
88
|
+
const fallback = this.fallback(flagKey, defaultValue, false);
|
|
89
|
+
return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getBoolean(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
|
|
90
|
+
}
|
|
91
|
+
/** Evaluate a `string` flag with synthesized evaluation metadata. */ async getStringDetails(flagKey, defaultValue, context) {
|
|
92
|
+
const fallback = this.fallback(flagKey, defaultValue, '');
|
|
93
|
+
return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getString(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
|
|
94
|
+
}
|
|
95
|
+
/** Evaluate a `number` flag with synthesized evaluation metadata. */ async getNumberDetails(flagKey, defaultValue, context) {
|
|
96
|
+
const fallback = this.fallback(flagKey, defaultValue, 0);
|
|
97
|
+
return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getNumber(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
|
|
98
|
+
}
|
|
99
|
+
/** Evaluate a typed object flag with synthesized evaluation metadata. */ async getObjectDetails(flagKey, defaultValue, context) {
|
|
100
|
+
const fallback = this.fallback(flagKey, defaultValue, {});
|
|
101
|
+
return this.safe(flagKey, async ()=>this.details(flagKey, await this.driver.getObject(flagKey, fallback, await this.context(context))), (error)=>this.errorDetails(flagKey, fallback, error));
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate every flag declared in the manifest and return a `{ key: value }`
|
|
105
|
+
* map. The evaluation method is chosen from each declared default's type.
|
|
106
|
+
* A throwing context resolver falls back to the manifest defaults rather than
|
|
107
|
+
* taking the batch down.
|
|
108
|
+
*/ async all(context) {
|
|
109
|
+
const keys = Object.keys(this.manifest);
|
|
110
|
+
let merged;
|
|
111
|
+
try {
|
|
112
|
+
merged = await this.context(context);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.logger.warn(`Feature flag context resolution failed on driver "${this.driver.name}"; returning manifest defaults.`, {
|
|
115
|
+
error: message(error)
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
...this.manifest
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const values = await Promise.all(keys.map((key)=>this.evaluate(key, this.manifest[key], merged)));
|
|
122
|
+
const result = {};
|
|
123
|
+
keys.forEach((key, i)=>{
|
|
124
|
+
result[key] = values[i];
|
|
125
|
+
});
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
// ==================== INTERNAL ====================
|
|
129
|
+
/** Immutable clone with a different driver and/or bound context. */ clone(overrides) {
|
|
130
|
+
return new FeatureFlagsService(this.registry, this.options, this.logger, overrides.context ?? this.boundContext, overrides.driver ?? this.driver);
|
|
131
|
+
}
|
|
132
|
+
/** Resolve the merged evaluation context (default context + per-call override). */ async context(callContext) {
|
|
133
|
+
const base = this.boundContext ?? ambientRequestContext();
|
|
134
|
+
if (!this.options.context || !base) return callContext;
|
|
135
|
+
const resolved = await this.options.context(base);
|
|
136
|
+
return callContext ? {
|
|
137
|
+
...resolved,
|
|
138
|
+
...callContext
|
|
139
|
+
} : resolved;
|
|
140
|
+
}
|
|
141
|
+
/** Pick the default: explicit arg, then manifest, then the type's zero value. */ fallback(flagKey, provided, zero) {
|
|
142
|
+
if (provided !== undefined) return provided;
|
|
143
|
+
if (flagKey in this.manifest) return this.manifest[flagKey];
|
|
144
|
+
return zero;
|
|
145
|
+
}
|
|
146
|
+
/** Evaluate a single flag, choosing the method from the declared default's type. */ evaluate(flagKey, declared, context) {
|
|
147
|
+
switch(typeof declared){
|
|
148
|
+
case 'boolean':
|
|
149
|
+
return this.safe(flagKey, ()=>this.driver.getBoolean(flagKey, declared, context), ()=>declared);
|
|
150
|
+
case 'number':
|
|
151
|
+
return this.safe(flagKey, ()=>this.driver.getNumber(flagKey, declared, context), ()=>declared);
|
|
152
|
+
case 'string':
|
|
153
|
+
return this.safe(flagKey, ()=>this.driver.getString(flagKey, declared, context), ()=>declared);
|
|
154
|
+
default:
|
|
155
|
+
return this.safe(flagKey, ()=>this.driver.getObject(flagKey, declared, context), ()=>declared);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Run an evaluation and absorb any failure into the fallback. A flag lookup
|
|
160
|
+
* must never take the caller down with it.
|
|
161
|
+
*/ async safe(flagKey, evaluate, onError) {
|
|
162
|
+
try {
|
|
163
|
+
return await evaluate();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.logger.warn(`Feature flag evaluation failed for "${flagKey}" on driver "${this.driver.name}"; returning the fallback value.`, {
|
|
166
|
+
error: message(error)
|
|
167
|
+
});
|
|
168
|
+
return onError(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
details(flagKey, value) {
|
|
172
|
+
return {
|
|
173
|
+
flagKey,
|
|
174
|
+
value,
|
|
175
|
+
reason: 'STATIC'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
errorDetails(flagKey, value, error) {
|
|
179
|
+
return {
|
|
180
|
+
flagKey,
|
|
181
|
+
value,
|
|
182
|
+
reason: 'ERROR',
|
|
183
|
+
errorMessage: message(error)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
FeatureFlagsService = _ts_decorate([
|
|
188
|
+
Injectable({
|
|
189
|
+
scope: Scope.TRANSIENT
|
|
190
|
+
}),
|
|
191
|
+
_ts_param(0, Inject(FEATURE_FLAG_TOKENS.DriverRegistry)),
|
|
192
|
+
_ts_param(1, Inject(FEATURE_FLAG_TOKENS.Options)),
|
|
193
|
+
_ts_param(2, Optional()),
|
|
194
|
+
_ts_param(2, Inject(Logger)),
|
|
195
|
+
_ts_param(3, Optional()),
|
|
196
|
+
_ts_param(4, Optional()),
|
|
197
|
+
_ts_metadata("design:type", Function),
|
|
198
|
+
_ts_metadata("design:paramtypes", [
|
|
199
|
+
typeof FeatureFlagDriverRegistry === "undefined" ? Object : FeatureFlagDriverRegistry,
|
|
200
|
+
typeof FeatureFlagsOptions === "undefined" ? Object : FeatureFlagsOptions,
|
|
201
|
+
typeof LoggerService === "undefined" ? Object : LoggerService,
|
|
202
|
+
typeof RequestContext === "undefined" ? Object : RequestContext,
|
|
203
|
+
typeof FeatureFlagDriver === "undefined" ? Object : FeatureFlagDriver
|
|
204
|
+
])
|
|
205
|
+
], FeatureFlagsService);
|
|
206
|
+
/** Extract a human-readable message from an unknown thrown value. */ function message(error) {
|
|
207
|
+
return error instanceof Error ? error.message : String(error);
|
|
208
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FeatureFlagDriverRegistry } from './drivers/registry';
|
|
2
|
+
import type { FeatureFlagsService } from './feature-flags.service';
|
|
3
|
+
import type { FeatureFlagsOptions } from './feature-flags.types';
|
|
4
|
+
/**
|
|
5
|
+
* Injection tokens for the feature-flags module. Named on the
|
|
6
|
+
* `vela:feature-flags:<thing>` convention so identity is stable across
|
|
7
|
+
* refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const FEATURE_FLAG_TOKENS: {
|
|
10
|
+
/** The resolved {@link FeatureFlagsOptions} (module options token). */
|
|
11
|
+
readonly Options: import("@velajs/vela").InjectionToken<FeatureFlagsOptions>;
|
|
12
|
+
/** The injectable {@link FeatureFlagsService}. */
|
|
13
|
+
readonly Service: import("@velajs/vela").InjectionToken<FeatureFlagsService>;
|
|
14
|
+
/** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */
|
|
15
|
+
readonly DriverRegistry: import("@velajs/vela").InjectionToken<FeatureFlagDriverRegistry>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { moduleToken } from "@velajs/vela";
|
|
2
|
+
/**
|
|
3
|
+
* Injection tokens for the feature-flags module. Named on the
|
|
4
|
+
* `vela:feature-flags:<thing>` convention so identity is stable across
|
|
5
|
+
* refactors and consumers can `@Inject(FEATURE_FLAG_TOKENS.Service)`.
|
|
6
|
+
*/ export const FEATURE_FLAG_TOKENS = {
|
|
7
|
+
/** The resolved {@link FeatureFlagsOptions} (module options token). */ Options: moduleToken('vela:feature-flags:options'),
|
|
8
|
+
/** The injectable {@link FeatureFlagsService}. */ Service: moduleToken('vela:feature-flags:service'),
|
|
9
|
+
/** The {@link FeatureFlagDriverRegistry} built from the options' drivers. */ DriverRegistry: moduleToken('vela:feature-flags:driver-registry')
|
|
10
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { RequestContext } from '@velajs/vela';
|
|
2
|
+
import type { FeatureFlagDriver } from './drivers/driver';
|
|
3
|
+
/**
|
|
4
|
+
* A value a feature flag can resolve to. Mirrors the four evaluation methods
|
|
5
|
+
* on {@link FeatureFlagDriver}.
|
|
6
|
+
*/
|
|
7
|
+
export type FlagValue = boolean | string | number | object;
|
|
8
|
+
/**
|
|
9
|
+
* A free-form evaluation context handed to a driver (for targeting: user id,
|
|
10
|
+
* plan, country, …). Drivers that don't do targeting ignore it.
|
|
11
|
+
*/
|
|
12
|
+
export type FlagContext = Record<string, unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Augment this interface to get typed flag keys on the service and the
|
|
15
|
+
* `@FeatureFlag()` decorator.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* declare module '@velajs/feature-flags' {
|
|
20
|
+
* interface FeatureFlagRegistry {
|
|
21
|
+
* 'new-checkout': boolean;
|
|
22
|
+
* 'checkout-flow': string;
|
|
23
|
+
* }
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface FeatureFlagRegistry {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A flag key. Narrows to the declared {@link FeatureFlagRegistry} keys when the
|
|
31
|
+
* app augments it; falls back to `string` otherwise.
|
|
32
|
+
*/
|
|
33
|
+
export type FlagKey = keyof FeatureFlagRegistry extends never ? string : keyof FeatureFlagRegistry & string;
|
|
34
|
+
/**
|
|
35
|
+
* A declared set of flags and their default values.
|
|
36
|
+
*
|
|
37
|
+
* Drivers have no enumeration API, so the flags you intend to evaluate as a
|
|
38
|
+
* batch (via {@link FeatureFlagsService.all}) must be declared once here. Each
|
|
39
|
+
* default also doubles as the type hint used to pick the evaluation method.
|
|
40
|
+
*/
|
|
41
|
+
export type FlagManifest = Record<string, FlagValue>;
|
|
42
|
+
/** Why an evaluation returned the value it did. */
|
|
43
|
+
export type FlagEvaluationReason = 'STATIC' | 'DEFAULT' | 'ERROR';
|
|
44
|
+
/** A flag value plus the metadata the service synthesizes around a driver read. */
|
|
45
|
+
export interface FlagEvaluationDetails<T extends FlagValue = FlagValue> {
|
|
46
|
+
flagKey: string;
|
|
47
|
+
value: T;
|
|
48
|
+
reason: FlagEvaluationReason;
|
|
49
|
+
errorMessage?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Feature-flags module configuration.
|
|
53
|
+
*/
|
|
54
|
+
export interface FeatureFlagsOptions {
|
|
55
|
+
/**
|
|
56
|
+
* The drivers this app can evaluate against. When omitted, a single
|
|
57
|
+
* in-memory {@link MemoryFlagDriver} named `"memory"` is used (all flags
|
|
58
|
+
* resolve to their declared defaults).
|
|
59
|
+
*/
|
|
60
|
+
drivers?: FeatureFlagDriver[];
|
|
61
|
+
/** Name of the driver the injected service targets. Defaults to `drivers[0].name`. */
|
|
62
|
+
default?: string;
|
|
63
|
+
/** Declared flags + defaults. Powers manifest defaults and {@link FeatureFlagsService.all}. */
|
|
64
|
+
manifest?: FlagManifest;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a per-request evaluation context (for example `{ userId }`) merged
|
|
67
|
+
* into every evaluation. Per-call context passed to a method overrides these.
|
|
68
|
+
* Receives the current request context; skipped outside request scope.
|
|
69
|
+
*/
|
|
70
|
+
context?: (ctx: RequestContext) => FlagContext | Promise<FlagContext>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CanActivate, type ExecutionContext } from '@velajs/vela';
|
|
2
|
+
import type { FeatureFlagsService } from '../feature-flags.service';
|
|
3
|
+
/**
|
|
4
|
+
* Route gate for `@FeatureFlag()`. Reads the handler/controller metadata, then
|
|
5
|
+
* `getBooleanValue(key)`; when the flag is off it throws `NotFoundException`
|
|
6
|
+
* (the route appears hidden) or `ForbiddenException` per the decorator option.
|
|
7
|
+
* Handlers with no `@FeatureFlag()` metadata pass through untouched, so the
|
|
8
|
+
* guard is safe to register app-wide (`FeatureFlagsModule.forRoot({ isGlobal:
|
|
9
|
+
* true })`) or per-route via `@UseGuards(FeatureFlagGuard)`.
|
|
10
|
+
*
|
|
11
|
+
* It does NOT inject `REQUEST_CONTEXT` — that would make the guard request-
|
|
12
|
+
* scoped and break lazy-module materialization (which constructs every provider
|
|
13
|
+
* once, at first use, possibly outside a request). Instead it reads the request
|
|
14
|
+
* context off the per-request child container carried on the Hono context, so
|
|
15
|
+
* the module's `context` resolver still runs for the gate decision.
|
|
16
|
+
*/
|
|
17
|
+
export declare class FeatureFlagGuard implements CanActivate {
|
|
18
|
+
private readonly flags;
|
|
19
|
+
private readonly reflector;
|
|
20
|
+
constructor(flags: FeatureFlagsService);
|
|
21
|
+
canActivate(context: ExecutionContext): Promise<boolean>;
|
|
22
|
+
/** The current request context, via the child container on the Hono context. */
|
|
23
|
+
private requestContext;
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
}
|
|
7
|
+
function _ts_metadata(k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
}
|
|
10
|
+
function _ts_param(paramIndex, decorator) {
|
|
11
|
+
return function(target, key) {
|
|
12
|
+
decorator(target, key, paramIndex);
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
import { ForbiddenException, Inject, Injectable, NotFoundException, REQUEST_CONTEXT, Reflector } from "@velajs/vela";
|
|
16
|
+
import { FEATURE_FLAG_METADATA } from "../decorators/feature-flag.decorator.js";
|
|
17
|
+
import { FEATURE_FLAG_TOKENS } from "../feature-flags.tokens.js";
|
|
18
|
+
export class FeatureFlagGuard {
|
|
19
|
+
flags;
|
|
20
|
+
reflector = new Reflector();
|
|
21
|
+
constructor(flags){
|
|
22
|
+
this.flags = flags;
|
|
23
|
+
}
|
|
24
|
+
async canActivate(context) {
|
|
25
|
+
const meta = this.reflector.getAllAndOverride(FEATURE_FLAG_METADATA, context);
|
|
26
|
+
if (!meta) return true;
|
|
27
|
+
const requestContext = this.requestContext(context);
|
|
28
|
+
const flags = requestContext ? this.flags.forRequest(requestContext) : this.flags;
|
|
29
|
+
const enabled = await flags.getBooleanValue(meta.key);
|
|
30
|
+
if (enabled) return true;
|
|
31
|
+
if (meta.onDisabled === 'forbidden') {
|
|
32
|
+
throw new ForbiddenException(`Feature "${meta.key}" is not enabled.`);
|
|
33
|
+
}
|
|
34
|
+
throw new NotFoundException();
|
|
35
|
+
}
|
|
36
|
+
/** The current request context, via the child container on the Hono context. */ requestContext(context) {
|
|
37
|
+
try {
|
|
38
|
+
const container = context.getContext().get('container');
|
|
39
|
+
return container?.resolve(REQUEST_CONTEXT);
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
FeatureFlagGuard = _ts_decorate([
|
|
46
|
+
Injectable(),
|
|
47
|
+
_ts_param(0, Inject(FEATURE_FLAG_TOKENS.Service)),
|
|
48
|
+
_ts_metadata("design:type", Function),
|
|
49
|
+
_ts_metadata("design:paramtypes", [
|
|
50
|
+
typeof FeatureFlagsService === "undefined" ? Object : FeatureFlagsService
|
|
51
|
+
])
|
|
52
|
+
], FeatureFlagGuard);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { FeatureFlagsModule } from './feature-flags.module';
|
|
2
|
+
export { FeatureFlagsService } from './feature-flags.service';
|
|
3
|
+
export { FEATURE_FLAG_TOKENS } from './feature-flags.tokens';
|
|
4
|
+
export type { FeatureFlagDriver } from './drivers/driver';
|
|
5
|
+
export { MemoryFlagDriver, memoryFlagDriver } from './drivers/memory.driver';
|
|
6
|
+
export type { MemoryFlagDriverOptions } from './drivers/memory.driver';
|
|
7
|
+
export { FeatureFlagDriverRegistry, buildDriverRegistry } from './drivers/registry';
|
|
8
|
+
export { FeatureFlagGuard } from './guards/feature-flag.guard';
|
|
9
|
+
export { FeatureFlag, FEATURE_FLAG_METADATA } from './decorators/feature-flag.decorator';
|
|
10
|
+
export type { FeatureFlagOptions, FeatureFlagMetadata, FeatureFlagDisabledBehavior, } from './decorators/feature-flag.decorator';
|
|
11
|
+
export { FeatureFlagError } from './feature-flags.error';
|
|
12
|
+
export type { FeatureFlagsOptions, FeatureFlagRegistry, FlagContext, FlagKey, FlagManifest, FlagValue, FlagEvaluationDetails, FlagEvaluationReason, } from './feature-flags.types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Public surface of @velajs/feature-flags (the `.` entry). Server-only: no
|
|
2
|
+
// React, no client hooks. Runtime-specific drivers (Cloudflare Flagship / KV)
|
|
3
|
+
// ship from @velajs/cloudflare against the FeatureFlagDriver contract exported
|
|
4
|
+
// here.
|
|
5
|
+
// --- DI: module / service / tokens ---------------------------------------
|
|
6
|
+
export { FeatureFlagsModule } from "./feature-flags.module.js";
|
|
7
|
+
export { FeatureFlagsService } from "./feature-flags.service.js";
|
|
8
|
+
export { FEATURE_FLAG_TOKENS } from "./feature-flags.tokens.js";
|
|
9
|
+
export { MemoryFlagDriver, memoryFlagDriver } from "./drivers/memory.driver.js";
|
|
10
|
+
export { FeatureFlagDriverRegistry, buildDriverRegistry } from "./drivers/registry.js";
|
|
11
|
+
// --- Guard + decorator ----------------------------------------------------
|
|
12
|
+
export { FeatureFlagGuard } from "./guards/feature-flag.guard.js";
|
|
13
|
+
export { FeatureFlag, FEATURE_FLAG_METADATA } from "./decorators/feature-flag.decorator.js";
|
|
14
|
+
// --- Errors ---------------------------------------------------------------
|
|
15
|
+
export { FeatureFlagError } from "./feature-flags.error.js";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MemoryFlagDriver } from '../drivers/memory.driver';
|
|
2
|
+
import { FeatureFlagsService } from '../feature-flags.service';
|
|
3
|
+
import type { FeatureFlagsOptions, FlagManifest } from '../feature-flags.types';
|
|
4
|
+
export { MemoryFlagDriver, memoryFlagDriver } from '../drivers/memory.driver';
|
|
5
|
+
export type { MemoryFlagDriverOptions } from '../drivers/memory.driver';
|
|
6
|
+
export interface TestFeatureFlags {
|
|
7
|
+
/** A real service bound to the memory driver below. */
|
|
8
|
+
service: FeatureFlagsService;
|
|
9
|
+
/** The backing memory driver — mutate flags with `.set()`/`.reset()`. */
|
|
10
|
+
driver: MemoryFlagDriver;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
|
|
14
|
+
* app bootstrap required. Flip flags on the returned `driver` and assert on the
|
|
15
|
+
* `service`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
|
|
20
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(true);
|
|
21
|
+
* driver.set('new-checkout', false);
|
|
22
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(false);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createTestFeatureFlags(values?: FlagManifest, options?: Pick<FeatureFlagsOptions, 'manifest' | 'context'>): TestFeatureFlags;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @velajs/feature-flags/testing — the memory driver IS the fake. This subpath
|
|
2
|
+
// re-exports it plus a zero-DI helper for unit tests that want a real
|
|
3
|
+
// FeatureFlagsService over an in-memory driver without bootstrapping a module.
|
|
4
|
+
import { FeatureFlagDriverRegistry } from "../drivers/registry.js";
|
|
5
|
+
import { MemoryFlagDriver } from "../drivers/memory.driver.js";
|
|
6
|
+
import { FeatureFlagsService } from "../feature-flags.service.js";
|
|
7
|
+
export { MemoryFlagDriver, memoryFlagDriver } from "../drivers/memory.driver.js";
|
|
8
|
+
/**
|
|
9
|
+
* Build a real {@link FeatureFlagsService} over an in-memory driver, no DI /
|
|
10
|
+
* app bootstrap required. Flip flags on the returned `driver` and assert on the
|
|
11
|
+
* `service`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const { service, driver } = createTestFeatureFlags({ 'new-checkout': true });
|
|
16
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(true);
|
|
17
|
+
* driver.set('new-checkout', false);
|
|
18
|
+
* expect(await service.getBooleanValue('new-checkout')).toBe(false);
|
|
19
|
+
* ```
|
|
20
|
+
*/ export function createTestFeatureFlags(values = {}, options = {}) {
|
|
21
|
+
const driver = new MemoryFlagDriver({
|
|
22
|
+
values
|
|
23
|
+
});
|
|
24
|
+
const registry = new FeatureFlagDriverRegistry([
|
|
25
|
+
driver
|
|
26
|
+
], driver.name);
|
|
27
|
+
const service = new FeatureFlagsService(registry, {
|
|
28
|
+
default: driver.name,
|
|
29
|
+
manifest: options.manifest,
|
|
30
|
+
context: options.context
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
service,
|
|
34
|
+
driver
|
|
35
|
+
};
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velajs/feature-flags",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Edge-first, driver-based feature flags for the Vela framework",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./testing": {
|
|
14
|
+
"types": "./dist/testing/index.d.ts",
|
|
15
|
+
"import": "./dist/testing/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"CHANGELOG.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"keywords": [
|
|
26
|
+
"vela",
|
|
27
|
+
"feature-flags",
|
|
28
|
+
"feature-toggles",
|
|
29
|
+
"driver",
|
|
30
|
+
"edge",
|
|
31
|
+
"cloudflare-workers",
|
|
32
|
+
"framework"
|
|
33
|
+
],
|
|
34
|
+
"author": "ksh",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/velajs/feature-flags.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/velajs/feature-flags#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/velajs/feature-flags/issues"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@velajs/vela": ">=1.11.0",
|
|
49
|
+
"hono": ">=4"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@swc/cli": "^0.8.1",
|
|
53
|
+
"@swc/core": "^1.15.43",
|
|
54
|
+
"@velajs/testing": "^0.4.0",
|
|
55
|
+
"@velajs/vela": "^1.12.0",
|
|
56
|
+
"hono": "^4.12.27",
|
|
57
|
+
"typescript": "^6.0.3",
|
|
58
|
+
"unplugin-swc": "^1.5.9",
|
|
59
|
+
"vitest": "^4.1.9"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
|
|
63
|
+
"test": "vitest run",
|
|
64
|
+
"typecheck": "tsc --noEmit"
|
|
65
|
+
}
|
|
66
|
+
}
|