@lakindu_perera/toren 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.
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/bin/toren.js +203 -0
- package/package.json +39 -0
- package/src/lifecycle.js +124 -0
- package/src/renderers/console-renderer.js +218 -0
- package/src/renderers/html-renderer.js +913 -0
- package/src/renderers/index.js +41 -0
- package/src/renderers/json-renderer.js +54 -0
- package/src/renderers/markdown-renderer.js +328 -0
- package/src/renderers/tree-renderer.js +67 -0
- package/src/scanner/scan.js +341 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Renderer Registry
|
|
3
|
+
*
|
|
4
|
+
* Maps format-name strings to their render functions.
|
|
5
|
+
*
|
|
6
|
+
* ┌─────────────────────────────────────────────────────────┐
|
|
7
|
+
* │ How to add a new output format: │
|
|
8
|
+
* │ │
|
|
9
|
+
* │ 1. Create src/renderers/<format>-renderer.js │
|
|
10
|
+
* │ and export: render(result, options?) => void │
|
|
11
|
+
* │ │
|
|
12
|
+
* │ 2. Import it here and add one line to RENDERERS. │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ No changes to bin/toren.js or scan.js are needed. │
|
|
15
|
+
* └─────────────────────────────────────────────────────────┘
|
|
16
|
+
*
|
|
17
|
+
* @module renderers/index
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { render as renderConsole } from './console-renderer.js';
|
|
21
|
+
import { render as renderJson } from './json-renderer.js';
|
|
22
|
+
import { render as renderMarkdown } from './markdown-renderer.js';
|
|
23
|
+
import { render as renderHtml } from './html-renderer.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Registry of all available output renderers.
|
|
27
|
+
*
|
|
28
|
+
* Keys — format names accepted by the `--format` flag.
|
|
29
|
+
* Values — render functions with the signature:
|
|
30
|
+
* render(result: ScanResult, options?: { cwd?: string }) => void
|
|
31
|
+
*
|
|
32
|
+
* @type {Record<string, function(import('../scanner/scan.js').ScanResult, object=): void>}
|
|
33
|
+
*/
|
|
34
|
+
const renderers = {
|
|
35
|
+
console: renderConsole,
|
|
36
|
+
json: renderJson,
|
|
37
|
+
markdown: renderMarkdown,
|
|
38
|
+
html: renderHtml,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default renderers;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — JSON Renderer
|
|
3
|
+
*
|
|
4
|
+
* Consumes a ScanResult and produces a clean JSON object for programmatic use.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
function mapTree(node) {
|
|
10
|
+
if (node.type === 'directory') {
|
|
11
|
+
return {
|
|
12
|
+
type: 'folder',
|
|
13
|
+
name: node.name,
|
|
14
|
+
children: (node.children || []).map(mapTree)
|
|
15
|
+
};
|
|
16
|
+
} else {
|
|
17
|
+
return {
|
|
18
|
+
type: 'file',
|
|
19
|
+
name: node.name
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function render(result, options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
rootPath,
|
|
27
|
+
projectType,
|
|
28
|
+
entryPoints,
|
|
29
|
+
tree,
|
|
30
|
+
flatFiles,
|
|
31
|
+
totalFolders,
|
|
32
|
+
scanDurationMs,
|
|
33
|
+
} = result;
|
|
34
|
+
|
|
35
|
+
const cwd = options.cwd ?? process.cwd();
|
|
36
|
+
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
37
|
+
|
|
38
|
+
const output = {
|
|
39
|
+
project: {
|
|
40
|
+
path: relRoot,
|
|
41
|
+
type: projectType,
|
|
42
|
+
framework: projectType,
|
|
43
|
+
},
|
|
44
|
+
summary: {
|
|
45
|
+
totalFiles: flatFiles.length,
|
|
46
|
+
totalFolders,
|
|
47
|
+
scanDurationMs: Math.round(scanDurationMs),
|
|
48
|
+
},
|
|
49
|
+
entryPoints,
|
|
50
|
+
structure: (tree.children || []).map(mapTree)
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
console.log(JSON.stringify(output, null, 2));
|
|
54
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Markdown Renderer
|
|
3
|
+
*
|
|
4
|
+
* Consumes a {@link ScanResult} and writes a polished, GitHub-flavored
|
|
5
|
+
* Markdown report to stdout.
|
|
6
|
+
*
|
|
7
|
+
* Design contract (mirrors all other renderers):
|
|
8
|
+
* - Accepts a ScanResult and an optional options object.
|
|
9
|
+
* - Never scans files or modifies the data it receives.
|
|
10
|
+
* - Produces only plain Markdown — no ANSI codes, no HTML, no emoji.
|
|
11
|
+
* - All output goes to stdout so users can redirect freely:
|
|
12
|
+
* toren --format markdown > PROJECT_REPORT.md
|
|
13
|
+
*
|
|
14
|
+
* Sections (in order):
|
|
15
|
+
* 1. Title
|
|
16
|
+
* 2. Project Summary (table)
|
|
17
|
+
* 3. Entry Points (list)
|
|
18
|
+
* 4. Folder Structure (fenced code block)
|
|
19
|
+
* 5. Statistics (table)
|
|
20
|
+
* 6. Scan Information
|
|
21
|
+
*
|
|
22
|
+
* @module renderers/markdown-renderer
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Plain-text tree builder (Markdown-safe, no ANSI, no emoji)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build an in-memory nested tree from a flat list of relative file paths.
|
|
33
|
+
* This avoids repeating the walk already done by the scanner while keeping
|
|
34
|
+
* the markdown renderer completely self-contained.
|
|
35
|
+
*
|
|
36
|
+
* @param {string[]} flatFiles - Relative file paths produced by scan()
|
|
37
|
+
* @returns {{ name: string, type: 'directory'|'file', children: object }[]}
|
|
38
|
+
*/
|
|
39
|
+
function buildTree(flatFiles) {
|
|
40
|
+
/** @type {{ type: string, children: Record<string, object> }} */
|
|
41
|
+
const root = { type: 'directory', children: {} };
|
|
42
|
+
|
|
43
|
+
for (const filePath of flatFiles) {
|
|
44
|
+
const parts = filePath.split(/[/\\]/).filter(Boolean);
|
|
45
|
+
let node = root;
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < parts.length; i++) {
|
|
48
|
+
const part = parts[i];
|
|
49
|
+
const isLeaf = i === parts.length - 1;
|
|
50
|
+
|
|
51
|
+
if (!node.children[part]) {
|
|
52
|
+
node.children[part] = isLeaf
|
|
53
|
+
? { type: 'file', name: part }
|
|
54
|
+
: { type: 'directory', name: part, children: {} };
|
|
55
|
+
}
|
|
56
|
+
node = node.children[part];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Recursively serialise a tree node into classic tree-connector lines.
|
|
65
|
+
* Output is plain ASCII — safe for any Markdown renderer.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} node - Current tree node
|
|
68
|
+
* @param {string} prefix - Accumulated prefix string for indentation
|
|
69
|
+
* @param {boolean} isLast - Whether this node is the last sibling
|
|
70
|
+
* @param {string[]} lines - Accumulator for output lines
|
|
71
|
+
* @param {number} depth - Current recursion depth
|
|
72
|
+
* @param {number} maxDepth - Maximum depth to render
|
|
73
|
+
*/
|
|
74
|
+
function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
|
|
75
|
+
if (depth >= maxDepth) return;
|
|
76
|
+
|
|
77
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
78
|
+
const childPad = isLast ? ' ' : '│ ';
|
|
79
|
+
const label = node.type === 'directory' ? `${node.name}/` : node.name;
|
|
80
|
+
|
|
81
|
+
lines.push(`${prefix}${connector}${label}`);
|
|
82
|
+
|
|
83
|
+
if (node.type === 'directory') {
|
|
84
|
+
const children = Object.values(node.children || {}).sort((a, b) => {
|
|
85
|
+
// Directories before files, then alphabetically.
|
|
86
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
87
|
+
return a.name.localeCompare(b.name);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Truncate deep directories with an ellipsis rather than cutting silently.
|
|
91
|
+
if (depth === maxDepth - 1 && children.length > 0) {
|
|
92
|
+
lines.push(`${prefix}${childPad}└── ...`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < children.length; i++) {
|
|
97
|
+
serializeNode(
|
|
98
|
+
children[i],
|
|
99
|
+
prefix + childPad,
|
|
100
|
+
i === children.length - 1,
|
|
101
|
+
lines,
|
|
102
|
+
depth + 1,
|
|
103
|
+
maxDepth,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convert a flat file list into a Markdown-safe tree string.
|
|
111
|
+
*
|
|
112
|
+
* @param {string[]} flatFiles
|
|
113
|
+
* @param {string} rootName - Display name for the root node (e.g. "my-app/")
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function buildTreeString(flatFiles, rootName) {
|
|
117
|
+
if (flatFiles.length === 0) return '';
|
|
118
|
+
|
|
119
|
+
const root = buildTree(flatFiles);
|
|
120
|
+
const lines = [`${rootName}/`];
|
|
121
|
+
const children = Object.values(root.children).sort((a, b) => {
|
|
122
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
123
|
+
return a.name.localeCompare(b.name);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < children.length; i++) {
|
|
127
|
+
serializeNode(children[i], '', i === children.length - 1, lines);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return lines.join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Formatting helpers
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Format a scan duration in milliseconds to a human-readable string.
|
|
139
|
+
*
|
|
140
|
+
* @param {number} ms
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function formatDuration(ms) {
|
|
144
|
+
if (ms < 1) return '< 1 ms';
|
|
145
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
|
146
|
+
return `${Math.round(ms)} ms`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build a Markdown table from an array of two-element [label, value] pairs.
|
|
151
|
+
* Column widths are padded to keep the source Markdown tidy.
|
|
152
|
+
*
|
|
153
|
+
* @param {[string, string][]} rows
|
|
154
|
+
* @returns {string}
|
|
155
|
+
*/
|
|
156
|
+
function markdownTable(rows) {
|
|
157
|
+
const colA = Math.max(8, ...rows.map(([k]) => k.length));
|
|
158
|
+
const colB = Math.max(5, ...rows.map(([, v]) => String(v).length));
|
|
159
|
+
|
|
160
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
161
|
+
const hr = `|${'-'.repeat(colA + 2)}|${'-'.repeat(colB + 2)}|`;
|
|
162
|
+
const head = `| ${pad('Property', colA)} | ${pad('Value', colB)} |`;
|
|
163
|
+
const body = rows.map(([k, v]) => `| ${pad(k, colA)} | ${pad(v, colB)} |`);
|
|
164
|
+
|
|
165
|
+
return [head, hr, ...body].join('\n');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build a right-aligned Markdown table (used for statistics).
|
|
170
|
+
*
|
|
171
|
+
* @param {[string, string|number][]} rows
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function statsTable(rows) {
|
|
175
|
+
const colA = Math.max(6, ...rows.map(([k]) => k.length));
|
|
176
|
+
const colB = Math.max(5, ...rows.map(([, v]) => String(v).length));
|
|
177
|
+
|
|
178
|
+
const padL = (s, n) => String(s).padEnd(n);
|
|
179
|
+
const padR = (s, n) => String(s).padStart(n);
|
|
180
|
+
const hr = `|${'-'.repeat(colA + 2)}|${'-'.repeat(colB + 1)}:|`;
|
|
181
|
+
const head = `| ${padL('Metric', colA)} | ${padR('Value', colB)} |`;
|
|
182
|
+
const body = rows.map(([k, v]) => `| ${padL(k, colA)} | ${padR(v, colB)} |`);
|
|
183
|
+
|
|
184
|
+
return [head, hr, ...body].join('\n');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Section builders (private — one function per report section)
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/** @param {string[]} out */
|
|
192
|
+
function sectionTitle(out) {
|
|
193
|
+
out.push('# Project Analysis Report');
|
|
194
|
+
out.push('');
|
|
195
|
+
out.push('Generated by **Toren** — Codebase Onboarding Intelligence');
|
|
196
|
+
out.push('');
|
|
197
|
+
out.push('---');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {string[]} out
|
|
202
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
203
|
+
* @param {string} relRoot
|
|
204
|
+
*/
|
|
205
|
+
function sectionSummary(out, result, relRoot) {
|
|
206
|
+
const { projectType, flatFiles, totalFolders } = result;
|
|
207
|
+
|
|
208
|
+
out.push('');
|
|
209
|
+
out.push('## Project Summary');
|
|
210
|
+
out.push('');
|
|
211
|
+
out.push(markdownTable([
|
|
212
|
+
['Project Type', projectType],
|
|
213
|
+
['Scan Path', relRoot],
|
|
214
|
+
['Total Files', String(flatFiles.length)],
|
|
215
|
+
['Total Folders', String(totalFolders)],
|
|
216
|
+
]));
|
|
217
|
+
out.push('');
|
|
218
|
+
out.push('---');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string[]} out
|
|
223
|
+
* @param {string[]} entryPoints
|
|
224
|
+
*/
|
|
225
|
+
function sectionEntryPoints(out, entryPoints) {
|
|
226
|
+
out.push('');
|
|
227
|
+
out.push('## Entry Points');
|
|
228
|
+
out.push('');
|
|
229
|
+
|
|
230
|
+
if (entryPoints.length === 0) {
|
|
231
|
+
out.push('No entry points detected.');
|
|
232
|
+
} else {
|
|
233
|
+
for (const ep of entryPoints) {
|
|
234
|
+
out.push(`- \`${ep}\``);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
out.push('');
|
|
239
|
+
out.push('---');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {string[]} out
|
|
244
|
+
* @param {string[]} flatFiles
|
|
245
|
+
* @param {string} rootName
|
|
246
|
+
*/
|
|
247
|
+
function sectionFolderStructure(out, flatFiles, rootName) {
|
|
248
|
+
out.push('');
|
|
249
|
+
out.push('## Folder Structure');
|
|
250
|
+
out.push('');
|
|
251
|
+
|
|
252
|
+
if (flatFiles.length === 0) {
|
|
253
|
+
out.push('No files scanned.');
|
|
254
|
+
} else {
|
|
255
|
+
const tree = buildTreeString(flatFiles, rootName);
|
|
256
|
+
out.push('```text');
|
|
257
|
+
out.push(tree);
|
|
258
|
+
out.push('```');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
out.push('');
|
|
262
|
+
out.push('---');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @param {string[]} out
|
|
267
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
268
|
+
*/
|
|
269
|
+
function sectionStatistics(out, result) {
|
|
270
|
+
const { flatFiles, totalFolders, scanDurationMs } = result;
|
|
271
|
+
|
|
272
|
+
out.push('');
|
|
273
|
+
out.push('## Statistics');
|
|
274
|
+
out.push('');
|
|
275
|
+
out.push(statsTable([
|
|
276
|
+
['Files', flatFiles.length],
|
|
277
|
+
['Folders', totalFolders],
|
|
278
|
+
['Scan Duration', formatDuration(scanDurationMs)],
|
|
279
|
+
]));
|
|
280
|
+
out.push('');
|
|
281
|
+
out.push('---');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* @param {string[]} out
|
|
286
|
+
*/
|
|
287
|
+
function sectionScanInfo(out) {
|
|
288
|
+
out.push('');
|
|
289
|
+
out.push('## Scan Information');
|
|
290
|
+
out.push('');
|
|
291
|
+
out.push('Generated by **Toren**');
|
|
292
|
+
out.push('');
|
|
293
|
+
out.push('Output Format: Markdown');
|
|
294
|
+
out.push('');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
// Public API
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Render a ScanResult as a GitHub-flavored Markdown report to stdout.
|
|
303
|
+
*
|
|
304
|
+
* All sections are built into an in-memory string array and joined once
|
|
305
|
+
* at the end — a single `console.log` call keeps stdout writes atomic.
|
|
306
|
+
*
|
|
307
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
308
|
+
* @param {{ cwd?: string }} [options]
|
|
309
|
+
*/
|
|
310
|
+
export function render(result, options = {}) {
|
|
311
|
+
const { rootPath, entryPoints, flatFiles } = result;
|
|
312
|
+
|
|
313
|
+
const cwd = options.cwd ?? process.cwd();
|
|
314
|
+
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
315
|
+
const rootName = path.basename(rootPath) || relRoot;
|
|
316
|
+
|
|
317
|
+
/** @type {string[]} */
|
|
318
|
+
const out = [];
|
|
319
|
+
|
|
320
|
+
sectionTitle(out);
|
|
321
|
+
sectionSummary(out, result, relRoot);
|
|
322
|
+
sectionEntryPoints(out, entryPoints);
|
|
323
|
+
sectionFolderStructure(out, flatFiles, rootName);
|
|
324
|
+
sectionStatistics(out, result);
|
|
325
|
+
sectionScanInfo(out);
|
|
326
|
+
|
|
327
|
+
console.log(out.join('\n'));
|
|
328
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Toren — Tree Renderer
|
|
3
|
+
*
|
|
4
|
+
* Formats a flat array of file paths into a clean CLI tree view.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a flat file list into a hierarchical tree structure and renders it.
|
|
9
|
+
*
|
|
10
|
+
* @param {Array<string>} flatFiles - Array of relative file paths
|
|
11
|
+
* @returns {{ output: string, totalFilesShown: number }}
|
|
12
|
+
*/
|
|
13
|
+
export function renderTree(flatFiles) {
|
|
14
|
+
const root = { type: 'directory', children: {} };
|
|
15
|
+
|
|
16
|
+
// 1. Build the tree structure
|
|
17
|
+
for (const p of flatFiles) {
|
|
18
|
+
const parts = p.split(/[/\\]/).filter(Boolean);
|
|
19
|
+
|
|
20
|
+
let current = root;
|
|
21
|
+
for (let i = 0; i < parts.length; i++) {
|
|
22
|
+
const part = parts[i];
|
|
23
|
+
const isFile = i === parts.length - 1;
|
|
24
|
+
|
|
25
|
+
if (!current.children[part]) {
|
|
26
|
+
current.children[part] = isFile
|
|
27
|
+
? { type: 'file', name: part }
|
|
28
|
+
: { type: 'directory', name: part, children: {} };
|
|
29
|
+
}
|
|
30
|
+
current = current.children[part];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const state = { output: [], filesCount: 0 };
|
|
35
|
+
|
|
36
|
+
// 2. Traverse and format the tree (depth limit 4)
|
|
37
|
+
function traverse(node, depth) {
|
|
38
|
+
if (depth >= 4) return;
|
|
39
|
+
|
|
40
|
+
const children = Object.values(node.children || {}).sort((a, b) => {
|
|
41
|
+
// Directories first, then files
|
|
42
|
+
if (a.type !== b.type) {
|
|
43
|
+
return a.type === 'directory' ? -1 : 1;
|
|
44
|
+
}
|
|
45
|
+
// Alphabetical sort
|
|
46
|
+
return a.name.localeCompare(b.name);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for (const child of children) {
|
|
50
|
+
const indent = ' '.repeat(depth);
|
|
51
|
+
if (child.type === 'directory') {
|
|
52
|
+
state.output.push(`${indent}📁 ${child.name}`);
|
|
53
|
+
traverse(child, depth + 1);
|
|
54
|
+
} else {
|
|
55
|
+
state.output.push(`${indent}└── ${child.name}`);
|
|
56
|
+
state.filesCount++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
traverse(root, 0);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
output: state.output.join('\n'),
|
|
65
|
+
totalFilesShown: state.filesCount
|
|
66
|
+
};
|
|
67
|
+
}
|