@sebassdc/crap4ts 0.1.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 +27 -0
- package/README.md +454 -0
- package/dist/api.d.ts +18 -0
- package/dist/api.js +49 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +91 -0
- package/dist/complexity.d.ts +7 -0
- package/dist/complexity.js +122 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +67 -0
- package/dist/core.d.ts +24 -0
- package/dist/core.js +68 -0
- package/dist/coverage.d.ts +19 -0
- package/dist/coverage.js +77 -0
- package/dist/crap.d.ts +13 -0
- package/dist/crap.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/options.d.ts +17 -0
- package/dist/options.js +126 -0
- package/dist/report.d.ts +22 -0
- package/dist/report.js +163 -0
- package/dist/skill-cmd.d.ts +13 -0
- package/dist/skill-cmd.js +93 -0
- package/package.json +62 -0
- package/src/skill/SKILL.md +179 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sebassdc
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Attribution: the CRAP (Change Risk Anti-Pattern) metric was introduced by
|
|
26
|
+
Alberto Savoia and Bob Evans. This project is inspired by crap4clj by
|
|
27
|
+
Robert C. Martin and shares its goals with crap4py.
|
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# crap4ts
|
|
2
|
+
|
|
3
|
+
**CRAP** (Change Risk Anti-Pattern) metric for TypeScript projects.
|
|
4
|
+
|
|
5
|
+
Combines cyclomatic complexity with test coverage to identify functions that are both complex and under-tested — the riskiest code to change.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
Install from source:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/sebassdc/crap4ts.git
|
|
13
|
+
cd crap4ts
|
|
14
|
+
npm install
|
|
15
|
+
npm run build
|
|
16
|
+
npm install -g .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Configure your test runner to emit Istanbul JSON coverage:
|
|
20
|
+
|
|
21
|
+
**Vitest** (`vitest.config.ts`):
|
|
22
|
+
```ts
|
|
23
|
+
export default defineConfig({
|
|
24
|
+
test: {
|
|
25
|
+
coverage: {
|
|
26
|
+
provider: 'v8', // or 'istanbul'
|
|
27
|
+
reporter: ['text', 'json'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Jest** (`jest.config.ts`):
|
|
34
|
+
```ts
|
|
35
|
+
export default {
|
|
36
|
+
coverageReporters: ['text', 'json'],
|
|
37
|
+
};
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run from your project root (where `src/` lives):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
crap4ts
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
crap4ts automatically deletes stale coverage data, runs your test suite with coverage, and prints the report.
|
|
47
|
+
|
|
48
|
+
## Output
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
CRAP Report
|
|
52
|
+
===========
|
|
53
|
+
Function Module CC Cov% CRAP
|
|
54
|
+
-------------------------------------------------------------------------------------
|
|
55
|
+
complexFn my.module 12 45.0% 130.2
|
|
56
|
+
simpleFn my.module 1 100.0% 1.0
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## CLI Options
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
crap4ts --help # show usage and available options
|
|
63
|
+
crap4ts --version # print version number
|
|
64
|
+
crap4ts --src lib # analyze from lib/ instead of src/
|
|
65
|
+
crap4ts --exclude dist # exclude paths containing "dist"
|
|
66
|
+
crap4ts --timeout 120 # set analysis timeout to 120 seconds
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configuration File
|
|
70
|
+
|
|
71
|
+
Instead of passing flags every time, create a `crap4ts.config.json` (or `.crap4tsrc.json`) in your project root:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"src": "lib",
|
|
76
|
+
"exclude": ["dist", "fixtures"],
|
|
77
|
+
"output": "json",
|
|
78
|
+
"failOnCrap": 30,
|
|
79
|
+
"timeout": 120
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### File Discovery
|
|
84
|
+
|
|
85
|
+
crap4ts looks for config files in the current working directory in this order:
|
|
86
|
+
|
|
87
|
+
1. `crap4ts.config.json` (preferred)
|
|
88
|
+
2. `.crap4tsrc.json` (fallback)
|
|
89
|
+
|
|
90
|
+
The first file found is used. If neither exists, all options use their defaults.
|
|
91
|
+
|
|
92
|
+
To load a config file from a custom path, use the `--config` flag:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
crap4ts --config configs/crap4ts.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### CLI Override Precedence
|
|
99
|
+
|
|
100
|
+
CLI flags always take precedence over config file values. For example, if your config file sets `"src": "lib"` but you run `crap4ts --src app`, the `app` directory is used.
|
|
101
|
+
|
|
102
|
+
### Supported Keys
|
|
103
|
+
|
|
104
|
+
| Key | Type | Description | Default |
|
|
105
|
+
|----------------------|------------|--------------------------------------------------|---------|
|
|
106
|
+
| `src` | `string` | Source directory to analyze | `"src"` |
|
|
107
|
+
| `exclude` | `string[]` | Exclude paths containing these patterns | `[]` |
|
|
108
|
+
| `output` | `string` | Output format: `"text"`, `"json"`, `"markdown"`, or `"csv"` | `"text"`|
|
|
109
|
+
| `runner` | `string` | Test runner: `"vitest"` or `"jest"` | auto |
|
|
110
|
+
| `coverageCommand` | `string` | Custom shell command to generate coverage | none |
|
|
111
|
+
| `failOnCrap` | `number` | Fail if any CRAP score >= this value | none |
|
|
112
|
+
| `failOnComplexity` | `number` | Fail if any cyclomatic complexity >= this value | none |
|
|
113
|
+
| `failOnCoverageBelow`| `number` | Fail if any function coverage < this % (0-100) | none |
|
|
114
|
+
| `top` | `number` | Show only the top N entries | all |
|
|
115
|
+
| `timeout` | `number` | Analysis timeout in seconds | `600` |
|
|
116
|
+
|
|
117
|
+
Unknown keys are silently ignored, so config files are forward-compatible with future versions.
|
|
118
|
+
|
|
119
|
+
## Programmatic API
|
|
120
|
+
|
|
121
|
+
crap4ts can be used as a library in your own tools and scripts. The API assumes coverage data already exists (run your test suite with coverage first).
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { generateReport, crapScore, extractFunctions } from '@sebassdc/crap4ts';
|
|
125
|
+
|
|
126
|
+
// High-level: analyze an entire source tree against existing coverage
|
|
127
|
+
const { entries } = generateReport({
|
|
128
|
+
srcDir: 'src',
|
|
129
|
+
coverageDir: 'coverage',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
entries.forEach(e => console.log(`${e.name}: CRAP ${e.crap}`));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### `generateReport(options)`
|
|
136
|
+
|
|
137
|
+
Finds source files, parses coverage, analyzes each file, and returns entries sorted by CRAP score. This does **not** run your test suite -- it reads from an existing `coverage-final.json`.
|
|
138
|
+
|
|
139
|
+
| Option | Type | Description | Default |
|
|
140
|
+
|---------------|------------|--------------------------------------------------|---------|
|
|
141
|
+
| `srcDir` | `string` | Source directory to scan for `.ts` files | -- |
|
|
142
|
+
| `coverageDir` | `string` | Directory containing `coverage-final.json` | -- |
|
|
143
|
+
| `filters` | `string[]` | Only include files matching these substrings | `[]` |
|
|
144
|
+
| `excludes` | `string[]` | Exclude files whose path contains these substrings| `[]` |
|
|
145
|
+
|
|
146
|
+
### Low-level exports
|
|
147
|
+
|
|
148
|
+
For fine-grained control, individual functions are also exported:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import {
|
|
152
|
+
extractFunctions, // parse a TS source string into FunctionInfo[]
|
|
153
|
+
parseCoverage, // read coverage-final.json from a directory
|
|
154
|
+
coverageForRange, // get coverage % for a line range
|
|
155
|
+
sourceToModule, // convert file path to dotted module name
|
|
156
|
+
crapScore, // compute CRAP score from complexity and coverage
|
|
157
|
+
sortByCrap, // sort CrapEntry[] by CRAP descending
|
|
158
|
+
formatReport, // render text table from CrapEntry[]
|
|
159
|
+
formatJsonReport, // render JSON string from CrapEntry[]
|
|
160
|
+
formatMarkdownReport, // render markdown table from CrapEntry[]
|
|
161
|
+
formatCsvReport, // render CSV string from CrapEntry[]
|
|
162
|
+
findSourceFiles, // find all .ts files in a directory
|
|
163
|
+
filterSources, // filter file list by substring patterns
|
|
164
|
+
analyzeFile, // analyze a single file against coverage data
|
|
165
|
+
} from '@sebassdc/crap4ts';
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
TypeScript types `CrapEntry`, `FunctionInfo`, `CoverageData`, and `FileCoverageData` are also exported.
|
|
169
|
+
|
|
170
|
+
## CI Integration
|
|
171
|
+
|
|
172
|
+
Use threshold flags to fail CI when code quality drops below acceptable levels:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Fail if any function has CRAP >= 30 or coverage below 70%
|
|
176
|
+
crap4ts --fail-on-crap 30 --fail-on-coverage-below 70
|
|
177
|
+
|
|
178
|
+
# Fail if any function has complexity >= 15, show only top 10
|
|
179
|
+
crap4ts --fail-on-complexity 15 --top 10
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Multiple thresholds can be combined. The report is always printed before any failure.
|
|
183
|
+
|
|
184
|
+
The `--top` flag limits displayed entries but all entries are evaluated against thresholds.
|
|
185
|
+
|
|
186
|
+
### Exit Codes
|
|
187
|
+
|
|
188
|
+
| Code | Meaning |
|
|
189
|
+
|------|---------|
|
|
190
|
+
| 0 | Pass -- no threshold violations |
|
|
191
|
+
| 1 | Threshold violated or runtime error |
|
|
192
|
+
| 2 | Usage error (invalid flags or arguments) |
|
|
193
|
+
|
|
194
|
+
## Output Formats
|
|
195
|
+
|
|
196
|
+
crap4ts supports four output formats:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
crap4ts # default text table
|
|
200
|
+
crap4ts --json # JSON (shorthand for --output json)
|
|
201
|
+
crap4ts --output markdown # Markdown table
|
|
202
|
+
crap4ts --output csv # CSV
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Text (default)
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
CRAP Report
|
|
209
|
+
===========
|
|
210
|
+
Function Module CC Cov% CRAP
|
|
211
|
+
-------------------------------------------------------------------------------------
|
|
212
|
+
complexFn my.module 12 45.0% 130.2
|
|
213
|
+
simpleFn my.module 1 100.0% 1.0
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### JSON
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"tool": "crap4ts",
|
|
221
|
+
"entries": [
|
|
222
|
+
{
|
|
223
|
+
"name": "complexFn",
|
|
224
|
+
"module": "my.module",
|
|
225
|
+
"complexity": 12,
|
|
226
|
+
"coverage": 45,
|
|
227
|
+
"crap": 130.2
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Markdown
|
|
234
|
+
|
|
235
|
+
```markdown
|
|
236
|
+
# CRAP Report
|
|
237
|
+
|
|
238
|
+
| Function | Module | CC | Cov% | CRAP |
|
|
239
|
+
|---|---|---:|---:|---:|
|
|
240
|
+
| complexFn | my.module | 12 | 45.0% | 130.2 |
|
|
241
|
+
| simpleFn | my.module | 1 | 100.0% | 1.0 |
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### CSV
|
|
245
|
+
|
|
246
|
+
```csv
|
|
247
|
+
Function,Module,CC,Coverage,CRAP
|
|
248
|
+
complexFn,my.module,12,45.0,130.2
|
|
249
|
+
simpleFn,my.module,1,100.0,1.0
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Text output is the default. Use `--json` as a shorthand or `--output <format>` for any format.
|
|
253
|
+
|
|
254
|
+
## Excluding Paths
|
|
255
|
+
|
|
256
|
+
Use `--exclude` to filter out files whose path contains a given substring. The flag is repeatable:
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# Skip dist and fixtures directories
|
|
260
|
+
crap4ts --exclude dist --exclude fixtures
|
|
261
|
+
|
|
262
|
+
# Analyze lib/ but skip generated code
|
|
263
|
+
crap4ts --src lib --exclude __generated__
|
|
264
|
+
|
|
265
|
+
# Combine with other options
|
|
266
|
+
crap4ts --src packages/core/src --exclude __mocks__ --exclude .stories --json
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Filtering
|
|
270
|
+
|
|
271
|
+
Pass module path fragments as arguments to filter:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
crap4ts parser validator # only files matching those strings
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## CRAP Formula
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
CRAP(fn) = CC² × (1 - coverage)³ + CC
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- **CC** = cyclomatic complexity (decision points + 1)
|
|
284
|
+
- **coverage** = fraction of statements covered by tests
|
|
285
|
+
|
|
286
|
+
| Score | Risk |
|
|
287
|
+
|-------|------|
|
|
288
|
+
| 1–5 | Low — clean code |
|
|
289
|
+
| 5–30 | Moderate — refactor or add tests |
|
|
290
|
+
| 30+ | High — complex and under-tested |
|
|
291
|
+
|
|
292
|
+
## What It Counts
|
|
293
|
+
|
|
294
|
+
Decision points that increase cyclomatic complexity:
|
|
295
|
+
|
|
296
|
+
- `if` / ternary (`c ? a : b`)
|
|
297
|
+
- `else if` (each adds 1)
|
|
298
|
+
- `for` / `for...of` / `for...in`
|
|
299
|
+
- `while` / `do...while`
|
|
300
|
+
- `catch` clauses (each adds 1)
|
|
301
|
+
- `case` clauses in `switch` (each `case` adds 1; `default` does not)
|
|
302
|
+
- `&&` / `||` / `??` operators (each operator adds 1)
|
|
303
|
+
|
|
304
|
+
Nested functions and class bodies are skipped — only the enclosing function's body is analyzed.
|
|
305
|
+
|
|
306
|
+
## Compatibility
|
|
307
|
+
|
|
308
|
+
| Layout | Status | Notes |
|
|
309
|
+
|--------|--------|-------|
|
|
310
|
+
| Standard (`src/`) | Supported | Default, no config needed |
|
|
311
|
+
| Custom source dir | Supported | Use `--src <dir>` |
|
|
312
|
+
| Monorepo workspace | Supported | Point `--src` to package source |
|
|
313
|
+
| Multiple src dirs | Supported | Use `--exclude` to filter |
|
|
314
|
+
| Windows paths | Supported | Normalized internally |
|
|
315
|
+
| Istanbul JSON coverage | Required | Other formats not supported |
|
|
316
|
+
| Branch coverage | Not used | Statement coverage only |
|
|
317
|
+
|
|
318
|
+
## Limitations
|
|
319
|
+
|
|
320
|
+
- Only TypeScript (`.ts`) files are analyzed — `.tsx`, `.js`, and `.jsx` files are ignored.
|
|
321
|
+
- Only functions found within the configured source directory (default: `src/`) are scanned.
|
|
322
|
+
- Coverage data must be in Istanbul JSON format (`coverage-final.json`). Other coverage formats are not supported.
|
|
323
|
+
- Runner detection is heuristic: crap4ts checks for Vitest config files first, then Jest config files, then falls back to the `scripts` field in `package.json`. Use `--runner vitest|jest` to override.
|
|
324
|
+
- Nested functions are attributed to their enclosing function rather than being extracted as separate symbols.
|
|
325
|
+
- Dynamic or computed method names (e.g., `[Symbol.iterator]()` or `["methodName"]()`) are not extracted.
|
|
326
|
+
- Only statement coverage is used when computing the coverage fraction — branch and function coverage are ignored.
|
|
327
|
+
- Coverage is calculated using statement-to-function overlap: a statement is attributed to a function if its line range overlaps the function's line range. This is an approximation; a multi-line statement that spans a function boundary may be counted for both the enclosing and the adjacent function.
|
|
328
|
+
|
|
329
|
+
For advanced usage patterns, see [docs/advanced-usage.md](docs/advanced-usage.md).
|
|
330
|
+
|
|
331
|
+
## Extracted Symbols
|
|
332
|
+
|
|
333
|
+
- Top-level `function` declarations
|
|
334
|
+
- Top-level `const f = () => {}` and `const f = function() {}`
|
|
335
|
+
- Class `constructor`, methods, getters, and setters (named as `ClassName.methodName`)
|
|
336
|
+
- Object literal methods, getters, and setters in top-level variable declarations (named as `varName.methodName` or `varName['string-key']`)
|
|
337
|
+
|
|
338
|
+
Nested functions (functions defined inside other functions, methods, or arrows) are intentionally excluded. They are not extracted as separate symbols; their complexity is attributed to the enclosing function.
|
|
339
|
+
|
|
340
|
+
## Cross-Agent Skill
|
|
341
|
+
|
|
342
|
+
crap4ts ships a bundled `SKILL.md` that you can install into the cross-agent
|
|
343
|
+
skill directory consumed by Claude Code, Codex, Pi, and any harness that reads
|
|
344
|
+
`.agents/skills/`.
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Global install for the current user (~/.agents/skills/crap4ts/SKILL.md)
|
|
348
|
+
crap4ts skill install
|
|
349
|
+
|
|
350
|
+
# Project-local install (./.agents/skills/crap4ts/SKILL.md)
|
|
351
|
+
crap4ts skill install --project
|
|
352
|
+
|
|
353
|
+
# Print the bundled skill
|
|
354
|
+
crap4ts skill show
|
|
355
|
+
|
|
356
|
+
# Print where the skill is (or would be) installed
|
|
357
|
+
crap4ts skill path
|
|
358
|
+
crap4ts skill path --project
|
|
359
|
+
|
|
360
|
+
# Remove
|
|
361
|
+
crap4ts skill uninstall
|
|
362
|
+
crap4ts skill uninstall --project
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The bundled skill lives inside the published package at `src/skill/SKILL.md`
|
|
366
|
+
and is shipped via the `files` field in `package.json`.
|
|
367
|
+
|
|
368
|
+
### Claude Code
|
|
369
|
+
|
|
370
|
+
Claude Code reads skills from `~/.claude/skills/`, not `~/.agents/skills/`.
|
|
371
|
+
After installing, symlink the skill so both directories stay in sync:
|
|
372
|
+
|
|
373
|
+
```bash
|
|
374
|
+
crap4ts skill install
|
|
375
|
+
ln -s ~/.agents/skills/crap4ts ~/.claude/skills/crap4ts
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For project-local installs, symlink into `.claude/skills/` at the repo root:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
crap4ts skill install --project
|
|
382
|
+
ln -s .agents/skills/crap4ts .claude/skills/crap4ts
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Runner Configuration
|
|
386
|
+
|
|
387
|
+
crap4ts supports three ways to run your test suite for coverage, applied in this order of precedence:
|
|
388
|
+
|
|
389
|
+
### 1. `--coverage-command` (highest priority)
|
|
390
|
+
|
|
391
|
+
Run an arbitrary shell command instead of the built-in runner logic. The command is executed with `shell: true`, so pipes, environment variables, and shell syntax all work.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# Monorepo: run tests only for a specific package
|
|
395
|
+
crap4ts --coverage-command "npm run test:api -- --coverage"
|
|
396
|
+
|
|
397
|
+
# Custom script with environment variables
|
|
398
|
+
crap4ts --coverage-command "CI=1 yarn test --coverage --coverageReporters=json"
|
|
399
|
+
|
|
400
|
+
# Turborepo / Nx workspace
|
|
401
|
+
crap4ts --coverage-command "npx turbo run test -- --coverage"
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
The command must produce a `coverage/coverage-final.json` file in Istanbul JSON format.
|
|
405
|
+
|
|
406
|
+
### 2. `--runner vitest|jest` (skip auto-detection)
|
|
407
|
+
|
|
408
|
+
Use the built-in runner invocation for Vitest or Jest, but skip the config-file heuristic:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
# Force Jest even if a vitest.config.ts exists
|
|
412
|
+
crap4ts --runner jest
|
|
413
|
+
|
|
414
|
+
# Force Vitest in a project without a vitest.config file
|
|
415
|
+
crap4ts --runner vitest
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### 3. Auto-detection (default)
|
|
419
|
+
|
|
420
|
+
When neither flag is provided, crap4ts detects the runner automatically:
|
|
421
|
+
|
|
422
|
+
1. If any `vitest.config.*` file exists, use Vitest.
|
|
423
|
+
2. If any `jest.config.*` file exists, use Jest.
|
|
424
|
+
3. If `package.json` lists `jest` as a dependency, use Jest.
|
|
425
|
+
4. Otherwise, default to Vitest.
|
|
426
|
+
|
|
427
|
+
## Troubleshooting
|
|
428
|
+
|
|
429
|
+
| Error | Fix |
|
|
430
|
+
|-------|-----|
|
|
431
|
+
| `Source directory 'src' not found` | Use `--src <dir>` to point to your source directory |
|
|
432
|
+
| `No TypeScript files found` | Verify your source directory contains `.ts` files |
|
|
433
|
+
| `No files match the filters` | Check your filter arguments match actual file paths |
|
|
434
|
+
| `Unable to parse package.json` | Fix your `package.json` or use `--runner vitest\|jest` |
|
|
435
|
+
| `Coverage run failed` | Ensure your test suite passes independently before running crap4ts |
|
|
436
|
+
| `No coverage-final.json found` | Configure your test runner to output Istanbul JSON coverage (see Quick Start) |
|
|
437
|
+
| `Coverage run timed out` | Increase timeout with `--timeout <seconds>` |
|
|
438
|
+
|
|
439
|
+
## Development
|
|
440
|
+
|
|
441
|
+
```bash
|
|
442
|
+
npm install
|
|
443
|
+
npm test # run tests
|
|
444
|
+
npm run build # compile to dist/
|
|
445
|
+
npm run coverage # run tests with coverage
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Inspiration
|
|
449
|
+
|
|
450
|
+
This project was inspired by [crap4clj](https://github.com/unclebob/crap4clj) by Uncle Bob.
|
|
451
|
+
|
|
452
|
+
## License
|
|
453
|
+
|
|
454
|
+
MIT
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { extractFunctions } from './complexity';
|
|
2
|
+
export type { FunctionInfo } from './complexity';
|
|
3
|
+
export { parseCoverage, coverageForRange, sourceToModule, normalizePath } from './coverage';
|
|
4
|
+
export type { CoverageData, FileCoverageData } from './coverage';
|
|
5
|
+
export { crapScore, sortByCrap, formatReport, formatJsonReport, formatMarkdownReport, formatCsvReport } from './crap';
|
|
6
|
+
export type { CrapEntry } from './crap';
|
|
7
|
+
export { findSourceFiles, filterSources, analyzeFile } from './core';
|
|
8
|
+
import type { CrapEntry } from './crap';
|
|
9
|
+
export interface ReportResult {
|
|
10
|
+
entries: CrapEntry[];
|
|
11
|
+
}
|
|
12
|
+
export interface GenerateReportOptions {
|
|
13
|
+
srcDir: string;
|
|
14
|
+
coverageDir: string;
|
|
15
|
+
filters?: string[];
|
|
16
|
+
excludes?: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function generateReport(opts: GenerateReportOptions): ReportResult;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Public programmatic API for crap4ts
|
|
3
|
+
// CLI users: import from 'crap4ts/cli' or use the bin entry point.
|
|
4
|
+
// Library users: import from 'crap4ts' (this file).
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.analyzeFile = exports.filterSources = exports.findSourceFiles = exports.formatCsvReport = exports.formatMarkdownReport = exports.formatJsonReport = exports.formatReport = exports.sortByCrap = exports.crapScore = exports.normalizePath = exports.sourceToModule = exports.coverageForRange = exports.parseCoverage = exports.extractFunctions = void 0;
|
|
7
|
+
exports.generateReport = generateReport;
|
|
8
|
+
var complexity_1 = require("./complexity");
|
|
9
|
+
Object.defineProperty(exports, "extractFunctions", { enumerable: true, get: function () { return complexity_1.extractFunctions; } });
|
|
10
|
+
var coverage_1 = require("./coverage");
|
|
11
|
+
Object.defineProperty(exports, "parseCoverage", { enumerable: true, get: function () { return coverage_1.parseCoverage; } });
|
|
12
|
+
Object.defineProperty(exports, "coverageForRange", { enumerable: true, get: function () { return coverage_1.coverageForRange; } });
|
|
13
|
+
Object.defineProperty(exports, "sourceToModule", { enumerable: true, get: function () { return coverage_1.sourceToModule; } });
|
|
14
|
+
Object.defineProperty(exports, "normalizePath", { enumerable: true, get: function () { return coverage_1.normalizePath; } });
|
|
15
|
+
var crap_1 = require("./crap");
|
|
16
|
+
Object.defineProperty(exports, "crapScore", { enumerable: true, get: function () { return crap_1.crapScore; } });
|
|
17
|
+
Object.defineProperty(exports, "sortByCrap", { enumerable: true, get: function () { return crap_1.sortByCrap; } });
|
|
18
|
+
Object.defineProperty(exports, "formatReport", { enumerable: true, get: function () { return crap_1.formatReport; } });
|
|
19
|
+
Object.defineProperty(exports, "formatJsonReport", { enumerable: true, get: function () { return crap_1.formatJsonReport; } });
|
|
20
|
+
Object.defineProperty(exports, "formatMarkdownReport", { enumerable: true, get: function () { return crap_1.formatMarkdownReport; } });
|
|
21
|
+
Object.defineProperty(exports, "formatCsvReport", { enumerable: true, get: function () { return crap_1.formatCsvReport; } });
|
|
22
|
+
var core_1 = require("./core");
|
|
23
|
+
Object.defineProperty(exports, "findSourceFiles", { enumerable: true, get: function () { return core_1.findSourceFiles; } });
|
|
24
|
+
Object.defineProperty(exports, "filterSources", { enumerable: true, get: function () { return core_1.filterSources; } });
|
|
25
|
+
Object.defineProperty(exports, "analyzeFile", { enumerable: true, get: function () { return core_1.analyzeFile; } });
|
|
26
|
+
const fs_1 = require("fs");
|
|
27
|
+
const path_1 = require("path");
|
|
28
|
+
const core_2 = require("./core");
|
|
29
|
+
const core_3 = require("./core");
|
|
30
|
+
const coverage_2 = require("./coverage");
|
|
31
|
+
const crap_2 = require("./crap");
|
|
32
|
+
function generateReport(opts) {
|
|
33
|
+
const srcDir = (0, path_1.resolve)(opts.srcDir);
|
|
34
|
+
const coverageDir = (0, path_1.resolve)(opts.coverageDir);
|
|
35
|
+
if (!(0, fs_1.existsSync)(coverageDir)) {
|
|
36
|
+
throw new Error(`Coverage directory not found: ${coverageDir}`);
|
|
37
|
+
}
|
|
38
|
+
const files = (0, core_2.findSourceFilesWithOptions)({
|
|
39
|
+
srcDirs: [srcDir],
|
|
40
|
+
excludes: opts.excludes ?? [],
|
|
41
|
+
});
|
|
42
|
+
const filtered = (0, core_3.filterSources)(files, opts.filters ?? []);
|
|
43
|
+
const coverageData = (0, coverage_2.parseCoverage)(coverageDir);
|
|
44
|
+
const entries = [];
|
|
45
|
+
for (const file of filtered) {
|
|
46
|
+
entries.push(...(0, core_3.analyzeFile)(file, coverageData, srcDir));
|
|
47
|
+
}
|
|
48
|
+
return { entries: (0, crap_2.sortByCrap)(entries) };
|
|
49
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runCli(argv: string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCli = runCli;
|
|
37
|
+
const fs_1 = require("fs");
|
|
38
|
+
const path_1 = require("path");
|
|
39
|
+
const report_1 = require("./report");
|
|
40
|
+
const options_1 = require("./options");
|
|
41
|
+
const config_1 = require("./config");
|
|
42
|
+
function getVersion() {
|
|
43
|
+
const pkg = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8'));
|
|
44
|
+
return pkg.version;
|
|
45
|
+
}
|
|
46
|
+
function formatHelp() {
|
|
47
|
+
return `Usage: crap4ts [options] [filters...]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--src <dir> Source directory to analyze (default: src)
|
|
51
|
+
--exclude <pattern> Exclude files whose path contains <pattern> (repeatable)
|
|
52
|
+
--timeout <seconds> Analysis timeout in seconds (default: 600)
|
|
53
|
+
--output <format> Output format: text, json, markdown, csv (default: text)
|
|
54
|
+
--json Shorthand for --output json
|
|
55
|
+
--runner <vitest|jest> Skip auto-detection, use specified test runner
|
|
56
|
+
--coverage-command <cmd> Run a custom shell command for coverage instead
|
|
57
|
+
--fail-on-crap <n> Exit 1 if any function CRAP score >= n
|
|
58
|
+
--fail-on-complexity <n> Exit 1 if any function complexity >= n
|
|
59
|
+
--fail-on-coverage-below <n> Exit 1 if any function coverage < n (0-100)
|
|
60
|
+
--top <n> Show only the top N entries (thresholds check all)
|
|
61
|
+
--config <path> Load config from a specific file
|
|
62
|
+
--help, -h Show this help message
|
|
63
|
+
--version, -v Show version number
|
|
64
|
+
|
|
65
|
+
Subcommands:
|
|
66
|
+
skill Manage the bundled AI skill (install | uninstall | show | path)`;
|
|
67
|
+
}
|
|
68
|
+
async function runCli(argv) {
|
|
69
|
+
if (argv[0] === 'skill') {
|
|
70
|
+
const { runSkillCommand } = await Promise.resolve().then(() => __importStar(require('./skill-cmd')));
|
|
71
|
+
return runSkillCommand(argv.slice(1));
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const opts = (0, options_1.parseOptions)(argv);
|
|
75
|
+
if (opts.mode === 'help') {
|
|
76
|
+
console.log(formatHelp());
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (opts.mode === 'version') {
|
|
80
|
+
console.log(getVersion());
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
const config = (0, config_1.loadConfig)(opts.configPath);
|
|
84
|
+
const merged = (0, config_1.mergeConfigIntoOptions)(opts, config);
|
|
85
|
+
return await (0, report_1.runReport)(merged);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
console.error(e.message);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
}
|