ts-analyzer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,431 @@
1
+ // src/code-complexity.ts
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import * as espree from 'espree';
5
+ import * as estraverse from 'estraverse';
6
+ import * as ts from 'typescript';
7
+ import { Node as EstreeNode } from 'estree';
8
+
9
+ // Define our own simplified Node interface
10
+ interface CustomNode {
11
+ type: string;
12
+ body?: any;
13
+ loc?: { start: { line: number }, end: { line: number } };
14
+ params?: any[];
15
+ id?: { name: string };
16
+ operator?: string;
17
+ }
18
+
19
+ interface SimplifiedProgram {
20
+ type: string;
21
+ body: any[];
22
+ }
23
+
24
+ interface FunctionData {
25
+ type: string;
26
+ name: string;
27
+ complexity: number;
28
+ nestingDepth: number;
29
+ paramCount: number;
30
+ lineCount: number;
31
+ }
32
+
33
+ export interface FileComplexityMetrics {
34
+ avgComplexity: string;
35
+ maxComplexity: number;
36
+ avgNestingDepth: string;
37
+ maxNestingDepth: number;
38
+ avgFunctionSize: string;
39
+ avgParams: string;
40
+ functionCount: number;
41
+ complexityScore: number;
42
+ complexityRating: string;
43
+ }
44
+
45
+ export interface CodeComplexityMetrics {
46
+ analyzedFiles: number;
47
+ avgComplexity: string;
48
+ maxComplexity: number;
49
+ avgNestingDepth: string;
50
+ maxNestingDepth: number;
51
+ avgFunctionSize: string;
52
+ totalFunctions: number;
53
+ complexFiles: number;
54
+ overallComplexity: string;
55
+ }
56
+
57
+ // Calculate cyclomatic complexity
58
+ function calculateCyclomaticComplexity(ast: any): number {
59
+ let complexity = 1; // Start with 1 (one path through the function)
60
+
61
+ estraverse.traverse(ast, {
62
+ enter(node: any) {
63
+ // Count conditional statements and operators that create new paths
64
+ switch (node.type) {
65
+ case 'IfStatement':
66
+ case 'ConditionalExpression':
67
+ case 'SwitchCase':
68
+ case 'ForStatement':
69
+ case 'ForInStatement':
70
+ case 'ForOfStatement':
71
+ case 'WhileStatement':
72
+ case 'DoWhileStatement':
73
+ complexity++;
74
+ break;
75
+ case 'LogicalExpression':
76
+ if (node.operator === '&&' || node.operator === '||') {
77
+ complexity++;
78
+ }
79
+ break;
80
+ }
81
+ }
82
+ });
83
+
84
+ return complexity;
85
+ }
86
+
87
+ // Calculate nesting depth
88
+ function calculateNestingDepth(ast: any): number {
89
+ let maxDepth = 0;
90
+ let currentDepth = 0;
91
+
92
+ estraverse.traverse(ast, {
93
+ enter(node: any) {
94
+ // Count nesting for blocks and control structures
95
+ switch (node.type) {
96
+ case 'BlockStatement':
97
+ case 'IfStatement':
98
+ case 'SwitchStatement':
99
+ case 'ForStatement':
100
+ case 'ForInStatement':
101
+ case 'ForOfStatement':
102
+ case 'WhileStatement':
103
+ case 'DoWhileStatement':
104
+ case 'TryStatement':
105
+ currentDepth++;
106
+ maxDepth = Math.max(maxDepth, currentDepth);
107
+ break;
108
+ }
109
+ },
110
+ leave(node: any) {
111
+ // Decrement depth when leaving a block
112
+ switch (node.type) {
113
+ case 'BlockStatement':
114
+ case 'IfStatement':
115
+ case 'SwitchStatement':
116
+ case 'ForStatement':
117
+ case 'ForInStatement':
118
+ case 'ForOfStatement':
119
+ case 'WhileStatement':
120
+ case 'DoWhileStatement':
121
+ case 'TryStatement':
122
+ currentDepth--;
123
+ break;
124
+ }
125
+ }
126
+ });
127
+
128
+ return maxDepth;
129
+ }
130
+
131
+ // Find function declarations and calculate their metrics
132
+ function analyzeFunctions(ast: any): FunctionData[] {
133
+ const functions: FunctionData[] = [];
134
+
135
+ estraverse.traverse(ast, {
136
+ enter(node: any) {
137
+ if (node.type === 'FunctionDeclaration' ||
138
+ node.type === 'FunctionExpression' ||
139
+ node.type === 'ArrowFunctionExpression') {
140
+
141
+ // Skip functions without a body
142
+ if (!node.body) return;
143
+
144
+ // Get the function's source code
145
+ let functionAst = node;
146
+ if (node.type === 'ArrowFunctionExpression' && node.body.type !== 'BlockStatement') {
147
+ functionAst = {
148
+ type: 'ArrowFunctionExpression',
149
+ body: {
150
+ type: 'BlockStatement',
151
+ body: [{ type: 'ReturnStatement', argument: node.body }]
152
+ }
153
+ };
154
+ }
155
+
156
+ // Calculate complexity
157
+ const complexity = calculateCyclomaticComplexity(functionAst);
158
+
159
+ // Calculate nesting
160
+ const nestingDepth = calculateNestingDepth(functionAst);
161
+
162
+ // Count function parameters
163
+ const paramCount = node.params ? node.params.length : 0;
164
+
165
+ // Estimate function size (lines)
166
+ let lineCount = 0;
167
+ if (node.loc) {
168
+ lineCount = node.loc.end.line - node.loc.start.line + 1;
169
+ }
170
+
171
+ functions.push({
172
+ type: node.type,
173
+ name: node.id ? node.id.name : 'anonymous',
174
+ complexity,
175
+ nestingDepth,
176
+ paramCount,
177
+ lineCount
178
+ });
179
+ }
180
+ }
181
+ });
182
+
183
+ return functions;
184
+ }
185
+
186
+ // Parse a TypeScript file using the TypeScript compiler
187
+ function parseTypeScriptFile(content: string): SimplifiedProgram {
188
+ try {
189
+ // Create a source file
190
+ const sourceFile = ts.createSourceFile(
191
+ 'temp.ts',
192
+ content,
193
+ ts.ScriptTarget.Latest,
194
+ true
195
+ );
196
+
197
+ // Create a simplified program AST structure
198
+ const simplifiedAst: SimplifiedProgram = {
199
+ type: 'Program',
200
+ body: []
201
+ };
202
+
203
+ // Function to visit TypeScript nodes and extract function information
204
+ function visit(node: ts.Node) {
205
+ if (ts.isFunctionDeclaration(node) ||
206
+ ts.isMethodDeclaration(node) ||
207
+ ts.isArrowFunction(node) ||
208
+ ts.isFunctionExpression(node)) {
209
+
210
+ const startLine = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
211
+ const endLine = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line + 1;
212
+
213
+ const functionNode = {
214
+ type: ts.isFunctionDeclaration(node) ? 'FunctionDeclaration' :
215
+ ts.isArrowFunction(node) ? 'ArrowFunctionExpression' : 'FunctionExpression',
216
+ id: ts.isFunctionDeclaration(node) && node.name ? { name: node.name.text } : undefined,
217
+ params: node.parameters ? node.parameters.map(() => ({})) : [],
218
+ body: { type: 'BlockStatement', body: [] as any[] },
219
+ loc: {
220
+ start: { line: startLine },
221
+ end: { line: endLine }
222
+ }
223
+ };
224
+
225
+ simplifiedAst.body.push(functionNode);
226
+ }
227
+
228
+ ts.forEachChild(node, visit);
229
+ }
230
+
231
+ visit(sourceFile);
232
+
233
+ return simplifiedAst;
234
+ } catch (error) {
235
+ console.error('Error parsing TypeScript file:', error);
236
+ return { type: 'Program', body: [] };
237
+ }
238
+ }
239
+
240
+ export async function analyzeFileComplexity(filePath: string): Promise<FileComplexityMetrics> {
241
+ try {
242
+ const content = await fs.readFile(filePath, 'utf8');
243
+ const extension = path.extname(filePath);
244
+
245
+ // Choose the appropriate parser based on file extension
246
+ let ast: any;
247
+
248
+ if (extension === '.ts' || extension === '.tsx') {
249
+ // Use TypeScript parser for .ts and .tsx files
250
+ ast = parseTypeScriptFile(content);
251
+ } else {
252
+ // Use espree for JavaScript files
253
+ try {
254
+ const parserOptions = {
255
+ ecmaVersion: 2022,
256
+ sourceType: 'module',
257
+ ecmaFeatures: {
258
+ jsx: extension === '.jsx'
259
+ }
260
+ };
261
+
262
+ ast = espree.parse(content, parserOptions as any);
263
+ } catch (parseError) {
264
+ console.error(`Parsing error in ${filePath}: ${(parseError as Error).message}`);
265
+ return {
266
+ avgComplexity: '0',
267
+ maxComplexity: 0,
268
+ avgNestingDepth: '0',
269
+ maxNestingDepth: 0,
270
+ avgFunctionSize: '0',
271
+ avgParams: '0',
272
+ functionCount: 0,
273
+ complexityScore: 0,
274
+ complexityRating: 'N/A'
275
+ };
276
+ }
277
+ }
278
+
279
+ // Analyze the functions in the file
280
+ const functions = analyzeFunctions(ast);
281
+
282
+ if (functions.length === 0) {
283
+ return {
284
+ avgComplexity: '0',
285
+ maxComplexity: 0,
286
+ avgNestingDepth: '0',
287
+ maxNestingDepth: 0,
288
+ avgFunctionSize: '0',
289
+ avgParams: '0',
290
+ functionCount: 0,
291
+ complexityScore: 0,
292
+ complexityRating: 'N/A'
293
+ };
294
+ }
295
+
296
+ // Calculate average and maximum values
297
+ const totalComplexity = functions.reduce((sum, fn) => sum + fn.complexity, 0);
298
+ const maxComplexity = Math.max(...functions.map(fn => fn.complexity));
299
+
300
+ const totalNestingDepth = functions.reduce((sum, fn) => sum + fn.nestingDepth, 0);
301
+ const maxNestingDepth = Math.max(...functions.map(fn => fn.nestingDepth));
302
+
303
+ const totalLineCount = functions.reduce((sum, fn) => sum + fn.lineCount, 0);
304
+ const totalParams = functions.reduce((sum, fn) => sum + fn.paramCount, 0);
305
+
306
+ const avgComplexity = totalComplexity / functions.length;
307
+ const avgNestingDepth = totalNestingDepth / functions.length;
308
+ const avgFunctionSize = totalLineCount / functions.length;
309
+ const avgParams = totalParams / functions.length;
310
+
311
+ // Calculate a complexity score (0-100, higher means more complex)
312
+ const complexityWeight = 0.4;
313
+ const nestingWeight = 0.3;
314
+ const sizeWeight = 0.2;
315
+ const paramsWeight = 0.1;
316
+
317
+ // Normalized scores (0-100)
318
+ const complexityScore = Math.min((avgComplexity / 10) * 100, 100);
319
+ const nestingScore = Math.min((avgNestingDepth / 5) * 100, 100);
320
+ const sizeScore = Math.min((avgFunctionSize / 30) * 100, 100);
321
+ const paramsScore = Math.min((avgParams / 6) * 100, 100);
322
+
323
+ const totalScore = (complexityScore * complexityWeight) +
324
+ (nestingScore * nestingWeight) +
325
+ (sizeScore * sizeWeight) +
326
+ (paramsScore * paramsWeight);
327
+
328
+ // Define complexity rating
329
+ let complexityRating: string;
330
+ if (totalScore < 30) complexityRating = 'Low';
331
+ else if (totalScore < 60) complexityRating = 'Medium';
332
+ else complexityRating = 'High';
333
+
334
+ return {
335
+ avgComplexity: avgComplexity.toFixed(1),
336
+ maxComplexity,
337
+ avgNestingDepth: avgNestingDepth.toFixed(1),
338
+ maxNestingDepth,
339
+ avgFunctionSize: avgFunctionSize.toFixed(1),
340
+ avgParams: avgParams.toFixed(1),
341
+ functionCount: functions.length,
342
+ complexityScore: Math.round(totalScore),
343
+ complexityRating
344
+ };
345
+ } catch (error) {
346
+ console.error(`Error analyzing file complexity for ${filePath}:`, error);
347
+ return {
348
+ avgComplexity: '0',
349
+ maxComplexity: 0,
350
+ avgNestingDepth: '0',
351
+ maxNestingDepth: 0,
352
+ avgFunctionSize: '0',
353
+ avgParams: '0',
354
+ functionCount: 0,
355
+ complexityScore: 0,
356
+ complexityRating: 'N/A'
357
+ };
358
+ }
359
+ }
360
+
361
+ export async function calculateProjectComplexity(projectPath: string, fileList: string[]): Promise<CodeComplexityMetrics> {
362
+ const jsExtensions = ['.js', '.jsx', '.ts', '.tsx'];
363
+ const jsFiles = fileList.filter(file => jsExtensions.includes(path.extname(file)));
364
+
365
+ if (jsFiles.length === 0) {
366
+ return {
367
+ analyzedFiles: 0,
368
+ avgComplexity: '0.0',
369
+ maxComplexity: 0,
370
+ avgNestingDepth: '0.0',
371
+ maxNestingDepth: 0,
372
+ avgFunctionSize: '0.0',
373
+ totalFunctions: 0,
374
+ complexFiles: 0,
375
+ overallComplexity: 'N/A'
376
+ };
377
+ }
378
+
379
+ let totalComplexity = 0;
380
+ let totalNestingDepth = 0;
381
+ let totalFunctionSize = 0;
382
+ let maxComplexityGlobal = 0;
383
+ let maxNestingDepthGlobal = 0;
384
+ let totalFunctions = 0;
385
+ let complexFiles = 0;
386
+
387
+ for (const file of jsFiles) {
388
+ const filePath = path.join(projectPath, file);
389
+ const metrics = await analyzeFileComplexity(filePath);
390
+
391
+ totalComplexity += parseFloat(metrics.avgComplexity) * metrics.functionCount;
392
+ totalNestingDepth += parseFloat(metrics.avgNestingDepth) * metrics.functionCount;
393
+ totalFunctionSize += parseFloat(metrics.avgFunctionSize) * metrics.functionCount;
394
+ maxComplexityGlobal = Math.max(maxComplexityGlobal, metrics.maxComplexity);
395
+ maxNestingDepthGlobal = Math.max(maxNestingDepthGlobal, metrics.maxNestingDepth);
396
+ totalFunctions += metrics.functionCount;
397
+
398
+ if (metrics.complexityRating === 'High') {
399
+ complexFiles++;
400
+ }
401
+ }
402
+
403
+ // Calculate project averages
404
+ const avgComplexity = totalFunctions > 0 ? (totalComplexity / totalFunctions).toFixed(1) : '0.0';
405
+ const avgNestingDepth = totalFunctions > 0 ? (totalNestingDepth / totalFunctions).toFixed(1) : '0.0';
406
+ const avgFunctionSize = totalFunctions > 0 ? (totalFunctionSize / totalFunctions).toFixed(1) : '0.0';
407
+
408
+ // Determine overall complexity
409
+ let overallComplexity: string;
410
+ const complexFilesPercentage = (complexFiles / jsFiles.length) * 100;
411
+
412
+ if (parseFloat(avgComplexity) < 4 && complexFilesPercentage < 10) {
413
+ overallComplexity = 'Low';
414
+ } else if (parseFloat(avgComplexity) < 8 && complexFilesPercentage < 25) {
415
+ overallComplexity = 'Medium';
416
+ } else {
417
+ overallComplexity = 'High';
418
+ }
419
+
420
+ return {
421
+ analyzedFiles: jsFiles.length,
422
+ avgComplexity,
423
+ maxComplexity: maxComplexityGlobal,
424
+ avgNestingDepth,
425
+ maxNestingDepth: maxNestingDepthGlobal,
426
+ avgFunctionSize,
427
+ totalFunctions,
428
+ complexFiles,
429
+ overallComplexity
430
+ };
431
+ }
package/src/index.ts ADDED
@@ -0,0 +1,180 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import { calculateProjectTypeSafety, TypeScriptSafetyMetrics } from './typescript-safety.js';
5
+ import { calculateProjectComplexity, CodeComplexityMetrics } from './code-complexity.js';
6
+
7
+ export const REACT_EXTENSIONS = [
8
+ '.js', '.jsx', '.ts', '.tsx',
9
+ '.css', '.scss', '.sass',
10
+ '.html', '.json'
11
+ ];
12
+
13
+ export const DEFAULT_IGNORE = [
14
+ 'node_modules',
15
+ 'build',
16
+ 'dist',
17
+ '.git',
18
+ 'coverage',
19
+ '.next',
20
+ 'out'
21
+ ];
22
+
23
+ export interface LineCount {
24
+ total: number;
25
+ nonEmpty: number;
26
+ code: number;
27
+ comments: number;
28
+ empty: number;
29
+ }
30
+
31
+ export interface FileTypeData {
32
+ files: number;
33
+ totalLines: number;
34
+ codeLines: number;
35
+ commentLines: number;
36
+ emptyLines: number;
37
+ }
38
+
39
+ export interface FileTypeFormatted {
40
+ 'Extension': string;
41
+ 'Files': string;
42
+ 'Total Lines': string;
43
+ 'Code Lines': string;
44
+ 'Comment Lines': string;
45
+ 'Empty Lines': string;
46
+ '% of Codebase': string;
47
+ }
48
+
49
+ export interface ProjectOptions {
50
+ excludePatterns?: string[];
51
+ additionalExtensions?: string[];
52
+ analyzeSafety?: boolean;
53
+ analyzeComplexity?: boolean;
54
+ }
55
+
56
+ export interface ProjectStats {
57
+ files: number;
58
+ totalLines: number;
59
+ codeLines: number;
60
+ commentLines: number;
61
+ emptyLines: number;
62
+ fileTypes: Record<string, FileTypeData>;
63
+ formattedFileTypes?: FileTypeFormatted[];
64
+ typescriptSafety: TypeScriptSafetyMetrics | null;
65
+ codeComplexity: CodeComplexityMetrics | null;
66
+ formatNumber: (num: number) => string;
67
+ }
68
+
69
+ export async function countLines(filePath: string): Promise<LineCount> {
70
+ try {
71
+ const content = await fs.readFile(filePath, 'utf8');
72
+ const lines: string[] = content.split('\n');
73
+ const nonEmptyLines = lines.filter(line => line.trim().length > 0);
74
+ const nonCommentLines = nonEmptyLines.filter(line => {
75
+ const trimmed = line.trim();
76
+ return !trimmed.startsWith('//') && !trimmed.startsWith('/*') && !trimmed.startsWith('*');
77
+ });
78
+
79
+ return {
80
+ total: lines.length,
81
+ nonEmpty: nonEmptyLines.length,
82
+ code: nonCommentLines.length,
83
+ comments: nonEmptyLines.length - nonCommentLines.length,
84
+ empty: lines.length - nonEmptyLines.length
85
+ };
86
+ } catch (error) {
87
+ console.error(`Error reading file ${filePath}:`, error);
88
+ return { total: 0, nonEmpty: 0, code: 0, comments: 0, empty: 0 };
89
+ }
90
+ }
91
+
92
+ export async function analyzeProject(projectPath = '.', options: ProjectOptions = {}): Promise<ProjectStats> {
93
+ const stats: ProjectStats = {
94
+ files: 0,
95
+ totalLines: 0,
96
+ codeLines: 0,
97
+ commentLines: 0,
98
+ emptyLines: 0,
99
+ fileTypes: {},
100
+ typescriptSafety: null,
101
+ codeComplexity: null,
102
+ formatNumber: (num: number) => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
103
+ };
104
+
105
+ const extensions = [...REACT_EXTENSIONS, ...(options.additionalExtensions || [])];
106
+ const ignorePatterns = [...DEFAULT_IGNORE, ...(options.excludePatterns || [])];
107
+
108
+ const allFilePaths: string[] = [];
109
+
110
+ async function traverse(currentPath: string): Promise<void> {
111
+ const files = await fs.readdir(currentPath);
112
+
113
+ for (const file of files) {
114
+ const fullPath = path.join(currentPath, file);
115
+ const stat = await fs.stat(fullPath);
116
+
117
+ if (stat.isDirectory()) {
118
+ if (!ignorePatterns.includes(file)) {
119
+ await traverse(fullPath);
120
+ }
121
+ continue;
122
+ }
123
+
124
+ const ext = path.extname(file);
125
+ if (!extensions.includes(ext)) continue;
126
+
127
+ stats.files++;
128
+ const relativePath = path.relative(projectPath, fullPath);
129
+ allFilePaths.push(relativePath);
130
+
131
+ if (!stats.fileTypes[ext]) {
132
+ stats.fileTypes[ext] = {
133
+ files: 0,
134
+ totalLines: 0,
135
+ codeLines: 0,
136
+ commentLines: 0,
137
+ emptyLines: 0
138
+ };
139
+ }
140
+
141
+ const { total, code, comments, empty } = await countLines(fullPath);
142
+
143
+ stats.totalLines += total;
144
+ stats.codeLines += code;
145
+ stats.commentLines += comments;
146
+ stats.emptyLines += empty;
147
+
148
+ stats.fileTypes[ext].files++;
149
+ stats.fileTypes[ext].totalLines += total;
150
+ stats.fileTypes[ext].codeLines += code;
151
+ stats.fileTypes[ext].commentLines += comments;
152
+ stats.fileTypes[ext].emptyLines += empty;
153
+ }
154
+ }
155
+
156
+ await traverse(projectPath);
157
+
158
+ stats.formattedFileTypes = Object.entries(stats.fileTypes)
159
+ .sort(([, a], [, b]) => b.files - a.files)
160
+ .map(([ext, data]) => ({
161
+ 'Extension': ext,
162
+ 'Files': stats.formatNumber(data.files),
163
+ 'Total Lines': stats.formatNumber(data.totalLines),
164
+ 'Code Lines': stats.formatNumber(data.codeLines),
165
+ 'Comment Lines': stats.formatNumber(data.commentLines),
166
+ 'Empty Lines': stats.formatNumber(data.emptyLines),
167
+ '% of Codebase': `${((data.codeLines / stats.codeLines) * 100).toFixed(1)}%`
168
+ }));
169
+
170
+ // Now analyze TypeScript safety and code complexity if requested
171
+ if (options.analyzeSafety !== false) {
172
+ stats.typescriptSafety = await calculateProjectTypeSafety(projectPath, allFilePaths);
173
+ }
174
+
175
+ if (options.analyzeComplexity !== false) {
176
+ stats.codeComplexity = await calculateProjectComplexity(projectPath, allFilePaths);
177
+ }
178
+
179
+ return stats;
180
+ }
@@ -0,0 +1,94 @@
1
+ // src/table-formatter.ts
2
+ /**
3
+ * Simple table formatter that handles ANSI color codes correctly
4
+ */
5
+ export function formatTable(data: Array<Record<string, any>>, options?: { title?: string }): void {
6
+ if (!data || data.length === 0) {
7
+ console.log('No data to display');
8
+ return;
9
+ }
10
+
11
+ // Print the title if provided
12
+ if (options?.title) {
13
+ console.log(`\n${options.title}`);
14
+ }
15
+
16
+ // Extract all unique keys from the data
17
+ const allKeys = new Set<string>();
18
+ data.forEach(item => {
19
+ Object.keys(item).forEach(key => allKeys.add(key));
20
+ });
21
+ const headers = Array.from(allKeys);
22
+
23
+ // Function to get visible string length (excluding ANSI color codes)
24
+ const getVisibleLength = (str: string): number => {
25
+ // Remove ANSI color codes for width calculation
26
+ return str.replace(/\x1B\[\d+m/g, '').length;
27
+ };
28
+
29
+ // Calculate the width of each column
30
+ const colWidths: Record<string, number> = {};
31
+ headers.forEach(header => {
32
+ // Start with the header width
33
+ colWidths[header] = header.length;
34
+
35
+ // Check each row for this column's value and update width if needed
36
+ data.forEach(row => {
37
+ const value = row[header] !== undefined ? String(row[header]) : '';
38
+ colWidths[header] = Math.max(colWidths[header], getVisibleLength(value));
39
+ });
40
+ });
41
+
42
+ // Function to pad a string to desired visible length, accounting for ANSI codes
43
+ const padString = (str: string, length: number, padChar = ' '): string => {
44
+ const visibleLength = getVisibleLength(str);
45
+ const paddingNeeded = Math.max(0, length - visibleLength);
46
+ return str + padChar.repeat(paddingNeeded);
47
+ };
48
+
49
+ // Create the header row
50
+ let headerRow = '│ ';
51
+ headers.forEach((header, index) => {
52
+ const isLast = index === headers.length - 1;
53
+ headerRow += padString(header, colWidths[header]) + (isLast ? ' │' : ' │ ');
54
+ });
55
+
56
+ // Create a separator row
57
+ let separator = '├─';
58
+ headers.forEach((header, index) => {
59
+ const isLast = index === headers.length - 1;
60
+ separator += '─'.repeat(colWidths[header]) + (isLast ? '─┤' : '─┼─');
61
+ });
62
+
63
+ // Create the top border
64
+ let topBorder = '┌─';
65
+ headers.forEach((header, index) => {
66
+ const isLast = index === headers.length - 1;
67
+ topBorder += '─'.repeat(colWidths[header]) + (isLast ? '─┐' : '─┬─');
68
+ });
69
+
70
+ // Create the bottom border
71
+ let bottomBorder = '└─';
72
+ headers.forEach((header, index) => {
73
+ const isLast = index === headers.length - 1;
74
+ bottomBorder += '─'.repeat(colWidths[header]) + (isLast ? '─┘' : '─┴─');
75
+ });
76
+
77
+ // Print the table
78
+ console.log(topBorder);
79
+ console.log(headerRow);
80
+ console.log(separator);
81
+
82
+ // Print each data row
83
+ data.forEach(row => {
84
+ let dataRow = '│ ';
85
+ headers.forEach((header, index) => {
86
+ const isLast = index === headers.length - 1;
87
+ const value = row[header] !== undefined ? String(row[header]) : '';
88
+ dataRow += padString(value, colWidths[header]) + (isLast ? ' │' : ' │ ');
89
+ });
90
+ console.log(dataRow);
91
+ });
92
+
93
+ console.log(bottomBorder);
94
+ }