@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 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
@@ -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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"}
@@ -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
+ }