flaglint 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,7 +5,98 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## Unreleased
9
+
10
+ ## [0.5.0] - 2026-05-26
11
+
12
+ ### Added
13
+
14
+ - **Configured imported OpenFeature client bindings** (`openFeatureClientBindings` in `.flaglintrc`):
15
+ Declare shared OpenFeature client exports by import name and glob module pattern.
16
+ `migrate --apply` recognises these imports as proven bindings without requiring every
17
+ service file to call `OpenFeature.getClient()` locally.
18
+ ```json
19
+ {
20
+ "openFeatureClientBindings": [
21
+ { "importName": "openFeatureClient", "modulePatterns": ["**/platform/feature-flags"] }
22
+ ]
23
+ }
24
+ ```
25
+
26
+ - **TypeScript ESM `.js` import compatibility**: `modulePatterns` globs now match TypeScript
27
+ source imports that carry a `.js` extension at the specifier level
28
+ (`import { openFeatureClient } from "../platform/feature-flags.js"`) even when the
29
+ configured pattern omits the extension (`**/platform/feature-flags`).
30
+
31
+ - **`validate --format sarif`**: `flaglint validate --no-direct-launchdarkly --format sarif
32
+ --output flaglint.sarif` emits SARIF 2.1.0 with rule id `flaglint.direct-launchdarkly`
33
+ and level `error`. Designed for GitHub Code Scanning upload — each direct LaunchDarkly
34
+ evaluation call produces a PR annotation. Zero violations produces a valid SARIF document
35
+ that GitHub Code Scanning interprets as "all clear".
36
+
37
+ - **Enterprise HTML audit report**: `flaglint scan --format html` now includes an Executive
38
+ Summary (total call-sites, unique flags, auto-migratable vs. manual-review breakdown),
39
+ Findings by Directory table, Recommended Next Steps workflow, and a Copy Markdown Summary
40
+ clipboard button.
41
+
42
+ - **Enterprise OpenFeature migration demo** (`examples/enterprise-checkout-service/`): end-to-end
43
+ walkthrough across five Node.js services (checkout, pricing, analytics, product, flags-wrapper).
44
+ Includes `before/`/`after/` snapshots, `after-complete/` (fully migrated, passes hard gate),
45
+ generated reports, `.flaglintrc` config, and a sample GitHub Actions CI workflow.
46
+
47
+ - **Docs site** (`www/docs/`): nine documentation pages covering getting started, all three
48
+ commands, supported scope, OpenFeature provider setup, CI/GitHub Actions integration,
49
+ OpenTelemetry observability guidance, safety model, and the enterprise demo.
50
+
51
+ - **Enterprise trust documentation**: `SECURITY.md`, `CONTRIBUTING.md`, and `CODE_OF_CONDUCT.md`
52
+ with SARIF rule-ID reference for the `flaglint.direct-launchdarkly` policy rule.
53
+
54
+ - **OpenFeature + OpenTelemetry observability guidance** (`www/docs/opentelemetry.html`):
55
+ documents how to instrument OpenFeature flag evaluations with OpenTelemetry using the
56
+ OpenFeature hooks API. FlagLint does not emit runtime telemetry; this page explains the
57
+ complementary integration pattern.
58
+
59
+ ### Fixed
60
+
61
+ - **Deterministic test execution from clean checkout**: `vitest` configuration no longer
62
+ relies on `process.env.INIT_CWD` for test file discovery, ensuring the test suite
63
+ is reproducible on first-time `npm ci && npm test` runs.
64
+
65
+ ### Changed
66
+
67
+ - Reposition README and homepage messaging around standardizing LaunchDarkly usage on
68
+ OpenFeature while keeping LaunchDarkly as the provider.
69
+ - Document the focused automation scope: LaunchDarkly Node.js server-side evaluation calls
70
+ in TypeScript and JavaScript, with dynamic keys, detail evaluations, bulk calls, browser
71
+ SDKs, React usage, and ambiguous patterns reported for manual review.
72
+ - Align supported runtime documentation and package metadata to Node.js `>=20`.
73
+ Resolves the Node.js engine metadata mismatch in `flaglint@0.4.1` (published as `>=22`).
74
+
75
+ ### Security
76
+
77
+ - Add Node.js 20/22 CI coverage, CodeQL analysis, Dependabot configuration, and vulnerability
78
+ reporting instructions.
79
+ - Update npm release workflow for Trusted Publishing/OIDC without long-lived npm publish tokens.
80
+
81
+ ### Scope boundaries (non-claims)
82
+
83
+ The following are explicitly out of scope for this release:
84
+ - LaunchDarkly replacement — LaunchDarkly remains the feature flag provider throughout.
85
+ - Automatic provider/bootstrap setup — `migrate --apply` never generates bootstrap files.
86
+ - Flag deletion or billing reduction — FlagLint does not evaluate live flag values.
87
+ - Built-in runtime OpenTelemetry instrumentation — see `www/docs/opentelemetry.html` for
88
+ the complementary integration pattern using OpenFeature hooks.
89
+
90
+ ## [0.4.1] - 2026-05-25
91
+
92
+ ### Fixed
93
+
94
+ - Detail evaluation methods are classified as manual review and excluded from safe auto-transform counts.
95
+ - Generated LaunchDarkly OpenFeature provider setup now uses the SDK key constructor correctly.
96
+ - Evaluation-context guidance now states that either OpenFeature `targetingKey` or existing LaunchDarkly `key` is accepted.
97
+ - Default one-file flags no longer trigger staleness solely because they occur in one file; explicit `minFileCount: 1` remains available.
98
+ - Reporter output now says `Flags with review signals` instead of implying flags are safe to remove.
99
+ - Public early-preview messaging now states the Node.js server-side migration scope and review/testing requirement.
9
100
 
