flaglint 0.4.0 → 0.4.1

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
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-05-25
11
+
12
+ ### Fixed
13
+
14
+ - Detail evaluation methods are classified as manual review and excluded from safe auto-transform counts.
15
+ - Generated LaunchDarkly OpenFeature provider setup now uses the SDK key constructor correctly.
16
+ - Evaluation-context guidance now states that either OpenFeature `targetingKey` or existing LaunchDarkly `key` is accepted.
17
+ - Default one-file flags no longer trigger staleness solely because they occur in one file; explicit `minFileCount: 1` remains available.
18
+ - Reporter output now says `Flags with review signals` instead of implying flags are safe to remove.
19
+ - Public early-preview messaging now states the Node.js server-side migration scope and review/testing requirement.
20
+
10
21
  ## [0.4.0] - 2026-05-24
11
22
 
12
23
  ### Added
package/README.md CHANGED
@@ -21,9 +21,12 @@
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.
24
+ > [!WARNING]
25
+ > FlagLint is currently an early preview.
26
+ >
27
+ > Automatic migration currently supports LaunchDarkly Node.js server-side SDK evaluation calls only. Generated changes must be reviewed and tested before merging.
28
+ >
29
+ > React hooks, higher-order components, browser/client-side SDK usage, bulk flag-state calls, detail evaluations, dynamic keys, and custom wrappers are not automatically migrated in this release.
27
30
 
28
31
  # FlagLint
29
32
 
@@ -209,10 +212,8 @@ Run `flaglint migrate --dry-run` to review the migration plan.
209
212
  | Dynamic flag key | ✗ manual | Key must be a static string literal |
210
213
  | `ldClient.allFlags()` / `allFlagsState()` | ✗ manual | Bulk calls — no single-flag codemod |
211
214
  | 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
215
 
215
- `flaglint scan` and `flaglint migrate --dry-run` report all detected patterns including manual-review cases.
216
+ `flaglint scan` and `flaglint migrate --dry-run` report detected LaunchDarkly Node.js server-side SDK patterns, including manual-review cases.
216
217
  `flaglint migrate --apply` rewrites only the ✓ rows above.
217
218
 
218
219
  ---
@@ -231,15 +232,15 @@ npm install @openfeature/server-sdk \
231
232
  Bootstrap file (do not apply automatically — bootstrap is intentionally manual):
232
233
 
233
234
  ```typescript
234
- import LaunchDarkly from "@launchdarkly/node-server-sdk";
235
235
  import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";
236
236
  import { OpenFeature } from "@openfeature/server-sdk";
237
237
 
238
- const ldClient = LaunchDarkly.init(process.env.LD_SDK_KEY!);
239
- await OpenFeature.setProviderAndWait(new LaunchDarklyProvider(ldClient));
238
+ const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);
239
+ await OpenFeature.setProviderAndWait(ldProvider);
240
240
 
241
- // Evaluation context must include targetingKey (or key):
242
- // { targetingKey: user.id }
241
+ // Evaluation context needs a targeting key.
242
+ // Use OpenFeature `targetingKey` or keep an existing LaunchDarkly `key`:
243
+ // { targetingKey: user.id } or { key: user.id }
243
244
  export const openFeatureClient = OpenFeature.getClient();
244
245
  ```
245
246
 
@@ -278,7 +279,7 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
278
279
  "include": ["**/*.{ts,tsx,js,jsx}"],
279
280
  "exclude": ["**/node_modules/**", "**/dist/**"],
280
281
  "provider": "launchdarkly",
281
- "minFileCount": 1,
282
+ "minFileCount": 0,
282
283
  "reportTitle": "My Project Flag Report"
283
284
  }
284
285
  ```
@@ -288,7 +289,7 @@ Create `.flaglintrc`, `.flaglintrc.json`, or `flaglint.config.json` in your proj
288
289
  | `include` | `string[]` | `["**/*.{ts,tsx,js,jsx}"]` | Glob patterns to scan |
289
290
  | `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
290
291
  | `provider` | `string` | `"launchdarkly"` | Feature flag provider |
