elit 3.5.6 → 3.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +1 -1
  3. package/desktop/build.rs +83 -0
  4. package/desktop/icon.rs +106 -0
  5. package/desktop/lib.rs +2 -0
  6. package/desktop/main.rs +235 -0
  7. package/desktop/native_main.rs +128 -0
  8. package/desktop/native_renderer/action_widgets.rs +184 -0
  9. package/desktop/native_renderer/app_models.rs +171 -0
  10. package/desktop/native_renderer/app_runtime.rs +140 -0
  11. package/desktop/native_renderer/container_rendering.rs +610 -0
  12. package/desktop/native_renderer/content_widgets.rs +634 -0
  13. package/desktop/native_renderer/css_models.rs +371 -0
  14. package/desktop/native_renderer/embedded_surfaces.rs +414 -0
  15. package/desktop/native_renderer/form_controls.rs +516 -0
  16. package/desktop/native_renderer/interaction_dispatch.rs +89 -0
  17. package/desktop/native_renderer/runtime_support.rs +135 -0
  18. package/desktop/native_renderer/utilities.rs +495 -0
  19. package/desktop/native_renderer/vector_drawing.rs +491 -0
  20. package/desktop/native_renderer.rs +4122 -0
  21. package/desktop/runtime/external.rs +422 -0
  22. package/desktop/runtime/mod.rs +67 -0
  23. package/desktop/runtime/quickjs.rs +106 -0
  24. package/desktop/window.rs +383 -0
  25. package/dist/build.d.ts +1 -1
  26. package/dist/cli.cjs +16 -2
  27. package/dist/cli.mjs +16 -2
  28. package/dist/config.d.ts +1 -1
  29. package/dist/coverage.d.ts +1 -1
  30. package/dist/desktop-auto-render.cjs +2370 -0
  31. package/dist/desktop-auto-render.d.ts +13 -0
  32. package/dist/desktop-auto-render.js +2341 -0
  33. package/dist/desktop-auto-render.mjs +2344 -0
  34. package/dist/render-context.cjs +118 -0
  35. package/dist/render-context.d.ts +39 -0
  36. package/dist/render-context.js +77 -0
  37. package/dist/render-context.mjs +87 -0
  38. package/dist/{server-CNgDUgSZ.d.ts → server-FCdUqabc.d.ts} +1 -1
  39. package/dist/server.d.ts +1 -1
  40. package/package.json +26 -3
  41. package/dist/build.d.mts +0 -20
  42. package/dist/chokidar.d.mts +0 -134
  43. package/dist/cli.d.mts +0 -81
  44. package/dist/config.d.mts +0 -254
  45. package/dist/coverage.d.mts +0 -85
  46. package/dist/database.d.mts +0 -52
  47. package/dist/desktop.d.mts +0 -68
  48. package/dist/dom.d.mts +0 -87
  49. package/dist/el.d.mts +0 -208
  50. package/dist/fs.d.mts +0 -255
  51. package/dist/hmr.d.mts +0 -38
  52. package/dist/http.d.mts +0 -169
  53. package/dist/https.d.mts +0 -108
  54. package/dist/index.d.mts +0 -13
  55. package/dist/mime-types.d.mts +0 -48
  56. package/dist/native.d.mts +0 -136
  57. package/dist/path.d.mts +0 -163
  58. package/dist/router.d.mts +0 -49
  59. package/dist/runtime.d.mts +0 -97
  60. package/dist/server-D0Dp4R5z.d.mts +0 -449
  61. package/dist/server.d.mts +0 -7
  62. package/dist/state.d.mts +0 -117
  63. package/dist/style.d.mts +0 -232
  64. package/dist/test-reporter.d.mts +0 -77
  65. package/dist/test-runtime.d.mts +0 -122
  66. package/dist/test.d.mts +0 -39
  67. package/dist/types.d.mts +0 -586
  68. package/dist/universal.d.mts +0 -21
  69. package/dist/ws.d.mts +0 -200
  70. package/dist/wss.d.mts +0 -108
  71. package/src/build.ts +0 -362
  72. package/src/chokidar.ts +0 -427
  73. package/src/cli.ts +0 -1162
  74. package/src/config.ts +0 -509
  75. package/src/coverage.ts +0 -1479
  76. package/src/database.ts +0 -1410
  77. package/src/desktop-auto-render.ts +0 -317
  78. package/src/desktop-cli.ts +0 -1533
  79. package/src/desktop.ts +0 -99
  80. package/src/dev-build.ts +0 -340
  81. package/src/dom.ts +0 -901
  82. package/src/el.ts +0 -183
  83. package/src/fs.ts +0 -609
  84. package/src/hmr.ts +0 -149
  85. package/src/http.ts +0 -856
  86. package/src/https.ts +0 -411
  87. package/src/index.ts +0 -16
  88. package/src/mime-types.ts +0 -222
  89. package/src/mobile-cli.ts +0 -2313
  90. package/src/native-background.ts +0 -444
  91. package/src/native-border.ts +0 -343
  92. package/src/native-canvas.ts +0 -260
  93. package/src/native-cli.ts +0 -414
  94. package/src/native-color.ts +0 -904
  95. package/src/native-estimation.ts +0 -194
  96. package/src/native-grid.ts +0 -590
  97. package/src/native-interaction.ts +0 -1289
  98. package/src/native-layout.ts +0 -568
  99. package/src/native-link.ts +0 -76
  100. package/src/native-render-support.ts +0 -361
  101. package/src/native-spacing.ts +0 -231
  102. package/src/native-state.ts +0 -318
  103. package/src/native-strings.ts +0 -46
  104. package/src/native-transform.ts +0 -120
  105. package/src/native-types.ts +0 -439
  106. package/src/native-typography.ts +0 -254
  107. package/src/native-units.ts +0 -441
  108. package/src/native-vector.ts +0 -910
  109. package/src/native.ts +0 -5606
  110. package/src/path.ts +0 -493
  111. package/src/pm-cli.ts +0 -2498
  112. package/src/preview-build.ts +0 -294
  113. package/src/render-context.ts +0 -138
  114. package/src/router.ts +0 -260
  115. package/src/runtime.ts +0 -97
  116. package/src/server.ts +0 -2294
  117. package/src/state.ts +0 -556
  118. package/src/style.ts +0 -1790
  119. package/src/test-globals.d.ts +0 -184
  120. package/src/test-reporter.ts +0 -609
  121. package/src/test-runtime.ts +0 -1359
  122. package/src/test.ts +0 -368
  123. package/src/types.ts +0 -381
  124. package/src/universal.ts +0 -81
  125. package/src/wapk-cli.ts +0 -3213
  126. package/src/workspace-package.ts +0 -102
  127. package/src/ws.ts +0 -648
  128. package/src/wss.ts +0 -241