10
101
  ## [0.4.0] - 2026-05-24
11
102
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>LaunchDarkly Node.js server SDK -> OpenFeature migration</strong>
6
+ <strong>Standardize LaunchDarkly usage on OpenFeature.</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -21,18 +21,19 @@
21
21
  </a>
22
22
  </p>
23
23
 
24
- > ⚠️ **Early preview.** Current scope: **LaunchDarkly Node.js server-side SDK** only.
25
- > React hooks, HOC, and client-side SDK patterns are detected by `scan` but are not
26
- > automatically migrated.
27
-
28
24
  # FlagLint
29
25
 
30
- FlagLint inventories direct LaunchDarkly Node.js server SDK calls in your TypeScript/JavaScript
31
- codebase, generates reviewable OpenFeature migration diffs, applies only guarded transformations,
32
- and enforces migration state in CI.
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.
33
30
 
34
31
  **LaunchDarkly remains your provider. OpenFeature becomes the evaluation API your application code calls.**
35
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
+
36
37
  ---
37
38
 
38
39
  ## Workflow
@@ -95,6 +96,8 @@ npm install -g flaglint
95
96
  npx flaglint
96
97
  ```
97
98
 
99
+ Requires Node.js 20 or newer. CI validates FlagLint on Node.js 20 and 22.
100
+
98
101
  ---
99
102
 
100
103
  ## Commands
@@ -146,8 +149,13 @@ flaglint migrate --exclude-tests # skip test and spec files
146
149
 
147
150
  **`--apply` safety contracts:**
148
151
  - Refuses on a dirty git working tree unless `--allow-dirty`
149
- - Skips any file that does not already contain a proven `openFeatureClient` binding
150
- (`openFeatureClient = OpenFeature.getClient()` from `@openfeature/server-sdk`)
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
151
159
  - Never touches detail methods, dynamic keys, unknown fallbacks, or bulk calls
152
160
  - Preserves `await` and original call arguments exactly
153
161
  - Idempotent: re-running with the same analysis has no effect
@@ -195,6 +203,17 @@ Run `flaglint migrate --dry-run` to review the migration plan.
195
203
 
196
204
  ---
197
205
 
206
+ ## Focused scope for safe automation
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
+
198
217
  ## Supported API matrix
199
218
 
200
219
  **Scope: LaunchDarkly Node.js server-side SDK** (`launchdarkly-node-server-sdk`).
@@ -209,10 +228,8 @@ Run `flaglint migrate --dry-run` to review the migration plan.
209
228
  | Dynamic flag key | ✗ manual | Key must be a static string literal |
210
229
  | `ldClient.allFlags()` / `allFlagsState()` | ✗ manual | Bulk calls — no single-flag codemod |
211
230
  | Unknown fallback type | ✗ manual | Fallback type must be determinable statically |
212
- | React `useFlags()`, `useLDClient()` | detect only | Client-side — outside Node.js server SDK scope |
213
- | React HOC / `<LDProvider>` | detect only | Client-side — outside Node.js server SDK scope |
214
231
 
215
- `flaglint scan` and `flaglint migrate --dry-run` report all detected patterns including manual-review cases.
232
+ `flaglint scan` and `flaglint migrate --dry-run` report detected LaunchDarkly Node.js server-side SDK patterns, including manual-review cases.
216
233
  `flaglint migrate --apply` rewrites only the ✓ rows above.
217
234
 
218
235
  ---
@@ -231,15 +248,15 @@ npm install @openfeature/server-sdk \
231
248
  Bootstrap file (do not apply automatically — bootstrap is intentionally manual):
232
249
 
233
250
  ```typescript
