@stratal/feature-flags 0.0.21
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 +54 -0
- package/dist/index.d.mts +166 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +233 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react.d.mts +24 -0
- package/dist/react.d.mts.map +1 -0
- package/dist/react.mjs +16 -0
- package/dist/react.mjs.map +1 -0
- package/dist/types-Dxuc-7SJ.d.mts +71 -0
- package/dist/types-Dxuc-7SJ.d.mts.map +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @stratal/feature-flags
|
|
2
|
+
|
|
3
|
+
[Cloudflare Flagship](https://developers.cloudflare.com/flagship/) feature flags for the [Stratal](https://stratal.dev) framework, using the native Worker **binding API** — with zero-config [Inertia.js](https://inertiajs.com) auto-sharing and typed React hooks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm i @stratal/feature-flags
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Add the Flagship binding to your Wrangler config and run `npx wrangler types`:
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
// wrangler.jsonc
|
|
15
|
+
{ "flagship": [{ "binding": "FLAGS", "app_id": "<APP_ID>" }] }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { FeatureFlagModule } from '@stratal/feature-flags'
|
|
22
|
+
|
|
23
|
+
@Module({
|
|
24
|
+
imports: [
|
|
25
|
+
FeatureFlagModule.forRoot({
|
|
26
|
+
apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],
|
|
27
|
+
context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
export class AppModule {}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
When `@stratal/inertia` is installed, declared flags are auto-shared to every Inertia page — no extra wiring.
|
|
35
|
+
|
|
36
|
+
Evaluate on the server:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const enabled = await this.flags.getBooleanValue('new-checkout') // uses manifest default
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Read on the client:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { useFlag } from '@stratal/feature-flags/react'
|
|
46
|
+
|
|
47
|
+
const showNewCheckout = useFlag('new-checkout')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
See the framework docs for the full API.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { a as FlagValue, i as FlagManifest, n as FeatureFlagModuleOptions, o as FlagshipBindingName, r as FeatureFlagRegistry, t as FeatureFlagApp } from "./types-Dxuc-7SJ.mjs";
|
|
2
|
+
import { AsyncModuleOptions, DynamicModule } from "stratal/module";
|
|
3
|
+
import { Middleware, Next, RouteConfigurable, Router, RouterContext } from "stratal/router";
|
|
4
|
+
import { ApplicationError } from "stratal/errors";
|
|
5
|
+
import { StratalEnv } from "stratal";
|
|
6
|
+
|
|
7
|
+
//#region src/feature-flags.module.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Feature Flag Module
|
|
10
|
+
*
|
|
11
|
+
* Evaluates Cloudflare Flagship feature flags through the native Worker binding.
|
|
12
|
+
* Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}
|
|
13
|
+
* to evaluate them.
|
|
14
|
+
*
|
|
15
|
+
* When `@stratal/inertia` is also present, declared flags are auto-shared to
|
|
16
|
+
* every Inertia page as the `featureFlags` prop (read them with `useFlag` /
|
|
17
|
+
* `useFeatureFlags` from `@stratal/feature-flags/react`) — no extra wiring. In
|
|
18
|
+
* a pure-API worker the auto-share middleware is a no-op.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* FeatureFlagModule.forRoot({
|
|
23
|
+
* apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],
|
|
24
|
+
* context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* // Or async, from config namespaces:
|
|
28
|
+
* FeatureFlagModule.forRootAsync({
|
|
29
|
+
* inject: [flagsConfig.KEY],
|
|
30
|
+
* useFactory: (cfg) => ({ apps: cfg.apps, default: cfg.default }),
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
declare class FeatureFlagModule implements RouteConfigurable {
|
|
35
|
+
/** Auto-shares declared flags to every Inertia page (no-op without Inertia). */
|
|
36
|
+
configureRoutes(router: Router): void;
|
|
37
|
+
/** Configure with static options. */
|
|
38
|
+
static forRoot(options: FeatureFlagModuleOptions): DynamicModule;
|
|
39
|
+
/** Configure with an async factory (when options depend on other services). */
|
|
40
|
+
static forRootAsync(options: AsyncModuleOptions<FeatureFlagModuleOptions>): DynamicModule;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/services/feature-flag.service.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* Feature Flag Service
|
|
46
|
+
*
|
|
47
|
+
* Type-safe wrapper around a Cloudflare Flagship binding (`env.FLAGS`). Mirrors
|
|
48
|
+
* the binding's evaluation methods 1:1, with two ergonomic additions:
|
|
49
|
+
*
|
|
50
|
+
* - **Manifest defaults** — when you omit a default, the value declared in the
|
|
51
|
+
* app's `flags` manifest is used (an explicit argument always wins).
|
|
52
|
+
* - **Default context** — the module's `context` resolver is merged into every
|
|
53
|
+
* evaluation (per-call context overrides it). Resolved from the current
|
|
54
|
+
* request; skipped automatically outside request scope.
|
|
55
|
+
*
|
|
56
|
+
* Switch to another Flagship app with {@link use}. Evaluation never throws — the
|
|
57
|
+
* binding returns the default value on error.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* @inject(FEATURE_FLAG_TOKENS.FeatureFlagService)
|
|
62
|
+
* private readonly flags: FeatureFlagService
|
|
63
|
+
*
|
|
64
|
+
* const enabled = await this.flags.getBooleanValue('new-checkout') // manifest default
|
|
65
|
+
* const layout = await this.flags.use('EXPERIMENT_FLAGS').getStringValue('layout', 'v1')
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @see https://developers.cloudflare.com/flagship/binding/
|
|
69
|
+
*/
|
|
70
|
+
declare class FeatureFlagService {
|
|
71
|
+
private readonly options;
|
|
72
|
+
private readonly env;
|
|
73
|
+
private readonly routerContext;
|
|
74
|
+
private readonly apps;
|
|
75
|
+
private bindingName;
|
|
76
|
+
private binding;
|
|
77
|
+
private manifest;
|
|
78
|
+
constructor(options: FeatureFlagModuleOptions, env: StratalEnv, routerContext: RouterContext | null);
|
|
79
|
+
/**
|
|
80
|
+
* Switch to a different configured Flagship app.
|
|
81
|
+
*
|
|
82
|
+
* Returns a new immutable instance bound to `binding`; the original is
|
|
83
|
+
* unchanged. The binding must be declared in the module's `apps`.
|
|
84
|
+
*/
|
|
85
|
+
use(binding: FlagshipBindingName): FeatureFlagService;
|
|
86
|
+
/** The binding name this instance currently targets. */
|
|
87
|
+
get app(): string;
|
|
88
|
+
/** Returns the raw flag value without type checking. */
|
|
89
|
+
get(flagKey: string, defaultValue?: unknown, context?: FlagshipEvaluationContext): Promise<unknown>;
|
|
90
|
+
/** Returns the flag value as a `boolean`. */
|
|
91
|
+
getBooleanValue(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<boolean>;
|
|
92
|
+
/** Returns the flag value as a `string`. */
|
|
93
|
+
getStringValue(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<string>;
|
|
94
|
+
/** Returns the flag value as a `number`. */
|
|
95
|
+
getNumberValue(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<number>;
|
|
96
|
+
/** Returns the flag value as a typed object. */
|
|
97
|
+
getObjectValue<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<T>;
|
|
98
|
+
/** Returns the `boolean` flag value with evaluation metadata. */
|
|
99
|
+
getBooleanDetails(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<boolean>>;
|
|
100
|
+
/** Returns the `string` flag value with evaluation metadata. */
|
|
101
|
+
getStringDetails(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<string>>;
|
|
102
|
+
/** Returns the `number` flag value with evaluation metadata. */
|
|
103
|
+
getNumberDetails(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<number>>;
|
|
104
|
+
/** Returns the typed object flag value with evaluation metadata. */
|
|
105
|
+
getObjectDetails<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<T>>;
|
|
106
|
+
/**
|
|
107
|
+
* Evaluates every flag declared in the current app's manifest and returns a
|
|
108
|
+
* `{ key: value }` map. The evaluation method is chosen from each declared
|
|
109
|
+
* default's type. Powers the Inertia auto-share.
|
|
110
|
+
*/
|
|
111
|
+
all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>>;
|
|
112
|
+
private bindTo;
|
|
113
|
+
/** Resolves the merged evaluation context (default context + per-call override). */
|
|
114
|
+
private context;
|
|
115
|
+
/** Picks the default: explicit arg, then manifest, then the type's zero value. */
|
|
116
|
+
private fallback;
|
|
117
|
+
/** Evaluates a single flag, choosing the method from the declared default's type. */
|
|
118
|
+
private evaluate;
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/feature-flag-share.middleware.d.ts
|
|
122
|
+
declare module 'stratal/router' {
|
|
123
|
+
interface RouterContext {
|
|
124
|
+
share(key: string, value: unknown): void;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Evaluates the declared flag manifest for the default app and shares it as the
|
|
129
|
+
* `featureFlags` prop on every Inertia page rendered during the request.
|
|
130
|
+
*
|
|
131
|
+
* Only runs on `GET` requests — page renders (full visits and partial reloads)
|
|
132
|
+
* are always `GET`, so mutating API calls don't trigger evaluation. No-ops when
|
|
133
|
+
* Inertia is not installed (`ctx.share` absent), so `FeatureFlagModule` is safe
|
|
134
|
+
* in pure-API workers. Registered by `FeatureFlagModule`.
|
|
135
|
+
*/
|
|
136
|
+
declare class FeatureFlagShareMiddleware implements Middleware {
|
|
137
|
+
private readonly flags;
|
|
138
|
+
constructor(flags: FeatureFlagService);
|
|
139
|
+
handle(ctx: RouterContext, next: Next): Promise<void>;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/feature-flags.tokens.d.ts
|
|
143
|
+
/**
|
|
144
|
+
* DI tokens for the feature-flags module.
|
|
145
|
+
*
|
|
146
|
+
* Use `Symbol.for(...)` so the tokens resolve to the same symbol across module
|
|
147
|
+
* boundaries (the global symbol registry).
|
|
148
|
+
*/
|
|
149
|
+
declare const FEATURE_FLAG_TOKENS: {
|
|
150
|
+
/** The resolved {@link FeatureFlagModuleOptions}. */readonly Options: symbol; /** The request-scoped {@link FeatureFlagService} bound to the default app. */
|
|
151
|
+
readonly FeatureFlagService: symbol;
|
|
152
|
+
};
|
|
153
|
+
type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS];
|
|
154
|
+
//#endregion
|
|
155
|
+
//#region src/feature-flags.error.d.ts
|
|
156
|
+
/**
|
|
157
|
+
* Thrown for feature-flag misconfiguration — an unknown app or a Flagship
|
|
158
|
+
* binding that is not present on the Worker environment.
|
|
159
|
+
*
|
|
160
|
+
* Note: flag *evaluation* never throws; the binding returns the supplied
|
|
161
|
+
* default value on error.
|
|
162
|
+
*/
|
|
163
|
+
declare class FeatureFlagError extends ApplicationError {}
|
|
164
|
+
//#endregion
|
|
165
|
+
export { FEATURE_FLAG_TOKENS, type FeatureFlagApp, FeatureFlagError, FeatureFlagModule, type FeatureFlagModuleOptions, type FeatureFlagRegistry, FeatureFlagService, FeatureFlagShareMiddleware, type FeatureFlagToken, type FlagManifest, type FlagValue, type FlagshipBindingName };
|
|
166
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/feature-flags.module.ts","../src/services/feature-flag.service.ts","../src/feature-flag-share.middleware.ts","../src/feature-flags.tokens.ts","../src/feature-flags.error.ts"],"mappings":";;;;;;;;;;;AAkCA;;;;;;;;;;;;;;;;;;;;;;cAKa,iBAAA,YAA6B,iBAAA;EAiBpB;EAfpB,eAAA,CAAgB,MAAA,EAAQ,MAAA;EAeiE;EAAA,OAVlF,OAAA,CAAQ,OAAA,EAAS,wBAAA,GAA2B,aAAA;;SAU5C,YAAA,CAAa,OAAA,EAAS,kBAAA,CAAmB,wBAAA,IAA4B,aAAA;AAAA;;;;;;;AAtB9E;;;;;;;;;;;;;;;;;;;;;;cCMa,kBAAA;EAAA,iBAO6C,OAAA;EAAA,iBACJ,GAAA;EAAA,iBACI,aAAA;EAAA,iBARvC,IAAA;EAAA,QACT,WAAA;EAAA,QACA,OAAA;EAAA,QACA,QAAA;cAGgD,OAAA,EAAS,wBAAA,EACb,GAAA,EAAK,UAAA,EACD,aAAA,EAAe,aAAA;EAT1C;;;;;;EAuB7B,GAAA,CAAI,OAAA,EAAS,mBAAA,GAAsB,kBAAA;EAe0B;EAAA,IAPzD,GAAA,CAAA;EAYqE;EALnE,GAAA,CAAI,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAUlB;EALjE,eAAA,CAAgB,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAU9B;EALjE,cAAA,CAAe,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAU5B;EALjE,cAAA,CAAe,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA;EAKqB;EAAlH,cAAA,kBAAA,CAAiC,OAAA,UAAiB,YAAA,GAAe,CAAA,EAAG,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,CAAA;EAK7C;EAArE,iBAAA,CAAkB,OAAA,UAAiB,YAAA,YAAwB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAAR;EAKjG,gBAAA,CAAiB,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAAA;EAKvG,gBAAA,CAAiB,OAAA,UAAiB,YAAA,WAAuB,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;EAApC;EAKnE,gBAAA,kBAAA,CAAmC,OAAA,UAAiB,YAAA,GAAe,CAAA,EAAG,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,yBAAA,CAA0B,CAAA;EAL/C;;;;;EAc/F,GAAA,CAAI,OAAA,GAAU,yBAAA,GAA4B,OAAA,CAAQ,MAAA,SAAe,SAAA;EAAA,QAa/D,MAAA;EAb+D;EAAA,QA+BzD,OAAA;EA/BkC;EAAA,QAsCxC,QAAA;EAtC+C;EAAA,QA6C/C,QAAA;AAAA;;;;YCnKE,aAAA;IACR,KAAA,CAAM,GAAA,UAAa,KAAA;EAAA;AAAA;AFwBvB;;;;;;;;;AAAA,cEVa,0BAAA,YAAsC,UAAA;EAAA,iBAEkB,KAAA;cAAA,KAAA,EAAO,kBAAA;EAGpE,MAAA,CAAO,GAAA,EAAK,aAAA,EAAe,IAAA,EAAM,IAAA,GAAO,OAAA;AAAA;;;;;;;;;cCvBnC,mBAAA;EHiCA,8DG5BH,OAAA;WAAA,kBAAA;AAAA;AAAA,KAEE,gBAAA,WAA2B,mBAAA,eAAkC,mBAAA;;;;;;;;;AHqBzE;cIzBa,gBAAA,SAAyB,gBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { Module } from "stratal/module";
|
|
2
|
+
import { DI_TOKENS, Request, Transient, inject } from "stratal/di";
|
|
3
|
+
import { ROUTER_TOKENS } from "stratal/router";
|
|
4
|
+
import { ApplicationError } from "stratal/errors";
|
|
5
|
+
//#region src/feature-flags.tokens.ts
|
|
6
|
+
/**
|
|
7
|
+
* DI tokens for the feature-flags module.
|
|
8
|
+
*
|
|
9
|
+
* Use `Symbol.for(...)` so the tokens resolve to the same symbol across module
|
|
10
|
+
* boundaries (the global symbol registry).
|
|
11
|
+
*/
|
|
12
|
+
const FEATURE_FLAG_TOKENS = {
|
|
13
|
+
/** The resolved {@link FeatureFlagModuleOptions}. */
|
|
14
|
+
Options: Symbol.for("stratal:feature-flags:options"),
|
|
15
|
+
/** The request-scoped {@link FeatureFlagService} bound to the default app. */
|
|
16
|
+
FeatureFlagService: Symbol.for("stratal:feature-flags:service")
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region \0@oxc-project+runtime@0.129.0/helpers/decorateMetadata.js
|
|
20
|
+
function __decorateMetadata(k, v) {
|
|
21
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region \0@oxc-project+runtime@0.129.0/helpers/decorateParam.js
|
|
25
|
+
function __decorateParam(paramIndex, decorator) {
|
|
26
|
+
return function(target, key) {
|
|
27
|
+
decorator(target, key, paramIndex);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region \0@oxc-project+runtime@0.129.0/helpers/decorate.js
|
|
32
|
+
function __decorate(decorators, target, key, desc) {
|
|
33
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
34
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
35
|
+
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;
|
|
36
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/feature-flag-share.middleware.ts
|
|
40
|
+
let FeatureFlagShareMiddleware = class FeatureFlagShareMiddleware {
|
|
41
|
+
flags;
|
|
42
|
+
constructor(flags) {
|
|
43
|
+
this.flags = flags;
|
|
44
|
+
}
|
|
45
|
+
async handle(ctx, next) {
|
|
46
|
+
if (ctx.c.req.method === "GET" && typeof ctx.share === "function") ctx.share("featureFlags", await this.flags.all());
|
|
47
|
+
await next();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
FeatureFlagShareMiddleware = __decorate([
|
|
51
|
+
Transient(),
|
|
52
|
+
__decorateParam(0, inject(FEATURE_FLAG_TOKENS.FeatureFlagService)),
|
|
53
|
+
__decorateMetadata("design:paramtypes", [Object])
|
|
54
|
+
], FeatureFlagShareMiddleware);
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/feature-flags.error.ts
|
|
57
|
+
/**
|
|
58
|
+
* Thrown for feature-flag misconfiguration — an unknown app or a Flagship
|
|
59
|
+
* binding that is not present on the Worker environment.
|
|
60
|
+
*
|
|
61
|
+
* Note: flag *evaluation* never throws; the binding returns the supplied
|
|
62
|
+
* default value on error.
|
|
63
|
+
*/
|
|
64
|
+
var FeatureFlagError = class extends ApplicationError {};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/services/feature-flag.service.ts
|
|
67
|
+
var _FeatureFlagService;
|
|
68
|
+
let FeatureFlagService = _FeatureFlagService = class FeatureFlagService {
|
|
69
|
+
options;
|
|
70
|
+
env;
|
|
71
|
+
routerContext;
|
|
72
|
+
apps = /* @__PURE__ */ new Map();
|
|
73
|
+
bindingName;
|
|
74
|
+
binding;
|
|
75
|
+
manifest;
|
|
76
|
+
constructor(options, env, routerContext) {
|
|
77
|
+
this.options = options;
|
|
78
|
+
this.env = env;
|
|
79
|
+
this.routerContext = routerContext;
|
|
80
|
+
for (const app of options.apps) this.apps.set(app.binding, app);
|
|
81
|
+
this.bindTo(options.default ?? options.apps[0]?.binding);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Switch to a different configured Flagship app.
|
|
85
|
+
*
|
|
86
|
+
* Returns a new immutable instance bound to `binding`; the original is
|
|
87
|
+
* unchanged. The binding must be declared in the module's `apps`.
|
|
88
|
+
*/
|
|
89
|
+
use(binding) {
|
|
90
|
+
if (binding === this.bindingName) return this;
|
|
91
|
+
const instance = new _FeatureFlagService(this.options, this.env, this.routerContext);
|
|
92
|
+
instance.bindTo(binding);
|
|
93
|
+
return instance;
|
|
94
|
+
}
|
|
95
|
+
/** The binding name this instance currently targets. */
|
|
96
|
+
get app() {
|
|
97
|
+
return this.bindingName;
|
|
98
|
+
}
|
|
99
|
+
/** Returns the raw flag value without type checking. */
|
|
100
|
+
async get(flagKey, defaultValue, context) {
|
|
101
|
+
return this.binding.get(flagKey, this.fallback(flagKey, defaultValue), await this.context(context));
|
|
102
|
+
}
|
|
103
|
+
/** Returns the flag value as a `boolean`. */
|
|
104
|
+
async getBooleanValue(flagKey, defaultValue, context) {
|
|
105
|
+
return this.binding.getBooleanValue(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context));
|
|
106
|
+
}
|
|
107
|
+
/** Returns the flag value as a `string`. */
|
|
108
|
+
async getStringValue(flagKey, defaultValue, context) {
|
|
109
|
+
return this.binding.getStringValue(flagKey, this.fallback(flagKey, defaultValue, ""), await this.context(context));
|
|
110
|
+
}
|
|
111
|
+
/** Returns the flag value as a `number`. */
|
|
112
|
+
async getNumberValue(flagKey, defaultValue, context) {
|
|
113
|
+
return this.binding.getNumberValue(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context));
|
|
114
|
+
}
|
|
115
|
+
/** Returns the flag value as a typed object. */
|
|
116
|
+
async getObjectValue(flagKey, defaultValue, context) {
|
|
117
|
+
return this.binding.getObjectValue(flagKey, this.fallback(flagKey, defaultValue, {}), await this.context(context));
|
|
118
|
+
}
|
|
119
|
+
/** Returns the `boolean` flag value with evaluation metadata. */
|
|
120
|
+
async getBooleanDetails(flagKey, defaultValue, context) {
|
|
121
|
+
return this.binding.getBooleanDetails(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context));
|
|
122
|
+
}
|
|
123
|
+
/** Returns the `string` flag value with evaluation metadata. */
|
|
124
|
+
async getStringDetails(flagKey, defaultValue, context) {
|
|
125
|
+
return this.binding.getStringDetails(flagKey, this.fallback(flagKey, defaultValue, ""), await this.context(context));
|
|
126
|
+
}
|
|
127
|
+
/** Returns the `number` flag value with evaluation metadata. */
|
|
128
|
+
async getNumberDetails(flagKey, defaultValue, context) {
|
|
129
|
+
return this.binding.getNumberDetails(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context));
|
|
130
|
+
}
|
|
131
|
+
/** Returns the typed object flag value with evaluation metadata. */
|
|
132
|
+
async getObjectDetails(flagKey, defaultValue, context) {
|
|
133
|
+
return this.binding.getObjectDetails(flagKey, this.fallback(flagKey, defaultValue, {}), await this.context(context));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Evaluates every flag declared in the current app's manifest and returns a
|
|
137
|
+
* `{ key: value }` map. The evaluation method is chosen from each declared
|
|
138
|
+
* default's type. Powers the Inertia auto-share.
|
|
139
|
+
*/
|
|
140
|
+
async all(context) {
|
|
141
|
+
const merged = await this.context(context);
|
|
142
|
+
const keys = Object.keys(this.manifest);
|
|
143
|
+
const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)));
|
|
144
|
+
const result = {};
|
|
145
|
+
keys.forEach((key, i) => {
|
|
146
|
+
result[key] = values[i];
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
bindTo(name) {
|
|
151
|
+
if (!name) throw new FeatureFlagError("No feature flag apps configured. Provide at least one app in FeatureFlagModule.forRoot({ apps: [...] }).");
|
|
152
|
+
const app = this.apps.get(name);
|
|
153
|
+
if (!app) throw new FeatureFlagError(`Feature flag app "${name}" is not configured.`);
|
|
154
|
+
const binding = this.env[name];
|
|
155
|
+
if (!binding) throw new FeatureFlagError(`Flagship binding "${name}" was not found in the environment.`);
|
|
156
|
+
this.bindingName = name;
|
|
157
|
+
this.binding = binding;
|
|
158
|
+
this.manifest = app.flags ?? {};
|
|
159
|
+
}
|
|
160
|
+
/** Resolves the merged evaluation context (default context + per-call override). */
|
|
161
|
+
async context(callContext) {
|
|
162
|
+
if (!this.options.context || !this.routerContext) return callContext;
|
|
163
|
+
const base = await this.options.context(this.routerContext);
|
|
164
|
+
return callContext ? {
|
|
165
|
+
...base,
|
|
166
|
+
...callContext
|
|
167
|
+
} : base;
|
|
168
|
+
}
|
|
169
|
+
/** Picks the default: explicit arg, then manifest, then the type's zero value. */
|
|
170
|
+
fallback(flagKey, provided, zero) {
|
|
171
|
+
if (provided !== void 0) return provided;
|
|
172
|
+
if (flagKey in this.manifest) return this.manifest[flagKey];
|
|
173
|
+
return zero;
|
|
174
|
+
}
|
|
175
|
+
/** Evaluates a single flag, choosing the method from the declared default's type. */
|
|
176
|
+
evaluate(flagKey, declared, context) {
|
|
177
|
+
switch (typeof declared) {
|
|
178
|
+
case "boolean": return this.binding.getBooleanValue(flagKey, declared, context);
|
|
179
|
+
case "number": return this.binding.getNumberValue(flagKey, declared, context);
|
|
180
|
+
case "string": return this.binding.getStringValue(flagKey, declared, context);
|
|
181
|
+
default: return this.binding.getObjectValue(flagKey, declared, context);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
FeatureFlagService = _FeatureFlagService = __decorate([
|
|
186
|
+
Request(FEATURE_FLAG_TOKENS.FeatureFlagService),
|
|
187
|
+
__decorateParam(0, inject(FEATURE_FLAG_TOKENS.Options)),
|
|
188
|
+
__decorateParam(1, inject(DI_TOKENS.CloudflareEnv)),
|
|
189
|
+
__decorateParam(2, inject(ROUTER_TOKENS.RouterContext)),
|
|
190
|
+
__decorateMetadata("design:paramtypes", [
|
|
191
|
+
Object,
|
|
192
|
+
Object,
|
|
193
|
+
Object
|
|
194
|
+
])
|
|
195
|
+
], FeatureFlagService);
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/feature-flags.module.ts
|
|
198
|
+
var _FeatureFlagModule;
|
|
199
|
+
let FeatureFlagModule = _FeatureFlagModule = class FeatureFlagModule {
|
|
200
|
+
/** Auto-shares declared flags to every Inertia page (no-op without Inertia). */
|
|
201
|
+
configureRoutes(router) {
|
|
202
|
+
router.use(FeatureFlagShareMiddleware);
|
|
203
|
+
}
|
|
204
|
+
/** Configure with static options. */
|
|
205
|
+
static forRoot(options) {
|
|
206
|
+
return {
|
|
207
|
+
module: _FeatureFlagModule,
|
|
208
|
+
providers: [{
|
|
209
|
+
provide: FEATURE_FLAG_TOKENS.Options,
|
|
210
|
+
useValue: options
|
|
211
|
+
}]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/** Configure with an async factory (when options depend on other services). */
|
|
215
|
+
static forRootAsync(options) {
|
|
216
|
+
return {
|
|
217
|
+
module: _FeatureFlagModule,
|
|
218
|
+
providers: [{
|
|
219
|
+
provide: FEATURE_FLAG_TOKENS.Options,
|
|
220
|
+
useFactory: options.useFactory,
|
|
221
|
+
inject: options.inject
|
|
222
|
+
}]
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
FeatureFlagModule = _FeatureFlagModule = __decorate([Module({ providers: [{
|
|
227
|
+
provide: FEATURE_FLAG_TOKENS.FeatureFlagService,
|
|
228
|
+
useClass: FeatureFlagService
|
|
229
|
+
}] })], FeatureFlagModule);
|
|
230
|
+
//#endregion
|
|
231
|
+
export { FEATURE_FLAG_TOKENS, FeatureFlagError, FeatureFlagModule, FeatureFlagService, FeatureFlagShareMiddleware };
|
|
232
|
+
|
|
233
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/feature-flags.tokens.ts","../src/feature-flag-share.middleware.ts","../src/feature-flags.error.ts","../src/services/feature-flag.service.ts","../src/feature-flags.module.ts"],"sourcesContent":["/**\n * DI tokens for the feature-flags module.\n *\n * Use `Symbol.for(...)` so the tokens resolve to the same symbol across module\n * boundaries (the global symbol registry).\n */\nexport const FEATURE_FLAG_TOKENS = {\n /** The resolved {@link FeatureFlagModuleOptions}. */\n Options: Symbol.for('stratal:feature-flags:options'),\n /** The request-scoped {@link FeatureFlagService} bound to the default app. */\n FeatureFlagService: Symbol.for('stratal:feature-flags:service'),\n} as const\n\nexport type FeatureFlagToken = (typeof FEATURE_FLAG_TOKENS)[keyof typeof FEATURE_FLAG_TOKENS]\n","import { Transient, inject } from 'stratal/di'\nimport type { Middleware, Next, RouterContext } from 'stratal/router'\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens'\nimport type { FeatureFlagService } from './services/feature-flag.service'\n\n// `ctx.share` is contributed at runtime by `@stratal/inertia` (an optional peer).\n// Declared here so this package types the call without importing Inertia; the\n// signature matches Inertia's, so the declarations merge when both are present.\ndeclare module 'stratal/router' {\n interface RouterContext {\n share(key: string, value: unknown): void\n }\n}\n\n/**\n * Evaluates the declared flag manifest for the default app and shares it as the\n * `featureFlags` prop on every Inertia page rendered during the request.\n *\n * Only runs on `GET` requests — page renders (full visits and partial reloads)\n * are always `GET`, so mutating API calls don't trigger evaluation. No-ops when\n * Inertia is not installed (`ctx.share` absent), so `FeatureFlagModule` is safe\n * in pure-API workers. Registered by `FeatureFlagModule`.\n */\n@Transient()\nexport class FeatureFlagShareMiddleware implements Middleware {\n constructor(\n @inject(FEATURE_FLAG_TOKENS.FeatureFlagService) private readonly flags: FeatureFlagService,\n ) {}\n\n async handle(ctx: RouterContext, next: Next): Promise<void> {\n if (ctx.c.req.method === 'GET' && typeof ctx.share === 'function') {\n ctx.share('featureFlags', await this.flags.all())\n }\n await next()\n }\n}\n","import { ApplicationError } from 'stratal/errors'\n\n/**\n * Thrown for feature-flag misconfiguration — an unknown app or a Flagship\n * binding that is not present on the Worker environment.\n *\n * Note: flag *evaluation* never throws; the binding returns the supplied\n * default value on error.\n */\nexport class FeatureFlagError extends ApplicationError {}\n","import type { StratalEnv } from 'stratal'\nimport { DI_TOKENS, Request, inject } from 'stratal/di'\nimport { ROUTER_TOKENS, type RouterContext } from 'stratal/router'\nimport { FeatureFlagError } from '../feature-flags.error'\nimport { FEATURE_FLAG_TOKENS } from '../feature-flags.tokens'\nimport type {\n FeatureFlagApp,\n FeatureFlagModuleOptions,\n FlagManifest,\n FlagValue,\n FlagshipBindingName,\n} from '../types'\n\n/**\n * Feature Flag Service\n *\n * Type-safe wrapper around a Cloudflare Flagship binding (`env.FLAGS`). Mirrors\n * the binding's evaluation methods 1:1, with two ergonomic additions:\n *\n * - **Manifest defaults** — when you omit a default, the value declared in the\n * app's `flags` manifest is used (an explicit argument always wins).\n * - **Default context** — the module's `context` resolver is merged into every\n * evaluation (per-call context overrides it). Resolved from the current\n * request; skipped automatically outside request scope.\n *\n * Switch to another Flagship app with {@link use}. Evaluation never throws — the\n * binding returns the default value on error.\n *\n * @example\n * ```typescript\n * @inject(FEATURE_FLAG_TOKENS.FeatureFlagService)\n * private readonly flags: FeatureFlagService\n *\n * const enabled = await this.flags.getBooleanValue('new-checkout') // manifest default\n * const layout = await this.flags.use('EXPERIMENT_FLAGS').getStringValue('layout', 'v1')\n * ```\n *\n * @see https://developers.cloudflare.com/flagship/binding/\n */\n@Request(FEATURE_FLAG_TOKENS.FeatureFlagService)\nexport class FeatureFlagService {\n private readonly apps = new Map<string, FeatureFlagApp>()\n private bindingName!: string\n private binding!: Flagship\n private manifest!: FlagManifest\n\n constructor(\n @inject(FEATURE_FLAG_TOKENS.Options) private readonly options: FeatureFlagModuleOptions,\n @inject(DI_TOKENS.CloudflareEnv) private readonly env: StratalEnv,\n @inject(ROUTER_TOKENS.RouterContext) private readonly routerContext: RouterContext | null,\n ) {\n for (const app of options.apps) {\n this.apps.set(app.binding, app)\n }\n this.bindTo(options.default ?? options.apps[0]?.binding)\n }\n\n /**\n * Switch to a different configured Flagship app.\n *\n * Returns a new immutable instance bound to `binding`; the original is\n * unchanged. The binding must be declared in the module's `apps`.\n */\n use(binding: FlagshipBindingName): FeatureFlagService {\n if (binding === this.bindingName) return this\n const instance = new FeatureFlagService(this.options, this.env, this.routerContext)\n instance.bindTo(binding)\n return instance\n }\n\n /** The binding name this instance currently targets. */\n get app(): string {\n return this.bindingName\n }\n\n // ==================== EVALUATION ====================\n\n /** Returns the raw flag value without type checking. */\n async get(flagKey: string, defaultValue?: unknown, context?: FlagshipEvaluationContext): Promise<unknown> {\n return this.binding.get(flagKey, this.fallback(flagKey, defaultValue), await this.context(context))\n }\n\n /** Returns the flag value as a `boolean`. */\n async getBooleanValue(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<boolean> {\n return this.binding.getBooleanValue(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context))\n }\n\n /** Returns the flag value as a `string`. */\n async getStringValue(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<string> {\n return this.binding.getStringValue(flagKey, this.fallback(flagKey, defaultValue, ''), await this.context(context))\n }\n\n /** Returns the flag value as a `number`. */\n async getNumberValue(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<number> {\n return this.binding.getNumberValue(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context))\n }\n\n /** Returns the flag value as a typed object. */\n async getObjectValue<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<T> {\n return this.binding.getObjectValue<T>(flagKey, this.fallback(flagKey, defaultValue, {} as T), await this.context(context))\n }\n\n /** Returns the `boolean` flag value with evaluation metadata. */\n async getBooleanDetails(flagKey: string, defaultValue?: boolean, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<boolean>> {\n return this.binding.getBooleanDetails(flagKey, this.fallback(flagKey, defaultValue, false), await this.context(context))\n }\n\n /** Returns the `string` flag value with evaluation metadata. */\n async getStringDetails(flagKey: string, defaultValue?: string, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<string>> {\n return this.binding.getStringDetails(flagKey, this.fallback(flagKey, defaultValue, ''), await this.context(context))\n }\n\n /** Returns the `number` flag value with evaluation metadata. */\n async getNumberDetails(flagKey: string, defaultValue?: number, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<number>> {\n return this.binding.getNumberDetails(flagKey, this.fallback(flagKey, defaultValue, 0), await this.context(context))\n }\n\n /** Returns the typed object flag value with evaluation metadata. */\n async getObjectDetails<T extends object>(flagKey: string, defaultValue?: T, context?: FlagshipEvaluationContext): Promise<FlagshipEvaluationDetails<T>> {\n return this.binding.getObjectDetails<T>(flagKey, this.fallback(flagKey, defaultValue, {} as T), await this.context(context))\n }\n\n /**\n * Evaluates every flag declared in the current app's manifest and returns a\n * `{ key: value }` map. The evaluation method is chosen from each declared\n * default's type. Powers the Inertia auto-share.\n */\n async all(context?: FlagshipEvaluationContext): Promise<Record<string, FlagValue>> {\n const merged = await this.context(context)\n const keys = Object.keys(this.manifest)\n const values = await Promise.all(keys.map((key) => this.evaluate(key, this.manifest[key], merged)))\n const result: Record<string, FlagValue> = {}\n keys.forEach((key, i) => {\n result[key] = values[i]\n })\n return result\n }\n\n // ==================== INTERNAL ====================\n\n private bindTo(name: string | undefined): void {\n if (!name) {\n throw new FeatureFlagError('No feature flag apps configured. Provide at least one app in FeatureFlagModule.forRoot({ apps: [...] }).')\n }\n const app = this.apps.get(name)\n if (!app) {\n throw new FeatureFlagError(`Feature flag app \"${name}\" is not configured.`)\n }\n const binding = (this.env as unknown as Record<string, unknown>)[name] as Flagship | undefined\n if (!binding) {\n throw new FeatureFlagError(`Flagship binding \"${name}\" was not found in the environment.`)\n }\n this.bindingName = name\n this.binding = binding\n this.manifest = app.flags ?? {}\n }\n\n /** Resolves the merged evaluation context (default context + per-call override). */\n private async context(callContext?: FlagshipEvaluationContext): Promise<FlagshipEvaluationContext | undefined> {\n if (!this.options.context || !this.routerContext) return callContext\n const base = await this.options.context(this.routerContext)\n return callContext ? { ...base, ...callContext } : base\n }\n\n /** Picks the default: explicit arg, then manifest, then the type's zero value. */\n private fallback<T>(flagKey: string, provided: T | undefined, zero?: T): T {\n if (provided !== undefined) return provided\n if (flagKey in this.manifest) return this.manifest[flagKey] as T\n return zero as T\n }\n\n /** Evaluates a single flag, choosing the method from the declared default's type. */\n private evaluate(flagKey: string, declared: FlagValue, context?: FlagshipEvaluationContext): Promise<FlagValue> {\n switch (typeof declared) {\n case 'boolean':\n return this.binding.getBooleanValue(flagKey, declared, context)\n case 'number':\n return this.binding.getNumberValue(flagKey, declared, context)\n case 'string':\n return this.binding.getStringValue(flagKey, declared, context)\n default:\n return this.binding.getObjectValue(flagKey, declared, context)\n }\n }\n}\n","import { Module } from 'stratal/module'\nimport type { AsyncModuleOptions, DynamicModule } from 'stratal/module'\nimport type { RouteConfigurable, Router } from 'stratal/router'\nimport { FeatureFlagShareMiddleware } from './feature-flag-share.middleware'\nimport { FEATURE_FLAG_TOKENS } from './feature-flags.tokens'\nimport { FeatureFlagService } from './services/feature-flag.service'\nimport type { FeatureFlagModuleOptions } from './types'\n\n/**\n * Feature Flag Module\n *\n * Evaluates Cloudflare Flagship feature flags through the native Worker binding.\n * Declare your apps (and the flags you use) once; inject {@link FeatureFlagService}\n * to evaluate them.\n *\n * When `@stratal/inertia` is also present, declared flags are auto-shared to\n * every Inertia page as the `featureFlags` prop (read them with `useFlag` /\n * `useFeatureFlags` from `@stratal/feature-flags/react`) — no extra wiring. In\n * a pure-API worker the auto-share middleware is a no-op.\n *\n * @example\n * ```typescript\n * FeatureFlagModule.forRoot({\n * apps: [{ binding: 'FLAGS', flags: { 'new-checkout': false } }],\n * context: (ctx) => ({ userId: ctx.user().id }), // ctx.user() from @stratal/framework\n * })\n *\n * // Or async, from config namespaces:\n * FeatureFlagModule.forRootAsync({\n * inject: [flagsConfig.KEY],\n * useFactory: (cfg) => ({ apps: cfg.apps, default: cfg.default }),\n * })\n * ```\n */\n@Module({\n providers: [\n { provide: FEATURE_FLAG_TOKENS.FeatureFlagService, useClass: FeatureFlagService },\n ],\n})\nexport class FeatureFlagModule implements RouteConfigurable {\n /** Auto-shares declared flags to every Inertia page (no-op without Inertia). */\n configureRoutes(router: Router): void {\n router.use(FeatureFlagShareMiddleware)\n }\n\n /** Configure with static options. */\n static forRoot(options: FeatureFlagModuleOptions): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n { provide: FEATURE_FLAG_TOKENS.Options, useValue: options },\n ],\n }\n }\n\n /** Configure with an async factory (when options depend on other services). */\n static forRootAsync(options: AsyncModuleOptions<FeatureFlagModuleOptions>): DynamicModule {\n return {\n module: FeatureFlagModule,\n providers: [\n {\n provide: FEATURE_FLAG_TOKENS.Options,\n useFactory: options.useFactory,\n inject: options.inject,\n },\n ],\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { r as FeatureFlagRegistry } from "./types-Dxuc-7SJ.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/react/use-feature-flags.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.
|
|
6
|
+
*/
|
|
7
|
+
declare function useFeatureFlags(): Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Returns a single shared feature flag value.
|
|
10
|
+
*
|
|
11
|
+
* When you augment {@link FeatureFlagRegistry}, the key and return type are
|
|
12
|
+
* checked against your declared flags. Otherwise pass an explicit default.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const showNewCheckout = useFlag('new-checkout') // typed via FeatureFlagRegistry
|
|
17
|
+
* const layout = useFlag('layout', 'v1') // loose fallback
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K];
|
|
21
|
+
declare function useFlag<T>(key: string, defaultValue: T): T;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { useFeatureFlags, useFlag };
|
|
24
|
+
//# sourceMappingURL=react.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.mts","names":[],"sources":["../src/react/use-feature-flags.ts"],"mappings":";;;;;AAWA;iBAAgB,eAAA,CAAA,GAAmB,MAAA;;;;AAgBnC;;;;;;;;;iBAAgB,OAAA,iBAAwB,mBAAA,CAAA,CAAqB,GAAA,EAAK,CAAA,GAAI,mBAAA,CAAoB,CAAA;AAAA,iBAC1E,OAAA,GAAA,CAAW,GAAA,UAAa,YAAA,EAAc,CAAA,GAAI,CAAA"}
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { usePage } from "@inertiajs/react";
|
|
2
|
+
//#region src/react/use-feature-flags.ts
|
|
3
|
+
/**
|
|
4
|
+
* Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.
|
|
5
|
+
*/
|
|
6
|
+
function useFeatureFlags() {
|
|
7
|
+
return usePage().props.featureFlags ?? {};
|
|
8
|
+
}
|
|
9
|
+
function useFlag(key, defaultValue) {
|
|
10
|
+
const flags = useFeatureFlags();
|
|
11
|
+
return key in flags ? flags[key] : defaultValue;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { useFeatureFlags, useFlag };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=react.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.mjs","names":[],"sources":["../src/react/use-feature-flags.ts"],"sourcesContent":["import type { PageProps } from '@inertiajs/core'\nimport { usePage } from '@inertiajs/react'\nimport type { FeatureFlagRegistry } from '../types'\n\ninterface FeatureFlagsPageProps extends PageProps {\n featureFlags?: Record<string, unknown>\n}\n\n/**\n * Returns the full map of feature flags shared by `FeatureFlagInertiaModule`.\n */\nexport function useFeatureFlags(): Record<string, unknown> {\n return usePage<FeatureFlagsPageProps>().props.featureFlags ?? {}\n}\n\n/**\n * Returns a single shared feature flag value.\n *\n * When you augment {@link FeatureFlagRegistry}, the key and return type are\n * checked against your declared flags. Otherwise pass an explicit default.\n *\n * @example\n * ```tsx\n * const showNewCheckout = useFlag('new-checkout') // typed via FeatureFlagRegistry\n * const layout = useFlag('layout', 'v1') // loose fallback\n * ```\n */\nexport function useFlag<K extends keyof FeatureFlagRegistry>(key: K): FeatureFlagRegistry[K]\nexport function useFlag<T>(key: string, defaultValue: T): T\nexport function useFlag(key: string, defaultValue?: unknown): unknown {\n const flags = useFeatureFlags()\n return key in flags ? flags[key] : defaultValue\n}\n"],"mappings":";;;;;AAWA,SAAgB,kBAA2C;CACzD,OAAO,SAAgC,CAAC,MAAM,gBAAgB,EAAE;;AAiBlE,SAAgB,QAAQ,KAAa,cAAiC;CACpE,MAAM,QAAQ,iBAAiB;CAC/B,OAAO,OAAO,QAAQ,MAAM,OAAO"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { RouterContext } from "stratal/router";
|
|
2
|
+
import { StratalEnv } from "stratal";
|
|
3
|
+
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* A value a feature flag can resolve to.
|
|
7
|
+
*
|
|
8
|
+
* @see https://developers.cloudflare.com/flagship/binding/methods/
|
|
9
|
+
*/
|
|
10
|
+
type FlagValue = boolean | string | number | object;
|
|
11
|
+
/**
|
|
12
|
+
* String keys of the augmented `StratalEnv` whose value is a Flagship binding.
|
|
13
|
+
*/
|
|
14
|
+
type FlagshipBindingFromEnv = Extract<{ [K in keyof StratalEnv]: StratalEnv[K] extends Flagship ? K : never }[keyof StratalEnv], string>;
|
|
15
|
+
/**
|
|
16
|
+
* Type-safe Flagship binding name.
|
|
17
|
+
*
|
|
18
|
+
* Resolves to the union of `Flagship`-typed binding keys on the augmented
|
|
19
|
+
* `StratalEnv`. Falls back to `string` when no Flagship bindings are visible
|
|
20
|
+
* (for example library code compiled outside an app's env context).
|
|
21
|
+
*/
|
|
22
|
+
type FlagshipBindingName = [FlagshipBindingFromEnv] extends [never] ? string : FlagshipBindingFromEnv;
|
|
23
|
+
/**
|
|
24
|
+
* Augment this interface to get typed flag keys for `useFlag()` and the service.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* declare module '@stratal/feature-flags' {
|
|
29
|
+
* interface FeatureFlagRegistry {
|
|
30
|
+
* 'new-checkout': boolean
|
|
31
|
+
* 'checkout-flow': string
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
interface FeatureFlagRegistry {}
|
|
37
|
+
/**
|
|
38
|
+
* A declared set of flags and their default values.
|
|
39
|
+
*
|
|
40
|
+
* Flagship has no enumeration API, so the flags you intend to evaluate (and
|
|
41
|
+
* auto-share to the frontend) must be declared once here. The default also
|
|
42
|
+
* doubles as the type hint used to pick the evaluation method in `all()`.
|
|
43
|
+
*/
|
|
44
|
+
type FlagManifest = Record<string, FlagValue>;
|
|
45
|
+
/**
|
|
46
|
+
* A single Flagship app bound to the Worker.
|
|
47
|
+
*/
|
|
48
|
+
interface FeatureFlagApp {
|
|
49
|
+
/** Flagship binding name from your Wrangler config (type-checked against `StratalEnv`). */
|
|
50
|
+
binding: FlagshipBindingName;
|
|
51
|
+
/** Declared flags + defaults for this app. Used for manifest defaults and Inertia auto-share. */
|
|
52
|
+
flags?: FlagManifest;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Feature-flags module configuration.
|
|
56
|
+
*/
|
|
57
|
+
interface FeatureFlagModuleOptions {
|
|
58
|
+
/** One or more Flagship apps. A Worker may bind to multiple apps. */
|
|
59
|
+
apps: FeatureFlagApp[];
|
|
60
|
+
/** Default app binding used by the injected `FeatureFlagService`. Defaults to `apps[0].binding`. */
|
|
61
|
+
default?: FlagshipBindingName;
|
|
62
|
+
/**
|
|
63
|
+
* Resolves a per-request evaluation context (for example `{ userId }`) merged
|
|
64
|
+
* into every evaluation. Per-call context passed to a method overrides these.
|
|
65
|
+
* Receives the current request context; skipped outside request scope.
|
|
66
|
+
*/
|
|
67
|
+
context?: (ctx: RouterContext) => FlagshipEvaluationContext | Promise<FlagshipEvaluationContext>;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { FlagValue as a, FlagManifest as i, FeatureFlagModuleOptions as n, FlagshipBindingName as o, FeatureFlagRegistry as r, FeatureFlagApp as t };
|
|
71
|
+
//# sourceMappingURL=types-Dxuc-7SJ.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-Dxuc-7SJ.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;AAQA;;;KAAY,SAAA;;AAA8C;;KAKrD,sBAAA,GAAyB,OAAA,eACd,UAAA,GAAa,UAAA,CAAW,CAAA,UAAW,QAAA,GAAW,CAAA,iBAAkB,UAAA;;;;;;;;KAWpE,mBAAA,IAAuB,sBAAA,6BAE/B,sBAAA;;;;;;;;;;AAFJ;;;;UAiBiB,mBAAA;;;;;AASjB;;;KAAY,YAAA,GAAe,MAAA,SAAe,SAAA;;AAK1C;;UAAiB,cAAA;EAIK;EAFpB,OAAA,EAAS,mBAAA;EAAA;EAET,KAAA,GAAQ,YAAA;AAAA;;;AAMV;UAAiB,wBAAA;;EAEf,IAAA,EAAM,cAAA;EAEI;EAAV,OAAA,GAAU,mBAAA;EAMwB;;;;;EAAlC,OAAA,IAAW,GAAA,EAAK,aAAA,KAAkB,yBAAA,GAA4B,OAAA,CAAQ,yBAAA;AAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stratal/feature-flags",
|
|
3
|
+
"version": "0.0.21",
|
|
4
|
+
"description": "Cloudflare Flagship feature flags for the Stratal framework — binding API wrapper with Inertia.js auto-sharing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Temitayo Fadojutimi",
|
|
8
|
+
"homepage": "https://github.com/strataljs/stratal#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/strataljs/stratal.git",
|
|
12
|
+
"directory": "packages/feature-flags"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/strataljs/stratal/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"stratal",
|
|
19
|
+
"feature-flags",
|
|
20
|
+
"flagship",
|
|
21
|
+
"cloudflare-workers",
|
|
22
|
+
"inertia",
|
|
23
|
+
"react"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.0.0"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public",
|
|
30
|
+
"provenance": false
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"types": "./dist/index.d.mts",
|
|
40
|
+
"import": "./dist/index.mjs"
|
|
41
|
+
},
|
|
42
|
+
"./react": {
|
|
43
|
+
"types": "./dist/react.d.mts",
|
|
44
|
+
"import": "./dist/react.mjs"
|
|
45
|
+
},
|
|
46
|
+
"./package.json": "./package.json"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsdown",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"lint": "npx oxlint .",
|
|
54
|
+
"lint:fix": "npx oxlint --fix ."
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@inertiajs/react": ">=3",
|
|
58
|
+
"@stratal/inertia": ">=0.0.21",
|
|
59
|
+
"hono": ">=4",
|
|
60
|
+
"react": ">=19",
|
|
61
|
+
"react-dom": ">=19",
|
|
62
|
+
"stratal": ">=0.0.21"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"@inertiajs/react": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"@stratal/inertia": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"react": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react-dom": {
|
|
75
|
+
"optional": true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@cloudflare/workers-types": "4.20260528.1",
|
|
80
|
+
"@inertiajs/core": "^3.1.1",
|
|
81
|
+
"@inertiajs/react": "^3.1.1",
|
|
82
|
+
"@types/node": "^25.6.2",
|
|
83
|
+
"@types/react": "^19.2.14",
|
|
84
|
+
"@types/react-dom": "^19.2.3",
|
|
85
|
+
"hono": "^4.12.18",
|
|
86
|
+
"react": "^19.2.6",
|
|
87
|
+
"react-dom": "^19.2.6",
|
|
88
|
+
"stratal": "workspace:*",
|
|
89
|
+
"tsdown": "^0.22.0",
|
|
90
|
+
"typescript": "^6.0.3",
|
|
91
|
+
"vitest": "~4.1.5"
|
|
92
|
+
}
|
|
93
|
+
}
|