@stravigor/banner 0.4.4 → 0.4.5

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,66 @@
1
+ # @stravigor/banner
2
+
3
+ Feature flags for the [Strav](https://www.npmjs.com/package/@stravigor/core) framework. Define, scope, and toggle features with database persistence, in-memory drivers, and per-user/team targeting.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @stravigor/banner
9
+ bun strav package:install banner
10
+ ```
11
+
12
+ Requires `@stravigor/core` as a peer dependency.
13
+
14
+ ## Setup
15
+
16
+ ```ts
17
+ import { BannerProvider } from '@stravigor/banner'
18
+
19
+ app.use(new BannerProvider())
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { banner } from '@stravigor/banner'
26
+
27
+ // Check if a feature is active
28
+ if (await banner.active('dark-mode')) {
29
+ // feature is enabled
30
+ }
31
+
32
+ // Scoped to a user or team
33
+ if (await banner.for(user).active('beta-dashboard')) {
34
+ // enabled for this user
35
+ }
36
+
37
+ // Rich values
38
+ const limit = await banner.value('upload-limit', 10)
39
+ ```
40
+
41
+ ## Middleware
42
+
43
+ ```ts
44
+ import { ensureFeature } from '@stravigor/banner'
45
+
46
+ router.get('/beta', ensureFeature('beta-dashboard'), betaHandler)
47
+ ```
48
+
49
+ ## Drivers
50
+
51
+ - **Database** — persistent feature flags in `_strav_features`
52
+ - **Array** — in-memory driver for testing
53
+
54
+ ## CLI
55
+
56
+ ```bash
57
+ bun strav banner:setup # Create the features table
58
+ ```
59
+
60
+ ## Documentation
61
+
62
+ See the full [Banner guide](../../guides/banner.md).
63
+
64
+ ## License
65
+
66
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stravigor/banner",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "type": "module",
5
5
  "description": "Feature flags for the Strav framework",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  "tsconfig.json"
19
19
  ],
20
20
  "peerDependencies": {
21
- "@stravigor/core": "0.4.3"
21
+ "@stravigor/core": "0.4.4"
22
22
  },
23
23
  "scripts": {
24
24
  "test": "bun test tests/",
@@ -65,10 +65,7 @@ export default class BannerManager {
65
65
 
66
66
  /** Get all defined feature names (closures + classes). */
67
67
  static defined(): string[] {
68
- return [
69
- ...BannerManager._definitions.keys(),
70
- ...BannerManager._classFeatures.keys(),
71
- ]
68
+ return [...BannerManager._definitions.keys(), ...BannerManager._classFeatures.keys()]
72
69
  }
73
70
 
74
71
  // ── Scope helpers ──────────────────────────────────────────────────
@@ -76,9 +73,7 @@ export default class BannerManager {
76
73
  static serializeScope(scope: Scopeable | null | undefined): ScopeKey {
77
74
  if (!scope) return GLOBAL_SCOPE
78
75
  const type =
79
- typeof scope.featureScope === 'function'
80
- ? scope.featureScope()
81
- : scope.constructor.name
76
+ typeof scope.featureScope === 'function' ? scope.featureScope() : scope.constructor.name
82
77
  return `${type}:${scope.id}`
83
78
  }
84
79
 
@@ -118,7 +113,7 @@ export default class BannerManager {
118
113
  }
119
114
 
120
115
  static async inactive(feature: string, scope?: Scopeable | null): Promise<boolean> {
121
- return !await BannerManager.active(feature, scope)
116
+ return !(await BannerManager.active(feature, scope))
122
117
  }
123
118
 
124
119
  static async when<TActive, TInactive>(
@@ -76,11 +76,16 @@ export function register(program: Command): void {
76
76
  console.log(chalk.bold(`Stored feature flags (${names.length}):\n`))
77
77
  for (const name of names) {
78
78
  const records = await BannerManager.store().allFor(name)
79
- console.log(` ${chalk.cyan(name)} ${chalk.dim(`(${records.length} scope${records.length === 1 ? '' : 's'})`)}`)
79
+ console.log(
80
+ ` ${chalk.cyan(name)} ${chalk.dim(`(${records.length} scope${records.length === 1 ? '' : 's'})`)}`
81
+ )
80
82
  for (const r of records) {
81
- const val = typeof r.value === 'boolean'
82
- ? (r.value ? chalk.green('active') : chalk.red('inactive'))
83
- : chalk.yellow(JSON.stringify(r.value))
83
+ const val =
84
+ typeof r.value === 'boolean'
85
+ ? r.value
86
+ ? chalk.green('active')
87
+ : chalk.red('inactive')
88
+ : chalk.yellow(JSON.stringify(r.value))
84
89
  console.log(` ${chalk.dim(r.scope)} → ${val}`)
85
90
  }
86
91
  }
@@ -33,7 +33,9 @@ export class ArrayDriver implements FeatureStore {
33
33
  })
34
34
  }
35
35
 
36
- async setMany(entries: Array<{ feature: string; scope: ScopeKey; value: unknown }>): Promise<void> {
36
+ async setMany(
37
+ entries: Array<{ feature: string; scope: ScopeKey; value: unknown }>
38
+ ): Promise<void> {
37
39
  for (const e of entries) await this.set(e.feature, e.scope, e.value)
38
40
  }
39
41
 
@@ -61,7 +61,9 @@ export class DatabaseDriver implements FeatureStore {
61
61
  `
62
62
  }
63
63
 
64
- async setMany(entries: Array<{ feature: string; scope: ScopeKey; value: unknown }>): Promise<void> {
64
+ async setMany(
65
+ entries: Array<{ feature: string; scope: ScopeKey; value: unknown }>
66
+ ): Promise<void> {
65
67
  if (entries.length === 0) return
66
68
  for (const e of entries) await this.set(e.feature, e.scope, e.value)
67
69
  }
package/src/helpers.ts CHANGED
@@ -1,12 +1,7 @@
1
1
  import BannerManager from './banner_manager.ts'
2
2
  import PendingScopedFeature from './pending_scope.ts'
3
3
  import type { FeatureStore } from './feature_store.ts'
4
- import type {
5
- Scopeable,
6
- FeatureResolver,
7
- FeatureClassConstructor,
8
- DriverConfig,
9
- } from './types.ts'
4
+ import type { Scopeable, FeatureResolver, FeatureClassConstructor, DriverConfig } from './types.ts'
10
5
 
11
6
  /**
12
7
  * Banner helper — the primary convenience API.
@@ -20,7 +20,7 @@ export function ensureFeature(
20
20
  return async (ctx, next) => {
21
21
  const scope = scopeExtractor
22
22
  ? scopeExtractor(ctx)
23
- : (ctx.get('user') as Scopeable | undefined) ?? null
23
+ : ((ctx.get('user') as Scopeable | undefined) ?? null)
24
24
 
25
25
  const isActive = await BannerManager.active(feature, scope)
26
26