flaglint 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +53 -440
- package/dist/bin/flaglint.js +30 -7
- package/package.json +10 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Added truth-gate coverage and scanner support for destructured CommonJS
|
|
13
|
+
LaunchDarkly initializer aliases such as
|
|
14
|
+
`const { init: ldInit } = require("@launchdarkly/node-server-sdk")`. This
|
|
15
|
+
keeps the public alias/CJS provenance claim backed across scan, validate,
|
|
16
|
+
validation SARIF, dry-run, and guarded apply flows.
|
|
17
|
+
|
|
18
|
+
## [0.5.2] - 2026-05-27
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Fixed LaunchDarkly client provenance for aliased named `init` imports from both
|
|
23
|
+
supported Node.js server SDK package names. Calls initialized via
|
|
24
|
+
`import { init as ldInit } from "@launchdarkly/node-server-sdk"` or legacy
|
|
25
|
+
`launchdarkly-node-server-sdk` are now consistently detected by `scan`,
|
|
26
|
+
enforced by `validate --no-direct-launchdarkly`, emitted in validation SARIF,
|
|
27
|
+
and handled by existing guarded migration dry-run/apply flows.
|
|
28
|
+
- No migration safety boundary changed: `migrate --apply` still requires a proven
|
|
29
|
+
OpenFeature client binding and continues to skip dynamic keys, detail
|
|
30
|
+
evaluations, bulk calls, unknown fallbacks, and ambiguous cases.
|
|
31
|
+
|
|
10
32
|
## [0.5.1] - 2026-05-27
|
|
11
33
|
|
|
12
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,49 +1,12 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="docs/assets/logo.png" alt="FlagLint" width="400" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<p align="center">
|
|
6
|
-
<strong>Standardize LaunchDarkly usage on OpenFeature.</strong>
|
|
7
|
-
</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<a href="https://github.com/flaglint/flaglint/actions/workflows/ci.yml">
|
|
11
|
-
<img src="https://github.com/flaglint/flaglint/actions/workflows/ci.yml/badge.svg" alt="CI" />
|
|
12
|
-
</a>
|
|
13
|
-
<a href="https://www.npmjs.com/package/flaglint">
|
|
14
|
-
<img src="https://img.shields.io/npm/v/flaglint.svg" alt="npm version" />
|
|
15
|
-
</a>
|
|
16
|
-
<a href="https://www.npmjs.com/package/flaglint">
|
|
17
|
-
<img src="https://img.shields.io/npm/dm/flaglint.svg" alt="downloads" />
|
|
18
|
-
</a>
|
|
19
|
-
<a href="https://opensource.org/licenses/MIT">
|
|
20
|
-
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="MIT License" />
|
|
21
|
-
</a>
|
|
22
|
-
</p>
|
|
23
|
-
|
|
24
1
|
# FlagLint
|
|
25
2
|
|
|
26
|
-
Standardize LaunchDarkly usage on OpenFeature
|
|
27
|
-
|
|
28
|
-
FlagLint inventories direct LaunchDarkly Node.js SDK calls, generates reviewable migration
|
|
29
|
-
plans, and prevents new vendor-coupled flag access from entering your codebase.
|
|
30
|
-
|
|
31
|
-
**LaunchDarkly remains your provider. OpenFeature becomes the evaluation API your application code calls.**
|
|
32
|
-
|
|
33
|
-
Docs: [Getting Started](https://flaglint.dev/docs/getting-started) · [Commands](https://flaglint.dev/docs/commands/scan) · [Supported Scope](https://flaglint.dev/docs/supported-scope) · [CI Integration](https://flaglint.dev/docs/ci-github-actions) · [Enterprise Demo](https://flaglint.dev/docs/demo)
|
|
34
|
-
|
|
35
|
-
[View enterprise demo source ->](./examples/enterprise-checkout-service)
|
|
36
|
-
|
|
37
|
-
---
|
|
3
|
+
**Standardize LaunchDarkly usage on OpenFeature.**
|
|
38
4
|
|
|
39
|
-
|
|
5
|
+
FlagLint inventories direct LaunchDarkly Node.js SDK calls, generates reviewable migration plans,
|
|
6
|
+
and prevents new vendor-coupled flag access from entering your codebase.
|
|
7
|
+
LaunchDarkly remains your provider. OpenFeature becomes the evaluation API your application code calls.
|
|
40
8
|
|
|
41
|
-
|
|
42
|
-
|------|---------|---------|
|
|
43
|
-
| 1 | `flaglint scan` | AST inventory of every direct LD Node server SDK call |
|
|
44
|
-
| 2 | `flaglint migrate --dry-run` | Reviewable before/after diffs; provider setup guidance appears when needed |
|
|
45
|
-
| 3 | `flaglint migrate --apply` | Apply only guarded, provably automatable transformations |
|
|
46
|
-
| 4 | `flaglint validate --no-direct-launchdarkly` | CI gate: exit 1 if direct LD calls remain |
|
|
9
|
+
**[Documentation](https://flaglint.dev/docs/quickstart)** · **[Quickstart](https://flaglint.dev/docs/quickstart)** · **[Enterprise Demo](https://flaglint.dev/docs/enterprise-demo)** · **[npm](https://npmjs.com/package/flaglint)** · **[Issues](https://github.com/flaglint/flaglint/issues)**
|
|
47
10
|
|
|
48
11
|
---
|
|
49
12
|
|
|
@@ -53,444 +16,94 @@ Docs: [Getting Started](https://flaglint.dev/docs/getting-started) · [Commands]
|
|
|
53
16
|
npx flaglint scan ./src
|
|
54
17
|
```
|
|
55
18
|
|
|
56
|
-
Example output:
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
✓ 15 flag usages found across 6 unique flags (48ms)
|
|
60
|
-
ℹ 1 dynamic flag key(s) require manual review
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Markdown report excerpt:
|
|
64
|
-
|
|
65
|
-
```markdown
|
|
66
|
-
## Flag Inventory
|
|
67
|
-
| Flag Key | Usages | Files | Call Types |
|
|
68
|
-
|----------|--------|-------|------------|
|
|
69
|
-
| checkout-v2 | 3 | 2 | boolVariation |
|
|
70
|
-
| color-theme | 1 | 1 | stringVariation |
|
|
71
|
-
| timeout-ms | 1 | 1 | numberVariation |
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### JSON output (`--format json`)
|
|
75
|
-
|
|
76
|
-
Pipe-friendly. Every usage includes file, line, call type, and staleness signals:
|
|
77
|
-
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"flagKey": "checkout-v2",
|
|
81
|
-
"isDynamic": false,
|
|
82
|
-
"file": "src/services/checkout.ts",
|
|
83
|
-
"line": 14,
|
|
84
|
-
"callType": "boolVariation",
|
|
85
|
-
"stalenessSignals": []
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Installation
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
npm install -g flaglint
|
|
95
|
-
# or use without installing
|
|
96
|
-
npx flaglint
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Requires Node.js 20 or newer. CI validates FlagLint on Node.js 20 and 22.
|
|
100
|
-
|
|
101
|
-
---
|
|
102
|
-
|
|
103
|
-
## Commands
|
|
104
|
-
|
|
105
|
-
### `flaglint scan [dir]`
|
|
106
|
-
|
|
107
|
-
AST-based inventory of direct LaunchDarkly Node.js server SDK calls.
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
flaglint scan ./src
|
|
111
|
-
flaglint scan --format json --output report.json
|
|
112
|
-
flaglint scan --format html --output report.html
|
|
113
|
-
flaglint scan --format sarif --output flaglint.sarif
|
|
114
19
|
```
|
|
20
|
+
✔ Scanning 5 files...
|
|
21
|
+
✔ Found 20 direct LaunchDarkly Node SDK calls across 11 unique flags
|
|
22
|
+
⚡ 6 dynamic flag keys — manual review required
|
|
23
|
+
↳ 2 detail method calls — manual review required
|
|
115
24
|
|
|
116
|
-
|
|
117
|
-
|--------|---------|-------------|
|
|
118
|
-
| `--format` | `markdown` | Output format: `json`, `markdown`, `html`, `sarif` |
|
|
119
|
-
| `--output` | stdout | Write report to file |
|
|
120
|
-
| `--config` | auto-detect | Path to a config file |
|
|
121
|
-
| `--exclude-tests` | — | Exclude test files from scan results |
|
|
122
|
-
|
|
123
|
-
Exit code `0` when no staleness signals detected, `1` when staleness signals are present —
|
|
124
|
-
enabling CI visibility into flag usage patterns.
|
|
125
|
-
|
|
126
|
-
---
|
|
127
|
-
|
|
128
|
-
### `flaglint migrate [dir]`
|
|
129
|
-
|
|
130
|
-
Analyzes migration readiness and generates an OpenFeature migration plan.
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
flaglint migrate ./src # write MIGRATION.md
|
|
134
|
-
flaglint migrate --dry-run # reviewable diffs to stdout
|
|
135
|
-
flaglint migrate --apply # guarded: apply only provably automatable transformations in-place
|
|
136
|
-
flaglint migrate --apply --allow-dirty # apply even on a dirty working tree
|
|
137
|
-
flaglint migrate --output plan.md # write to custom file
|
|
138
|
-
flaglint migrate --exclude-tests # skip test and spec files
|
|
25
|
+
→ Run flaglint migrate --dry-run for a reviewable migration diff
|
|
139
26
|
```
|
|
140
27
|
|
|
141
|
-
|
|
142
|
-
|--------|---------|-------------|
|
|
143
|
-
| `--output` | `MIGRATION.md` | Write migration plan to file |
|
|
144
|
-
| `--dry-run` | — | Print reviewable diffs to stdout; includes provider setup guidance when a diff needs it |
|
|
145
|
-
| `--apply` | — | Apply automatable transformations in-place (requires clean git tree) |
|
|
146
|
-
| `--allow-dirty` | — | Override dirty-tree guard for `--apply` |
|
|
147
|
-
| `--config` | auto-detect | Path to a config file |
|
|
148
|
-
| `--exclude-tests` | — | Skip `*.test.*`, `*.spec.*`, `__tests__/`, `tests/` |
|
|
149
|
-
|
|
150
|
-
**`--apply` safety contracts:**
|
|
151
|
-
- Refuses on a dirty git working tree unless `--allow-dirty`
|
|
152
|
-
- Skips any file that does not contain a proven OpenFeature client binding:
|
|
153
|
-
either a local `OpenFeature.getClient()` binding or a configured imported
|
|
154
|
-
shared client allowlisted with `openFeatureClientBindings`
|
|
155
|
-
- Imported client matching uses glob-safe `modulePatterns`; aliased imports
|
|
156
|
-
preserve the local identifier; TypeScript ESM `.js` runtime import specifiers
|
|
157
|
-
are recognized; ambiguous or unconfigured imports skip safely
|
|
158
|
-
- Provider/bootstrap setup is never inserted automatically
|
|
159
|
-
- Never touches detail methods, dynamic keys, unknown fallbacks, or bulk calls
|
|
160
|
-
- Preserves `await` and original call arguments exactly
|
|
161
|
-
- Idempotent: re-running with the same analysis has no effect
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
### `flaglint validate [dir]`
|
|
166
|
-
|
|
167
|
-
Validates that your codebase complies with feature flag policy rules.
|
|
168
|
-
Designed for CI enforcement after migration is complete.
|
|
28
|
+
Preview the migration before changing anything:
|
|
169
29
|
|
|
170
30
|
```bash
|
|
171
|
-
flaglint
|
|
172
|
-
flaglint validate --no-direct-launchdarkly # exit 1 on any direct LD eval call
|
|
173
|
-
flaglint validate --no-direct-launchdarkly \
|
|
174
|
-
--bootstrap-exclude src/provider/setup.ts # allow specific bootstrap file
|
|
175
|
-
flaglint validate --no-direct-launchdarkly \
|
|
176
|
-
--bootstrap-exclude "src/provider/**" # allow all provider-directory files
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
| Option | Default | Description |
|
|
180
|
-
|--------|---------|-------------|
|
|
181
|
-
| `--no-direct-launchdarkly` | — | Exit 1 if any direct LD Node server evaluation calls found |
|
|
182
|
-
| `--bootstrap-exclude <glob>` | — | Repeatable glob; matching files excluded from violations |
|
|
183
|
-
| `--config` | auto-detect | Path to a config file |
|
|
184
|
-
|
|
185
|
-
Exit codes: `0` = passed, `1` = violations found, `130` = SIGINT.
|
|
186
|
-
|
|
187
|
-
**Example pass output:**
|
|
188
|
-
```
|
|
189
|
-
✓ validate --no-direct-launchdarkly: no direct LaunchDarkly evaluation calls found.
|
|
190
|
-
Scanned 42 file(s).
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Example fail output:**
|
|
194
|
-
```
|
|
195
|
-
✗ validate --no-direct-launchdarkly: 2 direct LaunchDarkly evaluation call(s) found.
|
|
196
|
-
|
|
197
|
-
src/services/checkout.ts:42:8 — boolVariation("checkout-v2")
|
|
198
|
-
src/services/pricing.ts:17:4 — boolVariation(dynamic key — manual review required)
|
|
199
|
-
|
|
200
|
-
These files must migrate to OpenFeature before this rule passes.
|
|
201
|
-
Run `flaglint migrate --dry-run` to review the migration plan.
|
|
31
|
+
npx flaglint migrate ./src --dry-run
|
|
202
32
|
```
|
|
203
33
|
|
|
204
34
|
---
|
|
205
35
|
|
|
206
|
-
##
|
|
207
|
-
|
|
208
|
-
Automatic migration currently supports LaunchDarkly Node.js server-side evaluation calls in
|
|
209
|
-
TypeScript and JavaScript. That narrow scope is intentional: FlagLint only rewrites call sites
|
|
210
|
-
where the value type, static flag key, fallback, evaluation context, and OpenFeature client
|
|
211
|
-
binding are explicit enough to preserve.
|
|
212
|
-
|
|
213
|
-
Dynamic keys, detail evaluations, bulk flag-state calls, browser SDKs, React usage, and ambiguous
|
|
214
|
-
patterns are reported for manual review. They are inventoried so teams can plan the migration,
|
|
215
|
-
but they are not automatically transformed.
|
|
216
|
-
|
|
217
|
-
## Supported API matrix
|
|
218
|
-
|
|
219
|
-
**Scope: LaunchDarkly Node.js server-side SDK evaluation calls from
|
|
220
|
-
`@launchdarkly/node-server-sdk` and legacy `launchdarkly-node-server-sdk` imports.**
|
|
221
|
-
|
|
222
|
-
| LaunchDarkly call | Automatable | OpenFeature equivalent |
|
|
223
|
-
|---|---|---|
|
|
224
|
-
| `ldClient.boolVariation(key, ctx, false)` | ✓ | `openFeatureClient.getBooleanValue(key, false, ctx)` |
|
|
225
|
-
| `ldClient.stringVariation(key, ctx, "")` | ✓ | `openFeatureClient.getStringValue(key, "", ctx)` |
|
|
226
|
-
| `ldClient.numberVariation(key, ctx, 0)` | ✓ | `openFeatureClient.getNumberValue(key, 0, ctx)` |
|
|
227
|
-
| `ldClient.jsonVariation(key, ctx, {})` | ✓ | `openFeatureClient.getObjectValue(key, {}, ctx)` |
|
|
228
|
-
| `ldClient.*VariationDetail(...)` | ✗ manual | Detail result shapes differ — requires manual review |
|
|
229
|
-
| Dynamic flag key | ✗ manual | Key must be a static string literal |
|
|
230
|
-
| `ldClient.allFlags()` / `allFlagsState()` | ✗ manual | Bulk calls — no single-flag codemod |
|
|
231
|
-
| Unknown fallback type | ✗ manual | Fallback type must be determinable statically |
|
|
232
|
-
|
|
233
|
-
`flaglint scan` and `flaglint migrate --dry-run` report detected LaunchDarkly Node.js server-side SDK patterns, including manual-review cases.
|
|
234
|
-
`flaglint migrate --apply` rewrites only the ✓ rows above.
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## Provider setup (one-time manual step)
|
|
239
|
-
|
|
240
|
-
`flaglint migrate --dry-run` includes this guidance inline. **Complete provider setup in
|
|
241
|
-
one dedicated file before running `--apply`.**
|
|
242
|
-
|
|
243
|
-
```bash
|
|
244
|
-
npm install @openfeature/server-sdk \
|
|
245
|
-
@launchdarkly/node-server-sdk \
|
|
246
|
-
@launchdarkly/openfeature-node-server
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
Bootstrap file (do not apply automatically — bootstrap is intentionally manual):
|
|
250
|
-
|
|
251
|
-
```typescript
|
|
252
|
-
import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
|
|
253
|
-
import { OpenFeature } from "@openfeature/server-sdk";
|
|
254
|
-
|
|
255
|
-
const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
|
|
256
|
-
await OpenFeature.setProviderAndWait(ldProvider);
|
|
257
|
-
|
|
258
|
-
// Evaluation context needs a targeting key.
|
|
259
|
-
// Use OpenFeature `targetingKey` or keep an existing LaunchDarkly `key`:
|
|
260
|
-
// { targetingKey: user.id } or { key: user.id }
|
|
261
|
-
export const openFeatureClient = OpenFeature.getClient();
|
|
262
|
-
```
|
|
36
|
+
## Workflow
|
|
263
37
|
|
|
264
|
-
|
|
265
|
-
|
|
38
|
+
| Step | Command | What happens |
|
|
39
|
+
|------|---------|-------------|
|
|
40
|
+
| 1 | `flaglint scan ./src` | AST inventory of every direct LD Node server SDK call |
|
|
41
|
+
| 2 | `flaglint migrate --dry-run` | Reviewable before/after diffs; inline provider setup guidance |
|
|
42
|
+
| 3 | `flaglint migrate --apply` | Rewrites only guarded, provably automatable call-sites |
|
|
43
|
+
| 4 | `flaglint validate --no-direct-launchdarkly` | CI gate: exit 1 if direct LD evaluation calls remain |
|
|
266
44
|
|
|
267
45
|
---
|
|
268
46
|
|
|
269
|
-
##
|
|
47
|
+
## Before → After (real output from enterprise demo)
|
|
270
48
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
49
|
+
```diff
|
|
50
|
+
--- a/checkout.ts
|
|
51
|
+
+++ b/checkout.ts
|
|
52
|
+
- return ldClient.boolVariation("checkout-v2", ctx, false);
|
|
53
|
+
+ return openFeatureClient.getBooleanValue("checkout-v2", false, ctx);
|
|
274
54
|
|
|
275
|
-
|
|
55
|
+
- return ldClient.stringVariation("payment-provider", ctx, "stripe");
|
|
56
|
+
+ return openFeatureClient.getStringValue("payment-provider", "stripe", ctx);
|
|
276
57
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"
|
|
280
|
-
|
|
281
|
-
"importName": "openFeatureClient",
|
|
282
|
-
"modulePatterns": ["**/platform/feature-flags"]
|
|
283
|
-
}
|
|
284
|
-
]
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
`modulePatterns` are **glob patterns** matched against the module import specifier
|
|
289
|
-
(leading `./` and `../` traversal is stripped before matching). A pattern of
|
|
290
|
-
`"**/platform/feature-flags"` matches `"../platform/feature-flags"` and
|
|
291
|
-
`"../../shared/platform/feature-flags"`, but **not** `"../platform/feature-flags-legacy"` or
|
|
292
|
-
`"../other/platform/feature-flags-backup"`.
|
|
293
|
-
For TypeScript ESM projects, configured module patterns without `.js` also recognize
|
|
294
|
-
the corresponding `.js` runtime import specifier.
|
|
295
|
-
|
|
296
|
-
**Before:**
|
|
297
|
-
```typescript
|
|
298
|
-
// services/checkout.ts
|
|
299
|
-
import { openFeatureClient } from "../platform/feature-flags";
|
|
300
|
-
import LaunchDarkly from "launchdarkly-node-server-sdk";
|
|
301
|
-
|
|
302
|
-
const enabled = ldClient.boolVariation("checkout-v2", { key: user.id }, false);
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
**After (`flaglint migrate --apply`):**
|
|
306
|
-
```typescript
|
|
307
|
-
// services/checkout.ts
|
|
308
|
-
import { openFeatureClient } from "../platform/feature-flags";
|
|
309
|
-
import LaunchDarkly from "launchdarkly-node-server-sdk";
|
|
310
|
-
|
|
311
|
-
const enabled = openFeatureClient.getBooleanValue("checkout-v2", false, { key: user.id });
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
If the import is aliased (e.g. `import { openFeatureClient as flags } from "..."`), FlagLint
|
|
315
|
-
previews and applies the transformation using the **local alias name** (`flags.getBooleanValue(...)`).
|
|
316
|
-
|
|
317
|
-
When two configured bindings both match a file, FlagLint considers the result ambiguous and
|
|
318
|
-
**skips that file** rather than guessing. Skipped files are reported in `--dry-run` output.
|
|
319
|
-
|
|
320
|
-
Provider initialization remains the platform team's responsibility. FlagLint never inserts or
|
|
321
|
-
modifies bootstrap/provider setup code.
|
|
322
|
-
|
|
323
|
-
---
|
|
324
|
-
|
|
325
|
-
## Example transformation
|
|
326
|
-
|
|
327
|
-
**Before — direct LaunchDarkly Node.js server SDK:**
|
|
328
|
-
```typescript
|
|
329
|
-
const enabled = await ldClient.boolVariation("checkout-v2", { key: user.id }, false);
|
|
330
|
-
const theme = await ldClient.stringVariation("color-theme", { key: user.id }, "light");
|
|
331
|
-
const timeout = await ldClient.numberVariation("timeout-ms", { key: user.id }, 5000);
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
**After — OpenFeature via LaunchDarkly provider:**
|
|
335
|
-
```typescript
|
|
336
|
-
const enabled = await openFeatureClient.getBooleanValue("checkout-v2", false, { key: user.id });
|
|
337
|
-
const theme = await openFeatureClient.getStringValue("color-theme", "light", { key: user.id });
|
|
338
|
-
const timeout = await openFeatureClient.getNumberValue("timeout-ms", 5000, { key: user.id });
|
|
58
|
+
--- a/pricing.ts
|
|
59
|
+
+++ b/pricing.ts
|
|
60
|
+
- return ldClient.numberVariation("discount-percentage", ctx, 0);
|
|
61
|
+
+ return openFeatureClient.getNumberValue("discount-percentage", 0, ctx);
|
|
339
62
|
```
|
|
340
63
|
|
|
341
64
|
Flag key, fallback value, `await`, and evaluation context are preserved exactly.
|
|
342
|
-
LaunchDarkly continues to serve the flags — only the call-site API changes.
|
|
343
|
-
When authoring new OpenFeature-native bootstrap or application code, you may use
|
|
344
|
-
OpenFeature `targetingKey`; FlagLint does not silently rewrite existing
|
|
345
|
-
LaunchDarkly `key` contexts.
|
|
346
|
-
|
|
347
|
-
---
|
|
348
|
-
|
|
349
|
-
## Configuration
|
|
350
|
-
|
|
351
|
-
Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your project root:
|
|
352
|
-
|
|
353
|
-
```json
|
|
354
|
-
{
|
|
355
|
-
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
356
|
-
"exclude": ["**/node_modules/**", "**/dist/**"],
|
|
357
|
-
"provider": "launchdarkly",
|
|
358
|
-
"minFileCount": 0,
|
|
359
|
-
"reportTitle": "My Project Flag Report"
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
| Field | Type | Default | Description |
|
|
364
|
-
|-------|------|---------|-------------|
|
|
365
|
-
| `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
|
|
366
|
-
| `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
|
|
367
|
-
| `provider` | `string` | `"launchdarkly"` | Feature flag provider |
|
|
368
|
-
| `minFileCount` | `number` | `0` | Opt-in staleness heuristic. When set above 0, a flag is a staleness candidate if it appears in ≤ N files |
|
|
369
|
-
| `wrappers` | `string[]` | `[]` | Function names wrapping LD SDK calls. Example: `["flagPredicate", "useFlag"]` |
|
|
370
|
-
| `openFeatureClientBindings` | `{ importName: string; modulePatterns: string[] }[]` | `[]` | Allowlist shared imported OpenFeature client bindings for `--apply` eligibility. See [Using an existing shared OpenFeature client](#using-an-existing-shared-openfeature-client). |
|
|
371
|
-
| `reportTitle` | `string` | — | Custom title for generated reports |
|
|
372
|
-
| `outputDir` | `string` | `"."` | Default output directory |
|
|
373
|
-
|
|
374
|
-
FlagLint searches for config in this order: `--config` path → `.flaglintrc` → `.flaglintrc.json` → `flaglint.config.json`.
|
|
375
65
|
|
|
376
66
|
---
|
|
377
67
|
|
|
378
|
-
##
|
|
68
|
+
## Supported scope
|
|
379
69
|
|
|
380
|
-
|
|
70
|
+
LaunchDarkly Node.js server-side SDK calls from `launchdarkly-node-server-sdk` and
|
|
71
|
+
`@launchdarkly/node-server-sdk`. Both ESM import and CJS `require()` forms.
|
|
381
72
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
--bootstrap-exclude "src/provider/setup.ts"
|
|
387
|
-
# exits 1 if any direct LD evaluation calls remain outside the bootstrap file
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Full migration CI pipeline with SARIF annotations
|
|
391
|
-
|
|
392
|
-
```yaml
|
|
393
|
-
name: FlagLint
|
|
394
|
-
on: [pull_request]
|
|
395
|
-
|
|
396
|
-
jobs:
|
|
397
|
-
flaglint:
|
|
398
|
-
runs-on: ubuntu-latest
|
|
399
|
-
permissions:
|
|
400
|
-
security-events: write
|
|
401
|
-
contents: read
|
|
402
|
-
steps:
|
|
403
|
-
- uses: actions/checkout@v4
|
|
404
|
-
- uses: actions/setup-node@v4
|
|
405
|
-
with:
|
|
406
|
-
node-version: 20
|
|
407
|
-
|
|
408
|
-
- name: Inventory LaunchDarkly SDK usage
|
|
409
|
-
run: npx flaglint scan ./src --format html --output flaglint-inventory.html
|
|
410
|
-
continue-on-error: true
|
|
411
|
-
|
|
412
|
-
- name: Plan OpenFeature migration
|
|
413
|
-
run: npx flaglint migrate ./src --dry-run --output flaglint-migration.md
|
|
414
|
-
continue-on-error: true
|
|
415
|
-
|
|
416
|
-
- name: Validate direct LaunchDarkly policy
|
|
417
|
-
run: |
|
|
418
|
-
npx flaglint validate ./src \
|
|
419
|
-
--no-direct-launchdarkly \
|
|
420
|
-
--bootstrap-exclude "src/provider/setup.ts" \
|
|
421
|
-
--format sarif \
|
|
422
|
-
--output flaglint-validation.sarif
|
|
423
|
-
continue-on-error: true
|
|
424
|
-
|
|
425
|
-
- name: Upload to GitHub Code Scanning
|
|
426
|
-
uses: github/codeql-action/upload-sarif@v3
|
|
427
|
-
with:
|
|
428
|
-
sarif_file: flaglint-validation.sarif
|
|
429
|
-
```
|
|
73
|
+
`--apply` rewrites `boolVariation`, `stringVariation`, `numberVariation`, `jsonVariation`
|
|
74
|
+
where the flag key, fallback, and OpenFeature client binding are statically explicit.
|
|
75
|
+
Detail methods, dynamic keys, bulk calls, and unknown fallback types are reported for manual review.
|
|
76
|
+
Browser SDKs, React SDKs, and non-LaunchDarkly providers are outside current scope.
|
|
430
77
|
|
|
431
|
-
|
|
432
|
-
planning. `validate --no-direct-launchdarkly --format sarif` is for CI policy
|
|
433
|
-
annotations and enforcement. Code Scanning alerts show the exact file and line of
|
|
434
|
-
each direct LD call under the SARIF rule id `flaglint.direct-launchdarkly` —
|
|
435
|
-
reviewers see them in the PR without running anything locally.
|
|
78
|
+
Full coverage table: [Supported Scope](https://flaglint.dev/docs/reference/supported-scope)
|
|
436
79
|
|
|
437
80
|
---
|
|
438
81
|
|
|
439
|
-
##
|
|
82
|
+
## Provider setup (one-time, manual)
|
|
440
83
|
|
|
441
|
-
|
|
84
|
+
Before `--apply`, complete bootstrap setup once. Full instructions:
|
|
85
|
+
[OpenFeature Provider Setup](https://flaglint.dev/docs/integrations/openfeature-provider)
|
|
442
86
|
|
|
443
|
-
|
|
444
|
-
|
|
87
|
+
Key points:
|
|
88
|
+
- `new LaunchDarklyProvider(process.env.LD_SDK_KEY!)` — SDK key constructor
|
|
89
|
+
- Evaluation context accepts either `targetingKey` (OpenFeature-native) or an existing LaunchDarkly `key`
|
|
90
|
+
- **Do not remove LaunchDarkly packages** — the OpenFeature provider depends on them at runtime
|
|
445
91
|
|
|
446
92
|
---
|
|
447
93
|
|
|
448
|
-
##
|
|
449
|
-
|
|
450
|
-
See a realistic end-to-end migration walkthrough — multiple Node.js services,
|
|
451
|
-
mixed automatable and manual-review patterns, SARIF output, and CI enforcement:
|
|
452
|
-
|
|
453
|
-
**[View enterprise demo](./examples/enterprise-checkout-service/README.md)**
|
|
454
|
-
|
|
455
|
-
The demo shows scan inventory, exact dry-run preview, guarded apply,
|
|
456
|
-
migration-in-progress advisory findings, and a completed-state validation hard
|
|
457
|
-
gate.
|
|
458
|
-
|
|
459
|
-
---
|
|
460
|
-
|
|
461
|
-
## Security and trust
|
|
462
|
-
|
|
463
|
-
FlagLint runs entirely on your machine. No source code, flag keys, or file paths
|
|
464
|
-
are transmitted to any external service. The tool makes no outbound network
|
|
465
|
-
connections during a flag scan or migration. No LaunchDarkly SDK key or any
|
|
466
|
-
credentials are required.
|
|
467
|
-
|
|
468
|
-
`flaglint migrate --apply` refuses to write files on a dirty git working tree
|
|
469
|
-
(unless `--allow-dirty` is passed), requires a proven OpenFeature client binding
|
|
470
|
-
before touching a file, and verifies each source range against the original call
|
|
471
|
-
expression before rewriting.
|
|
472
|
-
|
|
473
|
-
The project release workflow is configured to publish through GitHub Actions
|
|
474
|
-
using npm Trusted Publishing/OIDC. The publish job uses Node 24 because npm
|
|
475
|
-
Trusted Publishing has stricter runtime requirements; FlagLint runtime support
|
|
476
|
-
remains Node.js >=20. npm-side Trusted Publisher configuration must be completed
|
|
477
|
-
before the first OIDC-based publication unless it has already been configured
|
|
478
|
-
and verified.
|
|
479
|
-
|
|
480
|
-
Core behavior is covered by automated tests executed in CI on supported Node
|
|
481
|
-
versions.
|
|
94
|
+
## Requirements
|
|
482
95
|
|
|
483
|
-
|
|
484
|
-
For a full trust and provenance statement, see [docs/trust.md](./docs/trust.md).
|
|
96
|
+
Node.js 20 or newer. No LaunchDarkly SDK key or credentials required for scan or migrate.
|
|
485
97
|
|
|
486
98
|
---
|
|
487
99
|
|
|
488
|
-
##
|
|
100
|
+
## Local analysis
|
|
489
101
|
|
|
490
|
-
|
|
102
|
+
FlagLint runs entirely on your machine. No source code, flag keys, or file paths are
|
|
103
|
+
transmitted to any external service. No outbound network connections during scan or migration.
|
|
491
104
|
|
|
492
105
|
---
|
|
493
106
|
|
|
494
|
-
##
|
|
107
|
+
## Links
|
|
495
108
|
|
|
496
|
-
|
|
109
|
+
[Security](./SECURITY.md) · [Contributing](./CONTRIBUTING.md) · [Changelog](./CHANGELOG.md) · [License](./LICENSE) · [Full docs](https://flaglint.dev/docs/quickstart)
|
package/dist/bin/flaglint.js
CHANGED
|
@@ -178,6 +178,7 @@ function walk(root, visit) {
|
|
|
178
178
|
}
|
|
179
179
|
function collectLDClients(ast) {
|
|
180
180
|
const ldNamespaces = /* @__PURE__ */ new Set();
|
|
181
|
+
const ldInitFunctions = /* @__PURE__ */ new Set();
|
|
181
182
|
for (const stmt of ast.body) {
|
|
182
183
|
if (stmt.type === "ImportDeclaration") {
|
|
183
184
|
const importDecl = stmt;
|
|
@@ -186,6 +187,12 @@ function collectLDClients(ast) {
|
|
|
186
187
|
if (spec.type === "ImportNamespaceSpecifier" || spec.type === "ImportDefaultSpecifier") {
|
|
187
188
|
ldNamespaces.add(spec.local.name);
|
|
188
189
|
}
|
|
190
|
+
if (spec.type === "ImportSpecifier") {
|
|
191
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
192
|
+
if (importedName === "init") {
|
|
193
|
+
ldInitFunctions.add(spec.local.name);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
189
196
|
}
|
|
190
197
|
}
|
|
191
198
|
continue;
|
|
@@ -193,17 +200,29 @@ function collectLDClients(ast) {
|
|
|
193
200
|
if (stmt.type === "VariableDeclaration") {
|
|
194
201
|
const varDecl = stmt;
|
|
195
202
|
for (const decl of varDecl.declarations) {
|
|
196
|
-
if (
|
|
203
|
+
if (!decl.init) continue;
|
|
197
204
|
const init = decl.init;
|
|
198
|
-
|
|
205
|
+
const isLDRequire = init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "require" && init.arguments.length >= 1 && init.arguments[0].type === "Literal" && LD_NODE_SERVER_PACKAGES.has(
|
|
199
206
|
init.arguments[0].value
|
|
200
|
-
)
|
|
207
|
+
);
|
|
208
|
+
if (!isLDRequire) continue;
|
|
209
|
+
if (decl.id.type === "Identifier") {
|
|
201
210
|
ldNamespaces.add(decl.id.name);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (decl.id.type === "ObjectPattern") {
|
|
214
|
+
for (const prop of decl.id.properties) {
|
|
215
|
+
if (prop.type !== "Property") continue;
|
|
216
|
+
const keyName = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" && typeof prop.key.value === "string" ? prop.key.value : void 0;
|
|
217
|
+
if (keyName === "init" && prop.value.type === "Identifier") {
|
|
218
|
+
ldInitFunctions.add(prop.value.name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
202
221
|
}
|
|
203
222
|
}
|
|
204
223
|
}
|
|
205
224
|
}
|
|
206
|
-
if (ldNamespaces.size === 0) return /* @__PURE__ */ new Set();
|
|
225
|
+
if (ldNamespaces.size === 0 && ldInitFunctions.size === 0) return /* @__PURE__ */ new Set();
|
|
207
226
|
const ldClients = /* @__PURE__ */ new Set();
|
|
208
227
|
walk(ast, (node) => {
|
|
209
228
|
if (node.type !== "VariableDeclaration") return;
|
|
@@ -211,6 +230,10 @@ function collectLDClients(ast) {
|
|
|
211
230
|
for (const decl of varDecl.declarations) {
|
|
212
231
|
if (decl.id.type !== "Identifier" || !decl.init || decl.init.type !== "CallExpression") continue;
|
|
213
232
|
const initCall = decl.init;
|
|
233
|
+
if (initCall.callee.type === "Identifier" && ldInitFunctions.has(initCall.callee.name)) {
|
|
234
|
+
ldClients.add(decl.id.name);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
214
237
|
if (initCall.callee.type !== "MemberExpression" || initCall.callee.computed) continue;
|
|
215
238
|
const initCallee = initCall.callee;
|
|
216
239
|
if (initCallee.object.type === "Identifier" && initCallee.property.type === "Identifier" && ldNamespaces.has(initCallee.object.name) && initCallee.property.name === "init") {
|
|
@@ -740,7 +763,7 @@ function formatHTML(result, options) {
|
|
|
740
763
|
<div class="card"><div class="card-num orange">${manualCount}</div><div class="card-label">Manual Review (${manualPct}%)</div></div>
|
|
741
764
|
<div class="card"><div class="card-num blue">${detailBulkCount}</div><div class="card-label">Detail/Bulk Calls</div></div>` : "";
|
|
742
765
|
const title = options.title ? esc(options.title) : "FlagLint Scan Report";
|
|
743
|
-
const version = true ? "0.5.
|
|
766
|
+
const version = true ? "0.5.3" : "0.1.0";
|
|
744
767
|
return `<!DOCTYPE html>
|
|
745
768
|
<html lang="en">
|
|
746
769
|
<head>
|
|
@@ -1193,7 +1216,7 @@ function formatMigrationReport(analysis) {
|
|
|
1193
1216
|
unsupportedUnknownCount
|
|
1194
1217
|
} = analysis;
|
|
1195
1218
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
1196
|
-
const version = true ? "0.5.
|
|
1219
|
+
const version = true ? "0.5.3" : "0.1.0";
|
|
1197
1220
|
const lines = [];
|
|
1198
1221
|
lines.push("# OpenFeature Migration Inventory");
|
|
1199
1222
|
lines.push(`Generated by FlagLint v${version} on ${date}`);
|
|
@@ -1957,7 +1980,7 @@ Examples:
|
|
|
1957
1980
|
// src/cli.ts
|
|
1958
1981
|
function createCLI() {
|
|
1959
1982
|
const program2 = new Command();
|
|
1960
|
-
program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.5.
|
|
1983
|
+
program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.5.3", "-v, --version", "output the current version").addHelpText(
|
|
1961
1984
|
"after",
|
|
1962
1985
|
`
|
|
1963
1986
|
Examples:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flaglint",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "LaunchDarkly Node.js server SDK -> OpenFeature migration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,15 +35,18 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"sync:www": "tsx scripts/sync-www.ts",
|
|
38
|
-
"build": "npm run sync:www && tsup",
|
|
38
|
+
"build:cli": "npm run sync:www && tsup",
|
|
39
|
+
"build:docs": "ASTRO_TELEMETRY_DISABLED=1 astro build",
|
|
40
|
+
"build": "npm run build:cli && npm run build:docs",
|
|
41
|
+
"dev:docs": "ASTRO_TELEMETRY_DISABLED=1 astro dev",
|
|
39
42
|
"dev": "tsup --watch",
|
|
40
43
|
"typecheck": "tsc --noEmit",
|
|
41
44
|
"typecheck:agent": "tsc --project tsconfig.agent.json",
|
|
42
|
-
"pretest": "npm run build",
|
|
45
|
+
"pretest": "npm run build:cli",
|
|
43
46
|
"test": "vitest",
|
|
44
|
-
"pretest:run": "npm run build",
|
|
47
|
+
"pretest:run": "npm run build:cli",
|
|
45
48
|
"test:run": "vitest run",
|
|
46
|
-
"pretest:coverage": "npm run build",
|
|
49
|
+
"pretest:coverage": "npm run build:cli",
|
|
47
50
|
"test:coverage": "vitest run --coverage",
|
|
48
51
|
"new-branch": "tsx scripts/new-branch.ts",
|
|
49
52
|
"release:patch": "tsx scripts/release.ts patch",
|
|
@@ -62,9 +65,11 @@
|
|
|
62
65
|
"zod": "^3.23.8"
|
|
63
66
|
},
|
|
64
67
|
"devDependencies": {
|
|
68
|
+
"@astrojs/starlight": "^0.39.2",
|
|
65
69
|
"@types/micromatch": "^4.0.10",
|
|
66
70
|
"@types/node": "^22.0.0",
|
|
67
71
|
"@vitest/coverage-v8": "^4.1.6",
|
|
72
|
+
"astro": "^6.3.8",
|
|
68
73
|
"clipboardy": "^4.0.0",
|
|
69
74
|
"tsup": "^8.2.4",
|
|
70
75
|
"tsx": "^4.19.0",
|