234
- import LaunchDarkly from "@launchdarkly/node-server-sdk";
235
251
  import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
236
252
  import { OpenFeature } from "@openfeature/server-sdk";
237
253
 
238
- const ldClient = LaunchDarkly.init(process.env.LD_SDK_KEY!);
239
- await OpenFeature.setProviderAndWait(new LaunchDarklyProvider(ldClient));
254
+ const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
255
+ await OpenFeature.setProviderAndWait(ldProvider);
240
256
 
241
- // Evaluation context must include targetingKey (or key):
242
- // { targetingKey: user.id }
257
+ // Evaluation context needs a targeting key.
258
+ // Use OpenFeature `targetingKey` or keep an existing LaunchDarkly `key`:
259
+ // { targetingKey: user.id } or { key: user.id }
243
260
  export const openFeatureClient = OpenFeature.getClient();
244
261
  ```
245
262
 
@@ -248,6 +265,62 @@ export const openFeatureClient = OpenFeature.getClient();
248
265
 
249
266
  ---
250
267
 
268
+ ## Using an existing shared OpenFeature client
269
+
270
+ If your platform team already exports an OpenFeature client from a shared internal module
271
+ (e.g. `platform/feature-flags.ts`), FlagLint can use that import as the proven binding for
272
+ `--apply` — no local `OpenFeature.getClient()` call is required in every file.
273
+
274
+ Declare the allowed import in `.flaglintrc`:
275
+
276
+ ```json
277
+ {
278
+ "openFeatureClientBindings": [
279
+ {
280
+ "importName": "openFeatureClient",
281
+ "modulePatterns": ["**/platform/feature-flags"]
282
+ }
283
+ ]
284
+ }
285
+ ```
286
+
287
+ `modulePatterns` are **glob patterns** matched against the module import specifier
288
+ (leading `./` and `../` traversal is stripped before matching). A pattern of
289
+ `"**/platform/feature-flags"` matches `"../platform/feature-flags"` and
290
+ `"../../shared/platform/feature-flags"`, but **not** `"../platform/feature-flags-legacy"` or
291
+ `"../other/platform/feature-flags-backup"`.
292
+ For TypeScript ESM projects, configured module patterns without `.js` also recognize
293
+ the corresponding `.js` runtime import specifier.
294
+
295
+ **Before:**
296
+ ```typescript
297
+ // services/checkout.ts
298
+ import { openFeatureClient } from "../platform/feature-flags";
299
+ import LaunchDarkly from "launchdarkly-node-server-sdk";
300
+
301
+ const enabled = ldClient.boolVariation("checkout-v2", { key: user.id }, false);
302
+ ```
303
+
304
+ **After (`flaglint migrate --apply`):**
305
+ ```typescript
306
+ // services/checkout.ts
307
+ import { openFeatureClient } from "../platform/feature-flags";
308
+ import LaunchDarkly from "launchdarkly-node-server-sdk";
309
+
310
+ const enabled = openFeatureClient.getBooleanValue("checkout-v2", false, { key: user.id });
311
+ ```
312
+
313
+ If the import is aliased (e.g. `import { openFeatureClient as flags } from "..."`), FlagLint
314
+ previews and applies the transformation using the **local alias name** (`flags.getBooleanValue(...)`).
315
+
316
+ When two configured bindings both match a file, FlagLint considers the result ambiguous and
317
+ **skips that file** rather than guessing. Skipped files are reported in `--dry-run` output.
318
+
319
+ Provider initialization remains the platform team's responsibility. FlagLint never inserts or
320
+ modifies bootstrap/provider setup code.
321
+
322
+ ---
323
+
251
324
  ## Example transformation
252
325
 
253
326
  **Before — direct LaunchDarkly Node.js server SDK:**
@@ -259,13 +332,16 @@ const timeout = await ldClient.numberVariation("timeout-ms", { key: user.id },
259
332
 
260
333
  **After — OpenFeature via LaunchDarkly provider:**
261
334
  ```typescript
