ts-unused 1.0.0 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +67 -2
  2. package/dist/cli.js +477 -64
  3. package/package.json +5 -3
package/README.md CHANGED
@@ -9,7 +9,8 @@ A CLI tool that analyzes TypeScript projects to find unused exports and unused p
9
9
  - **Finds Unused Exports**: Identifies functions, classes, types, interfaces, and constants that are exported but never imported or used elsewhere
10
10
  - **Finds Completely Unused Files**: Identifies files where all exports are unused, suggesting the entire file can be deleted
11
11
  - **Finds Unused Type Properties**: Detects properties in interfaces and type aliases that are defined but never accessed
12
- - **Auto-Fix Command**: Automatically removes unused exports, properties, and deletes unused files with git safety checks
12
+ - **Finds Never-Returned Types**: Detects union type branches in function return types that are declared but never actually returned
13
+ - **Auto-Fix Command**: Automatically removes unused exports, properties, deletes unused files, and fixes never-returned types with git safety checks
13
14
  - **Structural Property Equivalence**: Handles property re-declarations across multiple interfaces - properties are considered "used" if structurally equivalent properties (same name and type) are accessed in any interface
14
15
  - **Three-Tier Severity System**: Categorizes findings by severity level for better prioritization
15
16
  - **ERROR**: Completely unused code that should be removed
@@ -36,6 +37,7 @@ A CLI tool that analyzes TypeScript projects to find unused exports and unused p
36
37
  | **Goal** | **Report & Analyze** | **Remove (Tree-Shake)** | **Report** |
37
38
  | **Unused Exports** | ✅ Detects and reports | ✅ Detects and removes | ✅ Detects and reports |
38
39
  | **Unused Properties** | ✅ **Unique:** Checks `interface`/`type` properties | ❌ No property checks | ❌ No property checks |
40
+ | **Never-Returned Types** | ✅ **Unique:** Detects unused union branches | ❌ Not supported | ❌ Not supported |
39
41
  | **Test-Only Usage** | ✅ **Unique:** Identifies "Used only in tests" | ⚠️ May delete if not entrypoint | ❌ No distinction |
40
42
  | **Comment Support** | ✅ **Unique:** TODOs change severity | ✅ Skip/Ignore only | ✅ Skip/Ignore only |
41
43
  | **Unused Files** | ✅ Reports completely unused files | ✅ Deletes unreachable files | ✅ Explicit report flag |
@@ -113,7 +115,8 @@ The script outputs:
113
115
  1. **Completely Unused Files**: Files where all exports are unused (candidates for deletion)
114
116
  2. **Unused Exports**: Functions, types, interfaces, and constants that are exported but never used
115
117
  3. **Unused Properties**: Properties in types/interfaces that are defined but never accessed
116
- 3. **Summary**: Total count of unused items found
118
+ 4. **Never-Returned Types**: Union type branches in function return types that are never actually returned
119
+ 5. **Summary**: Total count of unused items found
117
120
 
118
121
  Each finding includes:
119
122
  - File path relative to the project root
@@ -144,9 +147,16 @@ packages/example/src/types.ts
144
147
  UserConfig.futureFeature:12:3-16 [WARNING] (Unused property: [TODO] implement this later)
145
148
  TestHelpers.mockData:8:3-11 [INFO] (Used only in tests)
146
149
 
150
+ Never-Returned Types:
151
+
152
+ packages/example/src/api.ts
153
+ processRequest.ErrorResult:15:17-28 [ERROR] (Never-returned type in union)
154
+ fetchData.TimeoutError:42:25-37 [ERROR] (Never-returned type in union)
155
+
147
156
  Summary:
148
157
  Unused exports: 3
149
158
  Unused properties: 3
