auditkit 0.1.1 → 0.1.3
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/Cargo.lock +501 -6
- package/Cargo.toml +3 -1
- package/README.md +2 -4
- package/package.json +2 -1
- package/scripts/auditkit/lighthouse-runner.mjs +148 -20
- package/scripts/auditkit/lighthouse.test.mjs +17 -0
- package/scripts/auditkit/postinstall.mjs +71 -0
- package/src/html_check.rs +113 -5
- package/src/lighthouse.rs +8 -5
- package/src/main.rs +77 -49
- package/src/report.rs +58 -5
- package/src/security.rs +94 -10
- package/src/templates.rs +24 -75
- package/src/ui.rs +533 -23
- package/src/workspace.rs +75 -0
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "auditkit"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
edition = "2021"
|
|
5
5
|
description = "Local hybrid CLI for agency website audits."
|
|
6
6
|
license = "MIT"
|
|
@@ -11,7 +11,9 @@ anyhow = "1.0"
|
|
|
11
11
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
12
12
|
clap = { version = "4.5", features = ["derive"] }
|
|
13
13
|
colored = "3.0"
|
|
14
|
+
crossterm = "0.29.0"
|
|
14
15
|
indicatif = "0.18"
|
|
16
|
+
ratatui = { version = "0.30.0", default-features = false, features = ["crossterm"] }
|
|
15
17
|
regex = "1.12"
|
|
16
18
|
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
|
17
19
|
serde_json = "1.0"
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local hybrid CLI for agency website audits.
|
|
4
4
|
|
|
5
|
-
Rust runs the core workflow: audit folders, quick HTML checks, security checks, and report generation. Node is used only for Lighthouse
|
|
5
|
+
Rust runs the core workflow: audit folders, quick HTML checks, security checks, and report generation. Node is used only for Lighthouse.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -31,9 +31,7 @@ ak report latest
|
|
|
31
31
|
`ak new` creates a workspace in `audits/`. Fill in:
|
|
32
32
|
|
|
33
33
|
- `findings.md`
|
|
34
|
-
- `
|
|
35
|
-
- `pages/*.md`
|
|
36
|
-
- `raw-notes.md`
|
|
34
|
+
- `workspace.md`
|
|
37
35
|
|
|
38
36
|
Then `ak report latest` creates:
|
|
39
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auditkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local hybrid CLI for agency website audits.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"audit:security": "cargo run --quiet -- security",
|
|
33
33
|
"audit:lighthouse": "cargo run --quiet -- lighthouse",
|
|
34
34
|
"audit:inspect": "cargo run --quiet -- inspect",
|
|
35
|
+
"postinstall": "node scripts/auditkit/postinstall.mjs",
|
|
35
36
|
"test": "npm run test:node && npm run test:rust",
|
|
36
37
|
"test:node": "node --test",
|
|
37
38
|
"test:rust": "cargo test"
|
|
@@ -25,16 +25,135 @@ const browserCandidates = [
|
|
|
25
25
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const colorEnabled = !process.env.NO_COLOR;
|
|
29
|
+
|
|
30
|
+
function color(code, value) {
|
|
31
|
+
return colorEnabled ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function positive(value) {
|
|
35
|
+
return color("1;32", value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function warning(value) {
|
|
39
|
+
return color("1;33", value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function critical(value) {
|
|
43
|
+
return color("1;31", value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function frame(value) {
|
|
47
|
+
return color("1;36", value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dim(value) {
|
|
51
|
+
return color("2", value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toneIcon(tone) {
|
|
55
|
+
if (tone === "positive") {
|
|
56
|
+
return positive("✓");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tone === "warning") {
|
|
60
|
+
return warning("◆");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return critical("●");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scoreTone(score) {
|
|
67
|
+
if (typeof score !== "number") {
|
|
68
|
+
return "warning";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (score >= 85) {
|
|
72
|
+
return "positive";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (score >= 65) {
|
|
76
|
+
return "warning";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return "critical";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function numericMetric(value) {
|
|
83
|
+
const match = String(value).match(/[0-9]+(?:\.[0-9]+)?/);
|
|
84
|
+
return match ? Number(match[0]) : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function lcpTone(value) {
|
|
88
|
+
const metric = numericMetric(value);
|
|
89
|
+
if (metric === null) {
|
|
90
|
+
return "warning";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (metric <= 2.5) {
|
|
94
|
+
return "positive";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (metric <= 4) {
|
|
98
|
+
return "warning";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return "critical";
|
|
30
102
|
}
|
|
31
103
|
|
|
32
|
-
function
|
|
33
|
-
|
|
104
|
+
function clsTone(value) {
|
|
105
|
+
const metric = numericMetric(value);
|
|
106
|
+
if (metric === null) {
|
|
107
|
+
return "warning";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (metric <= 0.1) {
|
|
111
|
+
return "positive";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (metric <= 0.25) {
|
|
115
|
+
return "warning";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return "critical";
|
|
34
119
|
}
|
|
35
120
|
|
|
36
|
-
function
|
|
37
|
-
|
|
121
|
+
function tbtTone(value) {
|
|
122
|
+
const metric = numericMetric(value);
|
|
123
|
+
if (metric === null) {
|
|
124
|
+
return "warning";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (metric <= 200) {
|
|
128
|
+
return "positive";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (metric <= 600) {
|
|
132
|
+
return "warning";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return "critical";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toneValue(tone, value) {
|
|
139
|
+
if (tone === "positive") {
|
|
140
|
+
return positive(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (tone === "warning") {
|
|
144
|
+
return warning(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return critical(value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function signalLine(label, value, tone) {
|
|
151
|
+
return ` ${toneIcon(tone)} ${dim(label.padEnd(21))} ${toneValue(tone, value)}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function feedbackLine(tone, value) {
|
|
155
|
+
const label = tone === "critical" ? critical("FIX") : warning("WATCH");
|
|
156
|
+
return ` ${toneIcon(tone)} ${label} ${toneValue(tone, value)}`;
|
|
38
157
|
}
|
|
39
158
|
|
|
40
159
|
export function findBrowserPath({ env = process.env, config = {}, exists = existsSync } = {}) {
|
|
@@ -92,21 +211,30 @@ export function summarizeLighthouse(lhr) {
|
|
|
92
211
|
}
|
|
93
212
|
|
|
94
213
|
export function formatLighthouseCli(summary) {
|
|
214
|
+
const opportunityLines = summary.opportunities.length
|
|
215
|
+
? summary.opportunities.map((item) =>
|
|
216
|
+
feedbackLine(
|
|
217
|
+
item.score < 50 ? "critical" : "warning",
|
|
218
|
+
`${item.title}${item.displayValue ? ` — ${item.displayValue}` : ""}`,
|
|
219
|
+
),
|
|
220
|
+
)
|
|
221
|
+
: [signalLine("Opportunities", "No major Lighthouse opportunities found.", "positive")];
|
|
222
|
+
|
|
95
223
|
return [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
224
|
+
"",
|
|
225
|
+
frame("╭─ Lighthouse Check"),
|
|
226
|
+
`${frame("│ URL ")} ${summary.url}`,
|
|
227
|
+
frame("╰─ Signals"),
|
|
228
|
+
signalLine("Performance", `${summary.scores.performance ?? "n/a"}/100`, scoreTone(summary.scores.performance)),
|
|
229
|
+
signalLine("Accessibility", `${summary.scores.accessibility ?? "n/a"}/100`, scoreTone(summary.scores.accessibility)),
|
|
230
|
+
signalLine("Best practices", `${summary.scores.bestPractices ?? "n/a"}/100`, scoreTone(summary.scores.bestPractices)),
|
|
231
|
+
signalLine("SEO", `${summary.scores.seo ?? "n/a"}/100`, scoreTone(summary.scores.seo)),
|
|
232
|
+
signalLine("LCP", summary.vitals.lcp, lcpTone(summary.vitals.lcp)),
|
|
233
|
+
signalLine("CLS", summary.vitals.cls, clsTone(summary.vitals.cls)),
|
|
234
|
+
signalLine("TBT", summary.vitals.tbt, tbtTone(summary.vitals.tbt)),
|
|
235
|
+
"",
|
|
236
|
+
frame("Feedback"),
|
|
237
|
+
...opportunityLines,
|
|
110
238
|
].join("\n");
|
|
111
239
|
}
|
|
112
240
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
formatLighthouseReport,
|
|
6
6
|
summarizeLighthouse,
|
|
7
7
|
} from "./lighthouse-runner.mjs";
|
|
8
|
+
import { shouldShowWelcome, welcomeMessage } from "./postinstall.mjs";
|
|
8
9
|
|
|
9
10
|
const lhr = {
|
|
10
11
|
finalDisplayedUrl: "https://example.com/",
|
|
@@ -76,3 +77,19 @@ test("findBrowserPath falls back to Helium app path", () => {
|
|
|
76
77
|
"/Applications/Helium.app/Contents/MacOS/Helium",
|
|
77
78
|
);
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
test("postinstall welcome explains first commands", () => {
|
|
82
|
+
const message = welcomeMessage();
|
|
83
|
+
const plainMessage = welcomeMessage({ NO_COLOR: "1" });
|
|
84
|
+
|
|
85
|
+
assert.match(message, /Audit Kit/);
|
|
86
|
+
assert.match(message, /ak new/);
|
|
87
|
+
assert.match(message, /ak inspect latest/);
|
|
88
|
+
assert.match(message, /\x1b\[/);
|
|
89
|
+
assert.doesNotMatch(plainMessage, /\x1b\[/);
|
|
90
|
+
assert.match(plainMessage, /Audit Kit/);
|
|
91
|
+
assert.equal(shouldShowWelcome({}), true);
|
|
92
|
+
assert.equal(shouldShowWelcome({ CI: "true" }), false);
|
|
93
|
+
assert.equal(shouldShowWelcome({ AUDITKIT_SKIP_WELCOME: "1" }), false);
|
|
94
|
+
assert.equal(shouldShowWelcome({ npm_config_loglevel: "silent" }), false);
|
|
95
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
export function shouldShowWelcome(env = process.env) {
|
|
4
|
+
return !env.CI && !env.AUDITKIT_SKIP_WELCOME && env.npm_config_loglevel !== "silent";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function welcomeMessage(env = process.env) {
|
|
8
|
+
const colors = !env.NO_COLOR;
|
|
9
|
+
const rows = [
|
|
10
|
+
line(" ", colors),
|
|
11
|
+
line(` ${paint("Audit Kit", 96, colors)} `, colors),
|
|
12
|
+
line(` ${paint("Local website audits from your terminal.", 90, colors)} `, colors),
|
|
13
|
+
line(" ", colors),
|
|
14
|
+
divider(colors),
|
|
15
|
+
line(` ${paint("Try first", 97, colors)} `, colors),
|
|
16
|
+
line(" ", colors),
|
|
17
|
+
commandLine("ak new", "create an audit workspace", colors),
|
|
18
|
+
commandLine("ak inspect latest", "run check, security, Lighthouse", colors),
|
|
19
|
+
commandLine("ak report latest", "generate report + client email", colors),
|
|
20
|
+
commandLine("ak list", "show saved audits", colors),
|
|
21
|
+
line(" ", colors),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
"",
|
|
26
|
+
glow(" · ✦ · · · ✧ · · · ✦ ·", colors),
|
|
27
|
+
`${corner("╭", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${corner("╮", colors)}`,
|
|
28
|
+
...rows,
|
|
29
|
+
`${corner("╰", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${corner("╯", colors)}`,
|
|
30
|
+
glow(" · · ✧ · · · ✦ · ·", colors),
|
|
31
|
+
].join("\n");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href && shouldShowWelcome()) {
|
|
35
|
+
console.log(welcomeMessage());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function commandLine(command, description, colors) {
|
|
39
|
+
return line(` ${paint(command.padEnd(18), 36, colors)} ${paint(description.padEnd(34), 90, colors)} `, colors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function line(content, colors) {
|
|
43
|
+
return `${border("│", colors)}${content}${border("│", colors)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function divider(colors) {
|
|
47
|
+
return `${border("├", colors)}${gradient("────────────────────────────────────────────────────────────", colors)}${border("┤", colors)}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function gradient(value, colors) {
|
|
51
|
+
const palette = [36, 96, 35, 93, 95, 94];
|
|
52
|
+
return [...value]
|
|
53
|
+
.map((character, index) => paint(character, palette[index % palette.length], colors))
|
|
54
|
+
.join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function border(value, colors) {
|
|
58
|
+
return paint(value, 96, colors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function corner(value, colors) {
|
|
62
|
+
return paint(value, 95, colors);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function glow(value, colors) {
|
|
66
|
+
return paint(value, 90, colors);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function paint(value, code, colors) {
|
|
70
|
+
return colors ? `\x1b[${code}m${value}\x1b[0m` : value;
|
|
71
|
+
}
|
package/src/html_check.rs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
use anyhow::Result;
|
|
2
2
|
use regex::Regex;
|
|
3
3
|
|
|
4
|
-
use crate::ui::score_status;
|
|
4
|
+
use crate::ui::{self, score_status, FeedbackTone};
|
|
5
5
|
|
|
6
6
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
7
7
|
pub struct HtmlCheck {
|
|
@@ -201,13 +201,65 @@ pub fn check_url(input_url: &str) -> Result<HtmlCheck> {
|
|
|
201
201
|
|
|
202
202
|
pub fn format_cli(result: &HtmlCheck) -> String {
|
|
203
203
|
let mut output = format!(
|
|
204
|
-
"\n
|
|
204
|
+
"\n{}\n{} {}\n{} {}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n\n{}\n",
|
|
205
|
+
ui::frame_line("╭─ Audit Kit Check"),
|
|
206
|
+
ui::frame_line("│ URL "),
|
|
207
|
+
result.url,
|
|
208
|
+
ui::frame_line("│ Score"),
|
|
209
|
+
ui::score_badge(result.score),
|
|
210
|
+
ui::frame_line("╰─ Signals"),
|
|
211
|
+
ui::signal_line("Status", result.status, status_tone(result.status)),
|
|
212
|
+
ui::signal_line(
|
|
213
|
+
"Response",
|
|
214
|
+
format!("{} bytes", result.bytes),
|
|
215
|
+
response_tone(result.bytes)
|
|
216
|
+
),
|
|
217
|
+
ui::signal_line("Title", title_value(result), title_tone(result)),
|
|
218
|
+
ui::signal_line(
|
|
219
|
+
"Meta description",
|
|
220
|
+
if result.meta_description_present {
|
|
221
|
+
"present"
|
|
222
|
+
} else {
|
|
223
|
+
"missing"
|
|
224
|
+
},
|
|
225
|
+
if result.meta_description_present {
|
|
226
|
+
FeedbackTone::Positive
|
|
227
|
+
} else {
|
|
228
|
+
FeedbackTone::Critical
|
|
229
|
+
}
|
|
230
|
+
),
|
|
231
|
+
ui::signal_line("H1 count", result.h1_count, h1_tone(result.h1_count)),
|
|
232
|
+
ui::signal_line(
|
|
233
|
+
"Images missing alt",
|
|
234
|
+
format!("{}/{}", result.image_missing_alt, result.image_total),
|
|
235
|
+
if result.image_missing_alt == 0 {
|
|
236
|
+
FeedbackTone::Positive
|
|
237
|
+
} else {
|
|
238
|
+
FeedbackTone::Critical
|
|
239
|
+
}
|
|
240
|
+
),
|
|
241
|
+
ui::frame_line("Feedback")
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
for item in &result.feedback {
|
|
245
|
+
output.push_str(&format!(
|
|
246
|
+
"{}\n",
|
|
247
|
+
ui::feedback_line(feedback_tone(item), item)
|
|
248
|
+
));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
output
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
pub fn format_markdown(result: &HtmlCheck) -> String {
|
|
255
|
+
let mut output = format!(
|
|
256
|
+
"# Automated Check\n\nURL: {}\nScore: {}/100 ({})\n\n## Signals\n\n- Status: {}\n- Response: {} bytes\n- Title: {}\n- Meta description: {}\n- H1 count: {}\n- Images missing alt: {}/{}\n\n## Feedback\n\n",
|
|
205
257
|
result.url,
|
|
206
258
|
result.score,
|
|
207
259
|
score_status(result.score),
|
|
208
260
|
result.status,
|
|
209
261
|
result.bytes,
|
|
210
|
-
|
|
262
|
+
title_value(result),
|
|
211
263
|
if result.meta_description_present { "present" } else { "missing" },
|
|
212
264
|
result.h1_count,
|
|
213
265
|
result.image_missing_alt,
|
|
@@ -221,8 +273,64 @@ pub fn format_cli(result: &HtmlCheck) -> String {
|
|
|
221
273
|
output
|
|
222
274
|
}
|
|
223
275
|
|
|
224
|
-
|
|
225
|
-
|
|
276
|
+
fn title_value(result: &HtmlCheck) -> &str {
|
|
277
|
+
if result.title.is_empty() {
|
|
278
|
+
"missing"
|
|
279
|
+
} else {
|
|
280
|
+
&result.title
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fn status_tone(status: u16) -> FeedbackTone {
|
|
285
|
+
if (200..400).contains(&status) {
|
|
286
|
+
FeedbackTone::Positive
|
|
287
|
+
} else {
|
|
288
|
+
FeedbackTone::Critical
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn response_tone(bytes: usize) -> FeedbackTone {
|
|
293
|
+
if bytes > 200_000 {
|
|
294
|
+
FeedbackTone::Warning
|
|
295
|
+
} else {
|
|
296
|
+
FeedbackTone::Positive
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn title_tone(result: &HtmlCheck) -> FeedbackTone {
|
|
301
|
+
if result.title.is_empty() {
|
|
302
|
+
FeedbackTone::Critical
|
|
303
|
+
} else if result.title.len() < 10 || result.title.len() > 65 {
|
|
304
|
+
FeedbackTone::Warning
|
|
305
|
+
} else {
|
|
306
|
+
FeedbackTone::Positive
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn h1_tone(count: usize) -> FeedbackTone {
|
|
311
|
+
match count {
|
|
312
|
+
1 => FeedbackTone::Positive,
|
|
313
|
+
0 => FeedbackTone::Critical,
|
|
314
|
+
_ => FeedbackTone::Warning,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn feedback_tone(value: &str) -> FeedbackTone {
|
|
319
|
+
let value = value.to_lowercase();
|
|
320
|
+
if value.starts_with("missing")
|
|
321
|
+
|| value.starts_with("no obvious")
|
|
322
|
+
|| value.contains("missing alt")
|
|
323
|
+
{
|
|
324
|
+
FeedbackTone::Critical
|
|
325
|
+
} else if value.starts_with("no canonical")
|
|
326
|
+
|| value.starts_with("title length")
|
|
327
|
+
|| value.starts_with("multiple")
|
|
328
|
+
|| value.contains("large")
|
|
329
|
+
{
|
|
330
|
+
FeedbackTone::Warning
|
|
331
|
+
} else {
|
|
332
|
+
FeedbackTone::Positive
|
|
333
|
+
}
|
|
226
334
|
}
|
|
227
335
|
|
|
228
336
|
#[cfg(test)]
|
package/src/lighthouse.rs
CHANGED
|
@@ -7,6 +7,7 @@ use anyhow::{Context, Result};
|
|
|
7
7
|
pub struct LighthousePaths {
|
|
8
8
|
pub markdown_path: PathBuf,
|
|
9
9
|
pub json_path: PathBuf,
|
|
10
|
+
pub cli_output: String,
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
pub fn run_lighthouse(
|
|
@@ -36,14 +37,16 @@ pub fn run_lighthouse(
|
|
|
36
37
|
.map(PathBuf::from)
|
|
37
38
|
.unwrap_or_else(|| output_folder.unwrap_or(root).join("lighthouse.json"));
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
let cli_output = stdout
|
|
41
|
+
.lines()
|
|
42
|
+
.filter(|line| !line.contains("LIGHTHOUSE_MD:") && !line.contains("LIGHTHOUSE_JSON:"))
|
|
43
|
+
.collect::<Vec<_>>()
|
|
44
|
+
.join("\n");
|
|
45
|
+
|
|
44
46
|
Ok(LighthousePaths {
|
|
45
47
|
markdown_path,
|
|
46
48
|
json_path,
|
|
49
|
+
cli_output,
|
|
47
50
|
})
|
|
48
51
|
}
|
|
49
52
|
|