ts-unused 1.0.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/LICENSE +21 -0
- package/README.md +295 -0
- package/dist/cli.js +915 -0
- package/package.json +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alex Gorbatchev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# ts-unused
|
|
2
|
+
|
|
3
|
+
A CLI tool that analyzes TypeScript projects to find unused exports and unused properties in types and interfaces.
|
|
4
|
+
|
|
5
|
+
**ts-unused** is built with AI-assisted development in mind. Its primary goal is to audit codebases, especially those heavily modified by AI coding assistants, to identify dead code, unused types, _properties_, and exports. The intended workflow is to feed the tool's output back into an AI agent, allowing it to autonomously clean up and refactor the project. VSCode problem matcher makes it a fully integrated experience.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Finds Unused Exports**: Identifies functions, classes, types, interfaces, and constants that are exported but never imported or used elsewhere
|
|
10
|
+
- **Finds Completely Unused Files**: Identifies files where all exports are unused, suggesting the entire file can be deleted
|
|
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
|
|
13
|
+
- **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
|
+
- **Three-Tier Severity System**: Categorizes findings by severity level for better prioritization
|
|
15
|
+
- **ERROR**: Completely unused code that should be removed
|
|
16
|
+
- **WARNING**: Items with TODO comments that need attention
|
|
17
|
+
- **INFO**: Items only used in tests (testing helpers)
|
|
18
|
+
- **TODO Comment Detection**: Identifies and highlights properties/types with TODO comments
|
|
19
|
+
- **Excludes Test Files**: Automatically excludes test files from analysis to avoid false positives (but tracks test-only usage)
|
|
20
|
+
- **Excludes @ts-nocheck Files**: Automatically excludes files with `// @ts-nocheck` on the first line
|
|
21
|
+
- **Inline Type Support**: Analyzes inline object types in addition to named interfaces and type aliases
|
|
22
|
+
- **VS Code Integration**: Problem matcher for displaying results in VS Code's Problems panel with severity indicators
|
|
23
|
+
|
|
24
|
+
## Limitations
|
|
25
|
+
|
|
26
|
+
- **Dynamic Access**: Properties accessed via computed property names (`obj[key]`) may not be detected
|
|
27
|
+
- **Re-exports**: Items that are re-exported through barrel files are considered used
|
|
28
|
+
- **Type Narrowing**: Properties accessed after type guards or conditional checks may not always be tracked through complex control flow
|
|
29
|
+
|
|
30
|
+
## Comparison With Similar Tools
|
|
31
|
+
|
|
32
|
+
### Feature Comparison
|
|
33
|
+
|
|
34
|
+
| Feature | `ts-unused` | [`tsr`](https://github.com/line/tsr) | [`ts-unused-exports`](https://github.com/pzavolinsky/ts-unused-exports) |
|
|
35
|
+
| :--- | :--- | :--- | :--- |
|
|
36
|
+
| **Goal** | **Report & Analyze** | **Remove (Tree-Shake)** | **Report** |
|
|
37
|
+
| **Unused Exports** | ✅ Detects and reports | ✅ Detects and removes | ✅ Detects and reports |
|
|
38
|
+
| **Unused Properties** | ✅ **Unique:** Checks `interface`/`type` properties | ❌ No property checks | ❌ No property checks |
|
|
39
|
+
| **Test-Only Usage** | ✅ **Unique:** Identifies "Used only in tests" | ⚠️ May delete if not entrypoint | ❌ No distinction |
|
|
40
|
+
| **Comment Support** | ✅ **Unique:** TODOs change severity | ✅ Skip/Ignore only | ✅ Skip/Ignore only |
|
|
41
|
+
| **Unused Files** | ✅ Reports completely unused files | ✅ Deletes unreachable files | ✅ Explicit report flag |
|
|
42
|
+
| **VS Code Integration** | ✅ **Problem Matcher provided** | ❌ Manual setup required | ❌ Manual setup / ESLint plugin |
|
|
43
|
+
| **Auto-Fix** | ✅ Removes unused code safely | ✅ **Primary Feature:** Auto-removes code | ❌ Manual removal required |
|
|
44
|
+
| **Accuracy** | ⭐️ **High** (Language Service) | ⚡️ **Fast** (Custom Graph) | ⚡️ **Fast** (Custom Parser) |
|
|
45
|
+
| **Entrypoints** | 🟢 Not required (Global scan) | 🔴 **Required** (Reachability graph) | 🟢 Not required (Global scan) |
|
|
46
|
+
|
|
47
|
+
### vs [tsr](https://github.com/line/tsr)
|
|
48
|
+
|
|
49
|
+
**tsr** is primarily a "tree-shaking" tool for source code, designed to automatically remove unused code.
|
|
50
|
+
|
|
51
|
+
- **Goal**: `tsr` focuses on **removing** code (autofix), while `ts-unused` focuses on **reporting** and analysis.
|
|
52
|
+
- **Detection**: `tsr` uses a reachability graph starting from defined entrypoints. If code isn't reachable, it's deleted. `ts-unused` scans all files and checks for global references using the TypeScript Language Service.
|
|
53
|
+
|
|
54
|
+
### vs [ts-unused-exports](https://github.com/pzavolinsky/ts-unused-exports)
|
|
55
|
+
|
|
56
|
+
**ts-unused-exports** is a specialized, high-performance tool for finding unused exported symbols.
|
|
57
|
+
|
|
58
|
+
- **Performance**: `ts-unused-exports` uses a custom parser and resolver, making it potentially faster on very large codebases than `ts-unused` (which uses the full TypeScript Language Service).
|
|
59
|
+
- **Accuracy**: `ts-unused` leverages the standard TypeScript Language Service, ensuring higher accuracy with complex re-exports, type inference, and aliasing.
|
|
60
|
+
- **Granularity**: `ts-unused` provides deeper analysis for **unused properties** and **test-only usage**, whereas `ts-unused-exports` focuses strictly on exported symbols.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g ts-unused
|
|
66
|
+
# or
|
|
67
|
+
bun add -g ts-unused
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### Check Command (Analysis Only)
|
|
73
|
+
|
|
74
|
+
Analyze your project and report unused items without making any changes:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
ts-unused check <path-to-tsconfig.json> [file-path-to-check]
|
|
78
|
+
# or simply:
|
|
79
|
+
ts-unused <path-to-tsconfig.json> [file-path-to-check]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Fix Command (Auto-Remove)
|
|
83
|
+
|
|
84
|
+
Automatically remove unused exports, properties, and delete unused files:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
ts-unused fix <path-to-tsconfig.json>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Safety Features:**
|
|
91
|
+
- Checks git status before modifying each file
|
|
92
|
+
- Skips files with uncommitted local changes
|
|
93
|
+
- Reports which files were skipped
|
|
94
|
+
- Continues processing other files if one fails
|
|
95
|
+
- Provides detailed per-file logging
|
|
96
|
+
|
|
97
|
+
### Example
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Analyze only
|
|
101
|
+
ts-unused ./tsconfig.json
|
|
102
|
+
|
|
103
|
+
# Auto-fix
|
|
104
|
+
ts-unused fix ./tsconfig.json
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Output
|
|
108
|
+
|
|
109
|
+
### Check Command Output
|
|
110
|
+
|
|
111
|
+
The script outputs:
|
|
112
|
+
|
|
113
|
+
1. **Completely Unused Files**: Files where all exports are unused (candidates for deletion)
|
|
114
|
+
2. **Unused Exports**: Functions, types, interfaces, and constants that are exported but never used
|
|
115
|
+
3. **Unused Properties**: Properties in types/interfaces that are defined but never accessed
|
|
116
|
+
3. **Summary**: Total count of unused items found
|
|
117
|
+
|
|
118
|
+
Each finding includes:
|
|
119
|
+
- File path relative to the project root
|
|
120
|
+
- Export/property name with location (`name:line:startColumn-endColumn`)
|
|
121
|
+
- Severity level (`[ERROR]`, `[WARNING]`, or `[INFO]`)
|
|
122
|
+
- Description of the issue
|
|
123
|
+
|
|
124
|
+
The column positions are 1-based (VS Code standard) and the range highlights the entire identifier name in VS Code's editor and Problems panel.
|
|
125
|
+
|
|
126
|
+
### Example Check Output
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
Analyzing TypeScript project: /path/to/tsconfig.json
|
|
130
|
+
|
|
131
|
+
Unused Exports:
|
|
132
|
+
|
|
133
|
+
packages/example/src/helpers.ts
|
|
134
|
+
unusedFunction:10:1-15 [ERROR] (Unused function)
|
|
135
|
+
createTestHelper:25:1-17 [INFO] (Used only in tests)
|
|
136
|
+
|
|
137
|
+
packages/example/src/constants.ts
|
|
138
|
+
UNUSED_CONSTANT:20:7-22 [ERROR] (Unused const)
|
|
139
|
+
|
|
140
|
+
Unused Type/Interface Properties:
|
|
141
|
+
|
|
142
|
+
packages/example/src/types.ts
|
|
143
|
+
UserConfig.unusedProp:5:3-13 [ERROR] (Unused property)
|
|
144
|
+
UserConfig.futureFeature:12:3-16 [WARNING] (Unused property: [TODO] implement this later)
|
|
145
|
+
TestHelpers.mockData:8:3-11 [INFO] (Used only in tests)
|
|
146
|
+
|
|
147
|
+
Summary:
|
|
148
|
+
Unused exports: 3
|
|
149
|
+
Unused properties: 3
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Example Fix Output
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
Fixing TypeScript project: /path/to/tsconfig.json
|
|
156
|
+
|
|
157
|
+
Deleting: src/unused-file.ts (all exports unused)
|
|
158
|
+
Fixing: src/helpers.ts
|
|
159
|
+
Removed unused export: unusedFunction
|
|
160
|
+
Fixing: src/types.ts
|
|
161
|
+
Removed unused property: UserConfig.unusedProp
|
|
162
|
+
Skipped: src/modified.ts (has local git changes)
|
|
163
|
+
|
|
164
|
+
Summary:
|
|
165
|
+
Fixed exports: 1
|
|
166
|
+
Fixed properties: 1
|
|
167
|
+
Deleted files: 1
|
|
168
|
+
Skipped files: 1
|
|
169
|
+
|
|
170
|
+
Skipped files (have local git changes):
|
|
171
|
+
src/modified.ts
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Severity Levels
|
|
175
|
+
|
|
176
|
+
- **[ERROR]** - Completely unused code (red in VS Code)
|
|
177
|
+
- Should be removed or investigated
|
|
178
|
+
- No references found in production or test code
|
|
179
|
+
|
|
180
|
+
- **[WARNING]** - Items with TODO comments (yellow in VS Code)
|
|
181
|
+
- Indicates planned but not yet implemented features
|
|
182
|
+
- Helps track technical debt
|
|
183
|
+
|
|
184
|
+
- **[INFO]** - Used only in tests (blue in VS Code)
|
|
185
|
+
- Testing helpers, mocks, or test-specific utilities
|
|
186
|
+
- Defined outside test files but only referenced from tests
|
|
187
|
+
- Not necessarily a problem, but good to know
|
|
188
|
+
|
|
189
|
+
## How It Works
|
|
190
|
+
|
|
191
|
+
1. **Project Loading**: Uses ts-morph to load the TypeScript project based on the provided tsconfig.json
|
|
192
|
+
2. **Export Analysis**: For each exported declaration, finds all references across the codebase
|
|
193
|
+
3. **Property Analysis**: For interfaces and type aliases, checks each property for external references
|
|
194
|
+
4. **Reference Categorization**: Tracks references separately:
|
|
195
|
+
- Total references (definition + all usages)
|
|
196
|
+
- Test references (from test files)
|
|
197
|
+
- Non-test references (from production code)
|
|
198
|
+
5. **Severity Assignment**:
|
|
199
|
+
- Items with TODO comments → WARNING
|
|
200
|
+
- Items only used in tests (nonTestReferences === 1 && testReferences > 0) → INFO
|
|
201
|
+
- Completely unused items → ERROR
|
|
202
|
+
6. **Structural Equivalence Checking**: When a property has no direct references, searches for structurally equivalent properties (same name and type signature) across all interfaces and type aliases - if any equivalent property is used, all are considered used
|
|
203
|
+
7. **File Exclusion**: Automatically filters out from being analyzed:
|
|
204
|
+
- Test files (files in `__tests__` directories or ending in `.test.ts` or `.spec.ts`)
|
|
205
|
+
- Files with `// @ts-nocheck` on the first line
|
|
206
|
+
- Note: Test files are still scanned for references to track test-only usage
|
|
207
|
+
|
|
208
|
+
## VS Code Integration
|
|
209
|
+
|
|
210
|
+
Add this task to your `.vscode/tasks.json` to run the analyzer with problem matcher support:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"label": "Find Unused Exports",
|
|
215
|
+
"type": "shell",
|
|
216
|
+
"command": "ts-unused",
|
|
217
|
+
"args": ["./tsconfig.json"],
|
|
218
|
+
"presentation": {
|
|
219
|
+
"echo": true,
|
|
220
|
+
"reveal": "always",
|
|
221
|
+
"focus": false,
|
|
222
|
+
"panel": "shared"
|
|
223
|
+
},
|
|
224
|
+
"problemMatcher": {
|
|
225
|
+
"owner": "ts-unused",
|
|
226
|
+
"fileLocation": ["relative", "${workspaceFolder}"],
|
|
227
|
+
"pattern": [
|
|
228
|
+
{
|
|
229
|
+
"regexp": "^([^\\s].*?)$",
|
|
230
|
+
"file": 1
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
"regexp": "^\\s+(.+?):(\\d+):(\\d+)-(\\d+)\\s+\\[(ERROR|WARNING|INFO)\\]\\s+\\((.+)\\)$",
|
|
234
|
+
"code": 1,
|
|
235
|
+
"line": 2,
|
|
236
|
+
"column": 3,
|
|
237
|
+
"endColumn": 4,
|
|
238
|
+
"severity": 5,
|
|
239
|
+
"message": 6,
|
|
240
|
+
"loop": true
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
This will:
|
|
248
|
+
- Display results in VS Code's Problems panel
|
|
249
|
+
- Show severity indicators (error, warning, info)
|
|
250
|
+
- Allow clicking to navigate to the exact file location
|
|
251
|
+
- Highlight the entire identifier (function name, property name, etc.) in the editor
|
|
252
|
+
- Provide quick fixes and context
|
|
253
|
+
|
|
254
|
+
## Advanced Features
|
|
255
|
+
|
|
256
|
+
### Structural Property Equivalence
|
|
257
|
+
|
|
258
|
+
The analyzer handles cases where properties are re-declared across multiple interfaces with the same name and type. This commonly occurs with:
|
|
259
|
+
|
|
260
|
+
- **Interface Composition**: When spreading values between different interface types
|
|
261
|
+
- **Type Transformations**: When converting from one type to another via object spreading
|
|
262
|
+
- **Shared Property Patterns**: When multiple interfaces define the same property structure
|
|
263
|
+
|
|
264
|
+
**Example:**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Source interface
|
|
268
|
+
interface SourceOptions {
|
|
269
|
+
timeout: number;
|
|
270
|
+
retryCount: number;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Different interface with same properties
|
|
274
|
+
interface ProcessedOptions {
|
|
275
|
+
timeout: number; // Same name and type
|
|
276
|
+
retryCount: number; // Same name and type
|
|
277
|
+
additionalOption: string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Usage through type conversion
|
|
281
|
+
function handler(sourceOpts: SourceOptions) {
|
|
282
|
+
const processedOpts: ProcessedOptions = {
|
|
283
|
+
...sourceOpts, // timeout and retryCount flow here
|
|
284
|
+
additionalOption: 'value'
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
console.log(processedOpts.timeout); // Accesses ProcessedOptions.timeout
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
In this case, `SourceOptions.timeout` and `SourceOptions.retryCount` are **not** flagged as unused, even though they're never directly accessed. The analyzer recognizes that `ProcessedOptions.timeout` and `ProcessedOptions.retryCount` are structurally equivalent and used, so their counterparts in `SourceOptions` are also considered used.
|
|
292
|
+
|
|
293
|
+
## License
|
|
294
|
+
|
|
295
|
+
MIT License
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import fs2 from "node:fs";
|
|
5
|
+
import path10 from "node:path";
|
|
6
|
+
|
|
7
|
+
// src/analyzeProject.ts
|
|
8
|
+
import path6 from "node:path";
|
|
9
|
+
import { Project } from "ts-morph";
|
|
10
|
+
|
|
11
|
+
// src/findUnusedExports.ts
|
|
12
|
+
import path2 from "node:path";
|
|
13
|
+
|
|
14
|
+
// src/checkExportUsage.ts
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { Node } from "ts-morph";
|
|
17
|
+
function getExportKind(declaration) {
|
|
18
|
+
if (Node.isFunctionDeclaration(declaration)) {
|
|
19
|
+
return "function";
|
|
20
|
+
}
|
|
21
|
+
if (Node.isClassDeclaration(declaration)) {
|
|
22
|
+
return "class";
|
|
23
|
+
}
|
|
24
|
+
if (Node.isInterfaceDeclaration(declaration)) {
|
|
25
|
+
return "interface";
|
|
26
|
+
}
|
|
27
|
+
if (Node.isTypeAliasDeclaration(declaration)) {
|
|
28
|
+
return "type";
|
|
29
|
+
}
|
|
30
|
+
if (Node.isEnumDeclaration(declaration)) {
|
|
31
|
+
return "enum";
|
|
32
|
+
}
|
|
33
|
+
if (Node.isModuleDeclaration(declaration)) {
|
|
34
|
+
return "namespace";
|
|
35
|
+
}
|
|
36
|
+
if (Node.isVariableDeclaration(declaration)) {
|
|
37
|
+
const parent = declaration.getParent();
|
|
38
|
+
if (Node.isVariableDeclarationList(parent)) {
|
|
39
|
+
const declarationKind = parent.getDeclarationKind();
|
|
40
|
+
if (declarationKind === "const") {
|
|
41
|
+
return "const";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return "variable";
|
|
45
|
+
}
|
|
46
|
+
return "export";
|
|
47
|
+
}
|
|
48
|
+
function getNameNode(declaration) {
|
|
49
|
+
if (Node.isFunctionDeclaration(declaration) || Node.isClassDeclaration(declaration) || Node.isInterfaceDeclaration(declaration) || Node.isTypeAliasDeclaration(declaration) || Node.isEnumDeclaration(declaration) || Node.isVariableDeclaration(declaration)) {
|
|
50
|
+
return declaration.getNameNode();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
function checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile) {
|
|
55
|
+
const firstDeclaration = declarations[0];
|
|
56
|
+
if (!firstDeclaration) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const declarationSourceFile = firstDeclaration.getSourceFile();
|
|
60
|
+
if (declarationSourceFile.getFilePath() !== sourceFile.getFilePath()) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (!Node.isReferenceFindable(firstDeclaration)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const references = firstDeclaration.findReferences();
|
|
67
|
+
let totalReferences = 0;
|
|
68
|
+
let testReferences = 0;
|
|
69
|
+
let nonTestReferences = 0;
|
|
70
|
+
for (const refGroup of references) {
|
|
71
|
+
const refs = refGroup.getReferences();
|
|
72
|
+
for (const ref of refs) {
|
|
73
|
+
const refSourceFile = ref.getSourceFile();
|
|
74
|
+
totalReferences++;
|
|
75
|
+
if (isTestFile(refSourceFile)) {
|
|
76
|
+
testReferences++;
|
|
77
|
+
} else {
|
|
78
|
+
nonTestReferences++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const onlyUsedInTests = nonTestReferences === 1 && testReferences > 0;
|
|
83
|
+
if (totalReferences > 1 && !onlyUsedInTests) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const kind = getExportKind(firstDeclaration);
|
|
87
|
+
const relativePath = path.relative(tsConfigDir, sourceFile.getFilePath());
|
|
88
|
+
const severity = onlyUsedInTests ? "info" : "error";
|
|
89
|
+
const nameNode = getNameNode(firstDeclaration);
|
|
90
|
+
const positionNode = nameNode || firstDeclaration;
|
|
91
|
+
const startPos = positionNode.getStart();
|
|
92
|
+
const lineStartPos = positionNode.getStartLinePos();
|
|
93
|
+
const character = startPos - lineStartPos + 1;
|
|
94
|
+
const endCharacter = character + exportName.length;
|
|
95
|
+
const result = {
|
|
96
|
+
filePath: relativePath,
|
|
97
|
+
exportName,
|
|
98
|
+
line: firstDeclaration.getStartLineNumber(),
|
|
99
|
+
character,
|
|
100
|
+
endCharacter,
|
|
101
|
+
kind,
|
|
102
|
+
severity,
|
|
103
|
+
onlyUsedInTests
|
|
104
|
+
};
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
|
|
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
|
+
// src/findUnusedExports.ts
|
|
120
|
+
function findUnusedExports(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
121
|
+
const results = [];
|
|
122
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
123
|
+
if (isTestFile(sourceFile)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (hasNoCheck(sourceFile)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (onProgress) {
|
|
133
|
+
const relativePath = path2.relative(tsConfigDir, sourceFile.getFilePath());
|
|
134
|
+
onProgress(relativePath);
|
|
135
|
+
}
|
|
136
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
137
|
+
for (const [exportName, declarations] of exports.entries()) {
|
|
138
|
+
const unusedExport = checkExportUsage(exportName, declarations, sourceFile, tsConfigDir, isTestFile);
|
|
139
|
+
if (unusedExport) {
|
|
140
|
+
results.push(unusedExport);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/findUnusedProperties.ts
|
|
148
|
+
import path5 from "node:path";
|
|
149
|
+
|
|
150
|
+
// src/analyzeInterfaces.ts
|
|
151
|
+
import path3 from "node:path";
|
|
152
|
+
|
|
153
|
+
// src/extractTodoComment.ts
|
|
154
|
+
function extractTodoComment(prop) {
|
|
155
|
+
const leadingComments = prop.getLeadingCommentRanges();
|
|
156
|
+
for (const comment of leadingComments) {
|
|
157
|
+
const commentText = comment.getText();
|
|
158
|
+
const singleLineMatch = commentText.match(/\/\/\s*TODO\s+(.+)/i);
|
|
159
|
+
if (singleLineMatch) {
|
|
160
|
+
const todoText = singleLineMatch[1];
|
|
161
|
+
if (todoText) {
|
|
162
|
+
return todoText.trim();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const multiLineMatch = commentText.match(/\/\*+\s*TODO\s+(.+?)\*\//is);
|
|
166
|
+
if (multiLineMatch) {
|
|
167
|
+
const todoText = multiLineMatch[1];
|
|
168
|
+
if (todoText) {
|
|
169
|
+
return todoText.trim().replace(/\s+/g, " ");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/findStructurallyEquivalentProperties.ts
|
|
177
|
+
import { SyntaxKind } from "ts-morph";
|
|
178
|
+
function checkInterfaceProperties(iface, propName, propType, originalProp, equivalentProps) {
|
|
179
|
+
const properties = iface.getProperties();
|
|
180
|
+
for (const p of properties) {
|
|
181
|
+
if (p === originalProp) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (p.getName() === propName) {
|
|
185
|
+
const pType = p.getType().getText();
|
|
186
|
+
if (pType === propType) {
|
|
187
|
+
equivalentProps.push(p);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function checkTypeAliasProperties(typeAlias, propName, propType, originalProp, equivalentProps) {
|
|
193
|
+
const typeNode = typeAlias.getTypeNode();
|
|
194
|
+
if (!typeNode || typeNode.getKind() !== SyntaxKind.TypeLiteral) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const properties = typeNode.getChildren().filter((child) => child.getKind() === SyntaxKind.PropertySignature);
|
|
198
|
+
for (const p of properties) {
|
|
199
|
+
if (p === originalProp) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (p.getKind() === SyntaxKind.PropertySignature && p.getName() === propName) {
|
|
203
|
+
const pType = p.getType().getText();
|
|
204
|
+
if (pType === propType) {
|
|
205
|
+
equivalentProps.push(p);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function findStructurallyEquivalentProperties(prop, project) {
|
|
211
|
+
const propName = prop.getName();
|
|
212
|
+
const propType = prop.getType().getText();
|
|
213
|
+
const equivalentProps = [];
|
|
214
|
+
const sourceFiles = project.getSourceFiles();
|
|
215
|
+
for (const sourceFile of sourceFiles) {
|
|
216
|
+
const interfaces = sourceFile.getInterfaces();
|
|
217
|
+
for (const iface of interfaces) {
|
|
218
|
+
checkInterfaceProperties(iface, propName, propType, prop, equivalentProps);
|
|
219
|
+
}
|
|
220
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
221
|
+
for (const typeAlias of typeAliases) {
|
|
222
|
+
checkTypeAliasProperties(typeAlias, propName, propType, prop, equivalentProps);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return equivalentProps;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/isPropertyUnused.ts
|
|
229
|
+
function countPropertyReferences(prop, isTestFile) {
|
|
230
|
+
const references = prop.findReferences();
|
|
231
|
+
let totalReferences = 0;
|
|
232
|
+
let testReferences = 0;
|
|
233
|
+
let nonTestReferences = 0;
|
|
234
|
+
for (const refGroup of references) {
|
|
235
|
+
const refs = refGroup.getReferences();
|
|
236
|
+
for (const ref of refs) {
|
|
237
|
+
const refSourceFile = ref.getSourceFile();
|
|
238
|
+
totalReferences++;
|
|
239
|
+
if (isTestFile(refSourceFile)) {
|
|
240
|
+
testReferences++;
|
|
241
|
+
} else {
|
|
242
|
+
nonTestReferences++;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const result = {
|
|
247
|
+
totalReferences,
|
|
248
|
+
testReferences,
|
|
249
|
+
nonTestReferences
|
|
250
|
+
};
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
function isPropertyUnused(prop, isTestFile, project) {
|
|
254
|
+
const counts = countPropertyReferences(prop, isTestFile);
|
|
255
|
+
const onlyUsedInTests = counts.nonTestReferences === 1 && counts.testReferences > 0;
|
|
256
|
+
if (counts.totalReferences > 1 && !onlyUsedInTests) {
|
|
257
|
+
const result2 = { isUnusedOrTestOnly: false, onlyUsedInTests: false };
|
|
258
|
+
return result2;
|
|
259
|
+
}
|
|
260
|
+
const equivalentProps = findStructurallyEquivalentProperties(prop, project);
|
|
261
|
+
for (const equivalentProp of equivalentProps) {
|
|
262
|
+
const equivalentCounts = countPropertyReferences(equivalentProp, isTestFile);
|
|
263
|
+
const equivalentOnlyUsedInTests = equivalentCounts.nonTestReferences === 1 && equivalentCounts.testReferences > 0;
|
|
264
|
+
if (equivalentCounts.totalReferences > 1 && !equivalentOnlyUsedInTests) {
|
|
265
|
+
const result2 = { isUnusedOrTestOnly: false, onlyUsedInTests: false };
|
|
266
|
+
return result2;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const result = { isUnusedOrTestOnly: true, onlyUsedInTests };
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/analyzeInterfaces.ts
|
|
274
|
+
function analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
275
|
+
const interfaces = sourceFile.getInterfaces();
|
|
276
|
+
for (const iface of interfaces) {
|
|
277
|
+
const interfaceName = iface.getName();
|
|
278
|
+
for (const prop of iface.getProperties()) {
|
|
279
|
+
const usage = isPropertyUnused(prop, isTestFile, project);
|
|
280
|
+
if (usage.isUnusedOrTestOnly) {
|
|
281
|
+
const relativePath = path3.relative(tsConfigDir, sourceFile.getFilePath());
|
|
282
|
+
const todoComment = extractTodoComment(prop);
|
|
283
|
+
let severity = "error";
|
|
284
|
+
if (todoComment) {
|
|
285
|
+
severity = "warning";
|
|
286
|
+
} else if (usage.onlyUsedInTests) {
|
|
287
|
+
severity = "info";
|
|
288
|
+
}
|
|
289
|
+
const propertyName = prop.getName();
|
|
290
|
+
const startPos = prop.getStart();
|
|
291
|
+
const lineStartPos = prop.getStartLinePos();
|
|
292
|
+
const character = startPos - lineStartPos + 1;
|
|
293
|
+
const endCharacter = character + propertyName.length;
|
|
294
|
+
const result = {
|
|
295
|
+
filePath: relativePath,
|
|
296
|
+
typeName: interfaceName,
|
|
297
|
+
propertyName,
|
|
298
|
+
line: prop.getStartLineNumber(),
|
|
299
|
+
character,
|
|
300
|
+
endCharacter,
|
|
301
|
+
todoComment,
|
|
302
|
+
severity,
|
|
303
|
+
onlyUsedInTests: usage.onlyUsedInTests
|
|
304
|
+
};
|
|
305
|
+
results.push(result);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/analyzeTypeAliases.ts
|
|
312
|
+
import path4 from "node:path";
|
|
313
|
+
import { Node as Node2 } from "ts-morph";
|
|
314
|
+
function analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
315
|
+
if (!Node2.isPropertySignature(member)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const usage = isPropertyUnused(member, isTestFile, project);
|
|
319
|
+
if (!usage.isUnusedOrTestOnly) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const relativePath = path4.relative(tsConfigDir, sourceFile.getFilePath());
|
|
323
|
+
const todoComment = extractTodoComment(member);
|
|
324
|
+
let severity = "error";
|
|
325
|
+
if (todoComment) {
|
|
326
|
+
severity = "warning";
|
|
327
|
+
} else if (usage.onlyUsedInTests) {
|
|
328
|
+
severity = "info";
|
|
329
|
+
}
|
|
330
|
+
const propertyName = member.getName();
|
|
331
|
+
const startPos = member.getStart();
|
|
332
|
+
const lineStartPos = member.getStartLinePos();
|
|
333
|
+
const character = startPos - lineStartPos + 1;
|
|
334
|
+
const endCharacter = character + propertyName.length;
|
|
335
|
+
const result = {
|
|
336
|
+
filePath: relativePath,
|
|
337
|
+
typeName,
|
|
338
|
+
propertyName,
|
|
339
|
+
line: member.getStartLineNumber(),
|
|
340
|
+
character,
|
|
341
|
+
endCharacter,
|
|
342
|
+
todoComment,
|
|
343
|
+
severity,
|
|
344
|
+
onlyUsedInTests: usage.onlyUsedInTests
|
|
345
|
+
};
|
|
346
|
+
results.push(result);
|
|
347
|
+
}
|
|
348
|
+
function analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
349
|
+
const typeName = typeAlias.getName();
|
|
350
|
+
const typeNode = typeAlias.getTypeNode();
|
|
351
|
+
if (!typeNode) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!Node2.isTypeLiteral(typeNode)) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
for (const member of typeNode.getMembers()) {
|
|
358
|
+
analyzeTypeLiteralMember(member, typeName, sourceFile, tsConfigDir, isTestFile, results, project);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project) {
|
|
362
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
363
|
+
for (const typeAlias of typeAliases) {
|
|
364
|
+
analyzeTypeAlias(typeAlias, sourceFile, tsConfigDir, isTestFile, results, project);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/findUnusedProperties.ts
|
|
369
|
+
function findUnusedProperties(project, tsConfigDir, isTestFile, onProgress, targetFilePath) {
|
|
370
|
+
const results = [];
|
|
371
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
372
|
+
if (isTestFile(sourceFile)) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (hasNoCheck(sourceFile)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (targetFilePath && sourceFile.getFilePath() !== targetFilePath) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (onProgress) {
|
|
382
|
+
const relativePath = path5.relative(tsConfigDir, sourceFile.getFilePath());
|
|
383
|
+
onProgress(relativePath);
|
|
384
|
+
}
|
|
385
|
+
analyzeInterfaces(sourceFile, tsConfigDir, isTestFile, results, project);
|
|
386
|
+
analyzeTypeAliases(sourceFile, tsConfigDir, isTestFile, results, project);
|
|
387
|
+
}
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/isTestFile.ts
|
|
392
|
+
var TEST_FILE_EXTENSIONS = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
393
|
+
function isTestFile(sourceFile) {
|
|
394
|
+
const filePath = sourceFile.getFilePath();
|
|
395
|
+
return filePath.includes("__tests__") || TEST_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/analyzeProject.ts
|
|
399
|
+
function analyzeProject(tsConfigPath, onProgress, targetFilePath, isTestFile2 = isTestFile) {
|
|
400
|
+
const project = new Project({
|
|
401
|
+
tsConfigFilePath: tsConfigPath
|
|
402
|
+
});
|
|
403
|
+
const tsConfigDir = path6.dirname(tsConfigPath);
|
|
404
|
+
const allSourceFiles = project.getSourceFiles();
|
|
405
|
+
const filesToAnalyze = allSourceFiles.filter((sf) => {
|
|
406
|
+
if (isTestFile2(sf)) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
if (hasNoCheck(sf)) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
if (targetFilePath && sf.getFilePath() !== targetFilePath) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
});
|
|
417
|
+
const totalFiles = filesToAnalyze.length;
|
|
418
|
+
let currentFile = 0;
|
|
419
|
+
const filesProcessed = new Set;
|
|
420
|
+
const progressCallback = onProgress ? (filePath) => {
|
|
421
|
+
if (!filesProcessed.has(filePath)) {
|
|
422
|
+
filesProcessed.add(filePath);
|
|
423
|
+
currentFile++;
|
|
424
|
+
onProgress(currentFile, totalFiles, filePath);
|
|
425
|
+
}
|
|
426
|
+
} : undefined;
|
|
427
|
+
const unusedExports = findUnusedExports(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
|
|
428
|
+
const unusedProperties = findUnusedProperties(project, tsConfigDir, isTestFile2, progressCallback, targetFilePath);
|
|
429
|
+
const unusedFiles = [];
|
|
430
|
+
const fileExportCounts = new Map;
|
|
431
|
+
for (const sourceFile of filesToAnalyze) {
|
|
432
|
+
const filePath = path6.relative(tsConfigDir, sourceFile.getFilePath());
|
|
433
|
+
const exports = sourceFile.getExportedDeclarations();
|
|
434
|
+
const totalExports = exports.size;
|
|
435
|
+
if (totalExports > 0) {
|
|
436
|
+
fileExportCounts.set(filePath, { total: totalExports, unused: 0 });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const unusedExport of unusedExports) {
|
|
440
|
+
const counts = fileExportCounts.get(unusedExport.filePath);
|
|
441
|
+
if (counts) {
|
|
442
|
+
counts.unused++;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const [filePath, counts] of fileExportCounts.entries()) {
|
|
446
|
+
if (counts.total > 0 && counts.unused === counts.total) {
|
|
447
|
+
unusedFiles.push(filePath);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const results = {
|
|
451
|
+
unusedExports,
|
|
452
|
+
unusedProperties,
|
|
453
|
+
unusedFiles
|
|
454
|
+
};
|
|
455
|
+
return results;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/fixProject.ts
|
|
459
|
+
import fs from "node:fs";
|
|
460
|
+
import path8 from "node:path";
|
|
461
|
+
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
462
|
+
|
|
463
|
+
// src/checkGitStatus.ts
|
|
464
|
+
import { execSync } from "node:child_process";
|
|
465
|
+
import path7 from "node:path";
|
|
466
|
+
function checkGitStatus(workingDir) {
|
|
467
|
+
const changedFiles = new Set;
|
|
468
|
+
try {
|
|
469
|
+
const gitRoot = execSync("git rev-parse --show-toplevel", {
|
|
470
|
+
cwd: workingDir,
|
|
471
|
+
encoding: "utf-8",
|
|
472
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
473
|
+
}).trim();
|
|
474
|
+
const output = execSync("git status --porcelain", {
|
|
475
|
+
cwd: workingDir,
|
|
476
|
+
encoding: "utf-8",
|
|
477
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
478
|
+
});
|
|
479
|
+
const lines = output.trim().split(`
|
|
480
|
+
`);
|
|
481
|
+
for (const line of lines) {
|
|
482
|
+
if (!line)
|
|
483
|
+
continue;
|
|
484
|
+
const filename = line.slice(3).trim();
|
|
485
|
+
const actualFilename = filename.includes(" -> ") ? filename.split(" -> ")[1] : filename;
|
|
486
|
+
if (actualFilename) {
|
|
487
|
+
const absolutePath = path7.resolve(gitRoot, actualFilename);
|
|
488
|
+
changedFiles.add(absolutePath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} catch (_error) {
|
|
492
|
+
return changedFiles;
|
|
493
|
+
}
|
|
494
|
+
return changedFiles;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/fixProject.ts
|
|
498
|
+
function fixProject(tsConfigPath, onProgress, isTestFile2 = isTestFile) {
|
|
499
|
+
const results = {
|
|
500
|
+
fixedExports: 0,
|
|
501
|
+
fixedProperties: 0,
|
|
502
|
+
deletedFiles: 0,
|
|
503
|
+
skippedFiles: [],
|
|
504
|
+
errors: []
|
|
505
|
+
};
|
|
506
|
+
const analysis = analyzeProject(tsConfigPath, undefined, undefined, isTestFile2);
|
|
507
|
+
const tsConfigDir = path8.dirname(path8.resolve(tsConfigPath));
|
|
508
|
+
const filesWithChanges = checkGitStatus(tsConfigDir);
|
|
509
|
+
const project = new Project2({
|
|
510
|
+
tsConfigFilePath: tsConfigPath
|
|
511
|
+
});
|
|
512
|
+
const deletedFiles = new Set;
|
|
513
|
+
for (const relativeFilePath of analysis.unusedFiles) {
|
|
514
|
+
const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
|
|
515
|
+
if (filesWithChanges.has(absoluteFilePath)) {
|
|
516
|
+
onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
|
|
517
|
+
results.skippedFiles.push(relativeFilePath);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
onProgress?.(`Deleting: ${relativeFilePath} (all exports unused)`);
|
|
522
|
+
fs.unlinkSync(absoluteFilePath);
|
|
523
|
+
deletedFiles.add(relativeFilePath);
|
|
524
|
+
results.deletedFiles++;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
527
|
+
results.errors.push({ file: relativeFilePath, error: errorMessage });
|
|
528
|
+
onProgress?.(`Error deleting ${relativeFilePath}: ${errorMessage}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (deletedFiles.size > 0) {
|
|
532
|
+
cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results);
|
|
533
|
+
}
|
|
534
|
+
const exportsByFile = new Map;
|
|
535
|
+
for (const unusedExport of analysis.unusedExports) {
|
|
536
|
+
if (!exportsByFile.has(unusedExport.filePath)) {
|
|
537
|
+
exportsByFile.set(unusedExport.filePath, []);
|
|
538
|
+
}
|
|
539
|
+
exportsByFile.get(unusedExport.filePath)?.push(unusedExport);
|
|
540
|
+
}
|
|
541
|
+
for (const [relativeFilePath, exports] of exportsByFile.entries()) {
|
|
542
|
+
const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
|
|
543
|
+
if (analysis.unusedFiles.includes(relativeFilePath)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (filesWithChanges.has(absoluteFilePath)) {
|
|
547
|
+
onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
|
|
548
|
+
results.skippedFiles.push(relativeFilePath);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const sourceFile = project.getSourceFile(absoluteFilePath);
|
|
553
|
+
if (!sourceFile) {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
onProgress?.(`Fixing: ${relativeFilePath}`);
|
|
557
|
+
for (const unusedExport of exports) {
|
|
558
|
+
if (removeExport(sourceFile, unusedExport.exportName)) {
|
|
559
|
+
onProgress?.(` ✓ Removed unused export: ${unusedExport.exportName}`);
|
|
560
|
+
results.fixedExports++;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
sourceFile.saveSync();
|
|
564
|
+
} catch (error) {
|
|
565
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
566
|
+
results.errors.push({ file: relativeFilePath, error: errorMessage });
|
|
567
|
+
onProgress?.(`Error fixing ${relativeFilePath}: ${errorMessage}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const propertiesByFile = new Map;
|
|
571
|
+
for (const unusedProperty of analysis.unusedProperties) {
|
|
572
|
+
if (!propertiesByFile.has(unusedProperty.filePath)) {
|
|
573
|
+
propertiesByFile.set(unusedProperty.filePath, []);
|
|
574
|
+
}
|
|
575
|
+
propertiesByFile.get(unusedProperty.filePath)?.push(unusedProperty);
|
|
576
|
+
}
|
|
577
|
+
for (const [relativeFilePath, properties] of propertiesByFile.entries()) {
|
|
578
|
+
const absoluteFilePath = path8.resolve(tsConfigDir, relativeFilePath);
|
|
579
|
+
if (analysis.unusedFiles.includes(relativeFilePath)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (filesWithChanges.has(absoluteFilePath)) {
|
|
583
|
+
if (!results.skippedFiles.includes(relativeFilePath)) {
|
|
584
|
+
onProgress?.(`Skipped: ${relativeFilePath} (has local git changes)`);
|
|
585
|
+
results.skippedFiles.push(relativeFilePath);
|
|
586
|
+
}
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
const sourceFile = project.getSourceFile(absoluteFilePath);
|
|
591
|
+
if (!sourceFile) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!exportsByFile.has(relativeFilePath)) {
|
|
595
|
+
onProgress?.(`Fixing: ${relativeFilePath}`);
|
|
596
|
+
}
|
|
597
|
+
for (const unusedProperty of properties) {
|
|
598
|
+
if (removeProperty(sourceFile, unusedProperty.typeName, unusedProperty.propertyName)) {
|
|
599
|
+
onProgress?.(` ✓ Removed unused property: ${unusedProperty.typeName}.${unusedProperty.propertyName}`);
|
|
600
|
+
results.fixedProperties++;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
sourceFile.saveSync();
|
|
604
|
+
} catch (error) {
|
|
605
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
606
|
+
results.errors.push({ file: relativeFilePath, error: errorMessage });
|
|
607
|
+
onProgress?.(`Error fixing ${relativeFilePath}: ${errorMessage}`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return results;
|
|
611
|
+
}
|
|
612
|
+
function removeExport(sourceFile, exportName) {
|
|
613
|
+
const exportedDeclarations = sourceFile.getExportedDeclarations();
|
|
614
|
+
const declarations = exportedDeclarations.get(exportName);
|
|
615
|
+
if (!declarations || declarations.length === 0) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
for (const declaration of declarations) {
|
|
619
|
+
if (declaration.getKind() === SyntaxKind2.VariableDeclaration) {
|
|
620
|
+
const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind2.VariableStatement);
|
|
621
|
+
if (variableStatement) {
|
|
622
|
+
variableStatement.remove();
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if ("remove" in declaration && typeof declaration.remove === "function") {
|
|
627
|
+
declaration.remove();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
function removeProperty(sourceFile, typeName, propertyName) {
|
|
633
|
+
const interfaces = sourceFile.getInterfaces();
|
|
634
|
+
for (const iface of interfaces) {
|
|
635
|
+
if (iface.getName() === typeName) {
|
|
636
|
+
const property = iface.getProperty(propertyName);
|
|
637
|
+
if (property) {
|
|
638
|
+
property.remove();
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const typeAliases = sourceFile.getTypeAliases();
|
|
644
|
+
for (const typeAlias of typeAliases) {
|
|
645
|
+
if (typeAlias.getName() === typeName) {
|
|
646
|
+
const typeNode = typeAlias.getTypeNode();
|
|
647
|
+
if (typeNode && typeNode.getKind() === SyntaxKind2.TypeLiteral) {
|
|
648
|
+
const typeLiteral = typeNode.asKindOrThrow(SyntaxKind2.TypeLiteral);
|
|
649
|
+
const members = typeLiteral.getMembers();
|
|
650
|
+
for (const member of members) {
|
|
651
|
+
if (member.getKind() === SyntaxKind2.PropertySignature) {
|
|
652
|
+
const propSig = member.asKind(SyntaxKind2.PropertySignature);
|
|
653
|
+
if (propSig?.getName() === propertyName) {
|
|
654
|
+
propSig.remove();
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
function cleanupBrokenImports(project, tsConfigDir, deletedFiles, filesWithChanges, onProgress, results) {
|
|
665
|
+
const sourceFiles = project.getSourceFiles();
|
|
666
|
+
for (const sourceFile of sourceFiles) {
|
|
667
|
+
const relativePath = path8.relative(tsConfigDir, sourceFile.getFilePath());
|
|
668
|
+
const absolutePath = sourceFile.getFilePath();
|
|
669
|
+
if (filesWithChanges.has(absolutePath)) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const exportDeclarations = sourceFile.getExportDeclarations();
|
|
673
|
+
let hasValidExports = false;
|
|
674
|
+
const brokenExports = [];
|
|
675
|
+
for (const exportDecl of exportDeclarations) {
|
|
676
|
+
const moduleSpecifier = exportDecl.getModuleSpecifierValue();
|
|
677
|
+
if (moduleSpecifier) {
|
|
678
|
+
const resolvedPath = resolveImportPath(sourceFile.getFilePath(), moduleSpecifier);
|
|
679
|
+
const relativeResolvedPath = resolvedPath ? path8.relative(tsConfigDir, resolvedPath) : null;
|
|
680
|
+
if (relativeResolvedPath && deletedFiles.has(relativeResolvedPath)) {
|
|
681
|
+
brokenExports.push(exportDecl);
|
|
682
|
+
} else {
|
|
683
|
+
hasValidExports = true;
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
hasValidExports = true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const hasOwnDeclarations = sourceFile.getFunctions().length > 0 || sourceFile.getClasses().length > 0 || sourceFile.getInterfaces().length > 0 || sourceFile.getTypeAliases().length > 0 || sourceFile.getVariableDeclarations().length > 0 || sourceFile.getEnums().length > 0;
|
|
690
|
+
if (hasOwnDeclarations) {
|
|
691
|
+
hasValidExports = true;
|
|
692
|
+
}
|
|
693
|
+
if (brokenExports.length > 0 && !hasValidExports) {
|
|
694
|
+
try {
|
|
695
|
+
onProgress?.(`Deleting: ${relativePath} (only re-exports from deleted files)`);
|
|
696
|
+
fs.unlinkSync(absolutePath);
|
|
697
|
+
if (results) {
|
|
698
|
+
results.deletedFiles++;
|
|
699
|
+
}
|
|
700
|
+
} catch (error) {
|
|
701
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
702
|
+
if (results) {
|
|
703
|
+
results.errors.push({ file: relativePath, error: errorMessage });
|
|
704
|
+
}
|
|
705
|
+
onProgress?.(`Error deleting ${relativePath}: ${errorMessage}`);
|
|
706
|
+
}
|
|
707
|
+
} else if (brokenExports.length > 0) {
|
|
708
|
+
try {
|
|
709
|
+
onProgress?.(`Fixing: ${relativePath}`);
|
|
710
|
+
for (const exportDecl of brokenExports) {
|
|
711
|
+
const moduleSpec = exportDecl.getModuleSpecifierValue();
|
|
712
|
+
exportDecl.remove();
|
|
713
|
+
onProgress?.(` ✓ Removed broken re-export from: ${moduleSpec}`);
|
|
714
|
+
}
|
|
715
|
+
sourceFile.saveSync();
|
|
716
|
+
} catch (error) {
|
|
717
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
718
|
+
if (results) {
|
|
719
|
+
results.errors.push({ file: relativePath, error: errorMessage });
|
|
720
|
+
}
|
|
721
|
+
onProgress?.(`Error fixing ${relativePath}: ${errorMessage}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function resolveImportPath(fromFile, importPath) {
|
|
727
|
+
const fromDir = path8.dirname(fromFile);
|
|
728
|
+
if (importPath.startsWith(".")) {
|
|
729
|
+
const resolved = path8.resolve(fromDir, importPath);
|
|
730
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
731
|
+
for (const ext of extensions) {
|
|
732
|
+
const withExt = resolved + ext;
|
|
733
|
+
if (fs.existsSync(withExt)) {
|
|
734
|
+
return withExt;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
for (const ext of extensions) {
|
|
738
|
+
const indexFile = path8.join(resolved, `index${ext}`);
|
|
739
|
+
if (fs.existsSync(indexFile)) {
|
|
740
|
+
return indexFile;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return `${resolved}.ts`;
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/formatResults.ts
|
|
749
|
+
import path9 from "node:path";
|
|
750
|
+
function getSeverityMarker(severity) {
|
|
751
|
+
const markers = {
|
|
752
|
+
error: "[ERROR]",
|
|
753
|
+
warning: "[WARNING]",
|
|
754
|
+
info: "[INFO]"
|
|
755
|
+
};
|
|
756
|
+
return markers[severity];
|
|
757
|
+
}
|
|
758
|
+
function formatExportLine(item) {
|
|
759
|
+
const marker = getSeverityMarker(item.severity);
|
|
760
|
+
if (item.onlyUsedInTests) {
|
|
761
|
+
return ` ${item.exportName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (Used only in tests)`;
|
|
762
|
+
}
|
|
763
|
+
return ` ${item.exportName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (Unused ${item.kind})`;
|
|
764
|
+
}
|
|
765
|
+
function formatPropertyLine(item) {
|
|
766
|
+
const status = item.onlyUsedInTests ? "Used only in tests" : "Unused property";
|
|
767
|
+
const todoSuffix = item.todoComment ? `: [TODO] ${item.todoComment}` : "";
|
|
768
|
+
const marker = getSeverityMarker(item.severity);
|
|
769
|
+
return ` ${item.typeName}.${item.propertyName}:${item.line}:${item.character}-${item.endCharacter} ${marker} (${status}${todoSuffix})`;
|
|
770
|
+
}
|
|
771
|
+
function formatGroupedItems(items, formatter, tsConfigDir, cwd) {
|
|
772
|
+
const lines = [];
|
|
773
|
+
const grouped = groupByFile(items);
|
|
774
|
+
for (const [filePath, groupItems] of grouped.entries()) {
|
|
775
|
+
const absolutePath = path9.resolve(tsConfigDir, filePath);
|
|
776
|
+
const relativePath = path9.relative(cwd, absolutePath);
|
|
777
|
+
lines.push(relativePath);
|
|
778
|
+
for (const item of groupItems) {
|
|
779
|
+
lines.push(formatter(item));
|
|
780
|
+
}
|
|
781
|
+
lines.push("");
|
|
782
|
+
}
|
|
783
|
+
return lines;
|
|
784
|
+
}
|
|
785
|
+
function formatResults(results, tsConfigDir) {
|
|
786
|
+
const lines = [];
|
|
787
|
+
const cwd = process.cwd();
|
|
788
|
+
const unusedExportNames = new Set;
|
|
789
|
+
for (const exportItem of results.unusedExports) {
|
|
790
|
+
unusedExportNames.add(exportItem.exportName);
|
|
791
|
+
}
|
|
792
|
+
const propertiesToReport = results.unusedProperties.filter((prop) => !unusedExportNames.has(prop.typeName));
|
|
793
|
+
const unusedFileSet = new Set(results.unusedFiles);
|
|
794
|
+
const exportsToReport = results.unusedExports.filter((exp) => !unusedFileSet.has(exp.filePath));
|
|
795
|
+
if (results.unusedFiles.length > 0) {
|
|
796
|
+
lines.push("Completely Unused Files:");
|
|
797
|
+
lines.push("");
|
|
798
|
+
for (const filePath of results.unusedFiles) {
|
|
799
|
+
const absolutePath = path9.resolve(tsConfigDir, filePath);
|
|
800
|
+
const relativePath = path9.relative(cwd, absolutePath);
|
|
801
|
+
lines.push(relativePath);
|
|
802
|
+
lines.push(" file:1:1-1 [ERROR] (All exports unused - file can be deleted)");
|
|
803
|
+
lines.push("");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (exportsToReport.length > 0) {
|
|
807
|
+
lines.push("Unused Exports:");
|
|
808
|
+
lines.push("");
|
|
809
|
+
lines.push(...formatGroupedItems(exportsToReport, formatExportLine, tsConfigDir, cwd));
|
|
810
|
+
}
|
|
811
|
+
if (propertiesToReport.length > 0) {
|
|
812
|
+
lines.push("Unused Type/Interface Properties:");
|
|
813
|
+
lines.push("");
|
|
814
|
+
lines.push(...formatGroupedItems(propertiesToReport, formatPropertyLine, tsConfigDir, cwd));
|
|
815
|
+
}
|
|
816
|
+
if (results.unusedFiles.length === 0 && exportsToReport.length === 0 && propertiesToReport.length === 0) {
|
|
817
|
+
lines.push("No unused exports or properties found!");
|
|
818
|
+
} else {
|
|
819
|
+
lines.push("Summary:");
|
|
820
|
+
lines.push(` Completely unused files: ${results.unusedFiles.length}`);
|
|
821
|
+
lines.push(` Unused exports: ${exportsToReport.length}`);
|
|
822
|
+
lines.push(` Unused properties: ${propertiesToReport.length}`);
|
|
823
|
+
}
|
|
824
|
+
return lines.join(`
|
|
825
|
+
`);
|
|
826
|
+
}
|
|
827
|
+
function groupByFile(items) {
|
|
828
|
+
const grouped = new Map;
|
|
829
|
+
for (const item of items) {
|
|
830
|
+
const existing = grouped.get(item.filePath);
|
|
831
|
+
if (existing) {
|
|
832
|
+
existing.push(item);
|
|
833
|
+
} else {
|
|
834
|
+
grouped.set(item.filePath, [item]);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return grouped;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// src/cli.ts
|
|
841
|
+
function main() {
|
|
842
|
+
const args = process.argv.slice(2);
|
|
843
|
+
if (args.length === 0) {
|
|
844
|
+
console.log("Usage: ts-unused <command> <path-to-tsconfig.json> [file-path-to-check]");
|
|
845
|
+
console.log("");
|
|
846
|
+
console.log("Commands:");
|
|
847
|
+
console.log(" check - Analyze and report unused exports/properties (default)");
|
|
848
|
+
console.log(" fix - Automatically remove unused exports/properties and delete unused files");
|
|
849
|
+
console.log("");
|
|
850
|
+
console.log("Examples:");
|
|
851
|
+
console.log(" ts-unused check tsconfig.json");
|
|
852
|
+
console.log(" ts-unused check ./project-dir # looks for tsconfig.json inside");
|
|
853
|
+
console.log(" ts-unused fix tsconfig.json");
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
const firstArg = args[0] ?? "";
|
|
857
|
+
let command = "check";
|
|
858
|
+
let configIndex = 0;
|
|
859
|
+
if (firstArg === "check" || firstArg === "fix") {
|
|
860
|
+
command = firstArg;
|
|
861
|
+
configIndex = 1;
|
|
862
|
+
}
|
|
863
|
+
let tsConfigPath = path10.resolve(args[configIndex] ?? "");
|
|
864
|
+
if (fs2.existsSync(tsConfigPath) && fs2.statSync(tsConfigPath).isDirectory()) {
|
|
865
|
+
tsConfigPath = path10.join(tsConfigPath, "tsconfig.json");
|
|
866
|
+
}
|
|
867
|
+
const targetFilePath = args[configIndex + 1] ? path10.resolve(args[configIndex + 1]) : undefined;
|
|
868
|
+
if (command === "fix") {
|
|
869
|
+
console.log(`Fixing TypeScript project: ${tsConfigPath}`);
|
|
870
|
+
console.log("");
|
|
871
|
+
const results = fixProject(tsConfigPath, (message) => {
|
|
872
|
+
console.log(message);
|
|
873
|
+
});
|
|
874
|
+
console.log("");
|
|
875
|
+
console.log("Summary:");
|
|
876
|
+
console.log(` Fixed exports: ${results.fixedExports}`);
|
|
877
|
+
console.log(` Fixed properties: ${results.fixedProperties}`);
|
|
878
|
+
console.log(` Deleted files: ${results.deletedFiles}`);
|
|
879
|
+
console.log(` Skipped files: ${results.skippedFiles.length}`);
|
|
880
|
+
if (results.errors.length > 0) {
|
|
881
|
+
console.log("");
|
|
882
|
+
console.log("Errors:");
|
|
883
|
+
for (const error of results.errors) {
|
|
884
|
+
console.log(` ${error.file}: ${error.error}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (results.skippedFiles.length > 0) {
|
|
888
|
+
console.log("");
|
|
889
|
+
console.log("Skipped files (have local git changes):");
|
|
890
|
+
for (const file of results.skippedFiles) {
|
|
891
|
+
console.log(` ${file}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
} else {
|
|
895
|
+
console.log(`Analyzing TypeScript project: ${tsConfigPath}`);
|
|
896
|
+
if (targetFilePath) {
|
|
897
|
+
console.log(`Checking only: ${targetFilePath}`);
|
|
898
|
+
}
|
|
899
|
+
console.log("");
|
|
900
|
+
const results = analyzeProject(tsConfigPath, (current, total, filePath) => {
|
|
901
|
+
const percentage = Math.min(100, Math.floor(current / total * 100));
|
|
902
|
+
const barLength = 40;
|
|
903
|
+
const filledLength = Math.min(barLength, Math.max(0, Math.floor(current / total * barLength)));
|
|
904
|
+
const emptyLength = Math.max(0, barLength - filledLength);
|
|
905
|
+
const bar = "█".repeat(filledLength) + "░".repeat(emptyLength);
|
|
906
|
+
const fileName = path10.basename(filePath);
|
|
907
|
+
process.stdout.write(`\r\x1B[KProgress: [${bar}] ${percentage}% (${current}/${total}) ${fileName}`);
|
|
908
|
+
}, targetFilePath);
|
|
909
|
+
process.stdout.write("\r\x1B[K");
|
|
910
|
+
const tsConfigDir = path10.dirname(path10.resolve(tsConfigPath));
|
|
911
|
+
const output = formatResults(results, tsConfigDir);
|
|
912
|
+
console.log(output);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-unused",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Find unused exports and properties in TypeScript projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ts-unused": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"clean": "rm -rf dist",
|
|
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",
|
|
17
|
+
"typecheck": "tsgo --noEmit",
|
|
18
|
+
"lint": "bun run fix && biome check src",
|
|
19
|
+
"fix": "biome check --write --unsafe src",
|
|
20
|
+
"sanity-check": "bun run scripts/sanity-check.ts"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@biomejs/biome": "^2.3.8",
|
|
24
|
+
"@types/bun": "latest",
|
|
25
|
+
"@types/node": "^24.10.1",
|
|
26
|
+
"@typescript/native-preview": "^7.0.0-dev.20251128.1"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ts-morph": "^27.0.2",
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
}
|
|
32
|
+
}
|