291
- | `minFileCount` | `number` | `1` | A flag is a staleness candidate if it appears in ≤ N files |
292
+ | `minFileCount` | `number` | `0` | Opt-in staleness heuristic. When set above 0, a flag is a staleness candidate if it appears in ≤ N files |
292
293
  | `wrappers` | `string[]` | `[]` | Function names wrapping LD SDK calls. Example: `["flagPredicate", "useFlag"]` |
293
294
  | `reportTitle` | `string` | — | Custom title for generated reports |
294
295
  | `outputDir` | `string` | `"."` | Default output directory |
@@ -53,6 +53,13 @@ var LD_FLAG_KEY_METHODS = /* @__PURE__ */ new Set([
53
53
  "jsonVariationDetail"
54
54
  ]);
55
55
  var LD_ALL_FLAGS_METHODS = /* @__PURE__ */ new Set(["allFlags", "allFlagsState"]);
56
+ var LD_DETAIL_METHODS = /* @__PURE__ */ new Set([
57
+ "variationDetail",
58
+ "boolVariationDetail",
59
+ "stringVariationDetail",
60
+ "numberVariationDetail",
61
+ "jsonVariationDetail"
62
+ ]);
56
63
  var LD_HOOKS = /* @__PURE__ */ new Set(["useFlags", "useLDClient"]);
57
64
  var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**"];