262
- const enabled = await openFeatureClient.getBooleanValue("checkout-v2", false, { targetingKey: user.id });
263
- const theme = await openFeatureClient.getStringValue("color-theme", "light", { targetingKey: user.id });
264
- const timeout = await openFeatureClient.getNumberValue("timeout-ms", 5000, { targetingKey: user.id });
335
+ const enabled = await openFeatureClient.getBooleanValue("checkout-v2", false, { key: user.id });
336
+ const theme = await openFeatureClient.getStringValue("color-theme", "light", { key: user.id });
337
+ const timeout = await openFeatureClient.getNumberValue("timeout-ms", 5000, { key: user.id });
265
338
  ```
266
339
 
267
340
  Flag key, fallback value, `await`, and evaluation context are preserved exactly.
268
341
  LaunchDarkly continues to serve the flags — only the call-site API changes.
342
+ When authoring new OpenFeature-native bootstrap or application code, you may use
343
+ OpenFeature `targetingKey`; FlagLint does not silently rewrite existing
344
+ LaunchDarkly `key` contexts.
269
345
 
270
346
  ---
271
347
 
@@ -278,7 +354,7 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
278
354
  "include": ["**/*.{ts,tsx,js,jsx}"],
279
355
  "exclude": ["**/node_modules/**", "**/dist/**"],
280
356
  "provider": "launchdarkly",
281
- "minFileCount": 1,
357
+ "minFileCount": 0,
282
358
  "reportTitle": "My Project Flag Report"
283
359
  }
284
360
  ```
@@ -288,8 +364,9 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
288
364
  | `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
289
365
  | `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
290
366
  | `provider` | `string` | `"launchdarkly"` | Feature flag provider |
291
- | `minFileCount` | `number` | `1` | A flag is a staleness candidate if it appears in ≤ N files |
367
+ | `minFileCount` | `number` | `0` | Opt-in staleness heuristic. When set above 0, a flag is a staleness candidate if it appears in ≤ N files |
292
368
  | `wrappers` | `string[]` | `[]` | Function names wrapping LD SDK calls. Example: `["flagPredicate", "useFlag"]` |
369
+ | `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). |
293
370
  | `reportTitle` | `string` | — | Custom title for generated reports |
294
371
  | `outputDir` | `string` | `"."` | Default output directory |
295
372
 
@@ -327,22 +404,34 @@ jobs:
327
404
  with:
328
405
  node-version: 20
329
406
 
330
- - name: Scan for LaunchDarkly SDK usage
331
- run: npx flaglint scan --format sarif --output flaglint.sarif
407
+ - name: Inventory LaunchDarkly SDK usage
408
+ run: npx flaglint scan ./src --format html --output flaglint-inventory.html
409
+ continue-on-error: true
410
+
411
+ - name: Plan OpenFeature migration
412
+ run: npx flaglint migrate ./src --dry-run --output flaglint-migration.md
413
+ continue-on-error: true
414
+
415
+ - name: Validate direct LaunchDarkly policy
416
+ run: |
417
+ npx flaglint validate ./src \
418
+ --no-direct-launchdarkly \
419
+ --bootstrap-exclude "src/provider/setup.ts" \
420
+ --format sarif \
421
+ --output flaglint-validation.sarif
332
422
  continue-on-error: true
333
423
 
334
424
  - name: Upload to GitHub Code Scanning
335
425
  uses: github/codeql-action/upload-sarif@v3
336
426
  with:
337
- sarif_file: flaglint.sarif
338
-
339
- - name: Enforce OpenFeature migration
340
- run: |
341
- npx flaglint validate --no-direct-launchdarkly \
342
- --bootstrap-exclude "src/provider/setup.ts"
427
+ sarif_file: flaglint-validation.sarif
343
428
  ```
