@lakindu_perera/toren 1.0.5 → 1.0.8
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/CHANGELOG.md +19 -0
- package/README.md +172 -63
- package/bin/toren.js +66 -44
- package/package.json +7 -3
- package/src/detectors/health-detector.js +72 -0
- package/src/detectors/important-files-detector.js +515 -0
- package/src/detectors/package-manager-detector.js +112 -0
- package/src/detectors/project-info-detector.js +89 -0
- package/src/detectors/script-detector.js +226 -10
- package/src/focused-output.js +161 -41
- package/src/renderers/console-renderer.js +170 -53
- package/src/renderers/html-renderer.js +212 -73
- package/src/renderers/json-renderer.js +133 -23
- package/src/renderers/markdown-renderer.js +127 -104
- package/src/scanner/scan.js +51 -10
|
@@ -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
|
/**
|
|
@@ -789,6 +804,118 @@ function renderConfigurationFiles(configs) {
|
|
|
789
804
|
* @param {Array<{name: string, command: string}>} scripts
|
|
790
805
|
* @returns {string}
|
|
791
806
|
*/
|
|
807
|
+
function renderProjectInfo(projectInfo, packageManager) {
|
|
808
|
+
const rows = [];
|
|
809
|
+
if (projectInfo?.name) rows.push(['Name', projectInfo.name]);
|
|
810
|
+
if (packageManager) rows.push(['Package Manager', packageManager]);
|
|
811
|
+
if (projectInfo?.runtime) rows.push(['Runtime', projectInfo.runtime]);
|
|
812
|
+
if (projectInfo?.language) rows.push(['Language', projectInfo.language]);
|
|
813
|
+
if (projectInfo?.architecture) rows.push(['Architecture', projectInfo.architecture]);
|
|
814
|
+
if (projectInfo?.framework) rows.push(['Framework', projectInfo.framework]);
|
|
815
|
+
if (projectInfo?.entryPoint) rows.push(['Entry Point', projectInfo.entryPoint]);
|
|
816
|
+
if (projectInfo?.sourceDirectory) rows.push(['Source Directory', projectInfo.sourceDirectory]);
|
|
817
|
+
|
|
818
|
+
if (rows.length === 0) return '';
|
|
819
|
+
|
|
820
|
+
const rowsHTML = rows.map(([prop, value]) => `
|
|
821
|
+
<tr>
|
|
822
|
+
<td>${esc(prop)}</td>
|
|
823
|
+
<td class="val-plain">${esc(value)}</td>
|
|
824
|
+
</tr>`).join('');
|
|
825
|
+
|
|
826
|
+
return `
|
|
827
|
+
<section class="section">
|
|
828
|
+
<div class="section-header">
|
|
829
|
+
<div class="section-icon">${icon.info()}</div>
|
|
830
|
+
<h2 class="section-title">Project Info</h2>
|
|
831
|
+
</div>
|
|
832
|
+
<div class="section-body" style="padding:0">
|
|
833
|
+
<table class="data-table">
|
|
834
|
+
<thead>
|
|
835
|
+
<tr>
|
|
836
|
+
<th>Property</th>
|
|
837
|
+
<th style="text-align:right">Value</th>
|
|
838
|
+
</tr>
|
|
839
|
+
</thead>
|
|
840
|
+
<tbody>${rowsHTML}</tbody>
|
|
841
|
+
</table>
|
|
842
|
+
</div>
|
|
843
|
+
</section>`;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function renderImportantFiles(importantFiles) {
|
|
847
|
+
const count = importantFiles.length;
|
|
848
|
+
|
|
849
|
+
const body = count === 0
|
|
850
|
+
? `<p class="empty-msg" style="padding: 1.5rem">No important files detected.</p>`
|
|
851
|
+
: `<table class="data-table">
|
|
852
|
+
<thead>
|
|
853
|
+
<tr>
|
|
854
|
+
<th>Path</th>
|
|
855
|
+
<th>Reason</th>
|
|
856
|
+
</tr>
|
|
857
|
+
</thead>
|
|
858
|
+
<tbody>
|
|
859
|
+
${importantFiles.map(f => `
|
|
860
|
+
<tr>
|
|
861
|
+
<td><code>${esc(f.path)}</code></td>
|
|
862
|
+
<td class="val-plain">${esc(f.reason)}</td>
|
|
863
|
+
</tr>`).join('')}
|
|
864
|
+
</tbody>
|
|
865
|
+
</table>`;
|
|
866
|
+
|
|
867
|
+
const countBadge = count > 0
|
|
868
|
+
? `<span class="section-count">${count} found</span>`
|
|
869
|
+
: '';
|
|
870
|
+
|
|
871
|
+
return `
|
|
872
|
+
<section class="section">
|
|
873
|
+
<div class="section-header">
|
|
874
|
+
<div class="section-icon">${icon.file()}</div>
|
|
875
|
+
<h2 class="section-title">Important Files</h2>
|
|
876
|
+
${countBadge}
|
|
877
|
+
</div>
|
|
878
|
+
<div class="section-body" style="padding:0">
|
|
879
|
+
${body}
|
|
880
|
+
</div>
|
|
881
|
+
</section>`;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function renderProjectHealth(health) {
|
|
885
|
+
const count = health.length;
|
|
886
|
+
|
|
887
|
+
const body = count === 0
|
|
888
|
+
? `<p class="empty-msg" style="padding: 1.5rem">No project health observations available.</p>`
|
|
889
|
+
: `<ul class="entry-list">
|
|
890
|
+
${health.map(h => {
|
|
891
|
+
let emoji = 'ℹ';
|
|
892
|
+
if (h.status === 'pass') emoji = '✓';
|
|
893
|
+
else if (h.status === 'warning') emoji = '⚠';
|
|
894
|
+
return `
|
|
895
|
+
<li class="entry-item">
|
|
896
|
+
<span>${esc(emoji)}</span>
|
|
897
|
+
${esc(h.message)}
|
|
898
|
+
</li>`;
|
|
899
|
+
}).join('')}
|
|
900
|
+
</ul>`;
|
|
901
|
+
|
|
902
|
+
const countBadge = count > 0
|
|
903
|
+
? `<span class="section-count">${count} observations</span>`
|
|
904
|
+
: '';
|
|
905
|
+
|
|
906
|
+
return `
|
|
907
|
+
<section class="section">
|
|
908
|
+
<div class="section-header">
|
|
909
|
+
<div class="section-icon">${icon.info()}</div>
|
|
910
|
+
<h2 class="section-title">Project Health</h2>
|
|
911
|
+
${countBadge}
|
|
912
|
+
</div>
|
|
913
|
+
<div class="section-body">
|
|
914
|
+
${body}
|
|
915
|
+
</div>
|
|
916
|
+
</section>`;
|
|
917
|
+
}
|
|
918
|
+
|
|
792
919
|
function renderPackageScripts(scripts) {
|
|
793
920
|
const count = scripts.length;
|
|
794
921
|
|
|
@@ -797,16 +924,21 @@ function renderPackageScripts(scripts) {
|
|
|
797
924
|
: `<table class="data-table">
|
|
798
925
|
<thead>
|
|
799
926
|
<tr>
|
|
800
|
-
<th>
|
|
927
|
+
<th>Usage</th>
|
|
928
|
+
<th>Description</th>
|
|
801
929
|
<th>Command</th>
|
|
802
930
|
</tr>
|
|
803
931
|
</thead>
|
|
804
932
|
<tbody>
|
|
805
|
-
${scripts.map(s =>
|
|
933
|
+
${scripts.map(s => {
|
|
934
|
+
const usage = s.usage || `npm run ${s.name}`;
|
|
935
|
+
return `
|
|
806
936
|
<tr>
|
|
807
|
-
<td><code>${esc(
|
|
937
|
+
<td><code>${esc(usage)}</code></td>
|
|
938
|
+
<td class="val-plain">${esc(s.description || '')}</td>
|
|
808
939
|
<td class="val-plain"><code>${esc(s.command)}</code></td>
|
|
809
|
-
</tr
|
|
940
|
+
</tr>`;
|
|
941
|
+
}).join('')}
|
|
810
942
|
</tbody>
|
|
811
943
|
</table>`;
|
|
812
944
|
|
|
@@ -815,31 +947,34 @@ function renderPackageScripts(scripts) {
|
|
|
815
947
|
: '';
|
|
816
948
|
|
|
817
949
|
return `
|
|
818
|
-
<
|
|
950
|
+
<section class="section">
|
|
819
951
|
<div class="section-header">
|
|
820
952
|
<div class="section-icon">${icon.terminal()}</div>
|
|
821
|
-
<
|
|
953
|
+
<h2 class="section-title">Package Scripts</h2>
|
|
822
954
|
${countBadge}
|
|
823
955
|
</div>
|
|
824
|
-
<div class="section-body" style="padding:0"
|
|
825
|
-
|
|
956
|
+
<div class="section-body" style="padding:0">
|
|
957
|
+
${body}
|
|
958
|
+
</div>
|
|
959
|
+
</section>`;
|
|
826
960
|
}
|
|
827
961
|
|
|
828
962
|
/**
|
|
829
|
-
* Render the folder structure section
|
|
963
|
+
* Render the folder structure section.
|
|
830
964
|
*
|
|
965
|
+
* @param {import('../scanner/scan.js').DirNode} tree
|
|
831
966
|
* @param {string[]} flatFiles
|
|
832
|
-
* @param {string}
|
|
967
|
+
* @param {string} rootName
|
|
833
968
|
* @returns {string}
|
|
834
969
|
*/
|
|
835
|
-
function renderFolderStructure(flatFiles, rootName) {
|
|
836
|
-
const treeStr = buildTreeString(
|
|
970
|
+
function renderFolderStructure(tree, flatFiles, rootName) {
|
|
971
|
+
const treeStr = buildTreeString(tree, rootName, flatFiles.length);
|
|
837
972
|
|
|
838
973
|
return `
|
|
839
|
-
<
|
|
974
|
+
<section class="section">
|
|
840
975
|
<div class="section-header">
|
|
841
976
|
<div class="section-icon">${icon.tree()}</div>
|
|
842
|
-
<
|
|
977
|
+
<h2 class="section-title">Folder Structure</h2>
|
|
843
978
|
${flatFiles.length > 0 ? `<span class="section-count">${flatFiles.length} files</span>` : ''}
|
|
844
979
|
</div>
|
|
845
980
|
<div class="section-body">
|
|
@@ -847,7 +982,7 @@ function renderFolderStructure(flatFiles, rootName) {
|
|
|
847
982
|
<pre><code>${esc(treeStr)}</code></pre>
|
|
848
983
|
</div>
|
|
849
984
|
</div>
|
|
850
|
-
</
|
|
985
|
+
</section>`;
|
|
851
986
|
}
|
|
852
987
|
|
|
853
988
|
/**
|
|
@@ -868,14 +1003,14 @@ function renderStats(result) {
|
|
|
868
1003
|
const rowsHTML = rows.map(([metric, value, isNum]) => `
|
|
869
1004
|
<tr>
|
|
870
1005
|
<td>${esc(metric)}</td>
|
|
871
|
-
<td class="${isNum ? 'val' : 'val-plain'}">${value}</td>
|
|
1006
|
+
<td class="${isNum ? 'val' : 'val-plain'}">${esc(value)}</td>
|
|
872
1007
|
</tr>`).join('');
|
|
873
1008
|
|
|
874
1009
|
return `
|
|
875
|
-
<
|
|
1010
|
+
<section class="section">
|
|
876
1011
|
<div class="section-header">
|
|
877
1012
|
<div class="section-icon">${icon.bar()}</div>
|
|
878
|
-
<
|
|
1013
|
+
<h2 class="section-title">Statistics</h2>
|
|
879
1014
|
</div>
|
|
880
1015
|
<div class="section-body">
|
|
881
1016
|
<table class="data-table">
|
|
@@ -888,7 +1023,7 @@ function renderStats(result) {
|
|
|
888
1023
|
<tbody>${rowsHTML}</tbody>
|
|
889
1024
|
</table>
|
|
890
1025
|
</div>
|
|
891
|
-
</
|
|
1026
|
+
</section>`;
|
|
892
1027
|
}
|
|
893
1028
|
|
|
894
1029
|
/**
|
|
@@ -909,17 +1044,17 @@ function renderScanInfo() {
|
|
|
909
1044
|
</tr>`).join('');
|
|
910
1045
|
|
|
911
1046
|
return `
|
|
912
|
-
<
|
|
1047
|
+
<section class="section">
|
|
913
1048
|
<div class="section-header">
|
|
914
1049
|
<div class="section-icon">${icon.info()}</div>
|
|
915
|
-
<
|
|
1050
|
+
<h2 class="section-title">Scan Information</h2>
|
|
916
1051
|
</div>
|
|
917
1052
|
<div class="section-body">
|
|
918
1053
|
<table class="data-table">
|
|
919
1054
|
<tbody>${rowsHTML}</tbody>
|
|
920
1055
|
</table>
|
|
921
1056
|
</div>
|
|
922
|
-
</
|
|
1057
|
+
</section>`;
|
|
923
1058
|
}
|
|
924
1059
|
|
|
925
1060
|
/**
|
|
@@ -950,7 +1085,7 @@ function renderFooter() {
|
|
|
950
1085
|
* @param {{ cwd?: string }} [options]
|
|
951
1086
|
*/
|
|
952
1087
|
export function render(result, options = {}) {
|
|
953
|
-
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles } = result;
|
|
1088
|
+
const { rootPath, projectType, entryPoints, configs = [], scripts = [], flatFiles, tree, importantFiles = [], health = [], projectInfo, packageManager } = result;
|
|
954
1089
|
|
|
955
1090
|
const cwd = options.cwd ?? process.cwd();
|
|
956
1091
|
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
@@ -974,11 +1109,15 @@ export function render(result, options = {}) {
|
|
|
974
1109
|
|
|
975
1110
|
<main>
|
|
976
1111
|
${renderSummaryCards(result)}
|
|
1112
|
+
${renderProjectInfo(projectInfo, packageManager)}
|
|
1113
|
+
${renderProjectHealth(health)}
|
|
1114
|
+
${renderImportantFiles(importantFiles)}
|
|
1115
|
+
${renderFrameworks(projectType)}
|
|
977
1116
|
${renderEntryPoints(entryPoints)}
|
|
978
1117
|
${renderConfigurationFiles(configs)}
|
|
979
1118
|
${renderPackageScripts(scripts)}
|
|
980
|
-
${renderFolderStructure(flatFiles, rootName)}
|
|
981
1119
|
${renderStats(result)}
|
|
1120
|
+
${renderFolderStructure(tree, flatFiles, rootName)}
|
|
982
1121
|
${renderScanInfo()}
|
|
983
1122
|
</main>
|
|
984
1123
|
|
|
@@ -1,57 +1,167 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Toren — JSON Renderer
|
|
3
3
|
*
|
|
4
|
-
* Consumes a ScanResult and produces a
|
|
4
|
+
* Consumes a ScanResult and produces a stable, consistently-ordered JSON
|
|
5
|
+
* object suitable for programmatic consumption.
|
|
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
|
+
* - All arrays are always present — never undefined or missing.
|
|
11
|
+
* - Property order is fixed and documented below.
|
|
12
|
+
* - Backward compatible: no existing field is removed or renamed.
|
|
13
|
+
*
|
|
14
|
+
* Output property order:
|
|
15
|
+
* 1. meta — schema version + generator provenance
|
|
16
|
+
* 2. project — name (basename), path (relative), detected type
|
|
17
|
+
* 3. frameworks — derived array of detected frameworks ([] when none)
|
|
18
|
+
* 4. entryPoints — array of detected entry-point paths
|
|
19
|
+
* 5. configs — array of detected configuration file paths
|
|
20
|
+
* 6. scripts — array of { name, command } objects
|
|
21
|
+
* 7. statistics — file/folder counts and scan duration (structured)
|
|
22
|
+
* 8. structure — recursive file-tree array
|
|
23
|
+
* 9. summary — retained for backward compatibility (same data as statistics)
|
|
5
24
|
*/
|
|
6
25
|
|
|
7
26
|
import path from 'node:path';
|
|
27
|
+
import { createRequire } from 'node:module';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const pkg = require('../../package.json');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tree mapper
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
8
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Recursively map an internal DirNode / FileNode to a clean JSON shape.
|
|
38
|
+
* Directory nodes always include a `children` array (never undefined).
|
|
39
|
+
*
|
|
40
|
+
* @param {import('../scanner/scan.js').DirNode | import('../scanner/scan.js').FileNode} node
|
|
41
|
+
* @returns {{ type: 'folder'|'file', name: string, children?: object[] }}
|
|
42
|
+
*/
|
|
9
43
|
function mapTree(node) {
|
|
10
44
|
if (node.type === 'directory') {
|
|
11
45
|
return {
|
|
12
|
-
type:
|
|
13
|
-
name:
|
|
14
|
-
children: (node.children || []).map(mapTree)
|
|
15
|
-
};
|
|
16
|
-
} else {
|
|
17
|
-
return {
|
|
18
|
-
type: 'file',
|
|
19
|
-
name: node.name
|
|
46
|
+
type: 'folder',
|
|
47
|
+
name: node.name,
|
|
48
|
+
children: (node.children || []).map(mapTree),
|
|
20
49
|
};
|
|
21
50
|
}
|
|
51
|
+
return {
|
|
52
|
+
type: 'file',
|
|
53
|
+
name: node.name,
|
|
54
|
+
};
|
|
22
55
|
}
|
|
23
56
|
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Helpers
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive a `frameworks` array from the scanner's `projectType` string.
|
|
63
|
+
* Returns a single-element array when a framework is detected, empty otherwise.
|
|
64
|
+
* This is a pure presentation decision — no business logic is added.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} projectType
|
|
67
|
+
* @returns {string[]}
|
|
68
|
+
*/
|
|
69
|
+
function deriveFrameworks(projectType) {
|
|
70
|
+
if (!projectType || projectType === 'Unknown') return [];
|
|
71
|
+
return [projectType];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Public API
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render a ScanResult as a stable JSON document to stdout.
|
|
80
|
+
*
|
|
81
|
+
* @param {import('../scanner/scan.js').ScanResult} result
|
|
82
|
+
* @param {{ cwd?: string }} [options]
|
|
83
|
+
*/
|
|
24
84
|
export function render(result, options = {}) {
|
|
25
85
|
const {
|
|
26
86
|
rootPath,
|
|
27
87
|
projectType,
|
|
28
|
-
entryPoints,
|
|
29
|
-
configs
|
|
30
|
-
scripts
|
|
88
|
+
entryPoints = [],
|
|
89
|
+
configs = [],
|
|
90
|
+
scripts = [],
|
|
91
|
+
packageManager,
|
|
92
|
+
importantFiles = [],
|
|
93
|
+
projectInfo = null,
|
|
94
|
+
health = [],
|
|
31
95
|
tree,
|
|
32
|
-
flatFiles,
|
|
33
|
-
totalFolders,
|
|
96
|
+
flatFiles = [],
|
|
97
|
+
totalFolders = 0,
|
|
34
98
|
scanDurationMs,
|
|
35
99
|
} = result;
|
|
36
100
|
|
|
37
|
-
const cwd
|
|
38
|
-
const relRoot
|
|
101
|
+
const cwd = options.cwd ?? process.cwd();
|
|
102
|
+
const relRoot = path.relative(cwd, rootPath) || '.';
|
|
103
|
+
const rootName = path.basename(rootPath) || relRoot;
|
|
104
|
+
|
|
105
|
+
// Shared statistics values — computed once, used in both `statistics` and
|
|
106
|
+
// the backward-compatible `summary` block.
|
|
107
|
+
// Guard against NaN/undefined: JSON.stringify(NaN) produces null, breaking
|
|
108
|
+
// the schema guarantee that durationMs is always a number.
|
|
109
|
+
const totalFiles = flatFiles.length;
|
|
110
|
+
const durationMs = Number.isFinite(scanDurationMs) ? Math.round(scanDurationMs) : 0;
|
|
39
111
|
|
|
40
112
|
const output = {
|
|
113
|
+
// 1. Provenance — lets consumers detect schema changes.
|
|
114
|
+
meta: {
|
|
115
|
+
generatedBy: 'Toren',
|
|
116
|
+
version: pkg.version,
|
|
117
|
+
schema: 1,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// 2. Project identity — name is the human-readable basename; path is the
|
|
121
|
+
// relative path used for filesystem resolution.
|
|
41
122
|
project: {
|
|
123
|
+
name: (projectInfo && projectInfo.name) ? projectInfo.name : rootName,
|
|
42
124
|
path: relRoot,
|
|
43
125
|
type: projectType,
|
|
44
|
-
|
|
126
|
+
packageManager: packageManager || null,
|
|
127
|
+
runtime: projectInfo ? projectInfo.runtime : null,
|
|
128
|
+
language: projectInfo ? projectInfo.language : null,
|
|
129
|
+
framework: projectInfo ? projectInfo.framework : null,
|
|
130
|
+
architecture: projectInfo ? projectInfo.architecture : null,
|
|
131
|
+
entryPoint: projectInfo ? projectInfo.entryPoint : null,
|
|
132
|
+
sourceDirectory: projectInfo ? projectInfo.sourceDirectory : null,
|
|
45
133
|
},
|
|
134
|
+
|
|
135
|
+
// 3. Detected frameworks — always an array.
|
|
136
|
+
frameworks: deriveFrameworks(projectType),
|
|
137
|
+
|
|
138
|
+
// 4–6. Discovery results — all arrays, always present, never null.
|
|
139
|
+
entryPoints: Array.isArray(entryPoints) ? entryPoints : [],
|
|
140
|
+
configs: Array.isArray(configs) ? configs : [],
|
|
141
|
+
scripts: Array.isArray(scripts) ? scripts : [],
|
|
142
|
+
|
|
143
|
+
importantFiles: Array.isArray(importantFiles) ? importantFiles : [],
|
|
144
|
+
health: Array.isArray(health) ? health : [],
|
|
145
|
+
|
|
146
|
+
// 7. Structured statistics — always a complete object with numeric values.
|
|
147
|
+
// durationMs is rounded to the nearest millisecond (integer).
|
|
148
|
+
statistics: {
|
|
149
|
+
files: totalFiles,
|
|
150
|
+
folders: totalFolders,
|
|
151
|
+
durationMs,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// 8. File-tree — array of root-level nodes; always present.
|
|
155
|
+
// Empty array when the scanned directory is empty.
|
|
156
|
+
structure: (tree?.children || []).map(mapTree),
|
|
157
|
+
|
|
158
|
+
// 9. Backward-compatible summary block — preserved for existing consumers.
|
|
159
|
+
// Contains the same data under the original field names.
|
|
46
160
|
summary: {
|
|
47
|
-
totalFiles
|
|
161
|
+
totalFiles,
|
|
48
162
|
totalFolders,
|
|
49
|
-
scanDurationMs:
|
|
163
|
+
scanDurationMs: durationMs,
|
|
50
164
|
},
|
|
51
|
-
entryPoints,
|
|
52
|
-
configs,
|
|
53
|
-
scripts,
|
|
54
|
-
structure: (tree.children || []).map(mapTree)
|
|
55
165
|
};
|
|
56
166
|
|
|
57
167
|
console.log(JSON.stringify(output, null, 2));
|