@crashbytes/dendro 1.0.2 → 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.
@@ -0,0 +1,50 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "npm"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ day: "monday"
8
+ time: "09:00"
9
+ open-pull-requests-limit: 10
10
+ groups:
11
+ # Group all production dependencies
12
+ production-dependencies:
13
+ applies-to: version-updates
14
+ dependency-type: "production"
15
+ update-types:
16
+ - "minor"
17
+ - "patch"
18
+
19
+ # Group development dependencies
20
+ development-dependencies:
21
+ applies-to: version-updates
22
+ dependency-type: "development"
23
+ update-types:
24
+ - "minor"
25
+ - "patch"
26
+
27
+ # Auto-assign PRs
28
+ assignees:
29
+ - "MichaelEakins"
30
+
31
+ # Labels for PRs
32
+ labels:
33
+ - "dependencies"
34
+ - "automated"
35
+
36
+ commit-message:
37
+ prefix: "chore(deps)"
38
+ include: "scope"
39
+
40
+ - package-ecosystem: "github-actions"
41
+ directory: "/"
42
+ schedule:
43
+ interval: "weekly"
44
+ day: "monday"
45
+ time: "09:00"
46
+ labels:
47
+ - "dependencies"
48
+ - "github-actions"
49
+ commit-message:
50
+ prefix: "chore(actions)"
@@ -0,0 +1,48 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ # CRITICAL: Required for npm Trusted Publishing (OIDC authentication)
9
+ permissions:
10
+ contents: write # Needed to create GitHub releases
11
+ id-token: write # Needed for npm provenance/trusted publishing
12
+
13
+ jobs:
14
+ release:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ fetch-depth: 0
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v6
23
+ with:
24
+ node-version: '20'
25
+ # NO registry-url - prevents .npmrc conflicts with OIDC
26
+
27
+ - name: Upgrade npm for Trusted Publishing
28
+ run: npm install -g npm@latest # Requires npm >= 11.5.1
29
+
30
+ - name: Install dependencies
31
+ run: npm ci
32
+
33
+ - name: Test
34
+ run: npm test
35
+
36
+ - name: Publish to npm with Provenance
37
+ run: npm publish --provenance --access public
38
+ # OIDC authentication via GitHub Actions - no tokens needed!
39
+
40
+ - name: Create GitHub Release
41
+ uses: softprops/action-gh-release@v2
42
+ with:
43
+ tag_name: ${{ github.ref }}
44
+ name: Release ${{ github.ref_name }}
45
+ body: |
46
+ See [CHANGELOG.md](https://github.com/CrashBytes/dendro/blob/main/CHANGELOG.md) for details.
47
+ draft: false
48
+ prerelease: false
@@ -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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to dendro will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2025-01-14
9
+
10
+ ### Changed
11
+ - 🔄 **BREAKING**: Converted project from CommonJS to ESM (ES Modules)
12
+ - ⬆️ Updated chalk from v4.1.2 to v5.6.2 (major version upgrade)
13
+ - 📦 Updated commander to v14.0.2 (already latest)
14
+ - 🔧 Added `"type": "module"` to package.json
15
+ - 📝 Updated all imports to use ESM syntax (`import`/`export` instead of `require`/`module.exports`)
16
+ - 🔗 Updated relative imports to include `.js` extensions for ESM compatibility
17
+
18
+ ### Migration Notes
19
+ - Node.js 20+ required (already specified in engines)
20
+ - If you were importing this package, update your code to use ESM imports
21
+ - All functionality remains the same, only the module system changed
22
+
8
23
  ## [1.0.1] - 2025-10-13
9
24
 
10
25
  ### Fixed
@@ -36,5 +51,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
36
51
  - 📊 Built-in statistics
37
52
  - 🔧 Both CLI and API
38
53
 
54
+ [1.1.0]: https://github.com/CrashBytes/dendro/releases/tag/v1.1.0
39
55
  [1.0.1]: https://github.com/CrashBytes/dendro/releases/tag/v1.0.1
40
56
  [1.0.0]: https://github.com/CrashBytes/dendro/releases/tag/v1.0.0
package/README.md CHANGED
@@ -11,7 +11,9 @@
11
11
 
12
12
  A beautiful, fast directory tree visualization CLI with intuitive file type icons.
13
13
 
14
- 🔗 **[GitHub Repository](https://github.com/CrashBytes/dendro)** | 📦 **[npm Package](https://www.npmjs.com/package/@crashbytes/dendro)**
14
+ 📚 **[Documentation](https://crashbytes.github.io/dendro/)** | 🔗 **[GitHub Repository](https://github.com/CrashBytes/dendro)** | 📦 **[npm Package](https://www.npmjs.com/package/@crashbytes/dendro)**
15
+
16
+ > **Note:** Requires Node.js 20.0.0 or higher
15
17
 
16
18
  ---
17
19
 
@@ -368,8 +370,6 @@ MIT License - see [LICENSE](https://github.com/CrashBytes/dendro/blob/main/LICEN
368
370
 
369
371
  ## Acknowledgments
370
372
 
371
- Built with ❤️ for developers who love beautiful CLIs.
372
-
373
373
  Special thanks to all [contributors](https://github.com/CrashBytes/dendro/graphs/contributors) who help make dendro better!
374
374
 
375
375
  ---
package/bin/cli.js CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { Command } = require('commander');
4
- const chalk = require('chalk');
5
- const path = require('path');
6
- const { buildTree, renderTree, getTreeStats } = require('../index');
3
+ import { Command } from 'commander';
4
+ import chalk from 'chalk';
5
+ import path from 'path';
6
+ import { buildTree, renderTree, getTreeStats } from '../index.js';
7
7
 
8
8
  const program = new Command();
9
9
 
10
10
  program
11
11
  .name('dendro')
12
12
  .description('Display directory tree structure with beautiful icons')
13
- .version('1.0.0')
13
+ .version('1.1.0')
14
14
  .argument('[path]', 'Directory path to display', '.')
15
15
  .option('-d, --max-depth <number>', 'Maximum depth to traverse', parseInt)
16
16
  .option('-a, --all', 'Show hidden files and directories', false)
package/deploy.sh ADDED
@@ -0,0 +1,95 @@
1
+ #!/bin/bash
2
+ set -e # Exit on any error
3
+
4
+ # Configuration
5
+ VERSION=$(node -p "require('./package.json').version")
6
+
7
+ echo "🚀 Dendro v${VERSION} Production Deployment"
8
+ echo "========================================"
9
+ echo ""
10
+
11
+ # Phase 1: Pre-flight Validation
12
+ echo "📋 Phase 1: Pre-flight Validation"
13
+ echo "-----------------------------------"
14
+
15
+ # Check Node.js version
16
+ NODE_VERSION=$(node -v)
17
+ echo "✓ Node.js: $NODE_VERSION"
18
+
19
+ # Check npm version
20
+ NPM_VERSION=$(npm -v)
21
+ echo "✓ npm: $NPM_VERSION"
22
+
23
+ # Verify authentication
24
+ echo -n "✓ npm authentication: "
25
+ npm whoami
26
+
27
+ echo ""
28
+
29
+ # Phase 2: Test Suite Execution
30
+ echo "🧪 Phase 2: Test Suite Execution"
31
+ echo "-----------------------------------"
32
+ npm test
33
+
34
+ echo ""
35
+
36
+ # Phase 3: Package Validation
37
+ echo "📦 Phase 3: Package Validation"
38
+ echo "-----------------------------------"
39
+ echo "Package contents preview:"
40
+ npm pack --dry-run
41
+
42
+ echo ""
43
+
44
+ # Phase 4: Git Operations
45
+ echo "📝 Phase 4: Version Control"
46
+ echo "-----------------------------------"
47
+
48
+ # Check git status
49
+ if [[ -n $(git status -s) ]]; then
50
+ echo "Committing changes..."
51
+ git add package.json .gitignore
52
+ git commit -m "chore: bump version to ${VERSION} - update repository links and secure credentials"
53
+ echo "✓ Changes committed"
54
+ else
55
+ echo "✓ Working directory clean"
56
+ fi
57
+
58
+ # Create and push tag
59
+ echo "Creating version tag v${VERSION}..."
60
+ git tag -f "v${VERSION}"
61
+ echo "✓ Tag created"
62
+
63
+ echo "Pushing to remote..."
64
+ git push origin main
65
+ git push origin "v${VERSION}" --force
66
+ echo "✓ Pushed to GitHub"
67
+
68
+ echo ""
69
+
70
+ # Phase 5: NPM Publication
71
+ echo "🚀 Phase 5: NPM Publication"
72
+ echo "-----------------------------------"
73
+ echo "Publishing @crashbytes/dendro@${VERSION}..."
74
+ npm publish --access public
75
+
76
+ echo ""
77
+
78
+ # Phase 6: Post-Deployment Verification
79
+ echo "✅ Phase 6: Post-Deployment Verification"
80
+ echo "-----------------------------------"
81
+ echo "Package details:"
82
+ npm view @crashbytes/dendro
83
+
84
+ echo ""
85
+ echo "=========================================="
86
+ echo "✨ Deployment Complete!"
87
+ echo "=========================================="
88
+ echo ""
89
+ echo "Verification commands:"
90
+ echo " npm install -g @crashbytes/dendro@${VERSION}"
91
+ echo " dendro --version"
92
+ echo ""
93
+ echo "Package URL:"
94
+ echo " https://www.npmjs.com/package/@crashbytes/dendro"
95
+ echo ""
package/docs/.nojekyll ADDED
File without changes
@@ -0,0 +1,360 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Dendro Documentation - Directory Tree Visualization</title>
7
+ <meta name="description" content="A beautiful directory tree visualization CLI with file type icons - dendro (δένδρο) means 'tree' in Greek">
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
17
+ line-height: 1.6;
18
+ color: #333;
19
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
20
+ min-height: 100vh;
21
+ padding: 2rem 1rem;
22
+ }
23
+
24
+ .container {
25
+ max-width: 900px;
26
+ margin: 0 auto;
27
+ background: white;
28
+ border-radius: 12px;
29
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
30
+ overflow: hidden;
31
+ }
32
+
33
+ .header {
34
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
35
+ color: white;
36
+ padding: 3rem 2rem;
37
+ text-align: center;
38
+ }
39
+
40
+ .header h1 {
41
+ font-size: 2.5rem;
42
+ margin-bottom: 0.5rem;
43
+ font-weight: 700;
44
+ }
45
+
46
+ .header .subtitle {
47
+ font-size: 1.1rem;
48
+ opacity: 0.9;
49
+ font-style: italic;
50
+ }
51
+
52
+ .content {
53
+ padding: 2rem;
54
+ }
55
+
56
+ .badge {
57
+ display: inline-block;
58
+ background: #27ae60;
59
+ color: white;
60
+ padding: 0.25rem 0.75rem;
61
+ border-radius: 12px;
62
+ font-size: 0.85rem;
63
+ font-weight: 600;
64
+ margin: 0.5rem 0.25rem;
65
+ }
66
+
67
+ .badge.warning {
68
+ background: #e67e22;
69
+ }
70
+
71
+ .requirements {
72
+ background: #fff3cd;
73
+ border-left: 4px solid #ffc107;
74
+ padding: 1rem;
75
+ margin: 1.5rem 0;
76
+ border-radius: 4px;
77
+ }
78
+
79
+ .requirements strong {
80
+ color: #856404;
81
+ }
82
+
83
+ .code-block-container {
84
+ position: relative;
85
+ margin: 1.5rem 0;
86
+ }
87
+
88
+ .copy-button {
89
+ position: absolute;
90
+ top: 0.5rem;
91
+ right: 0.5rem;
92
+ padding: 0.5rem 1rem;
93
+ background: #667eea;
94
+ color: white;
95
+ border: none;
96
+ border-radius: 6px;
97
+ cursor: pointer;
98
+ font-size: 0.875rem;
99
+ font-weight: 600;
100
+ transition: all 0.2s;
101
+ z-index: 10;
102
+ }
103
+
104
+ .copy-button:hover {
105
+ background: #764ba2;
106
+ transform: translateY(-1px);
107
+ }
108
+
109
+ .copy-button.copied {
110
+ background: #27ae60;
111
+ }
112
+
113
+ pre {
114
+ background: #2c3e50;
115
+ color: #ecf0f1;
116
+ border-radius: 8px;
117
+ padding: 1.5rem;
118
+ overflow-x: auto;
119
+ font-size: 0.9rem;
120
+ }
121
+
122
+ code {
123
+ font-family: 'Monaco', 'Courier New', monospace;
124
+ }
125
+
126
+ h2 {
127
+ color: #2c3e50;
128
+ font-size: 1.8rem;
129
+ margin: 2rem 0 1rem;
130
+ padding-bottom: 0.5rem;
131
+ border-bottom: 2px solid #667eea;
132
+ }
133
+
134
+ h3 {
135
+ color: #34495e;
136
+ font-size: 1.3rem;
137
+ margin: 1.5rem 0 0.75rem;
138
+ }
139
+
140
+ .section {
141
+ margin-bottom: 2rem;
142
+ }
143
+
144
+ .features {
145
+ display: grid;
146
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
147
+ gap: 1rem;
148
+ margin: 1.5rem 0;
149
+ }
150
+
151
+ .feature-card {
152
+ background: #f8f9fa;
153
+ padding: 1.5rem;
154
+ border-radius: 8px;
155
+ border-left: 4px solid #667eea;
156
+ }
157
+
158
+ .feature-card h4 {
159
+ color: #2c3e50;
160
+ margin-bottom: 0.5rem;
161
+ }
162
+
163
+ .footer {
164
+ background: #2c3e50;
165
+ color: white;
166
+ padding: 2rem;
167
+ text-align: center;
168
+ }
169
+
170
+ .footer a {
171
+ color: #667eea;
172
+ text-decoration: none;
173
+ font-weight: 600;
174
+ }
175
+
176
+ .footer a:hover {
177
+ color: #764ba2;
178
+ }
179
+
180
+ @media (max-width: 768px) {
181
+ .header h1 {
182
+ font-size: 2rem;
183
+ }
184
+
185
+ .content {
186
+ padding: 1.5rem;
187
+ }
188
+ }
189
+ </style>
190
+ </head>
191
+ <body>
192
+ <div class="container">
193
+ <div class="header">
194
+ <h1>🌳 Dendro</h1>
195
+ <p class="subtitle">δένδρο (dendro) means "tree" in Greek</p>
196
+ <div style="margin-top: 1rem;">
197
+ <span class="badge">v1.0.2</span>
198
+ <span class="badge">Node.js 20+</span>
199
+ <span class="badge">MIT License</span>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="content">
204
+ <section class="section">
205
+ <p style="font-size: 1.1rem; color: #555; margin-bottom: 1.5rem;">
206
+ A beautiful directory tree visualization CLI with file type icons.
207
+ Perfect for exploring project structures, creating documentation, and understanding codebases.
208
+ </p>
209
+
210
+ <div class="requirements">
211
+ <strong>⚠️ Requirements:</strong> Node.js version 20.0.0 or higher is required.
212
+ </div>
213
+ </section>
214
+
215
+ <section class="section">
216
+ <h2>✨ Features</h2>
217
+ <div class="features">
218
+ <div class="feature-card">
219
+ <h4>🎨 Beautiful Icons</h4>
220
+ <p>File type-specific icons for instant recognition</p>
221
+ </div>
222
+ <div class="feature-card">
223
+ <h4>⚡ Lightning Fast</h4>
224
+ <p>Optimized performance for large directories</p>
225
+ </div>
226
+ <div class="feature-card">
227
+ <h4>🎯 Configurable Depth</h4>
228
+ <p>Control how deep to traverse directories</p>
229
+ </div>
230
+ <div class="feature-card">
231
+ <h4>👁️ Hidden Files</h4>
232
+ <p>Toggle visibility of hidden files and folders</p>
233
+ </div>
234
+ </div>
235
+ </section>
236
+
237
+ <section class="section">
238
+ <h2>📦 Installation</h2>
239
+
240
+ <h3>Via npm (Recommended)</h3>
241
+ <div class="code-block-container">
242
+ <button class="copy-button" onclick="copyCode(this, 'install-npm')">Copy</button>
243
+ <pre><code id="install-npm">npm install -g @crashbytes/dendro</code></pre>
244
+ </div>
245
+
246
+ <h3>Via npx (No Installation)</h3>
247
+ <div class="code-block-container">
248
+ <button class="copy-button" onclick="copyCode(this, 'install-npx')">Copy</button>
249
+ <pre><code id="install-npx">npx @crashbytes/dendro</code></pre>
250
+ </div>
251
+
252
+ <h3>From Source (GitHub)</h3>
253
+ <div class="code-block-container">
254
+ <button class="copy-button" onclick="copyCode(this, 'install-source')">Copy</button>
255
+ <pre><code id="install-source">git clone https://github.com/CrashBytes/dendro.git
256
+ cd dendro
257
+ npm install
258
+ npm link</code></pre>
259
+ </div>
260
+ </section>
261
+
262
+ <section class="section">
263
+ <h2>🚀 Quick Start</h2>
264
+
265
+ <h3>Basic Usage</h3>
266
+ <div class="code-block-container">
267
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-1')">Copy</button>
268
+ <pre><code id="quickstart-1"># Visualize current directory
269
+ dendro</code></pre>
270
+ </div>
271
+
272
+ <div class="code-block-container">
273
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-2')">Copy</button>
274
+ <pre><code id="quickstart-2"># Visualize specific directory
275
+ dendro /path/to/project</code></pre>
276
+ </div>
277
+
278
+ <h3>Advanced Options</h3>
279
+ <div class="code-block-container">
280
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-3')">Copy</button>
281
+ <pre><code id="quickstart-3"># Limit depth to 3 levels
282
+ dendro ~/projects -d 3</code></pre>
283
+ </div>
284
+
285
+ <div class="code-block-container">
286
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-4')">Copy</button>
287
+ <pre><code id="quickstart-4"># Show all files including hidden
288
+ dendro -a</code></pre>
289
+ </div>
290
+
291
+ <div class="code-block-container">
292
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-5')">Copy</button>
293
+ <pre><code id="quickstart-5"># Combine options
294
+ dendro ~/my-project -d 2 -a</code></pre>
295
+ </div>
296
+
297
+ <h3>Get Help</h3>
298
+ <div class="code-block-container">
299
+ <button class="copy-button" onclick="copyCode(this, 'quickstart-6')">Copy</button>
300
+ <pre><code id="quickstart-6"># Show help
301
+ dendro --help</code></pre>
302
+ </div>
303
+ </section>
304
+
305
+ <section class="section">
306
+ <h2>📖 Command Options</h2>
307
+ <pre style="background: #f8f9fa; color: #2c3e50; border: 1px solid #e1e4e8;">
308
+ Usage: dendro [options] [directory]
309
+
310
+ Arguments:
311
+ directory Directory to visualize (default: current directory)
312
+
313
+ Options:
314
+ -V, --version Output the version number
315
+ -d, --depth <n> Maximum depth to traverse (default: unlimited)
316
+ -a, --all Show hidden files and directories
317
+ -h, --help Display help for command
318
+ </pre>
319
+ </section>
320
+ </div>
321
+
322
+ <div class="footer">
323
+ <p><a href="https://github.com/CrashBytes" target="_blank">CrashBytes</a></p>
324
+ <p style="margin-top: 0.5rem;">
325
+ <a href="https://github.com/CrashBytes/dendro" target="_blank">GitHub</a> •
326
+ <a href="https://www.npmjs.com/package/@crashbytes/dendro" target="_blank">npm</a> •
327
+ <a href="https://github.com/CrashBytes/dendro/issues" target="_blank">Report Issues</a>
328
+ </p>
329
+ </div>
330
+ </div>
331
+
332
+ <script>
333
+ async function copyCode(button, codeId) {
334
+ const codeElement = document.getElementById(codeId);
335
+ const textToCopy = codeElement.textContent;
336
+
337
+ try {
338
+ await navigator.clipboard.writeText(textToCopy);
339
+
340
+ // Visual feedback
341
+ const originalText = button.textContent;
342
+ button.textContent = '✓ Copied!';
343
+ button.classList.add('copied');
344
+
345
+ // Reset after 2 seconds
346
+ setTimeout(() => {
347
+ button.textContent = originalText;
348
+ button.classList.remove('copied');
349
+ }, 2000);
350
+ } catch (err) {
351
+ console.error('Failed to copy text:', err);
352
+ button.textContent = 'Failed';
353
+ setTimeout(() => {
354
+ button.textContent = 'Copy';
355
+ }, 2000);
356
+ }
357
+ }
358
+ </script>
359
+ </body>
360
+ </html>
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
- const fs = require('fs');
2
- const path = require('path');
1
+ import fs from 'fs';
2
+ import path from 'path';
3
3
 
4
4
  // Icon mappings for different file types and directories
5
5
  const icons = {
@@ -287,7 +287,7 @@ function getTreeStats(tree) {
287
287
  return { files, directories };
288
288
  }
289
289
 
290
- module.exports = {
290
+ export {
291
291
  buildTree,
292
292
  renderTree,
293
293
  getTreeStats,
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "@crashbytes/dendro",
3
- "version": "1.0.2",
3
+ "version": "1.1.1",
4
4
  "description": "A beautiful directory tree visualization CLI with file type icons - dendro (δένδρο) means 'tree' in Greek",
5
+ "type": "module",
5
6
  "main": "index.js",
6
7
  "bin": {
7
8
  "dendro": "bin/cli.js"
8
9
  },
9
10
  "scripts": {
10
- "test": "node test.js"
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
14
+ "test:ui": "vitest --ui"
11
15
  },
12
16
  "keywords": [
13
17
  "directory",
@@ -28,10 +32,15 @@
28
32
  },
29
33
  "homepage": "https://github.com/CrashBytes/dendro#readme",
30
34
  "dependencies": {
31
- "commander": "^11.1.0",
32
- "chalk": "^4.1.2"
35
+ "chalk": "^5.6.2",
36
+ "commander": "^14.0.3"
33
37
  },
34
38
  "engines": {
35
- "node": ">=14.0.0"
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"
36
45
  }
37
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/test.js CHANGED
@@ -1,5 +1,5 @@
1
- const { buildTree, renderTree, getTreeStats, getIcon } = require('./index');
2
- const path = require('path');
1
+ import { buildTree, renderTree, getTreeStats, getIcon } from './index.js';
2
+ import path from 'path';
3
3
 
4
4
  console.log('Running tests for dendro...\n');
5
5
 
@@ -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
+ });