344
429
 
345
- Code Scanning alerts show the exact file and line of each direct LD call — reviewers see them in the PR without running anything locally.
430
+ `scan` is for inventory and reporting. `migrate --dry-run` is for migration
431
+ planning. `validate --no-direct-launchdarkly --format sarif` is for CI policy
432
+ annotations and enforcement. Code Scanning alerts show the exact file and line of
433
+ each direct LD call under the SARIF rule id `flaglint.direct-launchdarkly` —
434
+ reviewers see them in the PR without running anything locally.
346
435
 
347
436
  ---
348
437
 
@@ -350,8 +439,48 @@ Code Scanning alerts show the exact file and line of each direct LD call — rev
350
439
 
351
440
  Validated against 120 deterministic benchmark cases within the supported LaunchDarkly Node.js server-side SDK scope. 100% precision and recall are limited to those 120 tested cases and to the Node.js server-side SDK call patterns explicitly listed in the Supported API matrix above.
352
441
 
353
- Detection is AST-based, not regex: client binding patterns, import aliases, CJS require forms,
354
- and custom wrappers are all resolved before matching.
442
+ Detection is AST-based, not regex: client binding patterns, import aliases, and CJS require
443
+ forms are resolved before matching.
444
+
445
+ ---
446
+
447
+ ## Enterprise demo
448
+
449
+ See a realistic end-to-end migration walkthrough — multiple Node.js services,
450
+ mixed automatable and manual-review patterns, SARIF output, and CI enforcement:
451
+
452
+ **[View enterprise demo](./examples/enterprise-checkout-service/README.md)**
453
+
454
+ The demo shows scan inventory, exact dry-run preview, guarded apply,
455
+ migration-in-progress advisory findings, and a completed-state validation hard
456
+ gate.
457
+
458
+ ---
459
+
460
+ ## Security and trust
461
+
462
+ FlagLint runs entirely on your machine. No source code, flag keys, or file paths
463
+ are transmitted to any external service. The tool makes no outbound network
464
+ connections during a flag scan or migration. No LaunchDarkly SDK key or any
465
+ credentials are required.
466
+
467
+ `flaglint migrate --apply` refuses to write files on a dirty git working tree
468
+ (unless `--allow-dirty` is passed), requires a proven OpenFeature client binding
469
+ before touching a file, and verifies each source range against the original call
470
+ expression before rewriting.
471
+
472
+ The project release workflow is configured to publish through GitHub Actions
473
+ using npm Trusted Publishing/OIDC. The publish job uses Node 24 because npm
474
+ Trusted Publishing has stricter runtime requirements; FlagLint runtime support
475
+ remains Node.js >=20. npm-side Trusted Publisher configuration must be completed
476
+ before the first OIDC-based publication unless it has already been configured
477
+ and verified.
478
+
479
+ Core behavior is covered by automated tests executed in CI on supported Node
480
+ versions.
481
+
482
+ For vulnerability reports, see [SECURITY.md](./SECURITY.md).
483
+ For a full trust and provenance statement, see [docs/trust.md](./docs/trust.md).
355
484
 
356
485
  ---
357
486
 
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ ApplyError,
4
+ applyMigration,
5
+ getOpenFeatureClientBindingName,
6
+ hasOpenFeatureClientBinding
7
+ } from "./chunk-MJLXM6GZ.js";
8
+ export {
9
+ ApplyError,
10
+ applyMigration,
11
+ getOpenFeatureClientBindingName,
12
+ hasOpenFeatureClientBinding
13
+ };