@youbamj/tree 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/fs-tree.d.ts +2 -0
- package/dist/fs-tree.js +167 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/renderer.d.ts +4 -0
- package/dist/renderer.js +84 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 youbamj
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# @youbamj/tree
|
|
2
|
+
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/@youbamj/tree">
|
|
5
|
+
<img src="https://img.shields.io/npm/v/@youbamj/tree" alt="NPM Version" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@youbamj/tree">
|
|
8
|
+
<img src="https://img.shields.io/npm/l/@youbamj/tree" alt="License" />
|
|
9
|
+
</a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
`@youbamj/tree` is a modern TypeScript + Bun package to render directory trees and custom object trees.
|
|
13
|
+
|
|
14
|
+
- CLI command: `ytree`
|
|
15
|
+
- Library functions: `getFileTree()` and `getStringTree()`
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
### Install CLI globally
|
|
20
|
+
|
|
21
|
+
Using npm:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @youbamj/tree
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Using bun:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add -g @youbamj/tree
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Install in your project
|
|
34
|
+
|
|
35
|
+
Using npm:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @youbamj/tree
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Using bun:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun add @youbamj/tree
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Use in terminal
|
|
48
|
+
|
|
49
|
+
You can run `ytree` in the shell:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
ytree -d ./src --ignore node_modules,.git --depth 3
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Example output:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
·
|
|
59
|
+
└── src
|
|
60
|
+
├── cli.ts
|
|
61
|
+
├── fs-tree.ts
|
|
62
|
+
├── index.ts
|
|
63
|
+
├── renderer.ts
|
|
64
|
+
└── types.ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Options
|
|
68
|
+
|
|
69
|
+
```text
|
|
70
|
+
Usage: ytree [options] [directory]
|
|
71
|
+
|
|
72
|
+
Arguments:
|
|
73
|
+
directory Directory to render (default: ".")
|
|
74
|
+
|
|
75
|
+
Options:
|
|
76
|
+
-v, --version Show version
|
|
77
|
+
-d, --dir <path> Directory to render (overrides positional arg)
|
|
78
|
+
-o, --out <file> Write output to a file
|
|
79
|
+
-i, --ignore <pattern> Ignore basename or relative path (repeatable and comma-separated)
|
|
80
|
+
-l, --depth <n> Max depth (root is level 1)
|
|
81
|
+
--no-hidden Exclude hidden files/folders
|
|
82
|
+
--follow-symlinks Traverse symbolic links
|
|
83
|
+
--no-gitignore Do not respect .gitignore rules
|
|
84
|
+
--sort <mode> Sorting mode: asc | desc | none
|
|
85
|
+
--max-nodes <n> Maximum number of nodes to include (default: 10000, use -1 for no cap)
|
|
86
|
+
--json Print JSON tree
|
|
87
|
+
-c, --color [name] Output color (default: "white")
|
|
88
|
+
--no-color Disable color output
|
|
89
|
+
-h, --help Display help
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Use in your project
|
|
93
|
+
|
|
94
|
+
`getFileTree()` uses a default safety cap of `10000` nodes.
|
|
95
|
+
Set `maxNodes: -1` to disable the cap.
|
|
96
|
+
|
|
97
|
+
### 1) Render a directory tree to string
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { getFileTree, getStringTree } from "@youbamj/tree";
|
|
101
|
+
|
|
102
|
+
const treeData = await getFileTree({
|
|
103
|
+
dir: "./",
|
|
104
|
+
ignore: ["node_modules", ".git"],
|
|
105
|
+
level: 3,
|
|
106
|
+
includeHidden: false,
|
|
107
|
+
gitignore: true,
|
|
108
|
+
sort: "asc"
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(getStringTree(treeData));
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Example output:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
·
|
|
118
|
+
└── my-project
|
|
119
|
+
├── package.json
|
|
120
|
+
├── src
|
|
121
|
+
│ ├── cli.ts
|
|
122
|
+
│ └── index.ts
|
|
123
|
+
└── tsconfig.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Bun ESM script example:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// scripts/show-tree.ts
|
|
130
|
+
import { getFileTree, getStringTree } from "@youbamj/tree";
|
|
131
|
+
|
|
132
|
+
const data = await getFileTree({ dir: ".", level: 2 });
|
|
133
|
+
console.log(getStringTree(data));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Run it with:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
bun run scripts/show-tree.ts
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### 2) Get raw `getFileTree()` JSON result
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { getFileTree } from "@youbamj/tree";
|
|
146
|
+
|
|
147
|
+
const treeData = await getFileTree({
|
|
148
|
+
dir: "./src",
|
|
149
|
+
level: 2
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log(JSON.stringify(treeData, null, 2));
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example result:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
[
|
|
159
|
+
{
|
|
160
|
+
"name": "src",
|
|
161
|
+
"path": "/absolute/path/to/src",
|
|
162
|
+
"type": "directory",
|
|
163
|
+
"children": [
|
|
164
|
+
{
|
|
165
|
+
"name": "cli.ts",
|
|
166
|
+
"path": "/absolute/path/to/src/cli.ts",
|
|
167
|
+
"type": "file"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"name": "index.ts",
|
|
171
|
+
"path": "/absolute/path/to/src/index.ts",
|
|
172
|
+
"type": "file"
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
]
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 3) Render custom object trees to string
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { getStringTree } from "@youbamj/tree";
|
|
183
|
+
|
|
184
|
+
const output = getStringTree(
|
|
185
|
+
[
|
|
186
|
+
{
|
|
187
|
+
title: "done",
|
|
188
|
+
items: [{ title: "hiking" }, { title: "camping" }]
|
|
189
|
+
}
|
|
190
|
+
],
|
|
191
|
+
{
|
|
192
|
+
labelKey: "title",
|
|
193
|
+
childrenKey: "items",
|
|
194
|
+
sanitizeLabels: true
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
console.log(output);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Example output:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
·
|
|
205
|
+
└── done
|
|
206
|
+
├── hiking
|
|
207
|
+
└── camping
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Why @youbamj/tree?
|
|
211
|
+
|
|
212
|
+
- 🌲 Render directory content in a clean tree structure
|
|
213
|
+
- 📝 Optionally write output to a file (`--out`)
|
|
214
|
+
- 🎨 Colorized CLI output
|
|
215
|
+
- 🧩 Convert custom arrays/objects to tree strings
|
|
216
|
+
- 🙈 `.gitignore` support by default
|
|
217
|
+
- 🔒 Terminal-safe label sanitization by default
|
|
218
|
+
- ⚡ TypeScript + Bun friendly
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
[MIT](./LICENSE)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Command, InvalidOptionArgumentError } from "commander";
|
|
5
|
+
import { getFileTree, getStringTree } from "./index.js";
|
|
6
|
+
const COLOR_CODES = {
|
|
7
|
+
white: "\u001b[37m",
|
|
8
|
+
red: "\u001b[31m",
|
|
9
|
+
green: "\u001b[32m",
|
|
10
|
+
yellow: "\u001b[33m",
|
|
11
|
+
blue: "\u001b[34m",
|
|
12
|
+
magenta: "\u001b[35m",
|
|
13
|
+
cyan: "\u001b[36m",
|
|
14
|
+
gray: "\u001b[90m",
|
|
15
|
+
};
|
|
16
|
+
const RESET = "\u001b[0m";
|
|
17
|
+
const VERSION = "0.1.0";
|
|
18
|
+
function applyColor(text, color, enabled) {
|
|
19
|
+
if (!enabled)
|
|
20
|
+
return text;
|
|
21
|
+
const normalized = (color || "white");
|
|
22
|
+
const code = COLOR_CODES[normalized] ?? COLOR_CODES.white;
|
|
23
|
+
return `${code}${text}${RESET}`;
|
|
24
|
+
}
|
|
25
|
+
function parseDepth(value) {
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
28
|
+
throw new InvalidOptionArgumentError("depth must be a positive integer");
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
function parseSort(value) {
|
|
33
|
+
if (value === "asc" || value === "desc" || value === "none") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
throw new InvalidOptionArgumentError("sort must be one of: asc, desc, none");
|
|
37
|
+
}
|
|
38
|
+
function parseMaxNodes(value) {
|
|
39
|
+
const parsed = Number(value);
|
|
40
|
+
if (!Number.isInteger(parsed) || (parsed < 1 && parsed !== -1)) {
|
|
41
|
+
throw new InvalidOptionArgumentError("max-nodes must be a positive integer or -1");
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function collectIgnore(value, previous) {
|
|
46
|
+
const parsed = value
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((item) => item.trim())
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
return [...previous, ...parsed];
|
|
51
|
+
}
|
|
52
|
+
function normalizeOutput(content, filename) {
|
|
53
|
+
const ext = path.extname(filename).toLowerCase();
|
|
54
|
+
if (ext === ".md") {
|
|
55
|
+
return `\
|
|
56
|
+
\`\`\`text
|
|
57
|
+
${content}
|
|
58
|
+
\`\`\`
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
return content;
|
|
62
|
+
}
|
|
63
|
+
const program = new Command();
|
|
64
|
+
program
|
|
65
|
+
.name("ytree")
|
|
66
|
+
.description("Render directory trees from the terminal")
|
|
67
|
+
.version(VERSION, "-v, --version", "Show version")
|
|
68
|
+
.argument("[directory]", "Directory to render", ".")
|
|
69
|
+
.option("-d, --dir <path>", "Directory to render (overrides positional arg)")
|
|
70
|
+
.option("-o, --out <file>", "Write output to a file")
|
|
71
|
+
.option("-i, --ignore <pattern>", "Ignore basename or relative path (repeatable and comma-separated)", collectIgnore, [])
|
|
72
|
+
.option("-l, --depth <n>", "Max depth (root is level 1)", parseDepth)
|
|
73
|
+
.option("--no-hidden", "Exclude hidden files/folders")
|
|
74
|
+
.option("--follow-symlinks", "Traverse symbolic links", false)
|
|
75
|
+
.option("--no-gitignore", "Do not respect .gitignore rules")
|
|
76
|
+
.option("--sort <mode>", "Sorting mode", parseSort, "asc")
|
|
77
|
+
.option("--max-nodes <n>", "Maximum number of nodes to include", parseMaxNodes)
|
|
78
|
+
.option("--json", "Print JSON tree", false)
|
|
79
|
+
.option("-c, --color [name]", "Output color", "white")
|
|
80
|
+
.option("--no-color", "Disable color output")
|
|
81
|
+
.action(async (directory, options) => {
|
|
82
|
+
const dir = options.dir ?? directory;
|
|
83
|
+
const treeData = await getFileTree({
|
|
84
|
+
dir,
|
|
85
|
+
ignore: options.ignore,
|
|
86
|
+
level: options.depth,
|
|
87
|
+
includeHidden: options.hidden,
|
|
88
|
+
followSymlinks: options.followSymlinks,
|
|
89
|
+
gitignore: options.gitignore,
|
|
90
|
+
sort: options.sort,
|
|
91
|
+
maxNodes: options.maxNodes,
|
|
92
|
+
});
|
|
93
|
+
const rendered = options.json ? JSON.stringify(treeData, null, 2) : getStringTree(treeData);
|
|
94
|
+
const colorEnabled = Boolean(options.color) && !options.json;
|
|
95
|
+
const colorName = typeof options.color === "string" ? options.color : "white";
|
|
96
|
+
const output = applyColor(rendered, colorName, colorEnabled);
|
|
97
|
+
console.log(output);
|
|
98
|
+
if (options.out) {
|
|
99
|
+
const fileContent = normalizeOutput(rendered, options.out);
|
|
100
|
+
await writeFile(options.out, fileContent, "utf8");
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
104
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
105
|
+
process.stderr.write(`[ERR] ${message}\n`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
package/dist/fs-tree.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import createIgnore from "ignore";
|
|
4
|
+
const DEFAULT_MAX_NODES = 10_000;
|
|
5
|
+
function normalizeSlashes(input) {
|
|
6
|
+
return input.replace(/\\/g, "/");
|
|
7
|
+
}
|
|
8
|
+
function parseIgnore(ignore) {
|
|
9
|
+
if (!ignore)
|
|
10
|
+
return [];
|
|
11
|
+
const list = Array.isArray(ignore) ? ignore : ignore.split(",");
|
|
12
|
+
return list
|
|
13
|
+
.map((item) => item.trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((item) => normalizeSlashes(item.replace(/\/$/, "")));
|
|
16
|
+
}
|
|
17
|
+
function normalizeMaxNodes(maxNodes) {
|
|
18
|
+
if (maxNodes === -1)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (!Number.isInteger(maxNodes) || maxNodes < 1) {
|
|
21
|
+
throw new Error("maxNodes must be a positive integer or -1 to disable the cap");
|
|
22
|
+
}
|
|
23
|
+
return maxNodes;
|
|
24
|
+
}
|
|
25
|
+
function shouldIgnoreByPatterns(relativePath, baseName, ignorePatterns) {
|
|
26
|
+
if (ignorePatterns.length === 0)
|
|
27
|
+
return false;
|
|
28
|
+
const rel = normalizeSlashes(relativePath);
|
|
29
|
+
for (const pattern of ignorePatterns) {
|
|
30
|
+
if (pattern.includes("/")) {
|
|
31
|
+
if (rel === pattern || rel.startsWith(`${pattern}/`)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (baseName === pattern) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function shouldIgnoreByGitignore(relativePath, isDirectory, gitignoreMatcher) {
|
|
43
|
+
if (!gitignoreMatcher || relativePath === "") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (gitignoreMatcher.ignores(relativePath)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (isDirectory && gitignoreMatcher.ignores(`${relativePath}/`)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
async function loadGitignore(rootDir, enabled) {
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
60
|
+
const content = await readFile(gitignorePath, "utf8");
|
|
61
|
+
const gitignoreMatcher = createIgnore();
|
|
62
|
+
gitignoreMatcher.add(content);
|
|
63
|
+
return gitignoreMatcher;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function compareNames(a, b, sort) {
|
|
70
|
+
if (sort === "none")
|
|
71
|
+
return 0;
|
|
72
|
+
const result = a.localeCompare(b);
|
|
73
|
+
return sort === "desc" ? -result : result;
|
|
74
|
+
}
|
|
75
|
+
async function buildNode(absolutePath, depth, options) {
|
|
76
|
+
let stats;
|
|
77
|
+
try {
|
|
78
|
+
stats = await lstat(absolutePath);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (depth === 1) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const relativePath = normalizeSlashes(path.relative(options.rootDir, absolutePath));
|
|
87
|
+
const name = path.basename(absolutePath);
|
|
88
|
+
const isDirectory = stats.isDirectory();
|
|
89
|
+
if (!options.includeHidden && name.startsWith(".")) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (shouldIgnoreByPatterns(relativePath, name, options.ignorePatterns)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (shouldIgnoreByGitignore(relativePath, isDirectory, options.gitignoreMatcher)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (options.maxNodes !== undefined && options.nodeCount >= options.maxNodes) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
const node = {
|
|
102
|
+
name,
|
|
103
|
+
path: absolutePath,
|
|
104
|
+
type: isDirectory ? "directory" : stats.isSymbolicLink() ? "symlink" : "file",
|
|
105
|
+
};
|
|
106
|
+
options.nodeCount += 1;
|
|
107
|
+
if (options.maxDepth !== undefined && depth >= options.maxDepth) {
|
|
108
|
+
return node;
|
|
109
|
+
}
|
|
110
|
+
const shouldTraverseDirectory = isDirectory;
|
|
111
|
+
const shouldTraverseSymlink = stats.isSymbolicLink() && options.followSymlinks;
|
|
112
|
+
if (!shouldTraverseDirectory && !shouldTraverseSymlink) {
|
|
113
|
+
return node;
|
|
114
|
+
}
|
|
115
|
+
let targetPath = absolutePath;
|
|
116
|
+
if (shouldTraverseSymlink) {
|
|
117
|
+
try {
|
|
118
|
+
targetPath = await realpath(absolutePath);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return node;
|
|
122
|
+
}
|
|
123
|
+
if (options.visited.has(targetPath)) {
|
|
124
|
+
return node;
|
|
125
|
+
}
|
|
126
|
+
options.visited.add(targetPath);
|
|
127
|
+
}
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = await readdir(targetPath);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return node;
|
|
134
|
+
}
|
|
135
|
+
entries = [...entries].sort((a, b) => compareNames(a, b, options.sort));
|
|
136
|
+
const children = [];
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const childPath = path.join(targetPath, entry);
|
|
139
|
+
const child = await buildNode(childPath, depth + 1, options);
|
|
140
|
+
if (child) {
|
|
141
|
+
children.push(child);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (children.length > 0) {
|
|
145
|
+
node.children = children;
|
|
146
|
+
}
|
|
147
|
+
return node;
|
|
148
|
+
}
|
|
149
|
+
export async function getFileTree({ dir, ignore, level, includeHidden = true, followSymlinks = false, sort = "asc", gitignore = true, maxNodes = DEFAULT_MAX_NODES, }) {
|
|
150
|
+
const rootDir = path.resolve(process.cwd(), dir);
|
|
151
|
+
const ignorePatterns = parseIgnore(ignore);
|
|
152
|
+
const gitignoreMatcher = await loadGitignore(rootDir, gitignore);
|
|
153
|
+
const options = {
|
|
154
|
+
rootDir,
|
|
155
|
+
maxDepth: level,
|
|
156
|
+
includeHidden,
|
|
157
|
+
followSymlinks,
|
|
158
|
+
sort,
|
|
159
|
+
ignorePatterns,
|
|
160
|
+
gitignoreMatcher,
|
|
161
|
+
maxNodes: normalizeMaxNodes(maxNodes),
|
|
162
|
+
nodeCount: 0,
|
|
163
|
+
visited: new Set(),
|
|
164
|
+
};
|
|
165
|
+
const rootNode = await buildNode(rootDir, 1, options);
|
|
166
|
+
return rootNode ? [rootNode] : [];
|
|
167
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RenderTreeOptions, TreeNode } from "./types.js";
|
|
2
|
+
export declare function getTreeLines<T extends object>(data?: T[], renderOptions?: RenderTreeOptions): string[];
|
|
3
|
+
export declare function getStringTree<T extends object>(data?: T[], renderOptions?: RenderTreeOptions): string;
|
|
4
|
+
export type { TreeNode };
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const DEFAULT_RENDER_OPTIONS = {
|
|
2
|
+
labelKey: "name",
|
|
3
|
+
childrenKey: "children",
|
|
4
|
+
rootMarker: "·",
|
|
5
|
+
indent: " ",
|
|
6
|
+
branch: "├── ",
|
|
7
|
+
lastBranch: "└── ",
|
|
8
|
+
vertical: "│ ",
|
|
9
|
+
sanitizeLabels: true,
|
|
10
|
+
};
|
|
11
|
+
function asRecord(value) {
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function stripAnsiSequences(value) {
|
|
15
|
+
let output = "";
|
|
16
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
17
|
+
const code = value.charCodeAt(i);
|
|
18
|
+
// CSI escape sequence: ESC [ ... final-byte
|
|
19
|
+
if (code === 0x1b && value.charCodeAt(i + 1) === 0x5b) {
|
|
20
|
+
i += 2;
|
|
21
|
+
while (i < value.length) {
|
|
22
|
+
const next = value.charCodeAt(i);
|
|
23
|
+
if (next >= 0x40 && next <= 0x7e) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
i += 1;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
output += value[i] ?? "";
|
|
31
|
+
}
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
function sanitizeControlCharacters(value) {
|
|
35
|
+
let output = "";
|
|
36
|
+
for (const char of value) {
|
|
37
|
+
const code = char.charCodeAt(0);
|
|
38
|
+
if (code < 32 || code === 127) {
|
|
39
|
+
output += `\\x${code.toString(16).padStart(2, "0")}`;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
output += char;
|
|
43
|
+
}
|
|
44
|
+
return output;
|
|
45
|
+
}
|
|
46
|
+
function sanitizeLabel(label) {
|
|
47
|
+
return sanitizeControlCharacters(stripAnsiSequences(label));
|
|
48
|
+
}
|
|
49
|
+
function readChildren(node, childrenKey) {
|
|
50
|
+
const value = asRecord(node)[childrenKey];
|
|
51
|
+
return Array.isArray(value) ? value : [];
|
|
52
|
+
}
|
|
53
|
+
function readLabel(node, labelKey, sanitize) {
|
|
54
|
+
const value = asRecord(node)[labelKey];
|
|
55
|
+
const label = value == null ? "" : String(value);
|
|
56
|
+
return sanitize ? sanitizeLabel(label) : label;
|
|
57
|
+
}
|
|
58
|
+
function walk(nodes, options, parentLastFlags, out) {
|
|
59
|
+
for (const [i, node] of nodes.entries()) {
|
|
60
|
+
const isLast = i === nodes.length - 1;
|
|
61
|
+
let row = "";
|
|
62
|
+
for (const parentIsLast of parentLastFlags) {
|
|
63
|
+
row += parentIsLast ? options.indent : options.vertical;
|
|
64
|
+
}
|
|
65
|
+
row += isLast ? options.lastBranch : options.branch;
|
|
66
|
+
row += readLabel(node, options.labelKey, options.sanitizeLabels);
|
|
67
|
+
out.push(row);
|
|
68
|
+
const children = readChildren(node, options.childrenKey);
|
|
69
|
+
if (children.length > 0) {
|
|
70
|
+
walk(children, options, [...parentLastFlags, isLast], out);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function getTreeLines(data = [], renderOptions = {}) {
|
|
75
|
+
const options = { ...DEFAULT_RENDER_OPTIONS, ...renderOptions };
|
|
76
|
+
if (data.length === 0)
|
|
77
|
+
return [];
|
|
78
|
+
const lines = [options.rootMarker];
|
|
79
|
+
walk(data, options, [], lines);
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
export function getStringTree(data = [], renderOptions = {}) {
|
|
83
|
+
return getTreeLines(data, renderOptions).join("\n");
|
|
84
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type NodeType = "file" | "directory" | "symlink";
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
name: string;
|
|
4
|
+
path?: string;
|
|
5
|
+
type?: NodeType;
|
|
6
|
+
children?: TreeNode[];
|
|
7
|
+
}
|
|
8
|
+
export interface RenderTreeOptions {
|
|
9
|
+
labelKey?: string;
|
|
10
|
+
childrenKey?: string;
|
|
11
|
+
rootMarker?: string;
|
|
12
|
+
indent?: string;
|
|
13
|
+
branch?: string;
|
|
14
|
+
lastBranch?: string;
|
|
15
|
+
vertical?: string;
|
|
16
|
+
sanitizeLabels?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export type SortOrder = "asc" | "desc" | "none";
|
|
19
|
+
export interface GetFileTreeOptions {
|
|
20
|
+
dir: string;
|
|
21
|
+
ignore?: string | string[];
|
|
22
|
+
level?: number;
|
|
23
|
+
includeHidden?: boolean;
|
|
24
|
+
followSymlinks?: boolean;
|
|
25
|
+
sort?: SortOrder;
|
|
26
|
+
gitignore?: boolean;
|
|
27
|
+
maxNodes?: number;
|
|
28
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@youbamj/tree",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Render directory and object trees in a clean, modern TypeScript package with CLI support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "youbamj",
|
|
8
|
+
"homepage": "https://github.com/youbamj/tree#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/youbamj/tree.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/youbamj/tree/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["tree", "cli", "directory", "typescript", "bun"],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"ytree": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"dev": "bun run src/cli.ts",
|
|
37
|
+
"test": "bun test",
|
|
38
|
+
"check:types": "tsc --noEmit",
|
|
39
|
+
"lint": "biome check .",
|
|
40
|
+
"format": "biome format --write .",
|
|
41
|
+
"check": "bun run check:types && bun run lint",
|
|
42
|
+
"precommit": "lint-staged && bun run check:types",
|
|
43
|
+
"prepare": "husky",
|
|
44
|
+
"changeset": "changeset",
|
|
45
|
+
"version-packages": "changeset version",
|
|
46
|
+
"release": "npm publish --provenance"
|
|
47
|
+
},
|
|
48
|
+
"lint-staged": {
|
|
49
|
+
"*.{ts,tsx,js,jsx,json,md}": ["biome check --write"]
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"commander": "^14.0.1",
|
|
53
|
+
"ignore": "^7.0.5"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@biomejs/biome": "^1.9.4",
|
|
57
|
+
"@changesets/cli": "^2.29.7",
|
|
58
|
+
"@types/bun": "latest",
|
|
59
|
+
"husky": "^9.1.7",
|
|
60
|
+
"lint-staged": "^16.2.0",
|
|
61
|
+
"typescript": "^5.6.3"
|
|
62
|
+
}
|
|
63
|
+
}
|