@lakindu_perera/toren 1.0.4 → 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 +81 -44
- package/bin/toren.js +65 -44
- package/package.json +2 -1
- package/src/detectors/config-detector.js +81 -0
- package/src/detectors/script-detector.js +32 -0
- package/src/focused-output.js +99 -24
- package/src/renderers/console-renderer.js +136 -44
- package/src/renderers/html-renderer.js +158 -62
- package/src/renderers/json-renderer.js +119 -19
- package/src/renderers/markdown-renderer.js +96 -87
- package/src/scanner/scan.js +37 -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]
|
|
@@ -173,6 +227,8 @@ export function render(result, options = {}) {
|
|
|
173
227
|
rootPath,
|
|
174
228
|
projectType,
|
|
175
229
|
entryPoints,
|
|
230
|
+
configs = [],
|
|
231
|
+
scripts = [],
|
|
176
232
|
tree,
|
|
177
233
|
flatFiles,
|
|
178
234
|
totalFolders,
|
|
@@ -187,33 +243,70 @@ export function render(result, options = {}) {
|
|
|
187
243
|
console.log('');
|
|
188
244
|
|
|
189
245
|
// ── Summary ───────────────────────────────────────────────────────────────
|
|
190
|
-
section('
|
|
246
|
+
section('Project Summary');
|
|
191
247
|
row('Path: ', paint(relRoot, C.cyan));
|
|
192
248
|
row('Project type: ', paint(projectType, C.bold, C.green));
|
|
193
249
|
row('Total files: ', paint(String(flatFiles.length), C.yellow));
|
|
194
250
|
row('Total folders:', paint(String(totalFolders), C.yellow));
|
|
195
|
-
|
|
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('');
|
|
196
264
|
|
|
197
265
|
// ── Entry Points ──────────────────────────────────────────────────────────
|
|
198
|
-
section('
|
|
266
|
+
section('Entry Points');
|
|
199
267
|
if (entryPoints.length === 0) {
|
|
200
|
-
console.log(paint('
|
|
268
|
+
console.log(paint('No entry points detected.', C.dim));
|
|
201
269
|
} else {
|
|
202
270
|
for (const ep of entryPoints) {
|
|
203
|
-
console.log(
|
|
271
|
+
console.log(paint(ep, C.white));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
console.log('');
|
|
275
|
+
|
|
276
|
+
// ── Configuration Files ───────────────────────────────────────────────────
|
|
277
|
+
section('Configuration Files');
|
|
278
|
+
if (configs.length === 0) {
|
|
279
|
+
console.log(paint('No configuration files detected.', C.dim));
|
|
280
|
+
} else {
|
|
281
|
+
for (const c of configs) {
|
|
282
|
+
console.log(paint(c, C.white));
|
|
204
283
|
}
|
|
205
284
|
}
|
|
285
|
+
console.log('');
|
|
286
|
+
|
|
287
|
+
// ── Package Scripts ───────────────────────────────────────────────────────
|
|
288
|
+
section('Package Scripts');
|
|
289
|
+
if (scripts.length === 0) {
|
|
290
|
+
console.log(paint('No package scripts detected.', C.dim));
|
|
291
|
+
} else {
|
|
292
|
+
const maxNameLen = Math.max(...scripts.map(s => s.name.length));
|
|
293
|
+
for (const s of scripts) {
|
|
294
|
+
const paddedName = s.name.padEnd(maxNameLen, ' ');
|
|
295
|
+
console.log(`${paint(paddedName, C.white)} ${paint(s.command, C.dim)}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
console.log('');
|
|
206
299
|
|
|
207
300
|
// ── Structure Preview ─────────────────────────────────────────────────────
|
|
208
|
-
section('
|
|
301
|
+
section('Folder Structure');
|
|
209
302
|
|
|
210
|
-
console.log(paint(`${tree.name || '.'}
|
|
303
|
+
console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
|
|
211
304
|
|
|
212
305
|
const counter = { count: 0, maxReached: false };
|
|
213
306
|
const children = tree.children ?? [];
|
|
214
307
|
for (let i = 0; i < children.length; i++) {
|
|
215
308
|
if (counter.count >= PREVIEW_LIMIT) {
|
|
216
|
-
console.log(
|
|
309
|
+
console.log(`${CHARS.corner}${paint('...', C.dim)}`);
|
|
217
310
|
break;
|
|
218
311
|
}
|
|
219
312
|
renderTree(children[i], '', i === children.length - 1, counter);
|
|
@@ -222,13 +315,12 @@ export function render(result, options = {}) {
|
|
|
222
315
|
|
|
223
316
|
if (flatFiles.length > PREVIEW_LIMIT) {
|
|
224
317
|
const hidden = flatFiles.length - PREVIEW_LIMIT;
|
|
225
|
-
console.log(paint(
|
|
318
|
+
console.log(paint(`… ${hidden} more file(s) not shown`, C.dim));
|
|
226
319
|
}
|
|
227
320
|
|
|
228
321
|
// ── Footer ────────────────────────────────────────────────────────────────
|
|
229
322
|
console.log('');
|
|
230
|
-
|
|
231
|
-
console.log(paint(' ✅ Scan complete.', C.green));
|
|
323
|
+
console.log(paint(`Scan completed in ${formatDuration(scanDurationMs)}`, C.green));
|
|
232
324
|
console.log('');
|
|
233
325
|
}
|
|
234
326
|
|
|
@@ -240,8 +332,8 @@ export function render(result, options = {}) {
|
|
|
240
332
|
export function renderStructure(result) {
|
|
241
333
|
const { tree, flatFiles } = result;
|
|
242
334
|
|
|
243
|
-
|
|
244
|
-
console.log(paint(`${tree.name || '.'}
|
|
335
|
+
section('Folder Structure');
|
|
336
|
+
console.log(paint(`${tree.name || '.'}${path.sep}`, C.bold, C.blue));
|
|
245
337
|
|
|
246
338
|
const counter = { count: 0, maxReached: false };
|
|
247
339
|
const children = tree.children ?? [];
|
|
@@ -249,7 +341,7 @@ export function renderStructure(result) {
|
|
|
249
341
|
|
|
250
342
|
for (let i = 0; i < children.length; i++) {
|
|
251
343
|
if (counter.count >= limit) {
|
|
252
|
-
console.log(
|
|
344
|
+
console.log(`${CHARS.corner}${paint('...', C.dim)}`);
|
|
253
345
|
break;
|
|
254
346
|
}
|
|
255
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
|
}
|
|
@@ -625,12 +592,26 @@ const icon = {
|
|
|
625
592
|
tree: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
|
|
626
593
|
bar: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>`,
|
|
627
594
|
info: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
|
|
595
|
+
terminal: () => `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`,
|
|
628
596
|
};
|
|
629
597
|
|
|
630
598
|
// ---------------------------------------------------------------------------
|
|
631
599
|
// Section builders (one function per report section)
|
|
632
600
|
// ---------------------------------------------------------------------------
|
|
633
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
|
+
|
|
634
615
|
/**
|
|
635
616
|
* Render the gradient page header.
|
|
636
617
|
*
|
|
@@ -695,7 +676,7 @@ function renderSummaryCards(result) {
|
|
|
695
676
|
{
|
|
696
677
|
label: 'Scan Duration',
|
|
697
678
|
icon: icon.clock(),
|
|
698
|
-
value: formatDuration(scanDurationMs),
|
|
679
|
+
value: esc(formatDuration(scanDurationMs)),
|
|
699
680
|
isText: true,
|
|
700
681
|
sub: 'wall-clock time',
|
|
701
682
|
},
|
|
@@ -714,6 +695,41 @@ function renderSummaryCards(result) {
|
|
|
714
695
|
return `<div class="cards">${cardHTML}</div>`;
|
|
715
696
|
}
|
|
716
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
|
+
|
|
717
733
|
/**
|
|
718
734
|
* Render the entry points section.
|
|
719
735
|
*
|
|
@@ -738,31 +754,108 @@ function renderEntryPoints(entryPoints) {
|
|
|
738
754
|
: '';
|
|
739
755
|
|
|
740
756
|
return `
|
|
741
|
-
<
|
|
757
|
+
<section class="section">
|
|
742
758
|
<div class="section-header">
|
|
743
759
|
<div class="section-icon">${icon.door()}</div>
|
|
744
|
-
<
|
|
760
|
+
<h2 class="section-title">Entry Points</h2>
|
|
745
761
|
${countBadge}
|
|
746
762
|
</div>
|
|
747
763
|
<div class="section-body">${body}</div>
|
|
748
|
-
</
|
|
764
|
+
</section>`;
|
|
749
765
|
}
|
|
750
766
|
|
|
751
767
|
/**
|
|
752
|
-
* Render the
|
|
768
|
+
* Render the configuration files section.
|
|
753
769
|
*
|
|
770
|
+
* @param {string[]} configs
|
|
771
|
+
* @returns {string}
|
|
772
|
+
*/
|
|
773
|
+
function renderConfigurationFiles(configs) {
|
|
774
|
+
const count = configs.length;
|
|
775
|
+
|
|
776
|
+
const body = count === 0
|
|
777
|
+
? `<p class="empty-msg">No configuration files detected.</p>`
|
|
778
|
+
: `<ul class="entry-list">
|
|
779
|
+
${configs.map(c => `
|
|
780
|
+
<li class="entry-item">
|
|
781
|
+
<span class="entry-dot"></span>
|
|
782
|
+
${esc(c)}
|
|
783
|
+
</li>`).join('')}
|
|
784
|
+
</ul>`;
|
|
785
|
+
|
|
786
|
+
const countBadge = count > 0
|
|
787
|
+
? `<span class="section-count">${count} found</span>`
|
|
788
|
+
: '';
|
|
789
|
+
|
|
790
|
+
return `
|
|
791
|
+
<section class="section">
|
|
792
|
+
<div class="section-header">
|
|
793
|
+
<div class="section-icon">${icon.file()}</div>
|
|
794
|
+
<h2 class="section-title">Configuration Files</h2>
|
|
795
|
+
${countBadge}
|
|
796
|
+
</div>
|
|
797
|
+
<div class="section-body">${body}</div>
|
|
798
|
+
</section>`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Render the package scripts section.
|
|
803
|
+
*
|
|
804
|
+
* @param {Array<{name: string, command: string}>} scripts
|
|
805
|
+
* @returns {string}
|
|
806
|
+
*/
|
|
807
|
+
function renderPackageScripts(scripts) {
|
|
808
|
+
const count = scripts.length;
|
|
809
|
+
|
|
810
|
+
const body = count === 0
|
|
811
|
+
? `<p class="empty-msg" style="padding: 1.5rem">No package scripts detected.</p>`
|
|
812
|
+
: `<table class="data-table">
|
|
813
|
+
<thead>
|
|
814
|
+
<tr>
|
|
815
|
+
<th>Script</th>
|
|
816
|
+
<th>Command</th>
|
|
817
|
+
</tr>
|
|
818
|
+
</thead>
|
|
819
|
+
<tbody>
|
|
820
|
+
${scripts.map(s => `
|
|
821
|
+
<tr>
|
|
822
|
+
<td><code>${esc(s.name)}</code></td>
|
|
823
|
+
<td class="val-plain"><code>${esc(s.command)}</code></td>
|
|
824
|
+
</tr>`).join('')}
|
|
825
|
+
</tbody>
|
|
826
|
+
</table>`;
|
|
827
|
+
|
|
828
|
+
const countBadge = count > 0
|
|
829
|
+
? `<span class="section-count">${count} found</span>`
|
|
830
|
+
: '';
|
|
831
|
+
|
|
832
|
+
return `
|
|
833
|
+
<section class="section">
|
|
834
|
+
<div class="section-header">
|
|
835
|
+
<div class="section-icon">${icon.terminal()}</div>
|
|
836
|
+
<h2 class="section-title">Package Scripts</h2>
|
|
837
|
+
${countBadge}
|
|
838
|
+
</div>
|
|
839
|
+
<div class="section-body" style="padding:0">${body}</div>
|
|
840
|
+
</section>`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Render the folder structure section.
|
|
845
|
+
*
|
|
846
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
754
847
|
* @param {string[]} flatFiles
|
|
755
|
-
* @param {string}
|
|
848
|
+
* @param {string} rootName
|
|
756
849
|
* @returns {string}
|
|
757
850
|
*/
|
|
758
|
-
function renderFolderStructure(flatFiles, rootName) {
|
|
759
|
-
const treeStr = buildTreeString(
|
|
851
|
+
function renderFolderStructure(tree, flatFiles, rootName) {
|
|
852
|
+
const treeStr = buildTreeString(tree, rootName, flatFiles.length);
|
|
760
853
|
|
|
761
854
|
return `
|
|
762
|
-
<
|
|
855
|
+
<section class="section">
|
|
763
856
|
<div class="section-header">
|
|
764
857
|
<div class="section-icon">${icon.tree()}</div>
|
|
765
|
-
<
|
|
858
|
+
<h2 class="section-title">Folder Structure</h2>
|
|
766
859
|
${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
|
|
767
860
|
</div>
|
|
768
861
|
<div class="section-body">
|
|
@@ -770,7 +863,7 @@ function renderFolderStructure(flatFiles, rootName) {
|
|
|
770
863
|
<pre><code>${esc(treeStr)}</code></pre>
|
|
771
864
|
</div>
|
|
772
865
|
</div>
|
|
773
|
-
</
|
|
866
|
+
</section>`;
|
|
774
867
|
}
|
|
775
868
|
|
|
776
869
|
/**
|
|
@@ -791,14 +884,14 @@ function renderStats(result) {
|
|
|
791
884
|
const rowsHTML = rows.map(([metric, value, isNum]) => `
|
|
792
885
|
<tr>
|
|
793
886
|
<td>${esc(metric)}</td>
|
|
794
|
-
<td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
|
|
887
|
+
<td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
|
|
795
888
|
</tr>`).join('');
|
|
796
889
|
|
|
797
890
|
return `
|
|
798
|
-
<
|
|
891
|
+
<section class="section">
|
|
799
892
|
<div class="section-header">
|
|
800
893
|
<div class="section-icon">${icon.bar()}</div>
|
|
801
|
-
<
|
|
894
|
+
<h2 class="section-title">Statistics</h2>
|
|
802
895
|
</div>
|
|
803
896
|
<div class="section-body">
|
|
804
897
|
<table class="data-table">
|
|
@@ -811,7 +904,7 @@ function renderStats(result) {
|
|
|
811
904
|
<tbody>${rowsHTML}</tbody>
|
|
812
905
|
</table>
|
|
813
906
|
</div>
|
|
814
|
-
</
|
|
907
|
+
</section>`;
|
|
815
908
|
}
|
|
816
909
|
|
|
817
910
|
/**
|
|
@@ -832,17 +925,17 @@ function renderScanInfo() {
|
|
|
832
925
|
</tr>`).join('');
|
|
833
926
|
|
|
834
927
|
return `
|
|
835
|
-
<
|
|
928
|
+
<section class="section">
|
|
836
929
|
<div class="section-header">
|
|
837
930
|
<div class="section-icon">${icon.info()}</div>
|
|
838
|
-
<
|
|
931
|
+
<h2 class="section-title">Scan Information</h2>
|
|
839
932
|
</div>
|
|
840
933
|
<div class="section-body">
|
|
841
934
|
<table class="data-table">
|
|
842
935
|
<tbody>${rowsHTML}</tbody>
|
|
843
936
|
</table>
|
|
844
937
|
</div>
|
|
845
|
-
</
|
|
938
|
+
</section>`;
|
|
846
939
|
}
|
|
847
940
|
|
|
848
941
|
/**
|
|
@@ -873,7 +966,7 @@ function renderFooter() {
|
|
|
873
966
|
* @param {{ cwd?: string }} [options]
|
|
874
967
|
*/
|
|
875
968
|
export function render(result, options = {}) {
|
|
876
|
-
const { rootPath, projectType, entryPoints, flatFiles } = result;
|
|
969
|
+
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree } = result;
|
|
877
970
|
|
|
878
971
|
const cwd = options.cwd ?? process.cwd();
|
|
879
972
|
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
@@ -897,9 +990,12 @@ export function render(result, options = {}) {
|
|
|
897
990
|
|
|
898
991
|
<main>
|
|
899
992
|
${renderSummaryCards(result)}
|
|
993
|
+
${renderFrameworks(projectType)}
|
|
900
994
|
${renderEntryPoints(entryPoints)}
|
|
901
|
-
${
|
|
995
|
+
${renderConfigurationFiles(configs)}
|
|
996
|
+
${renderPackageScripts(scripts)}
|
|
902
997
|
${renderStats(result)}
|
|
998
|
+
${renderFolderStructure(tree, flatFiles, rootName)}
|
|
903
999
|
${renderScanInfo()}
|
|
904
1000
|
</main>
|
|
905
1001
|
|