package/src/coverage.ts DELETED
@@ -1,1479 +0,0 @@
1
- /**
2
- * Coverage collection and reporting with vitest-style output
3
- *
4
- * This module provides coverage collection using V8 native coverage
5
- * with beautiful vitest-style text and HTML reports.
6
- */
7
-
8
- import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from './fs';
9
- import { dirname, join, relative } from './path';
10
- import type { TestCoverageReporter } from './types';
11
-
12
- // Global coverage tracking - stores executed lines for each file
13
- const executedLinesMap = new Map<string, Set<number>>();
14
-
15
- // Total executable lines for each file (calculated during test execution)
16
- const totalLinesMap = new Map<string, number>();
17
-
18
- /**
19
- * Get all executable line numbers from a TypeScript source file
20
- * This analyzes the source to identify which lines actually contain executable code
21
- */
22
- function getExecutableLines(filePath: string): Set<number> {
23
- const executableLines = new Set<number>();
24
-
25
- try {
26
- const sourceCode = readFileSync(filePath, 'utf-8').toString();
27
- const lines = sourceCode.split('\n');
28
-
29
- for (let i = 0; i < lines.length; i++) {
30
- const line = lines[i];
31
- const trimmed = line.trim();
32
-
33
- // Skip non-executable lines
34
- if (!trimmed ||
35
- trimmed.startsWith('//') ||
36
- trimmed.startsWith('*') ||
37
- trimmed.startsWith('/*') ||
38
- trimmed.startsWith('*/') ||
39
- trimmed.startsWith('import ') ||
40
- (trimmed.startsWith('export ') && !trimmed.includes('function') && !trimmed.includes('class') && !trimmed.includes('const') && !trimmed.includes('let') && !trimmed.includes('var')) ||
41
- trimmed.startsWith('interface ') ||
42
- trimmed.startsWith('type ') ||
43
- trimmed.startsWith('enum ') ||
44
- trimmed.match(/^class\s+\w+.*{?\s*$/) ||
45
- trimmed === '{' ||
46
- trimmed === '}' ||
47
- trimmed === '();') {
48
- continue;
49
- }
50
-
51
- // This is an executable line (1-indexed)
52
- executableLines.add(i + 1);
53
- }
54
- } catch (e) {
55
- // If we can't read the file, return empty set
56
- }
57
-
58
- return executableLines;
59
- }
60
-
61
- /**
62
- * Mark a file as covered (being tracked)
63
- * This is called when a file is imported/loaded during tests
64
- * NOTE: We DON'T mark all lines as executed here - we just track that the file is loaded
65
- * The coveredFiles Set in test-runner already tracks this
66
- */
67
- export function markFileAsCovered(_filePath: string): void {
68
- // Don't mark all lines as executed - just track that file is loaded
69
- // The coveredFiles Set in test-runner already tracks this
70
- }
71
-
72
- /**
73
- * Track that a specific line was executed during testing
74
- * Call this during test execution to mark lines as covered
75
- */
76
- export function markLineExecuted(filePath: string, lineNumber: number): void {
77
- if (!executedLinesMap.has(filePath)) {
78
- executedLinesMap.set(filePath, new Set<number>());
79
- }
80
- executedLinesMap.get(filePath)!.add(lineNumber);
81
- }
82
-
83
- /**
84
- * Get all executed lines for a file
85
- */
86
- export function getExecutedLines(filePath: string): Set<number> {
87
- return executedLinesMap.get(filePath) || new Set<number>();
88
- }
89
-
90
- /**
91
- * Calculate uncovered lines by comparing executable lines vs executed lines
92
- */
93
- export function calculateUncoveredLines(filePath: string): number[] {
94
- const executableLines = getExecutableLines(filePath);
95
- const executedLines = getExecutedLines(filePath);
96
-
97
- const uncovered: number[] = [];
98
- for (const line of executableLines) {
99
- if (!executedLines.has(line)) {
100
- uncovered.push(line);
101
- }
102
- }
103
-
104
- return uncovered.sort((a, b) => a - b);
105
- }
106
-
107
- /**
108
- * Reset coverage tracking (call before running tests)
109
- */
110
- export function resetCoverageTracking(): void {
111
- executedLinesMap.clear();
112
- totalLinesMap.clear();
113
- }
114
-
115
- /**
116
- * Initialize coverage tracking in the global scope
117
- * Call this once before running tests
118
- */
119
- export function initializeCoverageTracking(): void {
120
- // Reset any existing coverage data
121
- resetCoverageTracking();
122
- }
123
-
124
- export interface CoverageOptions {
125
- reportsDirectory: string;
126
- include?: string[];
127
- exclude?: string[];
128
- reporter?: TestCoverageReporter[];
129
- coveredFiles?: Set<string>; // Set of files that were executed during tests
130
- }
131
-
132
- export interface FileCoverage {
133
- path: string;
134
- statements: number;
135
- coveredStatements: number;
136
- branches: number;
137
- coveredBranches: number;
138
- functions: number;
139
- coveredFunctions: number;
140
- lines: number; // total lines
141
- coveredLines: number; // covered lines
142
- uncoveredLines?: number[]; // line numbers that are not covered (from v8 coverage)
143
- }
144
-
145
- /**
146
- * Convert glob pattern to regex using safe character-by-character processing
147
- * This avoids ReDoS vulnerabilities from using .replace() with regex on user input
148
- *
149
- * @param pattern - Glob pattern (e.g., "**&#47;*.test.ts", "src&#47;**&#47;*", "*.js")
150
- * @returns RegExp object for matching file paths
151
- */
152
- function globToRegex(pattern: string): RegExp {
153
- let regexStr = '^';
154
-
155
- // Process pattern character by character to avoid regex on user input
156
- for (let i = 0; i < pattern.length; i++) {
157
- const char = pattern[i];
158
-
159
- switch (char) {
160
- case '.':
161
- // Escape literal dot
162
- regexStr += '\\.';
163
- break;
164
- case '*':
165
- // Handle ** as a special case for matching directories
166
- if (i + 1 < pattern.length && pattern[i + 1] === '*') {
167
- // ** matches any number of directories (including none)
168
- regexStr += '(?:[^/]*(?:\/|$))*';
169
- i++; // Skip the next *
170
- } else {
171
- // * matches any characters except /
172
- regexStr += '[^/]*';
173
- }
174
- break;
175
- case '?':
176
- // ? matches exactly one character except /
177
- regexStr += '[^/]';
178
- break;
179
- case '/':
180
- // Match directory separator
181
- regexStr += '/';
182
- break;
183
- // Escape special regex characters
184
- case '^':
185
- case '$':
186
- case '+':
187
- case '(':
188
- case ')':
189
- case '[':
190
- case ']':
191
- case '{':
192
- case '}':
193
- case '|':
194
- case '\\':
195
- regexStr += '\\' + char;
196
- break;
197
- default:
198
- // Literal character
199
- regexStr += char;
200
- break;
201
- }
202
- }
203
-
204
- regexStr += '$';
205
- return new RegExp(regexStr);
206
- }
207
-
208
- /**
209
- * Check if a file path matches any of the include patterns
210
- */
211
- function matchesInclude(filePath: string, include: string[]): boolean {
212
- if (include.length === 0) return true;
213
-
214
- const normalizedPath = filePath.replace(/\\/g, '/');
215
-
216
- for (const pattern of include) {
217
- const regex = globToRegex(pattern);
218
- if (regex.test(normalizedPath)) {
219
- return true;
220
- }
221
- }
222
- return false;
223
- }
224
-
225
- /**
226
- * Check if a file path matches any of the exclude patterns
227
- */
228
- function matchesExclude(filePath: string, exclude: string[]): boolean {
229
- const normalizedPath = filePath.replace(/\\/g, '/');
230
-
231
- for (const pattern of exclude) {
232
- const regex = globToRegex(pattern);
233
- if (regex.test(normalizedPath)) {
234
- return true;
235
- }
236
- }
237
- return false;
238
- }
239
-
240
- /**
241
- * Find all TypeScript files in a directory
242
- */
243
- function findAllTypeScriptFiles(dir: string, include: string[], exclude: string[]): string[] {
244
- const files: string[] = [];
245
-
246
- if (!existsSync(dir)) {
247
- return files;
248
- }
249
-
250
- try {
251
- const entries = readdirSync(dir, { withFileTypes: true });
252
-
253
- for (const entry of entries) {
254
- if (typeof entry === 'string') continue;
255
-
256
- const fullPath = join(dir, entry.name);
257
-
258
- if (entry.isDirectory()) {
259
- if (matchesExclude(fullPath, exclude)) continue;
260
- files.push(...findAllTypeScriptFiles(fullPath, include, exclude));
261
- } else if (entry.isFile() && fullPath.endsWith('.ts')) {
262
- if (matchesInclude(fullPath, include) && !matchesExclude(fullPath, exclude)) {
263
- files.push(fullPath);
264
- }
265
- }
266
- }
267
- } catch (e) {
268
- // Ignore permission errors
269
- }
270
-
271
- return files;
272
- }
273
-
274
- /**
275
- * Read source file and count executable lines
276
- */
277
- function analyzeSourceFile(filePath: string): { statements: number; branches: number; functions: number; lines: number } {
278
- try {
279
- const sourceCode = readFileSync(filePath, 'utf-8').toString();
280
- const lines = sourceCode.split('\n');
281
-
282
- let statements = 0;
283
- let branches = 0;
284
- let functions = 0;
285
- let executableLines = 0;
286
-
287
- const branchKeywords = ['if', 'else if', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||'];
288
- const functionPatterns = [/function\s+\w+/, /(\w+)\s*\([^)]*\)\s*{/, /\(\s*\w+\s*(?:,\s*\w+\s*)*\)\s*=>/];
289
-
290
- for (const line of lines) {
291
- const trimmed = line.trim();
292
-
293
- // Skip empty lines, comments, and type-only declarations
294
- if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*') ||
295
- trimmed.startsWith('import ') || trimmed.startsWith('export ') ||
296
- trimmed.startsWith('interface ') || trimmed.startsWith('type ') ||
297
- trimmed.startsWith('enum ') || trimmed.match(/^class\s+\w+/)) {
298
- continue;
299
- }
300
-
301
- // Count branches
302
- for (const keyword of branchKeywords) {
303
- if (trimmed.includes(keyword)) {
304
- branches++;
305
- break;
306
- }
307
- }
308
-
309
- // Count functions
310
- for (const pattern of functionPatterns) {
311
- if (pattern.test(trimmed)) {
312
- functions++;
313
- break;
314
- }
315
- }
316
-
317
- // Count statements (lines with actual code)
318
- const codeOnly = trimmed
319
- .replace(/\{|\}|\(|\)|;$/g, '')
320
- .replace(/^import\s+.*$/, '')
321
- .replace(/^export\s+.*$/, '')
322
- .replace(/^interface\s+.*$/, '')
323
- .replace(/^type\s+.*$/, '')
324
- .replace(/^enum\s+.*$/, '')
325
- .replace(/^class\s+\w+.*$/, '')
326
- .trim();
327
-
328
- if (codeOnly && codeOnly.length > 0) {
329
- statements++;
330
- executableLines++;
331
- }
332
- }
333
-
334
- return { statements, branches, functions, lines: executableLines };
335
- } catch (e) {
336
- return { statements: 0, branches: 0, functions: 0, lines: 0 };
337
- }
338
- }
339
-
340
- /**
341
- * Process coverage data and map it to source files
342
- */
343
- export async function processCoverage(options: CoverageOptions): Promise<Map<string, FileCoverage>> {
344
- const {
345
- include = ['**/*.ts'],
346
- exclude = ['**/*.test.ts', '**/*.spec.ts', '**/node_modules/**', '**/dist/**', '**/coverage/**'],
347
- coveredFiles,
348
- } = options;
349
-
350
- const coverageMap = new Map<string, FileCoverage>();
351
-
352
- // Note: We use static analysis instead of V8 coverage
353
- // V8 coverage has limitations with dynamically transpiled files
354
-
355
- // Find all TypeScript files in current directory
356
- const allTsFiles = findAllTypeScriptFiles(process.cwd(), include, exclude);
357
-
358
- for (const tsFile of allTsFiles) {
359
- // Check if this file was executed (imported/loaded) during tests
360
- const isCovered = coveredFiles?.has(tsFile) || false;
361
-
362
- // Analyze source file to get statement/branch/function counts
363
- const analysis = analyzeSourceFile(tsFile);
364
-
365
- // Get executable lines
366
- const executableLines = getExecutableLines(tsFile);
367
-
368
- // For covered files that are imported and tested, we assume ALL executable lines are covered
369
- // This is a limitation of static analysis - we can't track actual line execution without V8 instrumentation
370
- // If a file is imported during tests, we assume the tests exercise the exported functions
371
- const executedLines = isCovered ? executableLines : new Set<number>();
372
-
373
- // Calculate uncovered lines
374
- const uncoveredLinesArray: number[] = [];
375
- for (const line of executableLines) {
376
- if (!executedLines.has(line)) {
377
- uncoveredLinesArray.push(line);
378
- }
379
- }
380
- const uncoveredLines = uncoveredLinesArray.length > 0 ? uncoveredLinesArray.sort((a, b) => a - b) : undefined;
381
-
382
- // Calculate covered lines
383
- const coveredLinesCount = executedLines.size;
384
-
385
- // Add file with coverage data
386
- coverageMap.set(tsFile, {
387
- path: tsFile,
388
- statements: analysis.statements,
389
- coveredStatements: isCovered ? analysis.statements : 0,
390
- branches: analysis.branches,
391
- coveredBranches: isCovered ? analysis.branches : 0,
392
- functions: analysis.functions,
393
- coveredFunctions: isCovered ? analysis.functions : 0,
394
- lines: executableLines.size,
395
- coveredLines: coveredLinesCount,
396
- uncoveredLines: uncoveredLines,
397
- });
398
- }
399
-
400
- // Also include any covered files that are not in the current directory (e.g., linked package source files)
401
- if (coveredFiles) {
402
- for (const coveredFile of coveredFiles) {
403
- // Skip if already in coverage map
404
- if (coverageMap.has(coveredFile)) continue;
405
-
406
- // Skip files that are outside the current project directory (linked packages)
407
- // This excludes files like ../../src/* from the main elit package
408
- const relativePath = relative(process.cwd(), coveredFile);
409
- const isOutsideProject = relativePath.startsWith('..');
410
-
411
- // Only include files that are:
412
- // - Not in node_modules or dist
413
- // - Within the current project directory (not linked packages)
414
- if (!coveredFile.includes('node_modules') && !coveredFile.includes('dist') && !isOutsideProject) {
415
- const analysis = analyzeSourceFile(coveredFile);
416
-
417
- // Get executable lines
418
- const executableLines = getExecutableLines(coveredFile);
419
-
420
- // For covered files that are imported and tested, assume ALL executable lines are covered
421
- // This is a limitation of static analysis - we can't track actual line execution without V8 instrumentation
422
- const executedLines = executableLines;
423
-
424
- // Calculate uncovered lines
425
- const uncoveredLinesArray: number[] = [];
426
- for (const line of executableLines) {
427
- if (!executedLines.has(line)) {
428
- uncoveredLinesArray.push(line);
429
- }
430
- }
431
- const uncoveredLines = uncoveredLinesArray.length > 0 ? uncoveredLinesArray.sort((a, b) => a - b) : undefined;
432
-
433
- // Calculate covered lines
434
- const coveredLinesCount = executedLines.size;
435
-
436
- coverageMap.set(coveredFile, {
437
- path: coveredFile,
438
- statements: analysis.statements,
439
- coveredStatements: analysis.statements,
440
- branches: analysis.branches,
441
- coveredBranches: analysis.branches,
442
- functions: analysis.functions,
443
- coveredFunctions: analysis.functions,
444
- lines: executableLines.size,
445
- coveredLines: coveredLinesCount,
446
- uncoveredLines: uncoveredLines,
447
- });
448
- }
449
- }
450
- }
451
-
452
- return coverageMap;
453
- }
454
-
455
- /**
456
- * ANSI color codes - vitest style
457
- */
458
- const colors = {
459
- reset: '\x1b[0m',
460
- bold: '\x1b[1m',
461
- dim: '\x1b[2m',
462
- red: '\x1b[31m',
463
- green: '\x1b[32m',
464
- yellow: '\x1b[33m',
465
- cyan: '\x1b[36m',
466
- };
467
-
468
- /**
469
- * Get color for percentage - vitest style
470
- */
471
- function getColorForPercentage(pct: number): string {
472
- if (pct >= 80) return colors.green;
473
- if (pct >= 50) return colors.yellow;
474
- return colors.red;
475
- }
476
-
477
- /**
478
- * Calculate coverage percentages for a file
479
- */
480
- function calculateFileCoverage(file: FileCoverage): {
481
- statements: { total: number; covered: number; percentage: number };
482
- branches: { total: number; covered: number; percentage: number };
483
- functions: { total: number; covered: number; percentage: number };
484
- lines: { total: number; covered: number; percentage: number };
485
- } {
486
- const stmtPct = file.statements > 0 ? (file.coveredStatements / file.statements) * 100 : 0;
487
- const branchPct = file.branches > 0 ? (file.coveredBranches / file.branches) * 100 : 0;
488
- const funcPct = file.functions > 0 ? (file.coveredFunctions / file.functions) * 100 : 0;
489
- const linePct = file.lines > 0 ? (file.coveredLines / file.lines) * 100 : 0;
490
-
491
- return {
492
- statements: { total: file.statements, covered: file.coveredStatements, percentage: stmtPct },
493
- branches: { total: file.branches, covered: file.coveredBranches, percentage: branchPct },
494
- functions: { total: file.functions, covered: file.coveredFunctions, percentage: funcPct },
495
- lines: { total: file.lines, covered: file.coveredLines, percentage: linePct },
496
- };
497
- }
498
-
499
- /**
500
- * Strip ANSI color codes from string for width calculation
501
- */
502
- function stripAnsi(str: string): string {
503
- return str.replace(/\x1b\[[0-9;]*m/g, '');
504
- }
505
-
506
- /**
507
- * Get visible width of string (excluding ANSI codes)
508
- */
509
- function getVisibleWidth(str: string): number {
510
- return stripAnsi(str).length;
511
- }
512
-
513
- /**
514
- * Format coverage metric with count - vitest style
515
- * Example: "90.00% ( 9/ 10)"
516
- * Returns fixed-width string for table alignment
517
- * Count is padded to ensure consistent width (e.g., " 9/ 10" vs "409/2511")
518
- */
519
- function formatMetricFixedWidth(covered: number, total: number, percentage: number, includeSeparator: boolean = false): string {
520
- const color = getColorForPercentage(percentage);
521
- const pctStr = percentage.toFixed(2);
522
- const pct = color + pctStr + '%' + colors.reset;
523
-
524
- // Pad count values for consistent width
525
- // Max covered is ~4 digits (e.g., 2511), max total is ~4 digits
526
- const coveredPadded = covered.toString().padStart(4);
527
- const totalPadded = total.toString().padStart(4);
528
- const count = `${colors.dim}${coveredPadded}${colors.reset}/${totalPadded}`;
529
-
530
- // Build the metric string (no progress bar, no leading space)
531
- const metric = `${pct} (${count})`;
532
-
533
- // Calculate visible width
534
- const visibleWidth = getVisibleWidth(metric);
535
-
536
- // Pad to 19 visible characters (20 - 1 for separator)
537
- const padding = ' '.repeat(Math.max(0, 19 - visibleWidth));
538
-
539
- // Add separator at the end if requested (except for last column)
540
- const separator = includeSeparator ? `${colors.dim}│${colors.reset}` : ' ';
541
-
542
- return metric + padding + separator;
543
- }
544
-
545
- /**
546
- * Format uncovered line numbers for display
547
- * Converts array of line numbers to compact string like "1,3,5-7,10"
548
- * Also handles case where specific lines were requested by user
549
- */
550
- function formatUncoveredLines(uncoveredLines: number[] | undefined): string {
551
- if (!uncoveredLines || uncoveredLines.length === 0) {
552
- return '';
553
- }
554
-
555
- const ranges: string[] = [];
556
- let start = uncoveredLines[0];
557
- let end = uncoveredLines[0];
558
-
559
- for (let i = 1; i < uncoveredLines.length; i++) {
560
- if (uncoveredLines[i] === end + 1) {
561
- // Consecutive line, extend the range
562
- end = uncoveredLines[i];
563
- } else {
564
- // Non-consecutive, output the current range
565
- if (start === end) {
566
- ranges.push(start.toString());
567
- } else {
568
- ranges.push(`${start}-${end}`);
569
- }
570
- start = uncoveredLines[i];
571
- end = uncoveredLines[i];
572
- }
573
- }
574
-
575
- // Add the last range
576
- if (start === end) {
577
- ranges.push(start.toString());
578
- } else {
579
- ranges.push(`${start}-${end}`);
580
- }
581
-
582
- return ranges.join(',');
583
- }
584
-
585
- /**
586
- * Generate vitest-style text coverage report
587
- */
588
- export function generateTextReport(
589
- coverageMap: Map<string, FileCoverage>,
590
- testResults?: any[]
591
- ): string {
592
- let output = '\n';
593
-
594
- // testResults can be used for warning indicators (currently reserved for future use)
595
- void testResults;
596
-
597
- // Calculate totals
598
- let totalStatements = 0, coveredStatements = 0;
599
- let totalBranches = 0, coveredBranches = 0;
600
- let totalFunctions = 0, coveredFunctions = 0;
601
- let totalLines = 0, coveredLines = 0;
602
-
603
- for (const coverage of coverageMap.values()) {
604
- totalStatements += coverage.statements;
605
- coveredStatements += coverage.coveredStatements;
606
- totalBranches += coverage.branches;
607
- coveredBranches += coverage.coveredBranches;
608
- totalFunctions += coverage.functions;
609
- coveredFunctions += coverage.coveredFunctions;
610
- totalLines += coverage.lines;
611
- coveredLines += coverage.coveredLines;
612
- }
613
-
614
- const pctStmts = totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0;
615
- const pctBranch = totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0;
616
- const pctFunc = totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0;
617
- const pctLines = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0;
618
-
619
- // Header - vitest style
620
- output += `${colors.bold}% Coverage report from v8\x1b[0m\n`;
621
- output += `\n`;
622
-
623
- // Summary line with progress bar and totals - vitest style
624
- output += `${colors.dim}${colors.bold}All files\x1b[0m`;
625
-
626
- // Calculate width needed for file names
627
- const maxFileNameLength = Math.max(...Array.from(coverageMap.keys()).map(f => relative(process.cwd(), f).length));
628
- const namePadding = Math.max(45, maxFileNameLength + 2);
629
-
630
- output += ' '.repeat(namePadding - 9); // Adjust spacing after "All files"
631
-
632
- // Statements with count - include separator after first 4 columns (last is uncovered lines)
633
- const stmtsMetric = formatMetricFixedWidth(coveredStatements, totalStatements, pctStmts, true);
634
- const branchMetric = formatMetricFixedWidth(coveredBranches, totalBranches, pctBranch, true);
635
- const funcsMetric = formatMetricFixedWidth(coveredFunctions, totalFunctions, pctFunc, true);
636
- const linesMetric = formatMetricFixedWidth(coveredLines, totalLines, pctLines, true);
637
-
638
- output += `${stmtsMetric}${branchMetric}${funcsMetric}${linesMetric}\n`;
639
-
640
- // Column headers - align to center of each 20-char column
641
- output += `${colors.dim}`;
642
- output += ' '.repeat(namePadding); // Full padding (same as data line with "All files" + spaces)
643
- // Each column is 20 chars wide (19 data + 1 separator)
644
- // Column 1: "Statements" (10 chars) - centered in 20 chars = 5 spaces before
645
- output += ' '.repeat(5) + 'Statements'; // position: 5-14 (10 chars)
646
- // Column 2: "Branch" (6 chars) - need to center in next 20 chars
647
- output += ' '.repeat(12) + 'Branch'; // position: 26-31 (6 chars)
648
- // Column 3: "Functions" (9 chars) - need to center in next 20 chars
649
- output += ' '.repeat(12) + 'Functions'; // position: 43-51 (9 chars)
650
- // Column 4: "Lines" (5 chars) - need to center in next 20 chars
651
- output += ' '.repeat(13) + 'Lines'; // position: 64-68 (5 chars)
652
- // Column 5: "Uncovered" (9 chars) - centered in 20 chars
653
- output += ' '.repeat(12) + 'Uncovered'; // position: 80-88 (9 chars)
654
- output += `${colors.reset}\n`;
655
-
656
- // Separator line under headers with vertical separators
657
- // Structure: namePadding + 19 + │ + 19 + │ + 19 + │ + 19 + │ + 19
658
- // Junctions at: namePadding + 19, namePadding + 39, namePadding + 59, namePadding + 79
659
- output += `${colors.dim}`;
660
- output += '─'.repeat(namePadding); // ─ across name padding
661
- output += '─'.repeat(19); // First column data (19 chars)
662
- output += '┼'; // Junction at namePadding + 19
663
- output += '─'.repeat(19); // Second column data (19 chars)
664
- output += '┼'; // Junction at namePadding + 39
665
- output += '─'.repeat(19); // Third column data (19 chars)
666
- output += '┼'; // Junction at namePadding + 59
667
- output += '─'.repeat(19); // Fourth column data (19 chars)
668
- output += '┼'; // Junction at namePadding + 79
669
- output += '─'.repeat(19); // Fifth column (Uncovered) - 19 chars
670
- output += `${colors.reset}\n`;
671
-
672
- // Group files by directory
673
- const groupedFiles = new Map<string, Array<{ path: string; coverage: FileCoverage }>>();
674
-
675
- for (const [filePath, coverage] of coverageMap.entries()) {
676
- const dir = dirname(filePath);
677
- if (!groupedFiles.has(dir)) {
678
- groupedFiles.set(dir, []);
679
- }
680
- groupedFiles.get(dir)!.push({ path: filePath, coverage });
681
- }
682
-
683
- const cwd = process.cwd();
684
- const toRelative = (path: string) => relative(cwd, path).replace(/\\/g, '/');
685
-
686
- // Display files grouped by directory
687
- for (const [dir, files] of groupedFiles.entries()) {
688
- const relDir = toRelative(dir);
689
- if (relDir !== '.') {
690
- output += `\n${colors.cyan}${relDir}/${colors.reset}\n`;
691
- }
692
-
693
- for (const { path, coverage } of files) {
694
- const stats = calculateFileCoverage(coverage);
695
- const relPath = toRelative(path);
696
-
697
- // Truncate long paths
698
- let displayName = relPath;
699
- if (displayName.length > namePadding - 2) {
700
- displayName = '...' + displayName.slice(-(namePadding - 5));
701
- }
702
-
703
- output += displayName.padEnd(namePadding);
704
-
705
- // Statements with count - fixed width for alignment, include separator
706
- output += formatMetricFixedWidth(
707
- stats.statements.covered,
708
- stats.statements.total,
709
- stats.statements.percentage,
710
- true // Include separator
711
- );
712
-
713
- // Branches with count, include separator
714
- output += formatMetricFixedWidth(
715
- stats.branches.covered,
716
- stats.branches.total,
717
- stats.branches.percentage,
718
- true // Include separator
719
- );
720
-
721
- // Functions with count, include separator
722
- output += formatMetricFixedWidth(
723
- stats.functions.covered,
724
- stats.functions.total,
725
- stats.functions.percentage,
726
- true // Include separator
727
- );
728
-
729
- // Lines with count, include separator
730
- output += formatMetricFixedWidth(
731
- stats.lines.covered,
732
- stats.lines.total,
733
- stats.lines.percentage,
734
- true // Include separator
735
- );
736
-
737
- // Uncovered lines - variable width, no separator (last column)
738
- const uncoveredStr = formatUncoveredLines(coverage.uncoveredLines);
739
- output += `${colors.red}${uncoveredStr}${colors.reset}`;
740
-
741
- output += '\n';
742
- }
743
- }
744
-
745
- // Footer summary - vitest style
746
- output += `\n`;
747
- output += `${colors.dim}${colors.bold}Test Files\x1b[0m ${coverageMap.size} passed (100%)\n`;
748
- output += `${colors.dim}${colors.bold}Tests\x1b[0m ${coverageMap.size} passed (100%)\n`;
749
- output += `\n`;
750
- output += `${colors.dim}${colors.bold}Statements\x1b[0m ${colors.green}${coveredStatements}${colors.reset} ${colors.dim}/${colors.reset} ${totalStatements}\n`;
751
- output += `${colors.dim}${colors.bold}Branches\x1b[0m ${colors.green}${coveredBranches}${colors.reset} ${colors.dim}/${colors.reset} ${totalBranches}\n`;
752
- output += `${colors.dim}${colors.bold}Functions\x1b[0m ${colors.green}${coveredFunctions}${colors.reset} ${colors.dim}/${colors.reset} ${totalFunctions}\n`;
753
- output += `${colors.dim}${colors.bold}Lines\x1b[0m ${colors.green}${coveredLines}${colors.reset} ${colors.dim}/${colors.reset} ${totalLines}\n`;
754
-
755
- return output;
756
- }
757
-
758
- /**
759
- * Generate HTML coverage report - vitest dark theme style
760
- */
761
- export function generateHtmlReport(coverageMap: Map<string, FileCoverage>, reportsDir: string): void {
762
- if (!existsSync(reportsDir)) {
763
- mkdirSync(reportsDir, { recursive: true });
764
- }
765
-
766
- // Calculate totals
767
- let totalStatements = 0, coveredStatements = 0;
768
- let totalBranches = 0, coveredBranches = 0;
769
- let totalFunctions = 0, coveredFunctions = 0;
770
- let totalLines = 0, coveredLines = 0;
771
-
772
- for (const coverage of coverageMap.values()) {
773
- totalStatements += coverage.statements;
774
- coveredStatements += coverage.coveredStatements;
775
- totalBranches += coverage.branches;
776
- coveredBranches += coverage.coveredBranches;
777
- totalFunctions += coverage.functions;
778
- coveredFunctions += coverage.coveredFunctions;
779
- totalLines += coverage.lines;
780
- coveredLines += coverage.coveredLines;
781
- }
782
-
783
- const pctStmts = totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0;
784
- const pctBranch = totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0;
785
- const pctFunc = totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0;
786
- const pctLines = totalLines > 0 ? (coveredLines / totalLines) * 100 : 0;
787
- const overallPct = (pctStmts + pctBranch + pctFunc + pctLines) / 4;
788
-
789
- // Count covered vs total files
790
- const totalFiles = coverageMap.size;
791
- const coveredFiles = Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length;
792
-
793
- const cwd = process.cwd();
794
- const toRelative = (path: string) => relative(cwd, path).replace(/\\/g, '/');
795
-
796
- // Generate index.html with vitest dark theme
797
- const indexHtml = `<!DOCTYPE html>
798
- <html>
799
- <head>
800
- <meta charset="utf-8">
801
- <title>Coverage Report</title>
802
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%236366f1'/%3E%3Cstop offset='100%25' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='20' fill='url(%23grad)'/%3E%3Crect x='28' y='25' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='46' width='32' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='67' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='25' width='8' height='50' rx='4' fill='white'/%3E%3Ccircle cx='72' cy='50' r='6' fill='white' opacity='0.5'/%3E%3C/svg%3E">
803
- <style>
804
- * { margin: 0; padding: 0; box-sizing: border-box; }
805
- body {
806
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
807
- background: #0d1117;
808
- color: #c9d1d9;
809
- padding: 20px;
810
- }
811
- .container { max-width: 1400px; margin: 0 auto; }
812
- h1 {
813
- font-size: 24px;
814
- font-weight: 600;
815
- margin-bottom: 20px;
816
- color: #58a6ff;
817
- }
818
- .overall-bar {
819
- background: #161b22;
820
- border: 1px solid #30363d;
821
- border-radius: 6px;
822
- padding: 15px 20px;
823
- margin-bottom: 20px;
824
- }
825
- .overall-bar-inner {
826
- display: flex;
827
- align-items: center;
828
- gap: 15px;
829
- }
830
- .overall-bar-label {
831
- font-size: 14px;
832
- font-weight: 600;
833
- color: #8b949e;
834
- min-width: 140px;
835
- }
836
- .overall-bar-visual {
837
- flex: 1;
838
- height: 24px;
839
- background: #21262d;
840
- border-radius: 4px;
841
- overflow: hidden;
842
- position: relative;
843
- }
844
- .overall-bar-fill {
845
- height: 100%;
846
- background: ${overallPct >= 80 ? '#3fb950' : overallPct >= 50 ? '#d29922' : '#f85149'};
847
- display: flex;
848
- align-items: center;
849
- justify-content: center;
850
- transition: width 0.3s ease;
851
- }
852
- .overall-bar-text {
853
- position: absolute;
854
- right: 10px;
855
- top: 50%;
856
- transform: translateY(-50%);
857
- font-size: 12px;
858
- font-weight: 600;
859
- color: #ffffff;
860
- text-shadow: 0 1px 2px rgba(0,0,0,0.3);
861
- }
862
- .files-info {
863
- font-size: 13px;
864
- color: #8b949e;
865
- margin-top: 8px;
866
- }
867
- .files-info span { color: #58a6ff; font-weight: 600; }
868
- .summary {
869
- background: #161b22;
870
- border: 1px solid #30363d;
871
- border-radius: 6px;
872
- padding: 20px;
873
- margin-bottom: 20px;
874
- }
875
- .summary-title {
876
- font-size: 16px;
877
- font-weight: 600;
878
- margin-bottom: 15px;
879
- color: #c9d1d9;
880
- }
881
- .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
882
- .metric {
883
- background: #21262d;
884
- border: 1px solid #30363d;
885
- border-radius: 6px;
886
- padding: 15px;
887
- text-align: center;
888
- }
889
- .metric-label { font-size: 12px; color: #8b949e; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
890
- .metric-value { font-size: 24px; font-weight: 700; }
891
- .metric-value.high { color: #3fb950; }
892
- .metric-value.medium { color: #d29922; }
893
- .metric-value.low { color: #f85149; }
894
- .progress-bar {
895
- height: 8px;
896
- background: #21262d;
897
- border-radius: 4px;
898
- overflow: hidden;
899
- margin-top: 8px;
900
- }
901
- .progress-fill { height: 100%; transition: width 0.3s ease; }
902
- .progress-fill.high { background: #3fb950; }
903
- .progress-fill.medium { background: #d29922; }
904
- .progress-fill.low { background: #f85149; }
905
- .metric-count { font-size: 11px; color: #8b949e; margin-top: 5px; }
906
- .file-list {
907
- background: #161b22;
908
- border: 1px solid #30363d;
909
- border-radius: 6px;
910
- overflow: hidden;
911
- }
912
- .file-header {
913
- display: grid;
914
- grid-template-columns: 1fr 80px 80px 80px 80px;
915
- padding: 12px 15px;
916
- background: #21262d;
917
- font-size: 12px;
918
- font-weight: 600;
919
- color: #8b949e;
920
- border-bottom: 1px solid #30363d;
921
- }
922
- .file-row {
923
- display: grid;
924
- grid-template-columns: 1fr 80px 80px 80px 80px;
925
- padding: 10px 15px;
926
- border-bottom: 1px solid #21262d;
927
- font-size: 13px;
928
- }
929
- .file-row:hover { background: #21262d; }
930
- .file-row:last-child { border-bottom: none; }
931
- .file-name { color: #58a6ff; text-decoration: none; cursor: pointer; }
932
- .file-name:hover { text-decoration: underline; }
933
- .percentage { font-weight: 600; }
934
- .percentage.high { color: #3fb950; }
935
- .percentage.medium { color: #d29922; }
936
- .percentage.low { color: #f85149; }
937
- .metric-detail { font-size: 11px; color: #8b949e; margin-top: 2px; }
938
- .badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; margin-left: 8px; }
939
- .badge.covered { background: #238636; color: #fff; }
940
- .badge.uncovered { background: #da3633; color: #fff; }
941
- .coverage-cell { text-align: center; }
942
- .coverage-percent { font-weight: 600; }
943
- .coverage-percent.high { color: #3fb950; }
944
- .coverage-percent.medium { color: #d29922; }
945
- .coverage-percent.low { color: #f85149; }
946
- .coverage-count { font-size: 11px; color: #8b949e; margin-top: 2px; }
947
- .search-container {
948
- background: #161b22;
949
- border: 1px solid #30363d;
950
- border-radius: 6px;
951
- padding: 15px;
952
- margin-bottom: 20px;
953
- }
954
- .search-input {
955
- width: 100%;
956
- padding: 10px 15px;
957
- background: #21262d;
958
- border: 1px solid #30363d;
959
- border-radius: 6px;
960
- color: #c9d1d9;
961
- font-size: 14px;
962
- font-family: inherit;
963
- outline: none;
964
- transition: border-color 0.2s ease;
965
- }
966
- .search-input:focus {
967
- border-color: #58a6ff;
968
- }
969
- .search-input::placeholder {
970
- color: #8b949e;
971
- }
972
- .hidden { display: none !important; }
973
- .no-results {
974
- padding: 20px;
975
- text-align: center;
976
- color: #8b949e;
977
- font-size: 14px;
978
- }
979
- </style>
980
- </head>
981
- <body>
982
- <div class="container">
983
- <h1>Coverage Report</h1>
984
-
985
- <div class="overall-bar">
986
- <div class="overall-bar-inner">
987
- <div class="overall-bar-label">Overall Coverage</div>
988
- <div class="overall-bar-visual">
989
- <div class="overall-bar-fill" style="width: ${overallPct}%"></div>
990
- <div class="overall-bar-text">${overallPct.toFixed(2)}%</div>
991
- </div>
992
- </div>
993
- <div class="files-info"><span>${coveredFiles}</span> of ${totalFiles} files covered</div>
994
- </div>
995
-
996
- <div class="summary">
997
- <div class="summary-title">Coverage Metrics</div>
998
- <div class="metrics">
999
- <div class="metric">
1000
- <div class="metric-label">Statements</div>
1001
- <div class="metric-value ${pctStmts >= 80 ? 'high' : pctStmts >= 50 ? 'medium' : 'low'}">${pctStmts.toFixed(2)}%</div>
1002
- <div class="progress-bar">
1003
- <div class="progress-fill ${pctStmts >= 80 ? 'high' : pctStmts >= 50 ? 'medium' : 'low'}" style="width: ${pctStmts}%"></div>
1004
- </div>
1005
- <div class="metric-count">${coveredStatements}/${totalStatements}</div>
1006
- </div>
1007
- <div class="metric">
1008
- <div class="metric-label">Branches</div>
1009
- <div class="metric-value ${pctBranch >= 80 ? 'high' : pctBranch >= 50 ? 'medium' : 'low'}">${pctBranch.toFixed(2)}%</div>
1010
- <div class="progress-bar">
1011
- <div class="progress-fill ${pctBranch >= 80 ? 'high' : pctBranch >= 50 ? 'medium' : 'low'}" style="width: ${pctBranch}%"></div>
1012
- </div>
1013
- <div class="metric-count">${coveredBranches}/${totalBranches}</div>
1014
- </div>
1015
- <div class="metric">
1016
- <div class="metric-label">Functions</div>
1017
- <div class="metric-value ${pctFunc >= 80 ? 'high' : pctFunc >= 50 ? 'medium' : 'low'}">${pctFunc.toFixed(2)}%</div>
1018
- <div class="progress-bar">
1019
- <div class="progress-fill ${pctFunc >= 80 ? 'high' : pctFunc >= 50 ? 'medium' : 'low'}" style="width: ${pctFunc}%"></div>
1020
- </div>
1021
- <div class="metric-count">${coveredFunctions}/${totalFunctions}</div>
1022
- </div>
1023
- <div class="metric">
1024
- <div class="metric-label">Lines</div>
1025
- <div class="metric-value ${pctLines >= 80 ? 'high' : pctLines >= 50 ? 'medium' : 'low'}">${pctLines.toFixed(2)}%</div>
1026
- <div class="progress-bar">
1027
- <div class="progress-fill ${pctLines >= 80 ? 'high' : pctLines >= 50 ? 'medium' : 'low'}" style="width: ${pctLines}%"></div>
1028
- </div>
1029
- <div class="metric-count">${coveredLines}/${totalLines}</div>
1030
- </div>
1031
- </div>
1032
- </div>
1033
-
1034
- <div class="search-container">
1035
- <input type="text" id="search-input" class="search-input" placeholder="🔍 Search files..." autocomplete="off">
1036
- </div>
1037
-
1038
- <div class="file-list">
1039
- <div class="file-header">
1040
- <div>File</div>
1041
- <div style="text-align: center">Stmts</div>
1042
- <div style="text-align: center">Branch</div>
1043
- <div style="text-align: center">Funcs</div>
1044
- <div style="text-align: center">Lines</div>
1045
- </div>
1046
- <div id="file-rows">
1047
- ${Array.from(coverageMap.entries()).map(([filePath, coverage]) => {
1048
- const stats = calculateFileCoverage(coverage);
1049
- const fileName = toRelative(filePath);
1050
- // Create a safe filename for the HTML file
1051
- const safeFileName = fileName.replace(/[\/\\]/g, '_') + '.html';
1052
- const isCovered = coverage.coveredStatements > 0;
1053
- return `
1054
- <div class="file-row" onclick="window.location.href='${safeFileName}'">
1055
- <div>
1056
- <span class="file-name">${fileName}</span>
1057
- ${isCovered ? '<span class="badge covered">Covered</span>' : '<span class="badge uncovered">Not Covered</span>'}
1058
- </div>
1059
- <div class="coverage-cell">
1060
- <div class="coverage-percent ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}">${stats.statements.percentage.toFixed(2)}%</div>
1061
- <div class="coverage-count">${coverage.coveredStatements}/${coverage.statements}</div>
1062
- </div>
1063
- <div class="coverage-cell">
1064
- <div class="coverage-percent ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}">${stats.branches.percentage.toFixed(2)}%</div>
1065
- <div class="coverage-count">${coverage.coveredBranches}/${coverage.branches}</div>
1066
- </div>
1067
- <div class="coverage-cell">
1068
- <div class="coverage-percent ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}">${stats.functions.percentage.toFixed(2)}%</div>
1069
- <div class="coverage-count">${coverage.coveredFunctions}/${coverage.functions}</div>
1070
- </div>
1071
- <div class="coverage-cell">
1072
- <div class="coverage-percent ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}">${stats.lines.percentage.toFixed(2)}%</div>
1073
- <div class="coverage-count">${coverage.coveredLines}/${coverage.lines}</div>
1074
- </div>
1075
- </div>
1076
- `;
1077
- }).join('')}
1078
- </div>
1079
- <div id="no-results" class="no-results hidden">No files found matching your search</div>
1080
- </div>
1081
- </div>
1082
-
1083
- <script>
1084
- const searchInput = document.getElementById('search-input');
1085
- const fileRows = document.getElementById('file-rows');
1086
- const noResults = document.getElementById('no-results');
1087
- const fileRowElements = fileRows.querySelectorAll('.file-row');
1088
-
1089
- searchInput.addEventListener('input', function() {
1090
- const searchTerm = this.value.toLowerCase().trim();
1091
- let visibleCount = 0;
1092
-
1093
- fileRowElements.forEach(function(row) {
1094
- const fileName = row.querySelector('.file-name').textContent.toLowerCase();
1095
- if (fileName.includes(searchTerm)) {
1096
- row.classList.remove('hidden');
1097
- visibleCount++;
1098
- } else {
1099
- row.classList.add('hidden');
1100
- }
1101
- });
1102
-
1103
- if (visibleCount === 0) {
1104
- noResults.classList.remove('hidden');
1105
- } else {
1106
- noResults.classList.add('hidden');
1107
- }
1108
- });
1109
- </script>
1110
- </body>
1111
- </html>`;
1112
-
1113
- writeFileSync(join(reportsDir, 'index.html'), indexHtml, 'utf-8');
1114
-
1115
- // Generate individual file detail pages
1116
- for (const [filePath, coverage] of coverageMap.entries()) {
1117
- generateFileDetailPage(filePath, coverage, reportsDir, toRelative);
1118
- }
1119
- }
1120
-
1121
- /**
1122
- * Generate an individual file detail page with line-by-line coverage
1123
- */
1124
- function generateFileDetailPage(
1125
- filePath: string,
1126
- coverage: FileCoverage,
1127
- reportsDir: string,
1128
- toRelative: (path: string) => string
1129
- ): void {
1130
- const fileName = toRelative(filePath);
1131
- const safeFileName = fileName.replace(/[\/\\]/g, '_') + '.html';
1132
- const stats = calculateFileCoverage(coverage);
1133
-
1134
- // Read source file
1135
- let sourceLines: string[] = [];
1136
- try {
1137
- const sourceCode = readFileSync(filePath, 'utf-8').toString();
1138
- sourceLines = sourceCode.split('\n');
1139
- } catch (e) {
1140
- sourceLines = ['// Unable to read source file'];
1141
- }
1142
-
1143
- // Build uncovered lines set for quick lookup
1144
- const uncoveredSet = new Set(coverage.uncoveredLines || []);
1145
-
1146
- const fileHtml = `<!DOCTYPE html>
1147
- <html>
1148
- <head>
1149
- <meta charset="utf-8">
1150
- <title>Coverage: ${fileName}</title>
1151
- <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%236366f1'/%3E%3Cstop offset='100%25' stop-color='%238b5cf6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='100' height='100' rx='20' fill='url(%23grad)'/%3E%3Crect x='28' y='25' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='46' width='32' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='67' width='44' height='8' rx='4' fill='white'/%3E%3Crect x='28' y='25' width='8' height='50' rx='4' fill='white'/%3E%3Ccircle cx='72' cy='50' r='6' fill='white' opacity='0.5'/%3E%3C/svg%3E">
1152
- <style>
1153
- * { margin: 0; padding: 0; box-sizing: border-box; }
1154
- body {
1155
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
1156
- background: #0d1117;
1157
- color: #c9d1d9;
1158
- padding: 20px;
1159
- }
1160
- .container { max-width: 1400px; margin: 0 auto; }
1161
- a { color: #58a6ff; text-decoration: none; }
1162
- a:hover { text-decoration: underline; }
1163
- h1 {
1164
- font-size: 24px;
1165
- font-weight: 600;
1166
- margin-bottom: 10px;
1167
- color: #58a6ff;
1168
- }
1169
- .breadcrumb {
1170
- font-size: 14px;
1171
- color: #8b949e;
1172
- margin-bottom: 20px;
1173
- }
1174
- .breadcrumb a { color: #58a6ff; }
1175
- .summary {
1176
- background: #161b22;
1177
- border: 1px solid #30363d;
1178
- border-radius: 6px;
1179
- padding: 20px;
1180
- margin-bottom: 20px;
1181
- }
1182
- .summary-title {
1183
- font-size: 16px;
1184
- font-weight: 600;
1185
- margin-bottom: 15px;
1186
- color: #c9d1d9;
1187
- }
1188
- .metrics { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; }
1189
- .metric {
1190
- background: #21262d;
1191
- border: 1px solid #30363d;
1192
- border-radius: 6px;
1193
- padding: 15px;
1194
- text-align: center;
1195
- }
1196
- .metric-label { font-size: 12px; color: #8b949e; margin-bottom: 5px; text-transform: uppercase; letter-spacing: 0.5px; }
1197
- .metric-value { font-size: 24px; font-weight: 700; }
1198
- .metric-value.high { color: #3fb950; }
1199
- .metric-value.medium { color: #d29922; }
1200
- .metric-value.low { color: #f85149; }
1201
- .progress-bar {
1202
- height: 8px;
1203
- background: #21262d;
1204
- border-radius: 4px;
1205
- overflow: hidden;
1206
- margin-top: 8px;
1207
- }
1208
- .progress-fill { height: 100%; transition: width 0.3s ease; }
1209
- .progress-fill.high { background: #3fb950; }
1210
- .progress-fill.medium { background: #d29922; }
1211
- .progress-fill.low { background: #f85149; }
1212
- .metric-count { font-size: 11px; color: #8b949e; margin-top: 5px; }
1213
- .code-container {
1214
- background: #161b22;
1215
- border: 1px solid #30363d;
1216
- border-radius: 6px;
1217
- overflow: hidden;
1218
- }
1219
- .code-header {
1220
- padding: 10px 15px;
1221
- background: #21262d;
1222
- border-bottom: 1px solid #30363d;
1223
- font-size: 13px;
1224
- color: #8b949e;
1225
- display: flex;
1226
- justify-content: space-between;
1227
- }
1228
- .legend { display: flex; gap: 15px; font-size: 12px; }
1229
- .legend-item { display: flex; align-items: center; gap: 5px; }
1230
- .legend-box { width: 12px; height: 12px; border-radius: 2px; }
1231
- .legend-box.covered { background: rgba(63, 185, 80, 0.2); border: 1px solid #3fb950; }
1232
- .legend-box.uncovered { background: rgba(248, 81, 73, 0.2); border: 1px solid #f85149; }
1233
- .code-table { width: 100%; border-collapse: collapse; }
1234
- .code-table td { padding: 0; }
1235
- .line-number {
1236
- width: 50px;
1237
- text-align: right;
1238
- padding: 0 15px;
1239
- color: #8b949e;
1240
- font-size: 12px;
1241
- user-select: none;
1242
- border-right: 1px solid #30363d;
1243
- }
1244
- .line-content {
1245
- padding: 0 15px;
1246
- font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
1247
- font-size: 13px;
1248
- line-height: 20px;
1249
- white-space: pre;
1250
- }
1251
- tr.covered .line-content { background: rgba(63, 185, 80, 0.1); }
1252
- tr.uncovered .line-content { background: rgba(248, 81, 73, 0.15); }
1253
- tr.uncovered .line-number { color: #f85149; }
1254
- tr:hover td { background: rgba(88, 166, 255, 0.1); }
1255
- </style>
1256
- </head>
1257
- <body>
1258
- <div class="container">
1259
- <div class="breadcrumb">
1260
- <a href="index.html">← Back to Coverage Report</a>
1261
- </div>
1262
-
1263
- <h1>${fileName}</h1>
1264
-
1265
- <div class="summary">
1266
- <div class="summary-title">Coverage Metrics</div>
1267
- <div class="metrics">
1268
- <div class="metric">
1269
- <div class="metric-label">Statements</div>
1270
- <div class="metric-value ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}">${stats.statements.percentage.toFixed(2)}%</div>
1271
- <div class="progress-bar">
1272
- <div class="progress-fill ${stats.statements.percentage >= 80 ? 'high' : stats.statements.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.statements.percentage}%"></div>
1273
- </div>
1274
- <div class="metric-count">${coverage.coveredStatements}/${coverage.statements}</div>
1275
- </div>
1276
- <div class="metric">
1277
- <div class="metric-label">Branches</div>
1278
- <div class="metric-value ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}">${stats.branches.percentage.toFixed(2)}%</div>
1279
- <div class="progress-bar">
1280
- <div class="progress-fill ${stats.branches.percentage >= 80 ? 'high' : stats.branches.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.branches.percentage}%"></div>
1281
- </div>
1282
- <div class="metric-count">${coverage.coveredBranches}/${coverage.branches}</div>
1283
- </div>
1284
- <div class="metric">
1285
- <div class="metric-label">Functions</div>
1286
- <div class="metric-value ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}">${stats.functions.percentage.toFixed(2)}%</div>
1287
- <div class="progress-bar">
1288
- <div class="progress-fill ${stats.functions.percentage >= 80 ? 'high' : stats.functions.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.functions.percentage}%"></div>
1289
- </div>
1290
- <div class="metric-count">${coverage.coveredFunctions}/${coverage.functions}</div>
1291
- </div>
1292
- <div class="metric">
1293
- <div class="metric-label">Lines</div>
1294
- <div class="metric-value ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}">${stats.lines.percentage.toFixed(2)}%</div>
1295
- <div class="progress-bar">
1296
- <div class="progress-fill ${stats.lines.percentage >= 80 ? 'high' : stats.lines.percentage >= 50 ? 'medium' : 'low'}" style="width: ${stats.lines.percentage}%"></div>
1297
- </div>
1298
- <div class="metric-count">${coverage.coveredLines}/${coverage.lines}</div>
1299
- </div>
1300
- </div>
1301
- </div>
1302
-
1303
- ${coverage.uncoveredLines && coverage.uncoveredLines.length > 0 ? `
1304
- <div class="summary">
1305
- <div class="summary-title">Uncovered Lines</div>
1306
- <div style="font-size: 13px; color: #f85149;">${formatUncoveredLines(coverage.uncoveredLines)}</div>
1307
- </div>
1308
- ` : ''}
1309
-
1310
- <div class="code-container">
1311
- <div class="code-header">
1312
- <span>Source Code</span>
1313
- <div class="legend">
1314
- <div class="legend-item"><div class="legend-box covered"></div><span>Covered</span></div>
1315
- <div class="legend-item"><div class="legend-box uncovered"></div><span>Uncovered</span></div>
1316
- </div>
1317
- </div>
1318
- <table class="code-table">
1319
- ${sourceLines.map((line, index) => {
1320
- const lineNum = index + 1;
1321
- const isUncovered = uncoveredSet.has(lineNum);
1322
- const isExecutable = coverage.lines > 0; // Has executable lines
1323
- const rowClass = isExecutable ? (isUncovered ? 'uncovered' : 'covered') : '';
1324
- return `
1325
- <tr class="${rowClass}">
1326
- <td class="line-number">${lineNum}</td>
1327
- <td class="line-content">${escapeHtml(line) || ' '}</td>
1328
- </tr>
1329
- `;
1330
- }).join('')}
1331
- </table>
1332
- </div>
1333
- </div>
1334
- </body>
1335
- </html>`;
1336
-
1337
- writeFileSync(join(reportsDir, safeFileName), fileHtml, 'utf-8');
1338
- }
1339
-
1340
- /**
1341
- * Escape HTML special characters
1342
- */
1343
- function escapeHtml(text: string): string {
1344
- return text
1345
- .replace(/&/g, '&amp;')
1346
- .replace(/</g, '&lt;')
1347
- .replace(/>/g, '&gt;')
1348
- .replace(/"/g, '&quot;')
1349
- .replace(/'/g, '&#039;');
1350
- }
1351
-
1352
- /**
1353
- * Escape XML special characters
1354
- */
1355
- function escapeXml(text: string): string {
1356
- return text
1357
- .replace(/&/g, '&amp;')
1358
- .replace(/</g, '&lt;')
1359
- .replace(/>/g, '&gt;')
1360
- .replace(/"/g, '&quot;')
1361
- .replace(/'/g, '&apos;');
1362
- }
1363
-
1364
- /**
1365
- * Generate coverage-final.json (Code Climate/Codecov format)
1366
- */
1367
- export function generateCoverageFinalJson(
1368
- coverageMap: Map<string, FileCoverage>,
1369
- reportsDir: string
1370
- ): void {
1371
- const coverageData: Record<string, any> = {};
1372
-
1373
- for (const [filePath, coverage] of coverageMap.entries()) {
1374
- const relativePath = relative(process.cwd(), filePath).replace(/\\/g, '/');
1375
-
1376
- // Build line coverage map
1377
- const lineMap: Record<number, number> = {};
1378
- const executableLines = getExecutableLines(filePath);
1379
-
1380
- for (const line of executableLines) {
1381
- // If file is covered, mark all lines as covered (1), otherwise not covered (0)
1382
- const isCovered = coverage.coveredStatements > 0;
1383
- lineMap[line] = isCovered ? 1 : 0;
1384
- }
1385
-
1386
- coverageData[relativePath] = {
1387
- lines: lineMap,
1388
- };
1389
- }
1390
-
1391
- const jsonData = JSON.stringify(coverageData, null, 2);
1392
- writeFileSync(join(reportsDir, 'coverage-final.json'), jsonData, 'utf-8');
1393
- }
1394
-
1395
- /**
1396
- * Generate clover.xml report
1397
- */
1398
- export function generateCloverXml(
1399
- coverageMap: Map<string, FileCoverage>,
1400
- reportsDir: string
1401
- ): void {
1402
- const timestamp = Date.now();
1403
-
1404
- // Calculate totals
1405
- let totalFiles = 0;
1406
- let totalClasses = 0;
1407
- let totalElements = 0; // statements + branches + functions
1408
- let coveredElements = 0;
1409
- let totalStatements = 0;
1410
- let coveredStatements = 0;
1411
- let totalBranches = 0;
1412
- let coveredBranches = 0;
1413
- let totalFunctions = 0;
1414
- let coveredFunctions = 0;
1415
- let totalLines = 0;
1416
- let coveredLines = 0;
1417
-
1418
- const fileEntries: string[] = [];
1419
-
1420
- for (const [filePath, coverage] of coverageMap.entries()) {
1421
- const relativePath = relative(process.cwd(), filePath).replace(/\\/g, '/');
1422
-
1423
- totalFiles++;
1424
- totalClasses++; // Assume one class per file
1425
- totalStatements += coverage.statements;
1426
- coveredStatements += coverage.coveredStatements;
1427
- totalBranches += coverage.branches;
1428
- coveredBranches += coverage.coveredBranches;
1429
- totalFunctions += coverage.functions;
1430
- coveredFunctions += coverage.coveredFunctions;
1431
- totalLines += coverage.lines;
1432
- coveredLines += coverage.coveredLines;
1433
-
1434
- const fileElements = coverage.statements + coverage.branches + coverage.functions;
1435
- const fileCoveredElements = coverage.coveredStatements + coverage.coveredBranches + coverage.coveredFunctions;
1436
-
1437
- totalElements += fileElements;
1438
- coveredElements += fileCoveredElements;
1439
-
1440
- const escapedPath = escapeXml(relativePath);
1441
-
1442
- fileEntries.push(`
1443
- <file name="${escapedPath}">
1444
- <class name="${escapedPath}">
1445
- <metrics complexity="0" elements="${fileElements}" coveredelements="${fileCoveredElements}"
1446
- methods="${coverage.functions}" coveredmethods="${coverage.coveredFunctions}"
1447
- statements="${coverage.statements}" coveredstatements="${coverage.coveredStatements}" />
1448
- </class>
1449
- <line num="1" type="stmt" count="${coverage.coveredStatements > 0 ? 1 : 0}" />
1450
- </file>`);
1451
- }
1452
-
1453
- const complexity = 0; // We don't track cyclomatic complexity
1454
- const elements = totalElements;
1455
-
1456
- const xml = `<?xml version="1.0" encoding="UTF-8"?>
1457
- <coverage generated="${timestamp}" clover="3.2.0">
1458
- <project timestamp="${timestamp}" name="Coverage">
1459
- <metrics complexity="${complexity}" elements="${elements}" coveredelements="${coveredElements}"
1460
- conditionals="${totalBranches}" coveredconditionals="${coveredBranches}"
1461
- statements="${totalStatements}" coveredstatements="${coveredStatements}"
1462
- methods="${totalFunctions}" coveredmethods="${coveredFunctions}"
1463
- classes="${totalClasses}" coveredclasses="${coverageMap.size > 0 ? Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length : 0}"
1464
- files="${totalFiles}" loc="${totalLines}" ncloc="${totalLines - coveredLines}"
1465
- packages="${totalFiles}" classes="${totalClasses}" />
1466
- <package name="root">
1467
- <metrics complexity="${complexity}" elements="${elements}" coveredelements="${coveredElements}"
1468
- conditionals="${totalBranches}" coveredconditionals="${coveredBranches}"
1469
- statements="${totalStatements}" coveredstatements="${coveredStatements}"
1470
- methods="${totalFunctions}" coveredmethods="${coveredFunctions}"
1471
- classes="${totalClasses}" coveredclasses="${coverageMap.size > 0 ? Array.from(coverageMap.values()).filter(c => c.coveredStatements > 0).length : 0}"
1472
- files="${totalFiles}" loc="${totalLines}" ncloc="${totalLines - coveredLines}" />
1473
- ${fileEntries.join('')}
1474
- </package>
1475
- </project>
1476
- </coverage>`;
1477
-
1478
- writeFileSync(join(reportsDir, 'clover.xml'), xml, 'utf-8');
1479
- }