tokenlean 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/README.md +248 -0
- package/bin/tl-api.mjs +515 -0
- package/bin/tl-blame.mjs +345 -0
- package/bin/tl-complexity.mjs +514 -0
- package/bin/tl-component.mjs +274 -0
- package/bin/tl-config.mjs +135 -0
- package/bin/tl-context.mjs +156 -0
- package/bin/tl-coverage.mjs +456 -0
- package/bin/tl-deps.mjs +474 -0
- package/bin/tl-diff.mjs +183 -0
- package/bin/tl-entry.mjs +256 -0
- package/bin/tl-env.mjs +376 -0
- package/bin/tl-exports.mjs +583 -0
- package/bin/tl-flow.mjs +324 -0
- package/bin/tl-history.mjs +289 -0
- package/bin/tl-hotspots.mjs +321 -0
- package/bin/tl-impact.mjs +345 -0
- package/bin/tl-prompt.mjs +175 -0
- package/bin/tl-related.mjs +227 -0
- package/bin/tl-routes.mjs +627 -0
- package/bin/tl-search.mjs +123 -0
- package/bin/tl-structure.mjs +161 -0
- package/bin/tl-symbols.mjs +430 -0
- package/bin/tl-todo.mjs +341 -0
- package/bin/tl-types.mjs +441 -0
- package/bin/tl-unused.mjs +494 -0
- package/package.json +55 -0
- package/src/config.mjs +271 -0
- package/src/output.mjs +251 -0
- package/src/project.mjs +277 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tl-coverage - Quick test coverage info for files
|
|
5
|
+
*
|
|
6
|
+
* Reads coverage data from common formats (lcov, istanbul, c8) and shows
|
|
7
|
+
* coverage percentages for files. No need to read full coverage reports.
|
|
8
|
+
*
|
|
9
|
+
* Usage: tl-coverage [file-or-dir] [--below N]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Prompt info for tl-prompt
|
|
13
|
+
if (process.argv.includes('--prompt')) {
|
|
14
|
+
console.log(JSON.stringify({
|
|
15
|
+
name: 'tl-coverage',
|
|
16
|
+
desc: 'Quick test coverage info for files',
|
|
17
|
+
when: 'before-modify',
|
|
18
|
+
example: 'tl-coverage src/'
|
|
19
|
+
}));
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
24
|
+
import { join, relative, dirname, basename } from 'path';
|
|
25
|
+
import {
|
|
26
|
+
createOutput,
|
|
27
|
+
parseCommonArgs,
|
|
28
|
+
formatTable,
|
|
29
|
+
COMMON_OPTIONS_HELP
|
|
30
|
+
} from '../src/output.mjs';
|
|
31
|
+
import { findProjectRoot } from '../src/project.mjs';
|
|
32
|
+
|
|
33
|
+
const HELP = `
|
|
34
|
+
tl-coverage - Quick test coverage info for files
|
|
35
|
+
|
|
36
|
+
Usage: tl-coverage [file-or-dir] [options]
|
|
37
|
+
|
|
38
|
+
Options:
|
|
39
|
+
--below N Only show files with coverage below N% (default: show all)
|
|
40
|
+
--above N Only show files with coverage above N%
|
|
41
|
+
--sort <field> Sort by: coverage, lines, name (default: coverage)
|
|
42
|
+
--uncovered Show uncovered line numbers for each file
|
|
43
|
+
${COMMON_OPTIONS_HELP}
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
tl-coverage # All files coverage
|
|
47
|
+
tl-coverage src/ # Coverage for src/ files
|
|
48
|
+
tl-coverage src/api.ts # Single file coverage
|
|
49
|
+
tl-coverage --below 80 # Files under 80% coverage
|
|
50
|
+
tl-coverage src/ --uncovered # Show uncovered lines
|
|
51
|
+
|
|
52
|
+
Coverage data sources (auto-detected):
|
|
53
|
+
- coverage/lcov.info (lcov format)
|
|
54
|
+
- coverage/coverage-final.json (istanbul/nyc)
|
|
55
|
+
- coverage/coverage-summary.json (jest)
|
|
56
|
+
- .nyc_output/*.json (nyc raw)
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// ─────────────────────────────────────────────────────────────
|
|
60
|
+
// Coverage Data Detection
|
|
61
|
+
// ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function findCoverageData(projectRoot) {
|
|
64
|
+
const coverageDir = join(projectRoot, 'coverage');
|
|
65
|
+
|
|
66
|
+
// Try different coverage formats in order of preference
|
|
67
|
+
const sources = [
|
|
68
|
+
{ path: join(coverageDir, 'lcov.info'), type: 'lcov' },
|
|
69
|
+
{ path: join(coverageDir, 'coverage-final.json'), type: 'istanbul' },
|
|
70
|
+
{ path: join(coverageDir, 'coverage-summary.json'), type: 'summary' },
|
|
71
|
+
{ path: join(projectRoot, '.nyc_output'), type: 'nyc' }
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const source of sources) {
|
|
75
|
+
if (existsSync(source.path)) {
|
|
76
|
+
return source;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────────────────────
|
|
84
|
+
// Coverage Parsers
|
|
85
|
+
// ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function parseLcov(content, projectRoot) {
|
|
88
|
+
const coverage = new Map();
|
|
89
|
+
let currentFile = null;
|
|
90
|
+
let currentData = null;
|
|
91
|
+
|
|
92
|
+
for (const line of content.split('\n')) {
|
|
93
|
+
const trimmed = line.trim();
|
|
94
|
+
|
|
95
|
+
if (trimmed.startsWith('SF:')) {
|
|
96
|
+
// Source file
|
|
97
|
+
currentFile = trimmed.slice(3);
|
|
98
|
+
// Make path relative
|
|
99
|
+
if (currentFile.startsWith(projectRoot)) {
|
|
100
|
+
currentFile = relative(projectRoot, currentFile);
|
|
101
|
+
}
|
|
102
|
+
currentData = {
|
|
103
|
+
lines: { found: 0, hit: 0 },
|
|
104
|
+
functions: { found: 0, hit: 0 },
|
|
105
|
+
branches: { found: 0, hit: 0 },
|
|
106
|
+
uncoveredLines: []
|
|
107
|
+
};
|
|
108
|
+
} else if (trimmed.startsWith('DA:')) {
|
|
109
|
+
// Line data: DA:lineNum,hitCount
|
|
110
|
+
const [lineNum, hitCount] = trimmed.slice(3).split(',').map(Number);
|
|
111
|
+
if (currentData) {
|
|
112
|
+
currentData.lines.found++;
|
|
113
|
+
if (hitCount > 0) {
|
|
114
|
+
currentData.lines.hit++;
|
|
115
|
+
} else {
|
|
116
|
+
currentData.uncoveredLines.push(lineNum);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else if (trimmed.startsWith('FNF:')) {
|
|
120
|
+
if (currentData) currentData.functions.found = parseInt(trimmed.slice(4), 10);
|
|
121
|
+
} else if (trimmed.startsWith('FNH:')) {
|
|
122
|
+
if (currentData) currentData.functions.hit = parseInt(trimmed.slice(4), 10);
|
|
123
|
+
} else if (trimmed.startsWith('BRF:')) {
|
|
124
|
+
if (currentData) currentData.branches.found = parseInt(trimmed.slice(4), 10);
|
|
125
|
+
} else if (trimmed.startsWith('BRH:')) {
|
|
126
|
+
if (currentData) currentData.branches.hit = parseInt(trimmed.slice(4), 10);
|
|
127
|
+
} else if (trimmed === 'end_of_record') {
|
|
128
|
+
if (currentFile && currentData) {
|
|
129
|
+
coverage.set(currentFile, currentData);
|
|
130
|
+
}
|
|
131
|
+
currentFile = null;
|
|
132
|
+
currentData = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return coverage;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseIstanbul(content, projectRoot) {
|
|
140
|
+
const coverage = new Map();
|
|
141
|
+
const data = JSON.parse(content);
|
|
142
|
+
|
|
143
|
+
for (const [filePath, fileData] of Object.entries(data)) {
|
|
144
|
+
let relPath = filePath;
|
|
145
|
+
if (filePath.startsWith(projectRoot)) {
|
|
146
|
+
relPath = relative(projectRoot, filePath);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const statementMap = fileData.statementMap || {};
|
|
150
|
+
const s = fileData.s || {};
|
|
151
|
+
const fnMap = fileData.fnMap || {};
|
|
152
|
+
const f = fileData.f || {};
|
|
153
|
+
const branchMap = fileData.branchMap || {};
|
|
154
|
+
const b = fileData.b || {};
|
|
155
|
+
|
|
156
|
+
// Calculate line coverage from statements
|
|
157
|
+
const lineHits = new Map();
|
|
158
|
+
for (const [stmtId, count] of Object.entries(s)) {
|
|
159
|
+
const stmt = statementMap[stmtId];
|
|
160
|
+
if (stmt && stmt.start) {
|
|
161
|
+
const line = stmt.start.line;
|
|
162
|
+
lineHits.set(line, (lineHits.get(line) || 0) + count);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const uncoveredLines = [];
|
|
167
|
+
for (const [line, count] of lineHits) {
|
|
168
|
+
if (count === 0) {
|
|
169
|
+
uncoveredLines.push(line);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
coverage.set(relPath, {
|
|
174
|
+
lines: {
|
|
175
|
+
found: Object.keys(s).length,
|
|
176
|
+
hit: Object.values(s).filter(c => c > 0).length
|
|
177
|
+
},
|
|
178
|
+
functions: {
|
|
179
|
+
found: Object.keys(f).length,
|
|
180
|
+
hit: Object.values(f).filter(c => c > 0).length
|
|
181
|
+
},
|
|
182
|
+
branches: {
|
|
183
|
+
found: Object.values(b).flat().length,
|
|
184
|
+
hit: Object.values(b).flat().filter(c => c > 0).length
|
|
185
|
+
},
|
|
186
|
+
uncoveredLines: uncoveredLines.sort((a, b) => a - b)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return coverage;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseSummary(content, projectRoot) {
|
|
194
|
+
const coverage = new Map();
|
|
195
|
+
const data = JSON.parse(content);
|
|
196
|
+
|
|
197
|
+
for (const [filePath, fileData] of Object.entries(data)) {
|
|
198
|
+
if (filePath === 'total') continue;
|
|
199
|
+
|
|
200
|
+
let relPath = filePath;
|
|
201
|
+
if (filePath.startsWith(projectRoot)) {
|
|
202
|
+
relPath = relative(projectRoot, filePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
coverage.set(relPath, {
|
|
206
|
+
lines: {
|
|
207
|
+
found: fileData.lines?.total || 0,
|
|
208
|
+
hit: fileData.lines?.covered || 0
|
|
209
|
+
},
|
|
210
|
+
functions: {
|
|
211
|
+
found: fileData.functions?.total || 0,
|
|
212
|
+
hit: fileData.functions?.covered || 0
|
|
213
|
+
},
|
|
214
|
+
branches: {
|
|
215
|
+
found: fileData.branches?.total || 0,
|
|
216
|
+
hit: fileData.branches?.covered || 0
|
|
217
|
+
},
|
|
218
|
+
uncoveredLines: [] // Summary doesn't have line details
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return coverage;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function loadCoverage(source, projectRoot) {
|
|
226
|
+
if (source.type === 'lcov') {
|
|
227
|
+
const content = readFileSync(source.path, 'utf-8');
|
|
228
|
+
return parseLcov(content, projectRoot);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (source.type === 'istanbul') {
|
|
232
|
+
const content = readFileSync(source.path, 'utf-8');
|
|
233
|
+
return parseIstanbul(content, projectRoot);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (source.type === 'summary') {
|
|
237
|
+
const content = readFileSync(source.path, 'utf-8');
|
|
238
|
+
return parseSummary(content, projectRoot);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (source.type === 'nyc') {
|
|
242
|
+
// Load all JSON files in .nyc_output
|
|
243
|
+
const combined = new Map();
|
|
244
|
+
const files = readdirSync(source.path).filter(f => f.endsWith('.json'));
|
|
245
|
+
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
const content = readFileSync(join(source.path, file), 'utf-8');
|
|
248
|
+
const fileCoverage = parseIstanbul(content, projectRoot);
|
|
249
|
+
for (const [path, data] of fileCoverage) {
|
|
250
|
+
combined.set(path, data);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return combined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return new Map();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─────────────────────────────────────────────────────────────
|
|
261
|
+
// Helpers
|
|
262
|
+
// ─────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
function calcPercentage(hit, found) {
|
|
265
|
+
if (found === 0) return 100;
|
|
266
|
+
return Math.round((hit / found) * 100);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatPercentage(pct) {
|
|
270
|
+
if (pct >= 80) return `${pct}%`;
|
|
271
|
+
if (pct >= 50) return `${pct}%`;
|
|
272
|
+
return `${pct}%`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function formatLineRanges(lines) {
|
|
276
|
+
if (lines.length === 0) return '';
|
|
277
|
+
|
|
278
|
+
const ranges = [];
|
|
279
|
+
let start = lines[0];
|
|
280
|
+
let end = lines[0];
|
|
281
|
+
|
|
282
|
+
for (let i = 1; i < lines.length; i++) {
|
|
283
|
+
if (lines[i] === end + 1) {
|
|
284
|
+
end = lines[i];
|
|
285
|
+
} else {
|
|
286
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
287
|
+
start = lines[i];
|
|
288
|
+
end = lines[i];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
ranges.push(start === end ? `${start}` : `${start}-${end}`);
|
|
292
|
+
|
|
293
|
+
return ranges.join(', ');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─────────────────────────────────────────────────────────────
|
|
297
|
+
// Main
|
|
298
|
+
// ─────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
const args = process.argv.slice(2);
|
|
301
|
+
const options = parseCommonArgs(args);
|
|
302
|
+
|
|
303
|
+
// Parse custom options
|
|
304
|
+
let belowThreshold = null;
|
|
305
|
+
let aboveThreshold = null;
|
|
306
|
+
let sortBy = 'coverage';
|
|
307
|
+
let showUncovered = false;
|
|
308
|
+
|
|
309
|
+
const remaining = [];
|
|
310
|
+
for (let i = 0; i < options.remaining.length; i++) {
|
|
311
|
+
const arg = options.remaining[i];
|
|
312
|
+
|
|
313
|
+
if (arg === '--below') {
|
|
314
|
+
belowThreshold = parseInt(options.remaining[++i], 10);
|
|
315
|
+
} else if (arg === '--above') {
|
|
316
|
+
aboveThreshold = parseInt(options.remaining[++i], 10);
|
|
317
|
+
} else if (arg === '--sort') {
|
|
318
|
+
sortBy = options.remaining[++i];
|
|
319
|
+
} else if (arg === '--uncovered') {
|
|
320
|
+
showUncovered = true;
|
|
321
|
+
} else if (!arg.startsWith('-')) {
|
|
322
|
+
remaining.push(arg);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const targetPath = remaining[0];
|
|
327
|
+
|
|
328
|
+
if (options.help) {
|
|
329
|
+
console.log(HELP);
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const projectRoot = findProjectRoot();
|
|
334
|
+
const out = createOutput(options);
|
|
335
|
+
|
|
336
|
+
// Find coverage data
|
|
337
|
+
const coverageSource = findCoverageData(projectRoot);
|
|
338
|
+
if (!coverageSource) {
|
|
339
|
+
console.error('No coverage data found. Run your tests with coverage first.');
|
|
340
|
+
console.error('Looked for: coverage/lcov.info, coverage/coverage-final.json, .nyc_output/');
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Load coverage
|
|
345
|
+
const coverage = loadCoverage(coverageSource, projectRoot);
|
|
346
|
+
|
|
347
|
+
if (coverage.size === 0) {
|
|
348
|
+
console.error('Coverage data is empty');
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Filter by target path if specified
|
|
353
|
+
let filteredCoverage = [...coverage.entries()];
|
|
354
|
+
|
|
355
|
+
if (targetPath) {
|
|
356
|
+
const targetStat = existsSync(targetPath) ? statSync(targetPath) : null;
|
|
357
|
+
|
|
358
|
+
if (targetStat?.isFile()) {
|
|
359
|
+
// Single file
|
|
360
|
+
const relPath = relative(projectRoot, targetPath);
|
|
361
|
+
filteredCoverage = filteredCoverage.filter(([path]) => path === relPath);
|
|
362
|
+
} else {
|
|
363
|
+
// Directory or pattern
|
|
364
|
+
const prefix = targetPath.replace(/\/$/, '');
|
|
365
|
+
filteredCoverage = filteredCoverage.filter(([path]) => path.startsWith(prefix));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Apply thresholds
|
|
370
|
+
if (belowThreshold !== null) {
|
|
371
|
+
filteredCoverage = filteredCoverage.filter(([, data]) => {
|
|
372
|
+
const pct = calcPercentage(data.lines.hit, data.lines.found);
|
|
373
|
+
return pct < belowThreshold;
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (aboveThreshold !== null) {
|
|
378
|
+
filteredCoverage = filteredCoverage.filter(([, data]) => {
|
|
379
|
+
const pct = calcPercentage(data.lines.hit, data.lines.found);
|
|
380
|
+
return pct >= aboveThreshold;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Sort
|
|
385
|
+
filteredCoverage.sort((a, b) => {
|
|
386
|
+
if (sortBy === 'name') {
|
|
387
|
+
return a[0].localeCompare(b[0]);
|
|
388
|
+
}
|
|
389
|
+
if (sortBy === 'lines') {
|
|
390
|
+
return b[1].lines.found - a[1].lines.found;
|
|
391
|
+
}
|
|
392
|
+
// Default: coverage (ascending - lowest first)
|
|
393
|
+
const pctA = calcPercentage(a[1].lines.hit, a[1].lines.found);
|
|
394
|
+
const pctB = calcPercentage(b[1].lines.hit, b[1].lines.found);
|
|
395
|
+
return pctA - pctB;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// Calculate totals
|
|
399
|
+
let totalLines = 0;
|
|
400
|
+
let totalHit = 0;
|
|
401
|
+
for (const [, data] of filteredCoverage) {
|
|
402
|
+
totalLines += data.lines.found;
|
|
403
|
+
totalHit += data.lines.hit;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Set JSON data
|
|
407
|
+
out.setData('source', basename(coverageSource.path));
|
|
408
|
+
out.setData('files', filteredCoverage.map(([path, data]) => ({
|
|
409
|
+
path,
|
|
410
|
+
coverage: calcPercentage(data.lines.hit, data.lines.found),
|
|
411
|
+
lines: data.lines,
|
|
412
|
+
functions: data.functions,
|
|
413
|
+
branches: data.branches,
|
|
414
|
+
uncoveredLines: data.uncoveredLines
|
|
415
|
+
})));
|
|
416
|
+
out.setData('totalCoverage', calcPercentage(totalHit, totalLines));
|
|
417
|
+
|
|
418
|
+
// Output
|
|
419
|
+
out.header(`📊 Coverage from ${basename(coverageSource.path)}`);
|
|
420
|
+
out.blank();
|
|
421
|
+
|
|
422
|
+
if (filteredCoverage.length === 0) {
|
|
423
|
+
out.add('No files match the criteria');
|
|
424
|
+
} else {
|
|
425
|
+
const rows = [];
|
|
426
|
+
|
|
427
|
+
for (const [path, data] of filteredCoverage) {
|
|
428
|
+
const linePct = calcPercentage(data.lines.hit, data.lines.found);
|
|
429
|
+
const funcPct = calcPercentage(data.functions.hit, data.functions.found);
|
|
430
|
+
const branchPct = calcPercentage(data.branches.hit, data.branches.found);
|
|
431
|
+
|
|
432
|
+
const indicator = linePct >= 80 ? '✓' : linePct >= 50 ? '◐' : '✗';
|
|
433
|
+
|
|
434
|
+
rows.push([
|
|
435
|
+
`${indicator} ${path}`,
|
|
436
|
+
`${linePct}%`,
|
|
437
|
+
`(${data.lines.hit}/${data.lines.found})`
|
|
438
|
+
]);
|
|
439
|
+
|
|
440
|
+
if (showUncovered && data.uncoveredLines.length > 0) {
|
|
441
|
+
const lineRanges = formatLineRanges(data.uncoveredLines);
|
|
442
|
+
rows.push(['', '', ` uncovered: ${lineRanges}`]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
formatTable(rows).forEach(line => out.add(line));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Summary
|
|
450
|
+
if (!options.quiet && filteredCoverage.length > 0) {
|
|
451
|
+
out.blank();
|
|
452
|
+
const totalPct = calcPercentage(totalHit, totalLines);
|
|
453
|
+
out.add(`Total: ${totalPct}% coverage (${totalHit}/${totalLines} lines) across ${filteredCoverage.length} file(s)`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
out.print();
|