@crashbytes/dendro 1.1.0 → 1.1.1
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/.github/workflows/test.yml +39 -0
- package/package.json +12 -4
- package/test/index.test.js +387 -0
- package/vitest.config.js +26 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
node-version: [20.x, 22.x]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Setup Node.js ${{ matrix.node-version }}
|
|
21
|
+
uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: ${{ matrix.node-version }}
|
|
24
|
+
cache: 'npm'
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Run tests with coverage
|
|
30
|
+
run: npm run test:coverage
|
|
31
|
+
|
|
32
|
+
- name: Upload coverage to Codecov
|
|
33
|
+
if: matrix.node-version == '20.x'
|
|
34
|
+
uses: codecov/codecov-action@v4
|
|
35
|
+
with:
|
|
36
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
37
|
+
files: ./coverage/lcov.info
|
|
38
|
+
flags: unittests
|
|
39
|
+
fail_ci_if_error: false
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crashbytes/dendro",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A beautiful directory tree visualization CLI with file type icons - dendro (δένδρο) means 'tree' in Greek",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"dendro": "bin/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"test:coverage": "vitest run --coverage",
|
|
14
|
+
"test:ui": "vitest --ui"
|
|
12
15
|
},
|
|
13
16
|
"keywords": [
|
|
14
17
|
"directory",
|
|
@@ -29,10 +32,15 @@
|
|
|
29
32
|
},
|
|
30
33
|
"homepage": "https://github.com/CrashBytes/dendro#readme",
|
|
31
34
|
"dependencies": {
|
|
32
|
-
"
|
|
33
|
-
"
|
|
35
|
+
"chalk": "^5.6.2",
|
|
36
|
+
"commander": "^14.0.3"
|
|
34
37
|
},
|
|
35
38
|
"engines": {
|
|
36
39
|
"node": ">=20.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
43
|
+
"@vitest/ui": "^4.0.18",
|
|
44
|
+
"vitest": "^4.0.18"
|
|
37
45
|
}
|
|
38
46
|
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { buildTree, renderTree, getTreeStats, getIcon, icons } from '../index.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
|
|
7
|
+
describe('getIcon', () => {
|
|
8
|
+
it('should return directory icon for directories', () => {
|
|
9
|
+
expect(getIcon('my-folder', true)).toBe(icons.directory);
|
|
10
|
+
expect(getIcon('src', true)).toBe(icons.directory);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should return correct icons for JavaScript files', () => {
|
|
14
|
+
expect(getIcon('app.js', false)).toBe(icons.javascript);
|
|
15
|
+
expect(getIcon('component.jsx', false)).toBe(icons.javascript);
|
|
16
|
+
expect(getIcon('module.mjs', false)).toBe(icons.javascript);
|
|
17
|
+
expect(getIcon('common.cjs', false)).toBe(icons.javascript);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return correct icons for TypeScript files', () => {
|
|
21
|
+
expect(getIcon('app.ts', false)).toBe(icons.typescript);
|
|
22
|
+
expect(getIcon('component.tsx', false)).toBe(icons.typescript);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return correct icons for config files', () => {
|
|
26
|
+
expect(getIcon('config.yaml', false)).toBe(icons.config);
|
|
27
|
+
expect(getIcon('config.yml', false)).toBe(icons.config);
|
|
28
|
+
expect(getIcon('config.toml', false)).toBe(icons.config);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
it('should return correct icons for markdown files', () => {
|
|
33
|
+
expect(getIcon('README.md', false)).toBe(icons.markdown);
|
|
34
|
+
expect(getIcon('CHANGELOG.mdx', false)).toBe(icons.markdown);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should return correct icons for data files', () => {
|
|
38
|
+
expect(getIcon('data.json', false)).toBe(icons.json);
|
|
39
|
+
expect(getIcon('package.json', false)).toBe(icons.json);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return correct icons for image files', () => {
|
|
43
|
+
expect(getIcon('photo.png', false)).toBe(icons.image);
|
|
44
|
+
expect(getIcon('logo.svg', false)).toBe(icons.image);
|
|
45
|
+
expect(getIcon('image.jpg', false)).toBe(icons.image);
|
|
46
|
+
expect(getIcon('pic.jpeg', false)).toBe(icons.image);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return correct icons for lock files', () => {
|
|
50
|
+
expect(getIcon('package-lock.json', false)).toBe(icons.lock);
|
|
51
|
+
expect(getIcon('yarn.lock', false)).toBe(icons.lock);
|
|
52
|
+
expect(getIcon('pnpm-lock.yaml', false)).toBe(icons.lock);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return correct icons for git files', () => {
|
|
56
|
+
expect(getIcon('.gitignore', false)).toBe(icons.git);
|
|
57
|
+
expect(getIcon('.gitattributes', false)).toBe(icons.git);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return default icon for unknown file types', () => {
|
|
61
|
+
expect(getIcon('unknown.xyz', false)).toBe(icons.default);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should be case-insensitive for extensions', () => {
|
|
65
|
+
expect(getIcon('FILE.JS', false)).toBe(icons.javascript);
|
|
66
|
+
expect(getIcon('FILE.PNG', false)).toBe(icons.image);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
describe('buildTree', () => {
|
|
72
|
+
let tempDir;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
// Create a temporary test directory structure
|
|
76
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dendro-test-'));
|
|
77
|
+
|
|
78
|
+
// Create test structure
|
|
79
|
+
fs.mkdirSync(path.join(tempDir, 'src'));
|
|
80
|
+
fs.mkdirSync(path.join(tempDir, 'test'));
|
|
81
|
+
fs.mkdirSync(path.join(tempDir, '.git'));
|
|
82
|
+
fs.writeFileSync(path.join(tempDir, 'README.md'), '# Test');
|
|
83
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), '{}');
|
|
84
|
+
fs.writeFileSync(path.join(tempDir, 'src', 'index.js'), 'console.log("test")');
|
|
85
|
+
fs.writeFileSync(path.join(tempDir, 'test', 'test.js'), 'test');
|
|
86
|
+
fs.writeFileSync(path.join(tempDir, '.git', 'config'), '');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
// Clean up temp directory
|
|
91
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should build a tree structure', () => {
|
|
95
|
+
const tree = buildTree(tempDir);
|
|
96
|
+
|
|
97
|
+
expect(tree).toBeDefined();
|
|
98
|
+
expect(tree.type).toBe('directory');
|
|
99
|
+
expect(tree.name).toBe(path.basename(tempDir));
|
|
100
|
+
expect(tree.children).toBeDefined();
|
|
101
|
+
expect(Array.isArray(tree.children)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should include files and directories', () => {
|
|
105
|
+
const tree = buildTree(tempDir);
|
|
106
|
+
|
|
107
|
+
expect(tree.children.length).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
const hasFiles = tree.children.some(child => child.type === 'file');
|
|
110
|
+
const hasDirs = tree.children.some(child => child.type === 'directory');
|
|
111
|
+
|
|
112
|
+
expect(hasFiles).toBe(true);
|
|
113
|
+
expect(hasDirs).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
it('should respect maxDepth option', () => {
|
|
118
|
+
// maxDepth: 1 means we go to depth 0 only (root), children at depth 1 return null
|
|
119
|
+
const tree1 = buildTree(tempDir, { maxDepth: 1 });
|
|
120
|
+
expect(tree1.children.every(child => !child.children || child.children.length === 0)).toBe(true);
|
|
121
|
+
|
|
122
|
+
// maxDepth: 2 means we go to depths 0 and 1, children at depth 2 return null
|
|
123
|
+
const tree2 = buildTree(tempDir, { maxDepth: 2 });
|
|
124
|
+
const srcDir = tree2.children.find(child => child.name === 'src' && child.type === 'directory');
|
|
125
|
+
// srcDir exists at depth 1, but its children (at depth 2) won't be built
|
|
126
|
+
if (srcDir) {
|
|
127
|
+
expect(srcDir.children).toBeDefined();
|
|
128
|
+
// Children array exists but should be empty since they're at maxDepth
|
|
129
|
+
expect(srcDir.children.length).toBe(0);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should hide hidden files by default', () => {
|
|
134
|
+
const tree = buildTree(tempDir, { showHidden: false });
|
|
135
|
+
|
|
136
|
+
const hasHidden = tree.children.some(child => child.name.startsWith('.'));
|
|
137
|
+
expect(hasHidden).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should show hidden files when showHidden is true', () => {
|
|
141
|
+
const tree = buildTree(tempDir, { showHidden: true });
|
|
142
|
+
|
|
143
|
+
const gitDir = tree.children.find(child => child.name === '.git');
|
|
144
|
+
expect(gitDir).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should exclude patterns', () => {
|
|
148
|
+
const tree = buildTree(tempDir, {
|
|
149
|
+
excludePatterns: [/^test$/]
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const testDir = tree.children.find(child => child.name === 'test');
|
|
153
|
+
expect(testDir).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should sort directories before files', () => {
|
|
157
|
+
const tree = buildTree(tempDir);
|
|
158
|
+
|
|
159
|
+
let lastWasDir = true;
|
|
160
|
+
for (const child of tree.children) {
|
|
161
|
+
if (child.type === 'file' && lastWasDir) {
|
|
162
|
+
lastWasDir = false;
|
|
163
|
+
} else if (child.type === 'directory' && !lastWasDir) {
|
|
164
|
+
// Found a directory after a file - sorting is wrong
|
|
165
|
+
expect(true).toBe(false);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
expect(true).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
it('should sort items alphabetically within type', () => {
|
|
173
|
+
const tree = buildTree(tempDir);
|
|
174
|
+
|
|
175
|
+
const dirs = tree.children.filter(c => c.type === 'directory');
|
|
176
|
+
const files = tree.children.filter(c => c.type === 'file');
|
|
177
|
+
|
|
178
|
+
// Check directories are sorted
|
|
179
|
+
for (let i = 0; i < dirs.length - 1; i++) {
|
|
180
|
+
expect(dirs[i].name.localeCompare(dirs[i + 1].name)).toBeLessThanOrEqual(0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check files are sorted
|
|
184
|
+
for (let i = 0; i < files.length - 1; i++) {
|
|
185
|
+
expect(files[i].name.localeCompare(files[i + 1].name)).toBeLessThanOrEqual(0);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should return null for non-existent paths', () => {
|
|
190
|
+
const tree = buildTree('/non/existent/path');
|
|
191
|
+
expect(tree).toBeNull();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle empty directories', () => {
|
|
195
|
+
const emptyDir = path.join(tempDir, 'empty');
|
|
196
|
+
fs.mkdirSync(emptyDir);
|
|
197
|
+
|
|
198
|
+
const tree = buildTree(emptyDir);
|
|
199
|
+
expect(tree).toBeDefined();
|
|
200
|
+
expect(tree.children).toBeDefined();
|
|
201
|
+
expect(tree.children.length).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
describe('renderTree', () => {
|
|
207
|
+
it('should render a tree structure as text', () => {
|
|
208
|
+
const tree = {
|
|
209
|
+
name: 'root',
|
|
210
|
+
type: 'directory',
|
|
211
|
+
icon: '📁',
|
|
212
|
+
children: [
|
|
213
|
+
{ name: 'file1.js', type: 'file', icon: '📜', path: '/root/file1.js' },
|
|
214
|
+
{ name: 'file2.md', type: 'file', icon: '📝', path: '/root/file2.md' }
|
|
215
|
+
]
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const output = renderTree(tree);
|
|
219
|
+
expect(output).toContain('root');
|
|
220
|
+
expect(output).toContain('file1.js');
|
|
221
|
+
expect(output).toContain('file2.md');
|
|
222
|
+
expect(output).toContain('📁');
|
|
223
|
+
expect(output).toContain('📜');
|
|
224
|
+
expect(output).toContain('📝');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should use tree connectors', () => {
|
|
228
|
+
const tree = {
|
|
229
|
+
name: 'root',
|
|
230
|
+
type: 'directory',
|
|
231
|
+
icon: '📁',
|
|
232
|
+
children: [
|
|
233
|
+
{ name: 'file1.js', type: 'file', icon: '📜', path: '/root/file1.js' }
|
|
234
|
+
]
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const output = renderTree(tree);
|
|
238
|
+
expect(output).toMatch(/[└├]/); // Should contain tree connectors
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should hide icons when showIcons is false', () => {
|
|
242
|
+
const tree = {
|
|
243
|
+
name: 'root',
|
|
244
|
+
type: 'directory',
|
|
245
|
+
icon: '📁',
|
|
246
|
+
children: [
|
|
247
|
+
{ name: 'file.js', type: 'file', icon: '📜', path: '/root/file.js' }
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const output = renderTree(tree, { showIcons: false });
|
|
252
|
+
expect(output).not.toContain('📁');
|
|
253
|
+
expect(output).not.toContain('📜');
|
|
254
|
+
expect(output).toContain('root');
|
|
255
|
+
expect(output).toContain('file.js');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
it('should show paths when showPaths is true', () => {
|
|
260
|
+
const tree = {
|
|
261
|
+
name: 'root',
|
|
262
|
+
type: 'directory',
|
|
263
|
+
icon: '📁',
|
|
264
|
+
path: '/test/root',
|
|
265
|
+
children: [
|
|
266
|
+
{ name: 'file.js', type: 'file', icon: '📜', path: '/test/root/file.js' }
|
|
267
|
+
]
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const output = renderTree(tree, { showPaths: true });
|
|
271
|
+
expect(output).toContain('/test/root');
|
|
272
|
+
expect(output).toContain('/test/root/file.js');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should return empty string for null tree', () => {
|
|
276
|
+
const output = renderTree(null);
|
|
277
|
+
expect(output).toBe('');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle nested structures', () => {
|
|
281
|
+
const tree = {
|
|
282
|
+
name: 'root',
|
|
283
|
+
type: 'directory',
|
|
284
|
+
icon: '📁',
|
|
285
|
+
children: [
|
|
286
|
+
{
|
|
287
|
+
name: 'src',
|
|
288
|
+
type: 'directory',
|
|
289
|
+
icon: '📁',
|
|
290
|
+
children: [
|
|
291
|
+
{ name: 'index.js', type: 'file', icon: '📜', path: '/root/src/index.js' },
|
|
292
|
+
{ name: 'utils.js', type: 'file', icon: '📜', path: '/root/src/utils.js' }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const output = renderTree(tree);
|
|
299
|
+
expect(output).toContain('root');
|
|
300
|
+
expect(output).toContain('src');
|
|
301
|
+
expect(output).toContain('index.js');
|
|
302
|
+
// With siblings in src, we should get vertical lines
|
|
303
|
+
expect(output).toMatch(/[│├]/);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
describe('getTreeStats', () => {
|
|
309
|
+
it('should count files and directories', () => {
|
|
310
|
+
const tree = {
|
|
311
|
+
name: 'root',
|
|
312
|
+
type: 'directory',
|
|
313
|
+
children: [
|
|
314
|
+
{ name: 'file1.js', type: 'file' },
|
|
315
|
+
{ name: 'file2.js', type: 'file' },
|
|
316
|
+
{
|
|
317
|
+
name: 'subdir',
|
|
318
|
+
type: 'directory',
|
|
319
|
+
children: [
|
|
320
|
+
{ name: 'file3.js', type: 'file' }
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const stats = getTreeStats(tree);
|
|
327
|
+
expect(stats.files).toBe(3);
|
|
328
|
+
expect(stats.directories).toBe(2); // root + subdir
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should return zero counts for null tree', () => {
|
|
332
|
+
const stats = getTreeStats(null);
|
|
333
|
+
expect(stats.files).toBe(0);
|
|
334
|
+
expect(stats.directories).toBe(0);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('should handle empty directories', () => {
|
|
338
|
+
const tree = {
|
|
339
|
+
name: 'empty',
|
|
340
|
+
type: 'directory',
|
|
341
|
+
children: []
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const stats = getTreeStats(tree);
|
|
345
|
+
expect(stats.files).toBe(0);
|
|
346
|
+
expect(stats.directories).toBe(1);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should handle single file', () => {
|
|
350
|
+
const tree = {
|
|
351
|
+
name: 'file.js',
|
|
352
|
+
type: 'file'
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const stats = getTreeStats(tree);
|
|
356
|
+
expect(stats.files).toBe(1);
|
|
357
|
+
expect(stats.directories).toBe(0);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should handle deeply nested structures', () => {
|
|
361
|
+
const tree = {
|
|
362
|
+
name: 'root',
|
|
363
|
+
type: 'directory',
|
|
364
|
+
children: [
|
|
365
|
+
{
|
|
366
|
+
name: 'level1',
|
|
367
|
+
type: 'directory',
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
name: 'level2',
|
|
371
|
+
type: 'directory',
|
|
372
|
+
children: [
|
|
373
|
+
{ name: 'deep.js', type: 'file' }
|
|
374
|
+
]
|
|
375
|
+
},
|
|
376
|
+
{ name: 'file1.js', type: 'file' }
|
|
377
|
+
]
|
|
378
|
+
},
|
|
379
|
+
{ name: 'file2.js', type: 'file' }
|
|
380
|
+
]
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const stats = getTreeStats(tree);
|
|
384
|
+
expect(stats.files).toBe(3);
|
|
385
|
+
expect(stats.directories).toBe(3); // root, level1, level2
|
|
386
|
+
});
|
|
387
|
+
});
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: 'v8',
|
|
9
|
+
reporter: ['text', 'html', 'lcov'],
|
|
10
|
+
exclude: [
|
|
11
|
+
'node_modules/**',
|
|
12
|
+
'test/**',
|
|
13
|
+
'bin/cli.js', // CLI is harder to test, focus on core logic
|
|
14
|
+
'*.config.js',
|
|
15
|
+
'coverage/**',
|
|
16
|
+
'dist/**'
|
|
17
|
+
],
|
|
18
|
+
thresholds: {
|
|
19
|
+
lines: 80,
|
|
20
|
+
functions: 80,
|
|
21
|
+
branches: 70,
|
|
22
|
+
statements: 80
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|