fallow-code-scan 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/package.json +38 -0
- package/public/app-copy.js +31 -0
- package/public/app-findings.js +54 -0
- package/public/app-format.js +148 -0
- package/public/app-icons.js +32 -0
- package/public/app.js +395 -0
- package/public/index.html +97 -0
- package/public/styles.css +791 -0
- package/src/fallowBinary.js +68 -0
- package/src/fallowReport.js +336 -0
- package/src/paths.js +30 -0
- package/src/server.js +309 -0
- package/src/start.js +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Code Scan
|
|
2
|
+
|
|
3
|
+
Local web dashboard for Fallow code analysis reports.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Run it from the root of the project you want to analyze:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx fallow-code-scan
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Open the printed local URL, then press **Refresh** to run a scan.
|
|
14
|
+
|
|
15
|
+
By default Code Scan listens on `127.0.0.1:5179`. Override it with environment
|
|
16
|
+
variables when needed:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
HOST=0.0.0.0 PORT=5180 npx fallow-code-scan
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What It Shows
|
|
23
|
+
|
|
24
|
+
- Blocking Findings from Fallow checks
|
|
25
|
+
- Refactoring Suggestions from maintainability analysis
|
|
26
|
+
- Code Health metrics and lowest maintainability files
|
|
27
|
+
- Copyable text reports for sharing findings
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js 18 or newer
|
|
32
|
+
- A JavaScript or TypeScript project supported by Fallow
|
|
33
|
+
|
|
34
|
+
Fallow is installed as a package dependency, so users do not need to install it
|
|
35
|
+
separately.
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fallow-code-scan",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local web dashboard for Fallow code analysis reports.",
|
|
5
|
+
"main": "src/server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"code-scan": "src/start.js",
|
|
8
|
+
"fallow-code-scan": "src/start.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"public",
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "node src/start.js",
|
|
17
|
+
"test": "node --test test/*.test.js",
|
|
18
|
+
"pack:dry": "npm pack --dry-run"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"fallow",
|
|
22
|
+
"code-analysis",
|
|
23
|
+
"dashboard",
|
|
24
|
+
"maintainability",
|
|
25
|
+
"dead-code"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"fallow": "2.91.0",
|
|
36
|
+
"lucide": "1.21.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
async function writeText(text) {
|
|
3
|
+
if (copyWithTextarea(text)) return;
|
|
4
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
5
|
+
await navigator.clipboard.writeText(text);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
throw new Error("Copy is not available in this browser.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function copyWithTextarea(text) {
|
|
12
|
+
const textarea = document.createElement("textarea");
|
|
13
|
+
textarea.value = text;
|
|
14
|
+
textarea.setAttribute("readonly", "");
|
|
15
|
+
textarea.className = "copy-buffer";
|
|
16
|
+
document.body.appendChild(textarea);
|
|
17
|
+
textarea.focus();
|
|
18
|
+
textarea.select();
|
|
19
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
return document.execCommand("copy");
|
|
23
|
+
} finally {
|
|
24
|
+
textarea.remove();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
window.codeScanCopy = {
|
|
29
|
+
writeText
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
function renderSection(section, escapeHtml) {
|
|
3
|
+
const node = document.createElement("details");
|
|
4
|
+
node.className = "finding-group-static";
|
|
5
|
+
node.open = true;
|
|
6
|
+
node.innerHTML = `
|
|
7
|
+
<summary>
|
|
8
|
+
<i class="finding-chevron chevron-open" data-lucide="chevron-down" aria-hidden="true"></i>
|
|
9
|
+
<i class="finding-chevron chevron-closed" data-lucide="chevron-right" aria-hidden="true"></i>
|
|
10
|
+
<i class="finding-type-icon" data-lucide="${typeIcon(section.id)}" aria-hidden="true"></i>
|
|
11
|
+
<h3>${escapeHtml(section.label)}</h3>
|
|
12
|
+
</summary>
|
|
13
|
+
<ul>${recordsHtml(section.records, escapeHtml)}</ul>
|
|
14
|
+
`;
|
|
15
|
+
return node;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function recordsHtml(records, escapeHtml) {
|
|
19
|
+
const html = records.map((record) => `
|
|
20
|
+
<li class="finding-record">
|
|
21
|
+
<div>
|
|
22
|
+
<span class="item-title">${escapeHtml(record.title)}</span>
|
|
23
|
+
<span class="item-detail">${escapeHtml(record.detail)}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</li>
|
|
26
|
+
`).join("");
|
|
27
|
+
return html || "<li><span>No samples included.</span></li>";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function typeIcon(sectionId) {
|
|
31
|
+
const icons = {
|
|
32
|
+
boundary_violations: "shield-alert",
|
|
33
|
+
circular_dependencies: "refresh-ccw",
|
|
34
|
+
complexity_findings: "gauge",
|
|
35
|
+
duplicate_exports: "copy-x",
|
|
36
|
+
duplicated_code: "files",
|
|
37
|
+
re_export_cycles: "repeat-2",
|
|
38
|
+
stale_suppressions: "message-square-warning",
|
|
39
|
+
unlisted_dependencies: "package-plus",
|
|
40
|
+
unresolved_imports: "import",
|
|
41
|
+
unused_class_members: "square-minus",
|
|
42
|
+
unused_dependencies: "package-minus",
|
|
43
|
+
unused_enum_members: "list-minus",
|
|
44
|
+
unused_exports: "package-x",
|
|
45
|
+
unused_files: "file-x-2",
|
|
46
|
+
unused_types: "braces"
|
|
47
|
+
};
|
|
48
|
+
return icons[sectionId] || "circle-x";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
window.codeScanFindings = {
|
|
52
|
+
renderSection
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
const STATUS_LABELS = {
|
|
3
|
+
failed: "Failed",
|
|
4
|
+
idle: "Idle",
|
|
5
|
+
ready: "Ready",
|
|
6
|
+
running: "Scanning"
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function effortTone(effort) {
|
|
10
|
+
if (effort === "high") return "critical";
|
|
11
|
+
if (effort === "medium") return "warn";
|
|
12
|
+
if (effort === "low") return "good";
|
|
13
|
+
return "neutral";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function formatNumber(value) {
|
|
17
|
+
return new Intl.NumberFormat().format(Number(value) || 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatRecommendation(recommendation) {
|
|
21
|
+
return String(recommendation || "")
|
|
22
|
+
.replace(/\((\d+) LOC\)/g, "($1 lines of code)")
|
|
23
|
+
.replace(/(\d+) dependents amplify every change/g, "$1 files depend on it")
|
|
24
|
+
.replace(/fan-in/g, "incoming references")
|
|
25
|
+
.replace(/fan-out/g, "outgoing dependencies");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function thresholdTone(value, goodMax, warnMax) {
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
if (number <= goodMax) return "good";
|
|
31
|
+
if (number <= warnMax) return "warn";
|
|
32
|
+
return "critical";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatReportText(report) {
|
|
36
|
+
const lines = [
|
|
37
|
+
"Code Scan",
|
|
38
|
+
`Generated: ${report.generatedAt}`,
|
|
39
|
+
`Status: ${report.status}`,
|
|
40
|
+
`Blocking findings: ${report.hardFindings.count}`,
|
|
41
|
+
`Refactoring suggestions: ${report.health.targets.length}`,
|
|
42
|
+
"",
|
|
43
|
+
"Blocking findings"
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
appendCheckSections(lines, report.hardFindings.sections);
|
|
47
|
+
appendTargets(lines, report.health.targets, true);
|
|
48
|
+
return `${lines.join("\n")}\n`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatBlockingFindingsText(report) {
|
|
52
|
+
const lines = ["Blocking findings"];
|
|
53
|
+
appendCheckSections(lines, report.hardFindings.sections);
|
|
54
|
+
return `${lines.join("\n")}\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatRiskSignalsText(report) {
|
|
58
|
+
const health = report.health;
|
|
59
|
+
const lines = [
|
|
60
|
+
"Risk signals",
|
|
61
|
+
`Functions over complexity limit: ${formatNumber(health.summary.functionsAboveThreshold)}`,
|
|
62
|
+
`90th percentile function complexity: ${formatNumber(health.vitalSigns.p90Cyclomatic)}`,
|
|
63
|
+
`Files with high coupling: ${health.vitalSigns.couplingHighPercent}% of scored files`
|
|
64
|
+
];
|
|
65
|
+
return `${lines.join("\n")}\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatRefactoringSuggestionsText(report) {
|
|
69
|
+
const lines = ["Refactoring suggestions"];
|
|
70
|
+
appendTargets(lines, report.health.targets, false);
|
|
71
|
+
return `${lines.join("\n")}\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function statusText(payload) {
|
|
75
|
+
if (payload.running) return "Scanning";
|
|
76
|
+
return statusLabel(payload.state);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runSummaryText(payload) {
|
|
80
|
+
if (!payload.report) return "No scan has run yet.";
|
|
81
|
+
const generatedAt = formatDate(payload.report.generatedAt || payload.finishedAt);
|
|
82
|
+
const seconds = Math.max(0.1, payload.report.durationMs / 1000).toFixed(1);
|
|
83
|
+
return `${generatedAt} · ${seconds} seconds · Fallow ${payload.report.version || "unknown"}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runSummaryHtml(payload) {
|
|
87
|
+
if (!payload.report) return "No scan has run yet.";
|
|
88
|
+
const generatedAt = formatDate(payload.report.generatedAt || payload.finishedAt);
|
|
89
|
+
const seconds = Math.max(0.1, payload.report.durationMs / 1000).toFixed(1);
|
|
90
|
+
const version = payload.report.version || "unknown";
|
|
91
|
+
return `
|
|
92
|
+
<span class="run-summary-item"><i data-lucide="clock"></i>${generatedAt} · ${seconds} seconds</span>
|
|
93
|
+
<span class="run-summary-item"><i data-lucide="git-commit"></i>Fallow ${version}</span>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function statusLabel(state) {
|
|
98
|
+
return STATUS_LABELS[state] || STATUS_LABELS.idle;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatDate(value) {
|
|
102
|
+
if (!value) return "No timestamp";
|
|
103
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
104
|
+
dateStyle: "medium",
|
|
105
|
+
timeStyle: "medium"
|
|
106
|
+
}).format(new Date(value));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function appendCheckSections(lines, sections) {
|
|
110
|
+
if (sections.length === 0) {
|
|
111
|
+
lines.push("- None");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sections.forEach((section) => {
|
|
116
|
+
lines.push(`- ${section.label}: ${section.count}`);
|
|
117
|
+
section.records.forEach((record) => {
|
|
118
|
+
lines.push(` - ${record.title}${record.detail ? ` (${record.detail})` : ""}`);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function appendTargets(lines, targets, includeHeading) {
|
|
124
|
+
if (includeHeading) lines.push("", "Refactoring suggestions");
|
|
125
|
+
if (targets.length === 0) {
|
|
126
|
+
lines.push("- None");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
targets.forEach((target) => {
|
|
131
|
+
lines.push(`- ${target.path}: ${formatRecommendation(target.recommendation)}`);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.codeScanFormat = {
|
|
136
|
+
effortTone,
|
|
137
|
+
formatBlockingFindingsText,
|
|
138
|
+
formatNumber,
|
|
139
|
+
formatRecommendation,
|
|
140
|
+
formatRefactoringSuggestionsText,
|
|
141
|
+
formatRiskSignalsText,
|
|
142
|
+
formatReportText,
|
|
143
|
+
runSummaryHtml,
|
|
144
|
+
runSummaryText,
|
|
145
|
+
statusText,
|
|
146
|
+
thresholdTone
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
function metricIconHtml(id) {
|
|
3
|
+
const icons = {
|
|
4
|
+
"blocking-findings": "circle-x",
|
|
5
|
+
maintainability: "bar-chart-3",
|
|
6
|
+
"refactoring-suggestions": "scissors"
|
|
7
|
+
};
|
|
8
|
+
const icon = icons[id];
|
|
9
|
+
return icon
|
|
10
|
+
? `<i class="metric-icon" data-lucide="${icon}" aria-hidden="true"></i>`
|
|
11
|
+
: "";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function refresh(root = document) {
|
|
15
|
+
if (!window.lucide) return;
|
|
16
|
+
window.lucide.createIcons({
|
|
17
|
+
attrs: { "stroke-width": 2 },
|
|
18
|
+
icons: window.lucide.icons,
|
|
19
|
+
root
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setButtonLabel(button, label) {
|
|
24
|
+
button.querySelector(".button-label").textContent = label;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
window.codeScanIcons = {
|
|
28
|
+
metricIconHtml,
|
|
29
|
+
refresh,
|
|
30
|
+
setButtonLabel
|
|
31
|
+
};
|
|
32
|
+
}
|