@lakindu_perera/toren 1.0.5 → 1.0.7
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/README.md +72 -52
- package/bin/toren.js +63 -44
- package/package.json +1 -1
- package/src/focused-output.js +91 -41
- package/src/renderers/console-renderer.js +118 -50
- package/src/renderers/html-renderer.js +85 -68
- package/src/renderers/json-renderer.js +119 -23
- package/src/renderers/markdown-renderer.js +61 -98
- package/src/scanner/scan.js +23 -4
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Consumes a {@link ScanResult} and produces styled terminal output.
|
|
5
5
|
*
|
|
6
6
|
* Design contract:
|
|
7
|
-
* -
|
|
7
|
+
* - Presentation-only. Every value rendered comes from ScanResult.
|
|
8
|
+
* Presentation-level derivations (e.g. frameworks list from projectType) are
|
|
9
|
+
* permitted here; business logic is not.
|
|
8
10
|
* - No imports from the scanner or any domain module.
|
|
9
11
|
* - Stateless: render() may be called multiple times safely.
|
|
10
12
|
* - The shape expected here matches the ScanResult typedef in scan.js.
|
|
@@ -42,6 +44,47 @@ const C = {
|
|
|
42
44
|
/** Maximum files shown in the structure preview. */
|
|
43
45
|
const PREVIEW_LIMIT = 20;
|
|
44
46
|
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Cross-platform capability detection
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
function shouldEnableColors() {
|
|
52
|
+
if ('FORCE_COLOR' in process.env) {
|
|
53
|
+
return process.env.FORCE_COLOR !== '0' && process.env.FORCE_COLOR !== 'false';
|
|
54
|
+
}
|
|
55
|
+
if ('NO_COLOR' in process.env) return false;
|
|
56
|
+
if (!process.stdout || !process.stdout.isTTY) return false;
|
|
57
|
+
if (process.env.TERM === 'dumb') return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isUnicodeSupported() {
|
|
62
|
+
if (process.platform !== 'win32') {
|
|
63
|
+
return process.env.TERM !== 'linux';
|
|
64
|
+
}
|
|
65
|
+
return Boolean(
|
|
66
|
+
process.env.CI ||
|
|
67
|
+
process.env.WT_SESSION ||
|
|
68
|
+
process.env.TERMINUS_SUBLIME ||
|
|
69
|
+
process.env.ConEmuTask === '{cmd::Cmder}' ||
|
|
70
|
+
process.env.TERM_PROGRAM === 'Terminus-Sublime' ||
|
|
71
|
+
process.env.TERM_PROGRAM === 'vscode' ||
|
|
72
|
+
process.env.TERM === 'xterm-256color' ||
|
|
73
|
+
process.env.TERM === 'alacritty' ||
|
|
74
|
+
process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const useColors = shouldEnableColors();
|
|
79
|
+
const useUnicode = isUnicodeSupported();
|
|
80
|
+
|
|
81
|
+
const CHARS = {
|
|
82
|
+
dash: useUnicode ? '─' : '-',
|
|
83
|
+
corner: useUnicode ? '└── ' : '\\-- ',
|
|
84
|
+
tee: useUnicode ? '├── ' : '+-- ',
|
|
85
|
+
pipe: useUnicode ? '│ ' : '| ',
|
|
86
|
+
};
|
|
87
|
+
|
|
45
88
|
// ---------------------------------------------------------------------------
|
|
46
89
|
// Low-level paint / layout helpers (private to this module)
|
|
47
90
|
// ---------------------------------------------------------------------------
|
|
@@ -53,27 +96,19 @@ const PREVIEW_LIMIT = 20;
|
|
|
53
96
|
* @returns {string}
|
|
54
97
|
*/
|
|
55
98
|
function paint(text, ...codes) {
|
|
99
|
+
if (!useColors) return text;
|
|
56
100
|
return `${codes.join('')}${text}${C.reset}`;
|
|
57
101
|
}
|
|
58
102
|
|
|
59
103
|
/**
|
|
60
|
-
* Print a
|
|
61
|
-
* @param {string} [char='─']
|
|
62
|
-
*/
|
|
63
|
-
function divider(char = '─') {
|
|
64
|
-
const width = Math.min(process.stdout.columns ?? 72, 80);
|
|
65
|
-
console.log(paint(char.repeat(width), C.dim));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Print a titled section header followed by a divider.
|
|
70
|
-
* @param {string} emoji
|
|
104
|
+
* Print a titled section header followed by a matched-length divider.
|
|
71
105
|
* @param {string} title
|
|
72
106
|
*/
|
|
73
|
-
function section(
|
|
107
|
+
function section(title) {
|
|
108
|
+
const cleanTitle = title.replace(/\x1b\[[0-9;]*m/g, '');
|
|
109
|
+
console.log(paint(title, C.bold, C.white));
|
|
110
|
+
console.log(paint(CHARS.dash.repeat(cleanTitle.length), C.dim));
|
|
74
111
|
console.log('');
|
|
75
|
-
console.log(`${emoji} ${paint(title, C.bold, C.white)}`);
|
|
76
|
-
divider();
|
|
77
112
|
}
|
|
78
113
|
|
|
79
114
|
/**
|
|
@@ -84,7 +119,32 @@ function section(emoji, title) {
|
|
|
84
119
|
*/
|
|
85
120
|
function row(label, value, ...valueCodes) {
|
|
86
121
|
const coloured = valueCodes.length ? paint(value, ...valueCodes) : value;
|
|
87
|
-
console.log(
|
|
122
|
+
console.log(`${paint(label, C.dim)} ${coloured}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format a scan duration into a human-readable string.
|
|
127
|
+
* Mirrors the formatDuration helpers in markdown-renderer and html-renderer.
|
|
128
|
+
* @param {number} ms
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
function formatDuration(ms) {
|
|
132
|
+
if (ms < 1) return '< 1 ms';
|
|
133
|
+
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
|
134
|
+
return `${Math.round(ms)} ms`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Derive a frameworks array from the projectType string.
|
|
139
|
+
* Returns [] when no specific framework is detected (projectType is falsy or 'Unknown').
|
|
140
|
+
* Mirrors the identical derivation in json-renderer.js — both must stay in sync.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} projectType
|
|
143
|
+
* @returns {string[]}
|
|
144
|
+
*/
|
|
145
|
+
function deriveFrameworks(projectType) {
|
|
146
|
+
if (!projectType || projectType === 'Unknown') return [];
|
|
147
|
+
return [projectType];
|
|
88
148
|
}
|
|
89
149
|
|
|
90
150
|
// ---------------------------------------------------------------------------
|
|
@@ -107,21 +167,21 @@ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth
|
|
|
107
167
|
if (counter.maxReached) return;
|
|
108
168
|
if (depth >= maxDepth) return;
|
|
109
169
|
|
|
110
|
-
const connector = isLast ?
|
|
111
|
-
const extension = isLast ? ' ' :
|
|
170
|
+
const connector = isLast ? CHARS.corner : CHARS.tee;
|
|
171
|
+
const extension = isLast ? ' ' : CHARS.pipe;
|
|
112
172
|
|
|
113
173
|
if (node.type === 'directory') {
|
|
114
|
-
console.log(`${prefix}${connector}${paint(`${node.name}
|
|
174
|
+
console.log(`${prefix}${connector}${paint(`${node.name}${path.sep}`, C.bold, C.blue)}`);
|
|
115
175
|
const children = node.children ?? [];
|
|
116
176
|
|
|
117
177
|
if (depth === maxDepth - 1 && children.length > 0) {
|
|
118
|
-
console.log(`${prefix}${extension}
|
|
178
|
+
console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
|
|
119
179
|
return;
|
|
120
180
|
}
|
|
121
181
|
|
|
122
182
|
for (let i = 0; i < children.length; i++) {
|
|
123
183
|
if (counter.count >= limit) {
|
|
124
|
-
console.log(`${prefix}${extension}
|
|
184
|
+
console.log(`${prefix}${extension}${CHARS.corner}${paint('...', C.dim)}`);
|
|
125
185
|
counter.maxReached = true;
|
|
126
186
|
break;
|
|
127
187
|
}
|
|
@@ -137,11 +197,6 @@ function renderTree(node, prefix, isLast, counter, limit = PREVIEW_LIMIT, depth
|
|
|
137
197
|
}
|
|
138
198
|
}
|
|
139
199
|
|
|
140
|
-
function formatDuration(ms) {
|
|
141
|
-
if (ms < 1) return '< 1 ms';
|
|
142
|
-
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
|
143
|
-
return `${Math.round(ms)} ms`;
|
|
144
|
-
}
|
|
145
200
|
|
|
146
201
|
// ---------------------------------------------------------------------------
|
|
147
202
|
// Banner (private)
|
|
@@ -151,8 +206,7 @@ function printBanner() {
|
|
|
151
206
|
const name = paint('Toren', C.bold, C.cyan);
|
|
152
207
|
const version = paint(`v${pkg.version}`, C.dim);
|
|
153
208
|
const tagline = paint('Codebase Onboarding Intelligence', C.dim);
|
|
154
|
-
console.log(
|
|
155
|
-
console.log(` ${name} ${version} — ${tagline}`);
|
|
209
|
+
console.log(`${name} ${version} — ${tagline}`);
|
|
156
210
|
}
|
|
157
211
|
|
|
158
212
|
// ---------------------------------------------------------------------------
|
|
@@ -162,8 +216,8 @@ function printBanner() {
|
|
|
162
216
|
/**
|
|
163
217
|
* Render a ScanResult to the terminal.
|
|
164
218
|
*
|
|
165
|
-
* All sections read
|
|
166
|
-
*
|
|
219
|
+
* All sections read from the ScanResult. Presentation-level derivations
|
|
220
|
+
* (e.g. frameworks list) are computed here; no business logic is added.
|
|
167
221
|
*
|
|
168
222
|
* @param {import('../scanner/scan.js').ScanResult} result
|
|
169
223
|
* @param {{ cwd?: string }} [options]
|
|
@@ -189,55 +243,70 @@ export function render(result, options = {}) {
|
|
|
189
243
|
console.log('');
|
|
190
244
|
|
|
191
245
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
192
|
-
section('
|
|
246
|
+
section('Project Summary');
|
|
193
247
|
row('Path: ', paint(relRoot, C.cyan));
|
|
194
248
|
row('Project type: ', paint(projectType, C.bold, C.green));
|
|
195
249
|
row('Total files: ', paint(String(flatFiles.length), C.yellow));
|
|
196
250
|
row('Total folders:', paint(String(totalFolders), C.yellow));
|
|
197
|
-
|
|
251
|
+
console.log('');
|
|
252
|
+
|
|
253
|
+
// ── Frameworks ────────────────────────────────────────────────────────────
|
|
254
|
+
section('Frameworks');
|
|
255
|
+
const frameworks = deriveFrameworks(projectType);
|
|
256
|
+
if (frameworks.length === 0) {
|
|
257
|
+
console.log(paint('No frameworks detected.', C.dim));
|
|
258
|
+
} else {
|
|
259
|
+
for (const fw of frameworks) {
|
|
260
|
+
console.log(paint(fw, C.white));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
console.log('');
|
|
198
264
|
|
|
199
265
|
// ── Entry Points ──────────────────────────────────────────────────────────
|
|
200
|
-
section('
|
|
266
|
+
section('Entry Points');
|
|
201
267
|
if (entryPoints.length === 0) {
|
|
202
|
-
console.log(paint('
|
|
268
|
+
console.log(paint('No entry points detected.', C.dim));
|
|
203
269
|
} else {
|
|
204
270
|
for (const ep of entryPoints) {
|
|
205
|
-
console.log(
|
|
271
|
+
console.log(paint(ep, C.white));
|
|
206
272
|
}
|
|
207
273
|
}
|
|
274
|
+
console.log('');
|
|
208
275
|
|
|
209
276
|
// ── Configuration Files ───────────────────────────────────────────────────
|
|
210
|
-
section('
|
|
277
|
+
section('Configuration Files');
|
|
211
278
|
if (configs.length === 0) {
|
|
212
|
-
console.log(paint('
|
|
279
|
+
console.log(paint('No configuration files detected.', C.dim));
|
|
213
280
|
} else {
|
|
214
281
|
for (const c of configs) {
|
|
215
|
-
console.log(
|
|
282
|
+
console.log(paint(c, C.white));
|
|
216
283
|
}
|
|
217
284
|
}
|
|
285
|
+
console.log('');
|
|
218
286
|
|
|
219
287
|
// ── Package Scripts ───────────────────────────────────────────────────────
|
|
220
|
-
section('
|
|
288
|
+
section('Package Scripts');
|
|
221
289
|
if (scripts.length === 0) {
|
|
222
|
-
console.log(paint('
|
|
290
|
+
console.log(paint('No package scripts detected.', C.dim));
|
|
223
291
|
} else {
|
|
224
292
|
const maxNameLen = Math.max(...scripts.map(s => s.name.length));
|
|
225
293
|
for (const s of scripts) {
|
|
226
294
|
const paddedName = s.name.padEnd(maxNameLen, ' ');
|
|
227
|
-
console.log(
|
|
295
|
+
console.log(`${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
|
|
228
296
|
}
|
|
229
297
|
}
|
|
298
|
+
console.log('');
|
|
230
299
|
|
|
231
300
|
// ── Structure Preview ─────────────────────────────────────────────────────
|
|
232
|
-
section('
|
|
301
|
+
section('Folder Structure');
|
|
233
302
|
|
|
234
|
-
console.log(paint(`${tree.name || '.'}
|
|
303
|
+
console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
|
|
235
304
|
|
|
236
305
|
const counter = { count: 0, maxReached: false };
|
|
237
306
|
const children = tree.children ?? [];
|
|
238
307
|
for (let i = 0; i < children.length; i++) {
|
|
239
308
|
if (counter.count >= PREVIEW_LIMIT) {
|
|
240
|
-
console.log(
|
|
309
|
+
console.log(`${CHARS.corner}${paint('...', C.dim)}`);
|
|
241
310
|
break;
|
|
242
311
|
}
|
|
243
312
|
renderTree(children[i], '', i === children.length - 1, counter);
|
|
@@ -246,13 +315,12 @@ export function render(result, options = {}) {
|
|
|
246
315
|
|
|
247
316
|
if (flatFiles.length > PREVIEW_LIMIT) {
|
|
248
317
|
const hidden = flatFiles.length - PREVIEW_LIMIT;
|
|
249
|
-
console.log(paint(
|
|
318
|
+
console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
|
|
250
319
|
}
|
|
251
320
|
|
|
252
321
|
// ── Footer ────────────────────────────────────────────────────────────────
|
|
253
322
|
console.log('');
|
|
254
|
-
|
|
255
|
-
console.log(paint(' ✅ Scan complete.', C.green));
|
|
323
|
+
console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
|
|
256
324
|
console.log('');
|
|
257
325
|
}
|
|
258
326
|
|
|
@@ -264,8 +332,8 @@ export function render(result, options = {}) {
|
|
|
264
332
|
export function renderStructure(result) {
|
|
265
333
|
const { tree, flatFiles } = result;
|
|
266
334
|
|
|
267
|
-
|
|
268
|
-
console.log(paint(`${tree.name || '.'}
|
|
335
|
+
section('Folder Structure');
|
|
336
|
+
console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
|
|
269
337
|
|
|
270
338
|
const counter = { count: 0, maxReached: false };
|
|
271
339
|
const children = tree.children ?? [];
|
|
@@ -273,7 +341,7 @@ export function renderStructure(result) {
|
|
|
273
341
|
|
|
274
342
|
for (let i = 0; i < children.length; i++) {
|
|
275
343
|
if (counter.count >= limit) {
|
|
276
|
-
console.log(
|
|
344
|
+
console.log(`${CHARS.corner}${paint('...', C.dim)}`);
|
|
277
345
|
break;
|
|
278
346
|
}
|
|
279
347
|
renderTree(children[i], '', i === children.length - 1, counter, limit, 0, Infinity);
|
|
@@ -31,34 +31,6 @@ import path from 'node:path';
|
|
|
31
31
|
// Tree builder (plain-text, HTML-safe — same algorithm as markdown-renderer)
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* Build an in-memory nested tree from a flat list of relative file paths.
|
|
36
|
-
*
|
|
37
|
-
* @param {string[]} flatFiles - Relative file paths produced by scan()
|
|
38
|
-
* @returns {{ type: string, children: Record<string, object> }}
|
|
39
|
-
*/
|
|
40
|
-
function buildInternalTree(flatFiles) {
|
|
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
34
|
|
|
63
35
|
/**
|
|
64
36
|
* Recursively serialise a tree node into classic tree-connector lines.
|
|
@@ -80,10 +52,7 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
|
|
|
80
52
|
lines.push(`${prefix}${connector}${label}`);
|
|
81
53
|
|
|
82
54
|
if (node.type === 'directory') {
|
|
83
|
-
const children =
|
|
84
|
-
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
85
|
-
return a.name.localeCompare(b.name);
|
|
86
|
-
});
|
|
55
|
+
const children = node.children || [];
|
|
87
56
|
|
|
88
57
|
if (depth === maxDepth - 1 && children.length > 0) {
|
|
89
58
|
lines.push(`${prefix}${childPad}└── ...`);
|
|
@@ -104,21 +73,18 @@ function serializeNode(node, prefix, isLast, lines, depth = 0, maxDepth = 5) {
|
|
|
104
73
|
}
|
|
105
74
|
|
|
106
75
|
/**
|
|
107
|
-
* Convert a
|
|
76
|
+
* Convert a ScanResult tree into a plain-text tree string.
|
|
108
77
|
*
|
|
109
|
-
* @param {
|
|
78
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
110
79
|
* @param {string} rootName
|
|
80
|
+
* @param {number} totalFiles
|
|
111
81
|
* @returns {string}
|
|
112
82
|
*/
|
|
113
|
-
function buildTreeString(
|
|
114
|
-
if (
|
|
83
|
+
function buildTreeString(tree, rootName, totalFiles) {
|
|
84
|
+
if (totalFiles === 0) return 'No files scanned.';
|
|
115
85
|
|
|
116
|
-
const root = buildInternalTree(flatFiles);
|
|
117
86
|
const lines = [`${rootName}/`];
|
|
118
|
-
const children =
|
|
119
|
-
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
120
|
-
return a.name.localeCompare(b.name);
|
|
121
|
-
});
|
|
87
|
+
const children = tree.children || [];
|
|
122
88
|
|
|
123
89
|
for (let i = 0; i < children.length; i++) {
|
|
124
90
|
serializeNode(children[i], '', i === children.length - 1, lines);
|
|
@@ -148,12 +114,13 @@ function esc(value) {
|
|
|
148
114
|
|
|
149
115
|
/**
|
|
150
116
|
* Format a scan duration in milliseconds to a human-readable string.
|
|
117
|
+
* Returns plain text only — callers are responsible for HTML-escaping via esc().
|
|
151
118
|
*
|
|
152
119
|
* @param {number} ms
|
|
153
120
|
* @returns {string}
|
|
154
121
|
*/
|
|
155
122
|
function formatDuration(ms) {
|
|
156
|
-
if (ms < 1) return '
|
|
123
|
+
if (ms < 1) return '< 1 ms';
|
|
157
124
|
if (ms >= 1000) return `${(ms / 1000).toFixed(2)} s`;
|
|
158
125
|
return `${Math.round(ms)} ms`;
|
|
159
126
|
}
|
|
@@ -632,6 +599,19 @@ const icon = {
|
|
|
632
599
|
// Section builders (one function per report section)
|
|
633
600
|
// ---------------------------------------------------------------------------
|
|
634
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Derive a frameworks array from the projectType string.
|
|
604
|
+
* Returns [] when no specific framework is detected.
|
|
605
|
+
* Mirrors the identical derivation in console-renderer.js and json-renderer.js.
|
|
606
|
+
*
|
|
607
|
+
* @param {string} projectType
|
|
608
|
+
* @returns {string[]}
|
|
609
|
+
*/
|
|
610
|
+
function deriveFrameworks(projectType) {
|
|
611
|
+
if (!projectType || projectType === 'Unknown') return [];
|
|
612
|
+
return [projectType];
|
|
613
|
+
}
|
|
614
|
+
|
|
635
615
|
/**
|
|
636
616
|
* Render the gradient page header.
|
|
637
617
|
*
|
|
@@ -696,7 +676,7 @@ function renderSummaryCards(result) {
|
|
|
696
676
|
{
|
|
697
677
|
label: 'Scan Duration',
|
|
698
678
|
icon: icon.clock(),
|
|
699
|
-
value: formatDuration(scanDurationMs),
|
|
679
|
+
value: esc(formatDuration(scanDurationMs)),
|
|
700
680
|
isText: true,
|
|
701
681
|
sub: 'wall-clock time',
|
|
702
682
|
},
|
|
@@ -715,6 +695,41 @@ function renderSummaryCards(result) {
|
|
|
715
695
|
return `<div class="cards">${cardHTML}</div>`;
|
|
716
696
|
}
|
|
717
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Render the frameworks section.
|
|
700
|
+
*
|
|
701
|
+
* @param {string} projectType
|
|
702
|
+
* @returns {string}
|
|
703
|
+
*/
|
|
704
|
+
function renderFrameworks(projectType) {
|
|
705
|
+
const frameworks = deriveFrameworks(projectType);
|
|
706
|
+
const count = frameworks.length;
|
|
707
|
+
|
|
708
|
+
const body = count === 0
|
|
709
|
+
? `<p class="empty-msg">No frameworks detected.</p>`
|
|
710
|
+
: `<ul class="entry-list">
|
|
711
|
+
${frameworks.map(fw => `
|
|
712
|
+
<li class="entry-item">
|
|
713
|
+
<span class="entry-dot"></span>
|
|
714
|
+
${esc(fw)}
|
|
715
|
+
</li>`).join('')}
|
|
716
|
+
</ul>`;
|
|
717
|
+
|
|
718
|
+
const countBadge = count > 0
|
|
719
|
+
? `<span class="section-count">${count} found</span>`
|
|
720
|
+
: '';
|
|
721
|
+
|
|
722
|
+
return `
|
|
723
|
+
<section class="section">
|
|
724
|
+
<div class="section-header">
|
|
725
|
+
<div class="section-icon">${icon.code()}</div>
|
|
726
|
+
<h2 class="section-title">Frameworks</h2>
|
|
727
|
+
${countBadge}
|
|
728
|
+
</div>
|
|
729
|
+
<div class="section-body">${body}</div>
|
|
730
|
+
</section>`;
|
|
731
|
+
}
|
|
732
|
+
|
|
718
733
|
/**
|
|
719
734
|
* Render the entry points section.
|
|
720
735
|
*
|
|
@@ -739,14 +754,14 @@ function renderEntryPoints(entryPoints) {
|
|
|
739
754
|
: '';
|
|
740
755
|
|
|
741
756
|
return `
|
|
742
|
-
<
|
|
757
|
+
<section class="section">
|
|
743
758
|
<div class="section-header">
|
|
744
759
|
<div class="section-icon">${icon.door()}</div>
|
|
745
|
-
<
|
|
760
|
+
<h2 class="section-title">Entry Points</h2>
|
|
746
761
|
${countBadge}
|
|
747
762
|
</div>
|
|
748
763
|
<div class="section-body">${body}</div>
|
|
749
|
-
</
|
|
764
|
+
</section>`;
|
|
750
765
|
}
|
|
751
766
|
|
|
752
767
|
/**
|
|
@@ -773,14 +788,14 @@ function renderConfigurationFiles(configs) {
|
|
|
773
788
|
: '';
|
|
774
789
|
|
|
775
790
|
return `
|
|
776
|
-
<
|
|
791
|
+
<section class="section">
|
|
777
792
|
<div class="section-header">
|
|
778
793
|
<div class="section-icon">${icon.file()}</div>
|
|
779
|
-
<
|
|
794
|
+
<h2 class="section-title">Configuration Files</h2>
|
|
780
795
|
${countBadge}
|
|
781
796
|
</div>
|
|
782
797
|
<div class="section-body">${body}</div>
|
|
783
|
-
</
|
|
798
|
+
</section>`;
|
|
784
799
|
}
|
|
785
800
|
|
|
786
801
|
/**
|
|
@@ -815,31 +830,32 @@ function renderPackageScripts(scripts) {
|
|
|
815
830
|
: '';
|
|
816
831
|
|
|
817
832
|
return `
|
|
818
|
-
<
|
|
833
|
+
<section class="section">
|
|
819
834
|
<div class="section-header">
|
|
820
835
|
<div class="section-icon">${icon.terminal()}</div>
|
|
821
|
-
<
|
|
836
|
+
<h2 class="section-title">Package Scripts</h2>
|
|
822
837
|
${countBadge}
|
|
823
838
|
</div>
|
|
824
839
|
<div class="section-body" style="padding:0">${body}</div>
|
|
825
|
-
</
|
|
840
|
+
</section>`;
|
|
826
841
|
}
|
|
827
842
|
|
|
828
843
|
/**
|
|
829
|
-
* Render the folder structure section
|
|
844
|
+
* Render the folder structure section.
|
|
830
845
|
*
|
|
846
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
831
847
|
* @param {string[]} flatFiles
|
|
832
|
-
* @param {string}
|
|
848
|
+
* @param {string} rootName
|
|
833
849
|
* @returns {string}
|
|
834
850
|
*/
|
|
835
|
-
function renderFolderStructure(flatFiles, rootName) {
|
|
836
|
-
const treeStr = buildTreeString(
|
|
851
|
+
function renderFolderStructure(tree, flatFiles, rootName) {
|
|
852
|
+
const treeStr = buildTreeString(tree, rootName, flatFiles.length);
|
|
837
853
|
|
|
838
854
|
return `
|
|
839
|
-
<
|
|
855
|
+
<section class="section">
|
|
840
856
|
<div class="section-header">
|
|
841
857
|
<div class="section-icon">${icon.tree()}</div>
|
|
842
|
-
<
|
|
858
|
+
<h2 class="section-title">Folder Structure</h2>
|
|
843
859
|
${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
|
|
844
860
|
</div>
|
|
845
861
|
<div class="section-body">
|
|
@@ -847,7 +863,7 @@ function renderFolderStructure(flatFiles, rootName) {
|
|
|
847
863
|
<pre><code>${esc(treeStr)}</code></pre>
|
|
848
864
|
</div>
|
|
849
865
|
</div>
|
|
850
|
-
</
|
|
866
|
+
</section>`;
|
|
851
867
|
}
|
|
852
868
|
|
|
853
869
|
/**
|
|
@@ -868,14 +884,14 @@ function renderStats(result) {
|
|
|
868
884
|
const rowsHTML = rows.map(([metric, value, isNum]) => `
|
|
869
885
|
<tr>
|
|
870
886
|
<td>${esc(metric)}</td>
|
|
871
|
-
<td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
|
|
887
|
+
<td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
|
|
872
888
|
</tr>`).join('');
|
|
873
889
|
|
|
874
890
|
return `
|
|
875
|
-
<
|
|
891
|
+
<section class="section">
|
|
876
892
|
<div class="section-header">
|
|
877
893
|
<div class="section-icon">${icon.bar()}</div>
|
|
878
|
-
<
|
|
894
|
+
<h2 class="section-title">Statistics</h2>
|
|
879
895
|
</div>
|
|
880
896
|
<div class="section-body">
|
|
881
897
|
<table class="data-table">
|
|
@@ -888,7 +904,7 @@ function renderStats(result) {
|
|
|
888
904
|
<tbody>${rowsHTML}</tbody>
|
|
889
905
|
</table>
|
|
890
906
|
</div>
|
|
891
|
-
</
|
|
907
|
+
</section>`;
|
|
892
908
|
}
|
|
893
909
|
|
|
894
910
|
/**
|
|
@@ -909,17 +925,17 @@ function renderScanInfo() {
|
|
|
909
925
|
</tr>`).join('');
|
|
910
926
|
|
|
911
927
|
return `
|
|
912
|
-
<
|
|
928
|
+
<section class="section">
|
|
913
929
|
<div class="section-header">
|
|
914
930
|
<div class="section-icon">${icon.info()}</div>
|
|
915
|
-
<
|
|
931
|
+
<h2 class="section-title">Scan Information</h2>
|
|
916
932
|
</div>
|
|
917
933
|
<div class="section-body">
|
|
918
934
|
<table class="data-table">
|
|
919
935
|
<tbody>${rowsHTML}</tbody>
|
|
920
936
|
</table>
|
|
921
937
|
</div>
|
|
922
|
-
</
|
|
938
|
+
</section>`;
|
|
923
939
|
}
|
|
924
940
|
|
|
925
941
|
/**
|
|
@@ -950,7 +966,7 @@ function renderFooter() {
|
|
|
950
966
|
* @param {{ cwd?: string }} [options]
|
|
951
967
|
*/
|
|
952
968
|
export function render(result, options = {}) {
|
|
953
|
-
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles } = result;
|
|
969
|
+
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
|
|
954
970
|
|
|
955
971
|
const cwd = options.cwd ?? process.cwd();
|
|
956
972
|
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
@@ -974,11 +990,12 @@ export function render(result, options = {}) {
|
|
|
974
990
|
|
|
975
991
|
<main>
|
|
976
992
|
${renderSummaryCards(result)}
|
|
993
|
+
${renderFrameworks(projectType)}
|
|
977
994
|
${renderEntryPoints(entryPoints)}
|
|
978
995
|
${renderConfigurationFiles(configs)}
|
|
979
996
|
${renderPackageScripts(scripts)}
|
|
980
|
-
${renderFolderStructure(flatFiles, rootName)}
|
|
981
997
|
${renderStats(result)}
|
|
998
|
+
${renderFolderStructure(tree, flatFiles, rootName)}
|
|
982
999
|
${renderScanInfo()}
|
|
983
1000
|
</main>
|
|
984
1001
|
|