159
+ Never-returned types: 2
150
160
  ```
151
161
 
152
162
  ### Example Fix Output
@@ -159,11 +169,15 @@ Fixing: src/helpers.ts
159
169
  Removed unused export: unusedFunction
160
170
  Fixing: src/types.ts
161
171
  Removed unused property: UserConfig.unusedProp
172
+ Fixing: src/api.ts
173
+ ✓ Removed never-returned type 'ErrorResult' from processRequest
174
+ ✓ Removed never-returned type 'TimeoutError' from fetchData
162
175
  Skipped: src/modified.ts (has local git changes)
163
176
 
164
177
  Summary:
165
178
  Fixed exports: 1
166
179
  Fixed properties: 1
180
+ Fixed never-returned types: 2
167
181
  Deleted files: 1
168
182
  Skipped files: 1
169
183
 
@@ -253,6 +267,57 @@ This will:
253
267
 
254
268
  ## Advanced Features
255
269
 
270
+ ### Never-Returned Types Detection
271
+
272
+ The analyzer detects when functions declare union types in their return type signature but never actually return certain branches of that union. This commonly occurs when:
273
+
274
+ - **Error Handling**: Functions declare error types but never return errors in practice
275
+ - **Refactoring**: Code evolution leaves unused type branches behind
276
+ - **Over-Engineering**: Return types are declared too broadly for actual usage
277
+
278
+ **Example:**
279
+
280
+ ```typescript
281
+ interface SuccessResult {
282
+ success: true;
283
+ data: string;
284
+ }
285
+
286
+ interface ErrorResult {
287
+ success: false;
288
+ error: string;
289
+ }
290
+
291
+ type ResultType = SuccessResult | ErrorResult;
292
+
293
+ // ErrorResult is never returned
294
+ export function processData(): ResultType {
295
+ // This function only ever returns SuccessResult
296
+ return { success: true, data: "processed" };
297
+ }
298
+ ```
299
+
300
+ **Detection Capabilities:**
301
+ - Analyzes all function return statements to determine actual return types
302
+ - Handles `Promise<Union>` types for async functions
303
+ - Works with type aliases and direct union declarations
304
+ - Supports primitive unions (`string | number | boolean`)
305
+ - Handles multi-way unions (3+ types)
306
+ - Normalizes boolean literals (`true | false` → `boolean`)
307
+
308
+ **Auto-Fix Behavior:**
309
+ - Removes never-returned types from the union declaration
310
+ - Preserves `Promise<>` wrapper for async functions
311
+ - Simplifies to single type when only one branch remains
312
+ - Skips functions that are completely unused (they get removed entirely)
313
+
314
+ After fixing the example above, the return type would become:
315
+ ```typescript
316
+ export function processData(): SuccessResult {
317
+ return { success: true, data: "processed" };
318
+ }
319
+ ```
320
+
256
321
  ### Structural Property Equivalence
257
322
 
258
323
  The analyzer handles cases where properties are re-declared across multiple interfaces with the same name and type. This commonly occurs with:
package/dist/cli.js CHANGED
@@ -2,17 +2,240 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import fs2 from "node:fs";
5
- import path10 from "node:path";
5
+ import path12 from "node:path";
6
6
 
7
7
  // src/analyzeProject.ts
8
- import path6 from "node:path";
8
+ import path8 from "node:path";
9
9
  import { Project } from "ts-morph";
10
10
 
11
- // src/findUnusedExports.ts
11
+ // src/findNeverReturnedTypes.ts
12
12
  import path2 from "node:path";
13
+ import { SyntaxKind as SyntaxKind2 } from "ts-morph";
13
14
 
14
- // src/checkExportUsage.ts
15
+ // src/analyzeFunctionReturnTypes.ts
15
16
  import path from "node:path";
17
+ import { SyntaxKind } from "ts-morph";
18
+ function analyzeFunctionReturnTypes(func, sourceFile, tsConfigDir) {
19
+ const results = [];
20
+ const functionName = func.getName();
21
+ if (!functionName) {
22
+ return results;
23
+ }
24
+ const returnTypeNode = func.getReturnTypeNode();
25
+ if (!returnTypeNode) {
26
+ return results;
27
+ }
28
+ const returnType = returnTypeNode.getType();
29
+ const unwrappedPromise = unwrapPromiseType(returnType);
30
+ const typeToCheck = unwrappedPromise || returnType;
31
+ if (!typeToCheck.isUnion()) {
32
+ return results;
33
+ }
34
+ const unionTypes = typeToCheck.getUnionTypes();
35
+ if (unionTypes.length < 2) {
36
+ return results;
37
+ }
38
+ const returnStatements = func.getDescendantsOfKind(SyntaxKind.ReturnStatement);
39
+ const returnedTypes = [];
40
+ for (const returnStmt of returnStatements) {
41
+ const expression = returnStmt.getExpression();
42
+ if (expression) {
43
+ const exprType = expression.getType();
44
+ const unwrapped = unwrapPromiseType(exprType);
45
+ returnedTypes.push(unwrapped || exprType);
46
+ }
47
+ }
48
+ if (returnedTypes.length === 0) {
49
+ return results;
50
+ }
51
+ for (const returnedType of returnedTypes) {
52
+ if (returnedType.isAssignableTo(typeToCheck)) {
53
+ let assignableToAnyBranch = false;
54
+ for (const unionBranch of unionTypes) {
55
+ if (returnedType.isAssignableTo(unionBranch)) {
56
+ assignableToAnyBranch = true;
57
+ break;
58
+ }
59
+ }
60
+ if (!assignableToAnyBranch) {
61
+ return results;
62
+ }
63
+ }
64
+ }
65
+ const enumGroups = new Map;
66
+ const enumsWithReturnedValues = new Set;
67
+ for (const unionBranch of unionTypes) {
68
+ if (unionBranch.isEnumLiteral()) {
69
+ const enumName = getEnumNameFromLiteral(unionBranch);
70
+ if (enumName) {
71
+ if (!enumGroups.has(enumName)) {
72
+ enumGroups.set(enumName, []);
73
+ }
74
+ enumGroups.get(enumName)?.push(unionBranch);
75
+ }
76
+ }
77
+ }
78
+ for (const [enumName, enumBranches] of enumGroups.entries()) {
79
+ for (const returnedType of returnedTypes) {
80
+ for (const enumBranch of enumBranches) {
81
+ if (isTypeAssignableTo(returnedType, enumBranch)) {
82
+ enumsWithReturnedValues.add(enumName);
83
+ break;
84
+ }
85
+ }
86
+ if (enumsWithReturnedValues.has(enumName)) {
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ const branchMap = new Map;
92
+ for (const unionBranch of unionTypes) {
93
+ const displayName = getTypeDisplayName(unionBranch);
94
+ if (!branchMap.has(displayName)) {
95
+ branchMap.set(displayName, unionBranch);
96
+ }
97
+ }
98
+ for (const [displayName, unionBranch] of branchMap.entries()) {
99
+ if (unionBranch.isEnumLiteral()) {
100
+ const enumName = getEnumNameFromLiteral(unionBranch);
101
+ if (enumName && enumsWithReturnedValues.has(enumName)) {
102
+ continue;
103
+ }
104
+ }
105
+ let isReturned = false;
106
+ for (const returnedType of returnedTypes) {
107
+ if (isTypeAssignableTo(returnedType, unionBranch)) {
108
+ isReturned = true;
109
+ break;
110
+ }
111
+ }
112
+ if (!isReturned) {
113
+ const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
114
+ const nameNode = func.getNameNode();
115
+ if (nameNode) {
116
+ const startPos = nameNode.getStart();
117
+ const lineStartPos = nameNode.getStartLinePos();
118
+ const character = startPos - lineStartPos + 1;
119
+ const endCharacter = character + functionName.length;
120
+ results.push({
121
+ filePath: relativePath,
122
+ functionName,
123
+ neverReturnedType: displayName,
124
+ line: nameNode.getStartLineNumber(),
125
+ character,
126
+ endCharacter,
127
+ severity: "error"
128
+ });
129
+ }
130
+ }
131
+ }
132
+ return results;
133
+ }
134
+ function unwrapPromiseType(type) {
135
+ const symbol = type.getSymbol();
136
+ if (symbol?.getName() === "Promise") {
137
+ const typeArgs = type.getTypeArguments();
138
+ if (typeArgs.length > 0 && typeArgs[0]) {
139
+ return typeArgs[0];
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ function isTypeAssignableTo(sourceType, targetType) {
145
+ if (sourceType.isAssignableTo(targetType)) {
146
+ return true;
147
+ }
148
+ const sourceText = sourceType.getText();
149
+ const targetText = targetType.getText();
150
+ const isBooleanLiteral = (text) => text === "true" || text === "false";
151
+ if (isBooleanLiteral(sourceText) && isBooleanLiteral(targetText)) {
152
+ return true;
153
+ }
154
+ return false;
155
+ }
156
+ function getEnumNameFromLiteral(type) {
157
+ if (!type.isEnumLiteral()) {
158
+ return null;
159
+ }
160
+ const typeText = type.getText();
161
+ const cleanedText = typeText.replace(/import\([^)]+\)\./g, "");
162
+ const lastDotIndex = cleanedText.lastIndexOf(".");
163
+ if (lastDotIndex > 0) {
164
+ return cleanedText.substring(0, lastDotIndex);
165
+ }
166
+ return null;
167
+ }
168
+ function getTypeDisplayName(type) {
169
+ const symbol = type.getSymbol();
170
+ if (symbol) {
171
+ const name = symbol.getName();
172
+ if (name && name !== "__type") {
173
+ return name;
174
+ }
175
+ }
176
+ let typeText = type.getText();
177
+ typeText = typeText.replace(/import\([^)]+\)\./g, "");
178
+ if (typeText === "string")
179
+ return "string";
180
+ if (typeText === "number")
181
+ return "number";
182
+ if (typeText === "boolean")
183
+ return "boolean";
184
+ if (typeText === "true" || typeText === "false")
185
+ return "boolean";
186
+ if (typeText === "null")
187
+ return "null";
188
+ if (typeText === "undefined")
189
+ return "undefined";
190
+ const MAX_TYPE_LENGTH = 100;
191
+ if (typeText.length > MAX_TYPE_LENGTH) {
192
+ return `${typeText.substring(0, MAX_TYPE_LENGTH)}...`;
193
+ }
194
+ return typeText;
195
+ }
196
+
197
+ // src/hasNoCheck.ts
198
+ function hasNoCheck(sourceFile) {
199
+ const fullText = sourceFile.getFullText();
200
+ const firstLine = fullText.split(`
201
+ `)[0];
202
+ if (!firstLine) {
203
+ return false;
204
+ }
205
+ return firstLine.trim() === "// @ts-nocheck";
206
+ }
207
+
208
+ // src/findNeverReturnedTypes.ts
209
+ function findNeverReturnedTypes(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
210
+ const results = [];
211
+ for (const sourceFile of project.getSourceFiles()) {
212
+ if (isTestFile(sourceFile)) {
213
+ continue;
214
+ }
215
+ if (hasNoCheck(sourceFile)) {
216
+ continue;
217
+ }
218
+ if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
219
+ continue;
220
+ }
221
+ if (onProgress) {
222
+ const relativePath = path2.relative(tsConfigDir, sourceFile.getFilePath());
223
+ onProgress(relativePath);
224
+ }
225
+ const functions = sourceFile.getDescendantsOfKind(SyntaxKind2.FunctionDeclaration);
226
+ for (const func of functions) {
227
+ const funcResults = analyzeFunctionReturnTypes(func, sourceFile, tsConfigDir);
228
+ results.push(...funcResults);
229
+ }
230
+ }
231
+ return results;
232
+ }
233
+
234
+ // src/findUnusedExports.ts
235
+ import path4 from "node:path";
236
+
237
+ // src/checkExportUsage.ts
238
+ import path3 from "node:path";
16
239
  import { Node } from "ts-morph";
17
240
  function getExportKind(declaration) {
18
241
  if (Node.isFunctionDeclaration(declaration)) {
@@ -84,7 +307,7 @@ function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isT
84
307
  return null;
85
308
  }
86
309
  const kind = getExportKind(firstDeclaration);
87
- const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
310
+ const relativePath = path3.relative(tsConfigDir, sourceFile.getFilePath());
88
311
  const severity = onlyUsedInTests ? "info" : "error";
89
312
  const nameNode = getNameNode(firstDeclaration);
90
313
  const positionNode = nameNode || firstDeclaration;
@@ -105,17 +328,6 @@ function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isT
105
328
  return result;
106
329
  }
107
330
 
108
- // src/hasNoCheck.ts
109
- function hasNoCheck(sourceFile) {
110
- const fullText = sourceFile.getFullText();
111
- const firstLine = fullText.split(`
112
- `)[0];
113
- if (!firstLine) {
114
- return false;
115
- }
116
- return firstLine.trim() === "// @ts-nocheck";
117
- }
118
-
119
331
  // src/findUnusedExports.ts
120
332
  function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
121
333
  const results = [];
@@ -130,7 +342,7 @@ function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetF
130
342
  continue;
131
343
  }
132
344
  if (onProgress) {
133
- const relativePath = path2.relative(tsConfigDir, sourceFile.getFilePath());
345
+ const relativePath = path4.relative(tsConfigDir, sourceFile.getFilePath());
134
346
  onProgress(relativePath);
135
347
  }
136
348
  const exports = sourceFile.getExportedDeclarations();
@@ -145,10 +357,10 @@ function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetF
145
357
  }
146
358
 
147
359
  // src/findUnusedProperties.ts
148
- import path5 from "node:path";
360
+ import path7 from "node:path";
149
361
 
150
362
  // src/analyzeInterfaces.ts
151
- import path3 from "node:path";
363
+ import path5 from "node:path";
152
364
 
153
365
  // src/extractTodoComment.ts
154
366
  function extractTodoComment(prop) {
@@ -174,7 +386,7 @@ function extractTodoComment(prop) {
174
386
  }
175
387
 
176
388
  // src/findStructurallyEquivalentProperties.ts
177
- import { SyntaxKind } from "ts-morph";
389
+ import { SyntaxKind as SyntaxKind3 } from "ts-morph";
178
390
  function checkInterfaceProperties(iface, propName, propType, originalProp, equivalentProps) {
179
391
  const properties = iface.getProperties();
180
392
  for (const p of properties) {
@@ -191,15 +403,15 @@ function checkInterfaceProperties(iface, propName, propType, originalProp, equiv
191
403
  }
192
404
  function checkTypeAliasProperties(typeAlias, propName, propType, originalProp, equivalentProps) {
193
405
  const typeNode = typeAlias.getTypeNode();
194
- if (!typeNode || typeNode.getKind() !== SyntaxKind.TypeLiteral) {
406
+ if (!typeNode || typeNode.getKind() !== SyntaxKind3.TypeLiteral) {
195
407
  return;
196
408
  }
197
- const properties = typeNode.getChildren().filter((child) => child.getKind() === SyntaxKind.PropertySignature);
409
+ const properties = typeNode.getChildren().filter((child) => child.getKind() === SyntaxKind3.PropertySignature);
198
410
  for (const p of properties) {
199
411
  if (p === originalProp) {
200
412
  continue;
201
413
  }
202
- if (p.getKind() === SyntaxKind.PropertySignature && p.getName() === propName) {
414
+ if (p.getKind() === SyntaxKind3.PropertySignature && p.getName() === propName) {
203
415
  const pType = p.getType().getText();
204
416
  if (pType === propType) {
205
417
  equivalentProps.push(p);
@@ -278,7 +490,7 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
278
490
  for (const prop of iface.getProperties()) {
279
491
  const usage = isPropertyUnused(prop, isTestFile, project);
280
492
  if (usage.isUnusedOrTestOnly) {
281
- const relativePath = path3.relative(tsConfigDir, sourceFile.getFilePath());
493
+ const relativePath = path5.relative(tsConfigDir, sourceFile.getFilePath());
282
494
  const todoComment = extractTodoComment(prop);
283
495
  let severity = "error";
284
496
  if (todoComment) {
@@ -309,7 +521,7 @@ function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project
309
521
  }
310
522
 
311
523
  // src/analyzeTypeAliases.ts
312
- import path4 from "node:path";
524
+ import path6 from "node:path";
313
525
  import { Node as Node2 } from "ts-morph";
314
526
  function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project) {
315
527
  if (!Node2.isPropertySignature(member)) {
@@ -319,7 +531,7 @@ function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isT
319
531
  if (!usage.isUnusedOrTestOnly) {
320
532
  return;
321
533
  }
322
- const relativePath = path4.relative(tsConfigDir, sourceFile.getFilePath());
534
+ const relativePath = path6.relative(tsConfigDir, sourceFile.getFilePath());
323
535
  const todoComment = extractTodoComment(member);
324
536
  let severity = "error";
325
537
  if (todoComment) {
@@ -379,7 +591,7 @@ function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targ
379
591
  continue;
380
592
  }
381
593
  if (onProgress) {
382
- const relativePath = path5.relative(tsConfigDir, sourceFile.getFilePath());
594
+ const relativePath = path7.relative(tsConfigDir, sourceFile.getFilePath());
383
595
  onProgress(relativePath);
384
596
  }
385
597
  analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project);
@@ -400,7 +612,7 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
400
612
  const project = new Project({
401
613
  tsConfigFilePath: tsConfigPath
402
614
  });
403
- const tsConfigDir = path6.dirname(tsConfigPath);
615
+ const tsConfigDir = path8.dirname(tsConfigPath);
404
616
  const allSourceFiles = project.getSourceFiles();
405
617
  const filesToAnalyze = allSourceFiles.filter((sf) => {
406
618
  if (isTestFile2(sf)) {
@@ -426,43 +638,50 @@ function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 =
426
638
  } : undefined;
427
639
  const unusedExports = findUnusedExports(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
428
640
  const unusedProperties = findUnusedProperties(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
641
+ const neverReturnedTypes = findNeverReturnedTypes(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
429
642
  const unusedFiles = [];
430
643
  const fileExportCounts = new Map;
431
644
  for (const sourceFile of filesToAnalyze) {
432
- const filePath = path6.relative(tsConfigDir, sourceFile.getFilePath());
645
+ const filePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
433
646
  const exports = sourceFile.getExportedDeclarations();
434
647
  const totalExports = exports.size;
435
648
  if (totalExports > 0) {
436
- fileExportCounts.set(filePath, { total: totalExports, unused: 0 });
649
+ fileExportCounts.set(filePath, { total: totalExports, unused: 0, testOnly: 0 });
437
650
  }
438
651
  }
439
652
  for (const unusedExport of unusedExports) {
440
653
  const counts = fileExportCounts.get(unusedExport.filePath);
441
654
  if (counts) {
442
655
  counts.unused++;
656
+ if (unusedExport.onlyUsedInTests) {
657
+ counts.testOnly++;
658
+ }
443
659
  }
444
660
  }
445
661
  for (const [filePath, counts] of fileExportCounts.entries()) {
446
- if (counts.total > 0 && counts.unused === counts.total) {
662
+ const allExportsUnused = counts.total > 0 && counts.unused === counts.total;
663
+ const hasAnyTestOnlyExports = counts.testOnly > 0;
664
+ if (allExportsUnused && !hasAnyTestOnlyExports) {
447
665
  unusedFiles.push(filePath);
448
666
  }
449
667
  }
450
668
  const results = {
451
669
  unusedExports,
452
670
  unusedProperties,
453
- unusedFiles
671
+ unusedFiles,
672
+ neverReturnedTypes
454
673
  };
455
674
  return results;
456
675
  }
457
676
 
458
677
  // src/fixProject.ts
459
678
  import fs from "node:fs";
460
- import path8 from "node:path";
461
- import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
679
+ import path10 from "node:path";
680
+ import { Project as Project2, SyntaxKind as SyntaxKind4 } from "ts-morph";
462
681
 
463
682
  // src/checkGitStatus.ts
464
683
  import { execSync } from "node:child_process";
465
- import path7 from "node:path";
684
+ import path9 from "node:path";
466
685
  function checkGitStatus(workingDir) {
467
686
  const changedFiles = new Set;
468
687
  try {
@@ -484,7 +703,7 @@ function checkGitStatus(workingDir) {
484
703
  const filename = line.slice(3).trim();
485
704
  const actualFilename = filename.includes(" -> ") ? filename.split(" -> ")[1] : filename;
486
705
  if (actualFilename) {
487
- const absolutePath = path7.resolve(gitRoot, actualFilename);
706
+ const absolutePath = path9.resolve(gitRoot, actualFilename);
488
707
  changedFiles.add(absolutePath);
489
708
  }
490
709
  }
@@ -499,19 +718,20 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
499
718
  const results = {
500
719
  fixedExports: 0,
501
720
  fixedProperties: 0,
721
+ fixedNeverReturnedTypes: 0,
502
722
  deletedFiles: 0,
503
723
  skippedFiles: [],
504
724
  errors: []
505
725
  };
506
726
  const analysis = analyzeProject(tsConfigPath, undefined, undefined, isTestFile2);
507
- const tsConfigDir = path8.dirname(path8.resolve(tsConfigPath));
727
+ const tsConfigDir = path10.dirname(path10.resolve(tsConfigPath));
508
728
  const filesWithChanges = checkGitStatus(tsConfigDir);
509
729
  const project = new Project2({
510
730
  tsConfigFilePath: tsConfigPath
511
731
  });
512
732
  const deletedFiles = new Set;
513
733
  for (const relativeFilePath of analysis.unusedFiles) {
514
- const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
734
+ const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
515
735
  if (filesWithChanges.has(absoluteFilePath)) {
516
736
  onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
517
737
  results.skippedFiles.push(relativeFilePath);
@@ -531,15 +751,71 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
531
751
  if (deletedFiles.size > 0) {
532
752
  cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results);
533
753
  }
754
+ const neverReturnedByFile = new Map;
755
+ for (const neverReturned of analysis.neverReturnedTypes || []) {
756
+ if (neverReturned.severity !== "error") {
757
+ continue;
758
+ }
759
+ if (!neverReturnedByFile.has(neverReturned.filePath)) {
760
+ neverReturnedByFile.set(neverReturned.filePath, []);
761
+ }
762
+ neverReturnedByFile.get(neverReturned.filePath)?.push(neverReturned);
763
+ }
764
+ const unusedExportNames = new Set;
765
+ for (const unusedExport of analysis.unusedExports) {
766
+ if (unusedExport.severity === "error") {
767
+ unusedExportNames.add(`${unusedExport.filePath}:${unusedExport.exportName}`);
768
+ }
769
+ }
770
+ for (const [relativeFilePath, neverReturnedItems] of neverReturnedByFile.entries()) {
771
+ const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
772
+ if (analysis.unusedFiles.includes(relativeFilePath)) {
773
+ continue;
774
+ }
775
+ if (filesWithChanges.has(absoluteFilePath)) {
776
+ if (!results.skippedFiles.includes(relativeFilePath)) {
777
+ onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
778
+ results.skippedFiles.push(relativeFilePath);
779
+ }
780
+ continue;
781
+ }
782
+ try {
783
+ const sourceFile = project.getSourceFile(absoluteFilePath);
784
+ if (!sourceFile) {
785
+ continue;
786
+ }
787
+ onProgress?.(`Fixing: ${relativeFilePath}`);
788
+ if (neverReturnedItems) {
789
+ for (const neverReturned of neverReturnedItems) {
790
+ const exportKey = `${relativeFilePath}:${neverReturned.functionName}`;
791
+ if (unusedExportNames.has(exportKey)) {
792
+ continue;
793
+ }
794
+ if (removeNeverReturnedType(sourceFile, neverReturned.functionName, neverReturned.neverReturnedType)) {
795
+ onProgress?.(` ✓ Removed never-returned type '${neverReturned.neverReturnedType}' from ${neverReturned.functionName}`);
796
+ results.fixedNeverReturnedTypes++;
797
+ }
798
+ }
799
+ }
800
+ sourceFile.saveSync();
801
+ } catch (error) {
802
+ const errorMessage = error instanceof Error ? error.message : String(error);
803
+ results.errors.push({ file: relativeFilePath, error: errorMessage });
804
+ onProgress?.(`Error fixing ${relativeFilePath}: ${errorMessage}`);
805
+ }
806
+ }
534
807
  const exportsByFile = new Map;
535
808
  for (const unusedExport of analysis.unusedExports) {
809
+ if (unusedExport.severity !== "error") {
810
+ continue;
811
+ }
536
812
  if (!exportsByFile.has(unusedExport.filePath)) {
537
813
  exportsByFile.set(unusedExport.filePath, []);
538
814
  }
539
815
  exportsByFile.get(unusedExport.filePath)?.push(unusedExport);
540
816
  }
541
817
  for (const [relativeFilePath, exports] of exportsByFile.entries()) {
542
- const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
818
+ const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
543
819
  if (analysis.unusedFiles.includes(relativeFilePath)) {
544
820
  continue;
545
821
  }
@@ -553,7 +829,9 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
553
829
  if (!sourceFile) {
554
830
  continue;
555
831
  }
556
- onProgress?.(`Fixing: ${relativeFilePath}`);
832
+ if (!neverReturnedByFile.has(relativeFilePath)) {
833
+ onProgress?.(`Fixing: ${relativeFilePath}`);
834
+ }
557
835
  for (const unusedExport of exports) {
558
836
  if (removeExport(sourceFile, unusedExport.exportName)) {
559
837
  onProgress?.(` ✓ Removed unused export: ${unusedExport.exportName}`);
@@ -569,13 +847,16 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
569
847
  }
570
848
  const propertiesByFile = new Map;
571
849
  for (const unusedProperty of analysis.unusedProperties) {
850
+ if (unusedProperty.severity !== "error") {
851
+ continue;
852
+ }
572
853
  if (!propertiesByFile.has(unusedProperty.filePath)) {
573
854
  propertiesByFile.set(unusedProperty.filePath, []);
574
855
  }
575
856
  propertiesByFile.get(unusedProperty.filePath)?.push(unusedProperty);
576
857
  }
577
858
  for (const [relativeFilePath, properties] of propertiesByFile.entries()) {
578
- const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
859
+ const absoluteFilePath = path10.resolve(tsConfigDir, relativeFilePath);
579
860
  if (analysis.unusedFiles.includes(relativeFilePath)) {
580
861
  continue;
581
862
  }
@@ -591,7 +872,7 @@ function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
591
872
  if (!sourceFile) {
592
873
  continue;
593
874
  }
594
- if (!exportsByFile.has(relativeFilePath)) {
875
+ if (!exportsByFile.has(relativeFilePath) && !neverReturnedByFile.has(relativeFilePath)) {
595
876
  onProgress?.(`Fixing: ${relativeFilePath}`);
596
877
  }
597
878
  for (const unusedProperty of properties) {
@@ -616,8 +897,8 @@ function removeExport(sourceFile, exportName) {
616
897
  return false;
617
898
  }
618
899
  for (const declaration of declarations) {
619
- if (declaration.getKind() === SyntaxKind2.VariableDeclaration) {
620
- const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind2.VariableStatement);
900
+ if (declaration.getKind() === SyntaxKind4.VariableDeclaration) {
901
+ const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind4.VariableStatement);
621
902
  if (variableStatement) {
622
903
  variableStatement.remove();
623
904
  continue;
@@ -644,12 +925,12 @@ function removeProperty(sourceFile, typeName, propertyName) {
644
925
  for (const typeAlias of typeAliases) {
645
926
  if (typeAlias.getName() === typeName) {
646
927
  const typeNode = typeAlias.getTypeNode();
647
- if (typeNode && typeNode.getKind() === SyntaxKind2.TypeLiteral) {
648
- const typeLiteral = typeNode.asKindOrThrow(SyntaxKind2.TypeLiteral);
928
+ if (typeNode && typeNode.getKind() === SyntaxKind4.TypeLiteral) {
929
+ const typeLiteral = typeNode.asKindOrThrow(SyntaxKind4.TypeLiteral);
649
930
  const members = typeLiteral.getMembers();
650
931
  for (const member of members) {
651
- if (member.getKind() === SyntaxKind2.PropertySignature) {
652
- const propSig = member.asKind(SyntaxKind2.PropertySignature);
932
+ if (member.getKind() === SyntaxKind4.PropertySignature) {
933
+ const propSig = member.asKind(SyntaxKind4.PropertySignature);
653
934
  if (propSig?.getName() === propertyName) {
654
935
  propSig.remove();
655
936
  return true;
@@ -661,10 +942,131 @@ function removeProperty(sourceFile, typeName, propertyName) {
661
942
  }
662
943
  return false;
663
944
  }
945
+ function removeNeverReturnedType(sourceFile, functionName, neverReturnedType) {
946
+ const functions = sourceFile.getFunctions();
947
+ for (const func of functions) {
948
+ if (func.getName() === functionName) {
949
+ const returnTypeNode = func.getReturnTypeNode();
950
+ if (!returnTypeNode) {
951
+ return false;
952
+ }
953
+ const returnType = returnTypeNode.getType();
954
+ let typeToCheck = returnType;
955
+ let isPromise = false;
956
+ const symbol = returnType.getSymbol();
957
+ if (symbol?.getName() === "Promise") {
958
+ const typeArgs = returnType.getTypeArguments();
959
+ if (typeArgs.length > 0 && typeArgs[0]) {
960
+ typeToCheck = typeArgs[0];
961
+ isPromise = true;
962
+ }
963
+ }
964
+ if (!typeToCheck.isUnion()) {
965
+ return false;
966
+ }
967
+ const unionTypes = typeToCheck.getUnionTypes();
968
+ const hasInlineObjectType = unionTypes.some((ut) => ut.getSymbol()?.getName() === "__type");
969
+ const hasEnumLiteral = unionTypes.some((ut) => ut.isEnumLiteral());
970
+ if (hasInlineObjectType || hasEnumLiteral) {
971
+ const originalTypeText = returnTypeNode.getText();
972
+ let typeTextToModify = originalTypeText;
973
+ let promiseWrapper = "";
974
+ if (isPromise) {
975
+ const promiseMatch = originalTypeText.match(/^Promise<(.+)>$/s);
976
+ if (promiseMatch?.[1]) {
977
+ typeTextToModify = promiseMatch[1];
978
+ promiseWrapper = "Promise<>";
979
+ }
980
+ }
981
+ const branches = splitUnionType(typeTextToModify);
982
+ const normalizedRemove = normalizeTypeText(neverReturnedType === "true" || neverReturnedType === "false" ? "boolean" : neverReturnedType);
983
+ const remainingBranches = branches.filter((branch) => {
984
+ const trimmed = branch.trim();
985
+ const normalized = normalizeTypeText(trimmed === "true" || trimmed === "false" ? "boolean" : trimmed);
986
+ return normalized !== normalizedRemove;
987
+ });
988
+ if (remainingBranches.length === 0 || remainingBranches.length === branches.length) {
989
+ return false;
990
+ }
991
+ let newReturnType = remainingBranches.join(" | ");
992
+ if (promiseWrapper) {
993
+ newReturnType = `Promise<${newReturnType}>`;
994
+ }
995
+ func.setReturnType(newReturnType);
996
+ return true;
997
+ } else {
998
+ const typesToKeep = [];
999
+ for (const ut of unionTypes) {
1000
+ const symbol2 = ut.getSymbol();
1001
+ const typeName = symbol2?.getName() || ut.getText();
1002
+ const normalizedName = typeName === "true" || typeName === "false" ? "boolean" : typeName;
1003
+ const normalizedRemove = neverReturnedType === "true" || neverReturnedType === "false" ? "boolean" : neverReturnedType;
1004
+ if (normalizedName !== normalizedRemove && !typesToKeep.includes(typeName)) {
1005
+ typesToKeep.push(typeName);
1006
+ }
1007
+ }
1008
+ let newReturnType;
1009
+ if (typesToKeep.length === 1 && typesToKeep[0]) {
1010
+ newReturnType = typesToKeep[0];
1011
+ } else if (typesToKeep.length > 1) {
1012
+ newReturnType = typesToKeep.join(" | ");
1013
+ }
1014
+ if (!newReturnType) {
1015
+ return false;
1016
+ }
1017
+ if (isPromise) {
1018
+ newReturnType = `Promise<${newReturnType}>`;
1019
+ }
1020
+ func.setReturnType(newReturnType);
1021
+ return true;
1022
+ }
1023
+ }
1024
+ }
1025
+ return false;
1026
+ }
1027
+ function splitUnionType(typeText) {
1028
+ const branches = [];
1029
+ let current = "";
1030
+ let depth = 0;
1031
+ let inString = false;
1032
+ let stringChar = "";
1033
+ for (let i = 0;i < typeText.length; i++) {
1034
+ const char = typeText[i];
1035
+ const prevChar = i > 0 ? typeText[i - 1] : "";
1036
+ if ((char === '"' || char === "'") && prevChar !== "\\") {
1037
+ if (!inString) {
1038
+ inString = true;
1039
+ stringChar = char;
1040
+ } else if (char === stringChar) {
1041
+ inString = false;
1042
+ stringChar = "";
1043
+ }
1044
+ }
1045
+ if (!inString) {
1046
+ if (char === "{" || char === "<" || char === "(") {
1047
+ depth++;
1048
+ } else if (char === "}" || char === ">" || char === ")") {
1049
+ depth--;
1050
+ } else if (char === "|" && depth === 0) {
1051
+ branches.push(current);
1052
+ current = "";
1053
+ continue;
1054
+ }
1055
+ }
1056
+ current += char;
1057
+ }
1058
+ if (current) {
1059
+ branches.push(current);
1060
+ }
1061
+ return branches;
1062
+ }
1063
+ function normalizeTypeText(typeText) {
1064
+ return typeText.replace(/;\s*([}\]>])/g, " $1").replace(/\s+/g, " ").trim();
1065
+ }
664
1066
  function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results) {
665
1067
  const sourceFiles = project.getSourceFiles();
666
1068
  for (const sourceFile of sourceFiles) {
667
- const relativePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
1069
+ const relativePath = path10.relative(tsConfigDir, sourceFile.getFilePath());
668
1070
  const absolutePath = sourceFile.getFilePath();
669
1071
  if (filesWithChanges.has(absolutePath)) {
670
1072
  continue;
@@ -676,7 +1078,7 @@ function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChang
676
1078
  const moduleSpecifier = exportDecl.getModuleSpecifierValue();
677
1079
  if (moduleSpecifier) {
678
1080
  const resolvedPath = resolveImportPath(sourceFile.getFilePath(), moduleSpecifier);
679
- const relativeResolvedPath = resolvedPath ? path8.relative(tsConfigDir, resolvedPath) : null;
1081
+ const relativeResolvedPath = resolvedPath ? path10.relative(tsConfigDir, resolvedPath) : null;
680
1082
  if (relativeResolvedPath && deletedFiles.has(relativeResolvedPath)) {
681
1083
  brokenExports.push(exportDecl);
682
1084
  } else {
@@ -724,9 +1126,9 @@ function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChang
724
1126
  }
725
1127
  }
726
1128
  function resolveImportPath(fromFile, importPath) {
727
- const fromDir = path8.dirname(fromFile);
1129
+ const fromDir = path10.dirname(fromFile);
728
1130
  if (importPath.startsWith(".")) {
729
- const resolved = path8.resolve(fromDir, importPath);
1131
+ const resolved = path10.resolve(fromDir, importPath);
730
1132
  const extensions = [".ts", ".tsx", ".js", ".jsx"];
731
1133
  for (const ext of extensions) {
732
1134
  const withExt = resolved + ext;
@@ -735,7 +1137,7 @@ function resolveImportPath(fromFile, importPath) {
735
1137
  }
736
1138
  }
737
1139
  for (const ext of extensions) {
738
- const indexFile = path8.join(resolved, `index${ext}`);
1140
+ const indexFile = path10.join(resolved, `index${ext}`);
739
1141
  if (fs.existsSync(indexFile)) {
740
1142
  return indexFile;
741
1143
  }
@@ -746,7 +1148,7 @@ function resolveImportPath(fromFile, importPath) {
746
1148
  }
747
1149
 
748
1150
  // src/formatResults.ts
749
- import path9 from "node:path";
1151
+ import path11 from "node:path";
750
1152
  function getSeverityMarker(severity) {
751
1153
  const markers = {
752
1154
  error: "[ERROR]",
@@ -768,12 +1170,16 @@ function formatPropertyLine(item) {
768
1170
  const marker = getSeverityMarker(item.severity);
769
1171
  return ` ${item.typeName}.${item.propertyName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (${status}${todoSuffix})`;
770
1172
  }
1173
+ function formatNeverReturnedLine(item) {
1174
+ const marker = getSeverityMarker(item.severity);
1175
+ return ` ${item.functionName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (Type '${item.neverReturnedType}' in return type is never returned)`;
1176
+ }
771
1177
  function formatGroupedItems(items, formatter, tsConfigDir, cwd) {
772
1178
  const lines = [];
773
1179
  const grouped = groupByFile(items);
774
1180
  for (const [filePath, groupItems] of grouped.entries()) {
775
- const absolutePath = path9.resolve(tsConfigDir, filePath);
776
- const relativePath = path9.relative(cwd, absolutePath);
1181
+ const absolutePath = path11.resolve(tsConfigDir, filePath);
1182
+ const relativePath = path11.relative(cwd, absolutePath);
777
1183
  lines.push(relativePath);
778
1184
  for (const item of groupItems) {
779
1185
  lines.push(formatter(item));
@@ -796,8 +1202,8 @@ function formatResults(results, tsConfigDir) {
796
1202
  lines.push("Completely Unused Files:");
797
1203
  lines.push("");
798
1204
  for (const filePath of results.unusedFiles) {
799
- const absolutePath = path9.resolve(tsConfigDir, filePath);
800
- const relativePath = path9.relative(cwd, absolutePath);
1205
+ const absolutePath = path11.resolve(tsConfigDir, filePath);
1206
+ const relativePath = path11.relative(cwd, absolutePath);
801
1207
  lines.push(relativePath);
802
1208
  lines.push(" file:1:1-1 [ERROR] (All exports unused - file can be deleted)");
803
1209
  lines.push("");
@@ -813,13 +1219,20 @@ function formatResults(results, tsConfigDir) {
813
1219
  lines.push("");
814
1220
  lines.push(...formatGroupedItems(propertiesToReport, formatPropertyLine, tsConfigDir, cwd));
815
1221
  }
816
- if (results.unusedFiles.length === 0 && exportsToReport.length === 0 && propertiesToReport.length === 0) {
1222
+ const neverReturnedTypes = results.neverReturnedTypes || [];
1223
+ if (neverReturnedTypes.length > 0) {
1224
+ lines.push("Never-Returned Types:");
1225
+ lines.push("");
1226
+ lines.push(...formatGroupedItems(neverReturnedTypes, formatNeverReturnedLine, tsConfigDir, cwd));
1227
+ }
1228
+ if (results.unusedFiles.length === 0 && exportsToReport.length === 0 && propertiesToReport.length === 0 && neverReturnedTypes.length === 0) {
817
1229
  lines.push("No unused exports or properties found!");
818
1230
  } else {
819
1231
  lines.push("Summary:");
820
1232
  lines.push(` Completely unused files: ${results.unusedFiles.length}`);
821
1233
  lines.push(` Unused exports: ${exportsToReport.length}`);
822
1234
  lines.push(` Unused properties: ${propertiesToReport.length}`);
1235
+ lines.push(` Never-returned types: ${neverReturnedTypes.length}`);
823
1236
  }
824
1237
  return lines.join(`
825
1238
  `);
@@ -860,11 +1273,11 @@ function main() {
860
1273
  command = firstArg;
861
1274
  configIndex = 1;
862
1275
  }
863
- let tsConfigPath = path10.resolve(args[configIndex] ?? "");
1276
+ let tsConfigPath = path12.resolve(args[configIndex] ?? "");
864
1277
  if (fs2.existsSync(tsConfigPath) && fs2.statSync(tsConfigPath).isDirectory()) {
865
- tsConfigPath = path10.join(tsConfigPath, "tsconfig.json");
1278
+ tsConfigPath = path12.join(tsConfigPath, "tsconfig.json");
866
1279
  }
867
- const targetFilePath = args[configIndex + 1] ? path10.resolve(args[configIndex + 1]) : undefined;
1280
+ const targetFilePath = args[configIndex + 1] ? path12.resolve(args[configIndex + 1]) : undefined;
868
1281
  if (command === "fix") {
869
1282
  console.log(`Fixing TypeScript project: ${tsConfigPath}`);
870
1283
  console.log("");
@@ -903,11 +1316,11 @@ function main() {
903
1316
  const filledLength = Math.min(barLength, Math.max(0, Math.floor(current / total * barLength)));
904
1317
  const emptyLength = Math.max(0, barLength - filledLength);
905
1318
  const bar = "█".repeat(filledLength) + "░".repeat(emptyLength);
906
- const fileName = path10.basename(filePath);
1319
+ const fileName = path12.basename(filePath);
907
1320
  process.stdout.write(`\r\x1B[KProgress: [${bar}] ${percentage}% (${current}/${total}) ${fileName}`);
908
1321
  }, targetFilePath);
909
1322
  process.stdout.write("\r\x1B[K");
910
- const tsConfigDir = path10.dirname(path10.resolve(tsConfigPath));
1323
+ const tsConfigDir = path12.dirname(path12.resolve(tsConfigPath));
911
1324
  const output = formatResults(results, tsConfigDir);
912
1325
  console.log(output);
913
1326
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-unused",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Find unused exports and properties in TypeScript projects",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -13,11 +13,13 @@
13
13
  "scripts": {
14
14
  "clean": "rm -rf dist",
15
15
  "build:js": "bun build src/cli.ts --outdir dist --target node --external ts-morph --external typescript",
16
- "build": "bun lint && bun test && bun clean && bun build:js && bun sanity-check --no-build",
16
+ "build": "bun clean && bun build:js",
17
17
  "typecheck": "tsgo --noEmit",
18
18
  "lint": "bun run fix && biome check src",
19
19
  "fix": "biome check --write --unsafe src",
20
- "sanity-check": "bun run scripts/sanity-check.ts"
20
+ "sanity-check": "bun run scripts/sanity-check.ts",
21
+ "prepublish": "bun lint && bun test && bun run build && bun sanity-check --no-build",
22
+ "cli": "bun run src/cli.ts"
21
23
  },
22
24
  "devDependencies": {
23
25
  "@biomejs/biome": "^2.3.8",