58
65
  function extractFlagKey(arg) {
@@ -120,7 +127,7 @@ function buildMigrationInventoryItem(code, filePath, loc, call, methodName, args
120
127
  }
121
128
  const fallback = args[2];
122
129
  const valueType = inferValueType(methodName, fallback);
123
- const manualReviewReason = isDynamic ? "dynamic-key" : valueType === "unknown" ? "unknown-fallback" : void 0;
130
+ const manualReviewReason = isDynamic ? "dynamic-key" : LD_DETAIL_METHODS.has(methodName) ? "detail-method" : valueType === "unknown" ? "unknown-fallback" : void 0;
124
131
  return {
125
132
  file: filePath,
126
133
  line: loc.line,
@@ -528,7 +535,7 @@ function formatMarkdown(result, options) {
528
535
  }
529
536
  if (staleFlags.length > 0) {
530
537
  lines.push("## \u26A0 Stale Flag Candidates");
531
- lines.push("Flags that may be safe to remove:");
538
+ lines.push("Flags with review signals:");
532
539
  lines.push("| Flag Key | Reason | Location |");
533
540
  lines.push("|----------|--------|----------|");
534
541
  for (const [key, data] of staleFlags) {
@@ -681,7 +688,7 @@ function formatHTML(result, options) {
681
688
  return `<tr class="${cls}"><td><code>${esc(key)}</code></td><td>${data.usages.length}</td><td>${fileList}</td><td>${[...data.callTypes].map(esc).join(", ")}</td><td>${status}</td></tr>`;
682
689
  }).join("\n ");
683
690
  const title = options.title ? esc(options.title) : "FlagLint Scan Report";
684
- const version = true ? "0.4.0" : "0.1.0";
691
+ const version = true ? "0.4.1" : "0.1.0";
685
692
  return `<!DOCTYPE html>
686
693
  <html lang="en">
687
694
  <head>
@@ -779,7 +786,7 @@ var FlagLintConfigSchema = z.object({
779
786
  ]),
780
787
  provider: z.enum(["launchdarkly", "unleash", "growthbook", "custom"]).default("launchdarkly"),
781
788
  // TODO v0.3: replace minFileCount with real date-based staleness via git log
782
- minFileCount: z.number().int().min(0).default(1),
789
+ minFileCount: z.number().int().min(0).default(0),
783
790
  wrappers: z.array(z.string()).default([]),
784
791
  reportTitle: z.string().optional(),
785
792
  outputDir: z.string().default(".")
@@ -1011,6 +1018,8 @@ function reasonLabel(reason) {
1011
1018
  return "dynamic key";
1012
1019
  case "unknown-fallback":
1013
1020
  return "unsupported or unknown fallback";
1021
+ case "detail-method":
1022
+ return "detail method";
1014
1023
  case "bulk-inventory-call":
1015
1024
  return "bulk inventory call";
1016
1025
  default:
@@ -1094,7 +1103,7 @@ function formatMigrationReport(analysis) {
1094
1103
  unsupportedUnknownCount
1095
1104
  } = analysis;
1096
1105
  const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
1097
- const version = true ? "0.4.0" : "0.1.0";
1106
+ const version = true ? "0.4.1" : "0.1.0";
1098
1107
  const lines = [];
1099
1108
  lines.push("# OpenFeature Migration Inventory");
1100
1109
  lines.push(`Generated by FlagLint v${version} on ${date}`);
@@ -1179,6 +1188,9 @@ function methodForType(valueType) {
1179
1188
  function manualReason(item) {
1180
1189
  if (item.manualReviewReason === "dynamic-key") return "dynamic key requires manual review";
1181
1190
  if (item.manualReviewReason === "unknown-fallback") return "unknown fallback type requires manual review";
1191
+ if (item.manualReviewReason === "detail-method") {
1192
+ return "detail methods skipped: OpenFeature detail APIs exist, but LaunchDarkly/OpenFeature detail result parity requires manual review";
1193
+ }
1182
1194
  if (item.manualReviewReason === "bulk-inventory-call") return "bulk inventory call has no single-flag codemod";
1183
1195
  return "manual review required";
1184
1196
  }
@@ -1268,7 +1280,7 @@ function formatProviderSetupSection() {
1268
1280
  lines.push('import { OpenFeature } from "@openfeature/server-sdk";');
1269
1281
  lines.push('import { LaunchDarklyProvider } from "@launchdarkly/openfeature-node-server";');
1270
1282
  lines.push("");
1271
- lines.push('const ldProvider = new LaunchDarklyProvider("<your-sdk-key>");');
1283
+ lines.push("const ldProvider = new LaunchDarklyProvider(process.env.LD_SDK_KEY!);");
1272
1284
  lines.push("await OpenFeature.setProviderAndWait(ldProvider);");
1273
1285
  lines.push("");
1274
1286
  lines.push("// Share this client across your application.");
@@ -1278,11 +1290,14 @@ function formatProviderSetupSection() {
1278
1290
  lines.push("");
1279
1291
  lines.push("### 3. Evaluation context \u2014 targeting key");
1280
1292
  lines.push("");
1281
- lines.push("LaunchDarkly requires a `targetingKey` field in every evaluation context.");
1282
- lines.push("Replace the context arguments shown in the diffs with an object that includes it:");
1293
+ lines.push("LaunchDarkly requires a targeting key in every evaluation context. The");
1294
+ lines.push("provider accepts either OpenFeature `targetingKey` or an existing");
1295
+ lines.push("LaunchDarkly `key`.");
1296
+ lines.push("Keep your existing LaunchDarkly `key` contexts, or use `targetingKey`");
1297
+ lines.push("for new OpenFeature-native contexts:");
1283
1298
  lines.push("");
1284
1299
  lines.push("```typescript");
1285
- lines.push("{ targetingKey: user.key, ...otherAttributes }");
1300
+ lines.push("{ targetingKey: user.id } // or { key: user.id }");
1286
1301
  lines.push("```");
1287
1302
  lines.push("");
1288
1303
  return lines;
@@ -1881,7 +1896,7 @@ Examples:
1881
1896
  // src/cli.ts
1882
1897
  function createCLI() {
1883
1898
  const program2 = new Command();
1884
- program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.4.0", "-v, --version", "output the current version").addHelpText(
1899
+ program2.name("flaglint").description("LaunchDarkly Node.js server SDK -> OpenFeature migration.").version("0.4.1", "-v, --version", "output the current version").addHelpText(
1885
1900
  "after",
1886
1901
  `
1887
1902
  Examples:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flaglint",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "LaunchDarkly Node.js server SDK -> OpenFeature migration.",
5
5
  "type": "module",
6
6
  "bin": {