flaglint 0.2.0 → 0.2.2
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 +23 -0
- package/README.md +1 -0
- package/dist/bin/flaglint.js +52 -33
- package/package.json +1 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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
|
+
## [0.2.2] - 2026-05-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Config mutation**: `--exclude-tests` no longer mutates the loaded config object in both `scan` and `migrate` commands — uses spread instead of `push()` so the original config is never modified.
|
|
13
|
+
- **Typed scan warnings**: `ScanResult.warnings` is now a typed `ScanWarning` union (`read-failure` | `parse-failure`) instead of opaque strings, preserving structured data at the domain boundary.
|
|
14
|
+
- **StalenessEvaluator wired**: The `StalenessEvaluator` interface now has a call site in `scan()` — pass an `evaluator` to inject API-based staleness signals without touching core scanner logic.
|
|
15
|
+
- **ScanConfig boundary**: `scan()` now accepts `ScanConfig` (scan-relevant fields only) rather than the full `FlagLintConfig`, decoupling the scanner from CLI output concerns (`reportTitle`, `outputDir`).
|
|
16
|
+
|
|
17
|
+
## [0.2.1] - 2026-05-23
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Parse failure on generic TypeScript arrow functions** — `flagPredicate = <T>(...)` and similar generic arrows in `.ts` files now parse correctly. Root cause: `@typescript-eslint/typescript-estree` wasn't receiving a `filePath`, so it couldn't apply TypeScript's extension-based JSX rules. Adding `filePath` tells the compiler to treat `.ts` files as non-JSX (generics parse cleanly) and `.tsx` as JSX (LDProvider detection still works). Validated against LaunchDarkly's own docs codebase.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- **`wrappers` config option** — detect custom wrapper functions as flag usages. Add your wrapper names to `.flaglintrc`:
|
|
26
|
+
```json
|
|
27
|
+
{ "wrappers": ["flagPredicate", "useFlag", "getFlag", "isEnabled"] }
|
|
28
|
+
```
|
|
29
|
+
FlagLint will treat calls to these functions as `variation`-equivalent. Supports static and dynamic flag keys. Default is `[]` — no behaviour change for existing users.
|
|
30
|
+
|
|
8
31
|
## [0.2.0] - 2026-05-22
|
|
9
32
|
|
|
10
33
|
### Breaking Changes
|
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ Create `.flaglintrc` in your project root:
|
|
|
118
118
|
| `exclude` | `string[]` | `["**/node_modules/**", ...]` | Glob patterns to ignore |
|
|
119
119
|
| `provider` | `string` | `"launchdarkly"` | Feature flag provider |
|
|
120
120
|
| `minFileCount` | `number` | `1` | A flag is stale if it appears in ≤ N files (default: 1) |
|
|
121
|
+
| `wrappers` | `string[]` | `[]` | Function names that wrap LD SDK calls. FlagLint will detect calls to these functions as flag usages. Example: `["flagPredicate", "useFlag", "getFlag", "isEnabled"]` |
|
|
121
122
|
| `reportTitle` | `string` | — | Custom title for generated reports |
|
|
122
123
|
| `outputDir` | `string` | `"."` | Default output directory |
|
|
123
124
|
|
package/dist/bin/flaglint.js
CHANGED
|
@@ -76,7 +76,7 @@ function walk(root, visit) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
function detectUsages(ast, filePath) {
|
|
79
|
+
function detectUsages(ast, filePath, wrappers) {
|
|
80
80
|
const usages = [];
|
|
81
81
|
walk(ast, (node) => {
|
|
82
82
|
if (node.type === "CallExpression") {
|
|
@@ -141,6 +141,20 @@ function detectUsages(ast, filePath) {
|
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
if (wrappers.length > 0 && callee.type === "Identifier" && wrappers.includes(callee.name) && call.arguments.length >= 1) {
|
|
145
|
+
const { flagKey, isDynamic } = extractFlagKey(call.arguments[0]);
|
|
146
|
+
const sig = checkStale(flagKey, filePath);
|
|
147
|
+
usages.push({
|
|
148
|
+
flagKey,
|
|
149
|
+
isDynamic,
|
|
150
|
+
file: filePath,
|
|
151
|
+
line: loc.line,
|
|
152
|
+
column: loc.column,
|
|
153
|
+
callType: "variation",
|
|
154
|
+
stalenessSignals: sig ? [sig] : []
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
144
158
|
if (callee.type === "CallExpression" && callee.callee.type === "Identifier" && callee.callee.name === "withLDConsumer") {
|
|
145
159
|
const sig = checkStale("*", filePath);
|
|
146
160
|
usages.push({
|
|
@@ -174,7 +188,7 @@ function detectUsages(ast, filePath) {
|
|
|
174
188
|
});
|
|
175
189
|
return usages;
|
|
176
190
|
}
|
|
177
|
-
async function scan(source, config, onProgress) {
|
|
191
|
+
async function scan(source, config, onProgress, evaluator) {
|
|
178
192
|
const start = Date.now();
|
|
179
193
|
for (const pattern of config.include) {
|
|
180
194
|
if (pattern.startsWith("/") || pattern.startsWith("..")) {
|
|
@@ -193,7 +207,7 @@ async function scan(source, config, onProgress) {
|
|
|
193
207
|
code = await source.readFile(file);
|
|
194
208
|
} catch (err) {
|
|
195
209
|
const fsCode = err.code ?? "UNKNOWN";
|
|
196
|
-
return { usages: [], warning:
|
|
210
|
+
return { usages: [], warning: { kind: "read-failure", file, fsCode } };
|
|
197
211
|
}
|
|
198
212
|
let ast;
|
|
199
213
|
try {
|
|
@@ -202,12 +216,13 @@ async function scan(source, config, onProgress) {
|
|
|
202
216
|
loc: true,
|
|
203
217
|
range: false,
|
|
204
218
|
comment: false,
|
|
205
|
-
tokens: false
|
|
219
|
+
tokens: false,
|
|
220
|
+
filePath: file
|
|
206
221
|
});
|
|
207
222
|
} catch {
|
|
208
|
-
return { usages: [], warning:
|
|
223
|
+
return { usages: [], warning: { kind: "parse-failure", file } };
|
|
209
224
|
}
|
|
210
|
-
return { usages: detectUsages(ast, file), warning: null };
|
|
225
|
+
return { usages: detectUsages(ast, file, config.wrappers), warning: null };
|
|
211
226
|
}
|
|
212
227
|
const limit = pLimit(50);
|
|
213
228
|
const results = await Promise.all(
|
|
@@ -246,6 +261,9 @@ async function scan(source, config, onProgress) {
|
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
}
|
|
264
|
+
if (evaluator) {
|
|
265
|
+
await evaluator.evaluate(allUsages, config);
|
|
266
|
+
}
|
|
249
267
|
const uniqueFlags = [
|
|
250
268
|
...new Set(
|
|
251
269
|
allUsages.filter((u) => !u.isDynamic && u.flagKey !== "*").map((u) => u.flagKey)
|
|
@@ -389,7 +407,7 @@ function formatHTML(result, options) {
|
|
|
389
407
|
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>`;
|
|
390
408
|
}).join("\n ");
|
|
391
409
|
const title = options.title ? esc(options.title) : "FlagLint Scan Report";
|
|
392
|
-
const version = true ? "0.2.
|
|
410
|
+
const version = true ? "0.2.2" : "0.1.0";
|
|
393
411
|
return `<!DOCTYPE html>
|
|
394
412
|
<html lang="en">
|
|
395
413
|
<head>
|
|
@@ -486,6 +504,7 @@ var FlagLintConfigSchema = z.object({
|
|
|
486
504
|
provider: z.enum(["launchdarkly", "unleash", "growthbook", "custom"]).default("launchdarkly"),
|
|
487
505
|
// TODO v0.3: replace minFileCount with real date-based staleness via git log
|
|
488
506
|
minFileCount: z.number().int().min(0).default(1),
|
|
507
|
+
wrappers: z.array(z.string()).default([]),
|
|
489
508
|
reportTitle: z.string().optional(),
|
|
490
509
|
outputDir: z.string().default(".")
|
|
491
510
|
});
|
|
@@ -569,16 +588,15 @@ Examples:
|
|
|
569
588
|
process.stderr.write(chalk.red(String(err instanceof Error ? err.message : err)) + "\n");
|
|
570
589
|
process.exit(1);
|
|
571
590
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
591
|
+
const TEST_PATTERNS = [
|
|
592
|
+
"**/*.test.ts",
|
|
593
|
+
"**/*.test.tsx",
|
|
594
|
+
"**/*.spec.ts",
|
|
595
|
+
"**/*.spec.tsx",
|
|
596
|
+
"**/__tests__/**",
|
|
597
|
+
"**/tests/**"
|
|
598
|
+
];
|
|
599
|
+
const scanConfig = options.excludeTests ? { ...config, exclude: [...config.exclude, ...TEST_PATTERNS] } : config;
|
|
582
600
|
const format = options.format;
|
|
583
601
|
const spinner = ora(`Scanning ${dir}...`).start();
|
|
584
602
|
process.once("SIGINT", () => {
|
|
@@ -588,7 +606,7 @@ Examples:
|
|
|
588
606
|
let lastSpinnerUpdate = 0;
|
|
589
607
|
let result;
|
|
590
608
|
try {
|
|
591
|
-
result = await scan(new LocalFileSource(dir),
|
|
609
|
+
result = await scan(new LocalFileSource(dir), scanConfig, (filesScanned) => {
|
|
592
610
|
if (filesScanned - lastSpinnerUpdate >= 50) {
|
|
593
611
|
spinner.text = `Scanning... (${filesScanned} files)`;
|
|
594
612
|
lastSpinnerUpdate = filesScanned;
|
|
@@ -601,7 +619,8 @@ Examples:
|
|
|
601
619
|
process.exit(1);
|
|
602
620
|
}
|
|
603
621
|
for (const w of result.warnings) {
|
|
604
|
-
|
|
622
|
+
const msg = w.kind === "read-failure" ? `warn: could not read ${w.file} (${w.fsCode})` : `warn: failed to parse ${w.file}`;
|
|
623
|
+
process.stderr.write(chalk.yellow(msg + "\n"));
|
|
605
624
|
}
|
|
606
625
|
if (result.scannedFiles === 0) {
|
|
607
626
|
process.stderr.write(
|
|
@@ -808,7 +827,7 @@ function analyze(result) {
|
|
|
808
827
|
function formatMigrationReport(analysis) {
|
|
809
828
|
const { readinessScore, requiredPackages, items, manualReviewCount, autoMigrateCount } = analysis;
|
|
810
829
|
const date = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
811
|
-
const version = true ? "0.2.
|
|
830
|
+
const version = true ? "0.2.2" : "0.1.0";
|
|
812
831
|
let scoreLabel;
|
|
813
832
|
if (readinessScore >= 80) scoreLabel = "\u2713 Your codebase is ready for migration";
|
|
814
833
|
else if (readinessScore >= 50) scoreLabel = "\u26A0 Some manual work required before migration";
|
|
@@ -921,16 +940,15 @@ Examples:
|
|
|
921
940
|
process.stderr.write(chalk2.red(String(err instanceof Error ? err.message : err)) + "\n");
|
|
922
941
|
process.exit(1);
|
|
923
942
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
943
|
+
const TEST_PATTERNS = [
|
|
944
|
+
"**/*.test.ts",
|
|
945
|
+
"**/*.test.tsx",
|
|
946
|
+
"**/*.spec.ts",
|
|
947
|
+
"**/*.spec.tsx",
|
|
948
|
+
"**/__tests__/**",
|
|
949
|
+
"**/tests/**"
|
|
950
|
+
];
|
|
951
|
+
const scanConfig = options.excludeTests ? { ...config, exclude: [...config.exclude, ...TEST_PATTERNS] } : config;
|
|
934
952
|
const spinner = ora2(`Scanning ${dir}...`).start();
|
|
935
953
|
process.once("SIGINT", () => {
|
|
936
954
|
spinner.stop();
|
|
@@ -938,7 +956,7 @@ Examples:
|
|
|
938
956
|
});
|
|
939
957
|
let scanResult;
|
|
940
958
|
try {
|
|
941
|
-
scanResult = await scan(new LocalFileSource(dir),
|
|
959
|
+
scanResult = await scan(new LocalFileSource(dir), scanConfig, (filesScanned) => {
|
|
942
960
|
spinner.text = `Scanning files... ${filesScanned}`;
|
|
943
961
|
});
|
|
944
962
|
spinner.text = "Analyzing migration readiness...";
|
|
@@ -967,7 +985,8 @@ Examples:
|
|
|
967
985
|
const analysis = analyze(scanResult);
|
|
968
986
|
spinner.stop();
|
|
969
987
|
for (const w of scanResult.warnings) {
|
|
970
|
-
|
|
988
|
+
const msg = w.kind === "read-failure" ? `warn: could not read ${w.file} (${w.fsCode})` : `warn: failed to parse ${w.file}`;
|
|
989
|
+
process.stderr.write(chalk2.yellow(msg + "\n"));
|
|
971
990
|
}
|
|
972
991
|
const { readinessScore } = analysis;
|
|
973
992
|
const scoreColor = readinessScore >= 80 ? chalk2.green : readinessScore >= 50 ? chalk2.yellow : chalk2.red;
|
|
@@ -1006,7 +1025,7 @@ Examples:
|
|
|
1006
1025
|
// src/cli.ts
|
|
1007
1026
|
function createCLI() {
|
|
1008
1027
|
const program2 = new Command();
|
|
1009
|
-
program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.2.
|
|
1028
|
+
program2.name("flaglint").description("Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.").version("0.2.2", "-v, --version", "output the current version").addHelpText(
|
|
1010
1029
|
"after",
|
|
1011
1030
|
`
|
|
1012
1031
|
Examples:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flaglint",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Find stale feature flags. Detect flag debt. Plan your OpenFeature migration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,10 +41,6 @@
|
|
|
41
41
|
"test": "vitest",
|
|
42
42
|
"test:run": "vitest run",
|
|
43
43
|
"test:coverage": "vitest run --coverage",
|
|
44
|
-
"agent": "tsx scripts/agent/agent.ts",
|
|
45
|
-
"agent:launch": "tsx scripts/agent/agent.ts launch",
|
|
46
|
-
"agent:parallel": "tsx scripts/agent/agent.ts parallel",
|
|
47
|
-
"agent:sync": "tsx scripts/agent/agent.ts sync-docs",
|
|
48
44
|
"release:patch": "tsx scripts/release.ts patch",
|
|
49
45
|
"release:minor": "tsx scripts/release.ts minor",
|
|
50
46
|
"release:major": "tsx scripts/release.ts major"
|