auditkit 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/Cargo.lock +1992 -0
- package/Cargo.toml +18 -0
- package/README.md +153 -0
- package/auditkit.config.json +10 -0
- package/docs/ARCHITECTURE.md +44 -0
- package/package.json +46 -0
- package/scripts/auditkit/ak +22 -0
- package/scripts/auditkit/lighthouse-runner.mjs +183 -0
- package/scripts/auditkit/lighthouse.test.mjs +78 -0
- package/scripts/auditkit/postinstall.mjs +31 -0
- package/src/audit.rs +59 -0
- package/src/html_check.rs +240 -0
- package/src/lib.rs +21 -0
- package/src/lighthouse.rs +71 -0
- package/src/main.rs +261 -0
- package/src/report.rs +250 -0
- package/src/security.rs +193 -0
- package/src/templates.rs +197 -0
- package/src/ui.rs +84 -0
- package/src/workspace.rs +111 -0
package/Cargo.toml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "auditkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
anyhow = "1.0"
|
|
8
|
+
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
|
9
|
+
clap = { version = "4.5", features = ["derive"] }
|
|
10
|
+
colored = "3.0"
|
|
11
|
+
indicatif = "0.18"
|
|
12
|
+
regex = "1.12"
|
|
13
|
+
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"], default-features = false }
|
|
14
|
+
serde_json = "1.0"
|
|
15
|
+
url = "2.5"
|
|
16
|
+
|
|
17
|
+
[dev-dependencies]
|
|
18
|
+
tempfile = "3.23"
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Audit Kit
|
|
2
|
+
|
|
3
|
+
Local hybrid CLI for agency website audits.
|
|
4
|
+
|
|
5
|
+
Rust runs the core workflow: audit folders, quick HTML checks, security checks, and report generation. Node is used only for Lighthouse because Lighthouse is a Node tool.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Global install from npm/Bun:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install -g auditkit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
NPM also works:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -g auditkit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run without installing globally:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bunx auditkit --help
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Local development install from this project folder:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
npm link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Prerequisites:
|
|
35
|
+
|
|
36
|
+
- Rust toolchain with `cargo`
|
|
37
|
+
- Node.js 24+
|
|
38
|
+
- Helium, Chrome, Chromium, Brave, or Edge for Lighthouse
|
|
39
|
+
|
|
40
|
+
## Basic Workflow
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
ak new
|
|
44
|
+
ak inspect latest
|
|
45
|
+
ak report latest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`ak new` creates a workspace in `audits/`. Fill in:
|
|
49
|
+
|
|
50
|
+
- `findings.md`
|
|
51
|
+
- `scorecard.md`
|
|
52
|
+
- `pages/*.md`
|
|
53
|
+
- `raw-notes.md`
|
|
54
|
+
|
|
55
|
+
Then `ak report latest` creates:
|
|
56
|
+
|
|
57
|
+
- `final-report.md`
|
|
58
|
+
- `client-email.md`
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
- `ak new` - create a new audit folder
|
|
63
|
+
- `ak check latest` - run automated feedback for the latest audit and save it
|
|
64
|
+
- `ak security latest` - run security header check for the latest audit and save it
|
|
65
|
+
- `ak lighthouse latest` - run Lighthouse for the latest audit and save markdown + JSON
|
|
66
|
+
- `ak inspect latest` - run automated feedback, security, and Lighthouse
|
|
67
|
+
- `ak check <url>` - fetch a website and print quick feedback
|
|
68
|
+
- `ak security <url>` - fetch a website and print security feedback
|
|
69
|
+
- `ak lighthouse <url>` - run Lighthouse for a website
|
|
70
|
+
- `ak check <url> --save <audit-folder>` - fetch a website and save feedback into an audit
|
|
71
|
+
- `ak security <url> --save <audit-folder>` - save security feedback into an audit
|
|
72
|
+
- `ak lighthouse <url> --save <audit-folder>` - save Lighthouse output into an audit
|
|
73
|
+
- `ak report latest` - generate `final-report.md` and `client-email.md`
|
|
74
|
+
- `ak list` - list existing audits
|
|
75
|
+
|
|
76
|
+
Long form also works:
|
|
77
|
+
|
|
78
|
+
- `auditkit new`
|
|
79
|
+
- `auditkit check latest`
|
|
80
|
+
- `auditkit report latest`
|
|
81
|
+
- `auditkit list`
|
|
82
|
+
|
|
83
|
+
NPM scripts:
|
|
84
|
+
|
|
85
|
+
- `npm run audit -- new` - create a new audit folder
|
|
86
|
+
- `npm run audit -- check latest` - run automated feedback for the latest audit and save it
|
|
87
|
+
- `npm run audit -- check <url>` - fetch a website and print quick feedback
|
|
88
|
+
- `npm run audit -- check <url> --save <audit-folder>` - fetch a website and save feedback into an audit
|
|
89
|
+
- `npm run audit -- report latest` - generate `final-report.md` and `client-email.md`
|
|
90
|
+
- `npm run audit -- list` - list existing audits
|
|
91
|
+
|
|
92
|
+
## Quick Website Check
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
ak check https://example.com
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The check is intentionally lightweight. It reviews basic HTML signals:
|
|
99
|
+
|
|
100
|
+
- title tag
|
|
101
|
+
- meta description
|
|
102
|
+
- H1 count
|
|
103
|
+
- image alt text
|
|
104
|
+
- viewport tag
|
|
105
|
+
- canonical link
|
|
106
|
+
- obvious CTA language
|
|
107
|
+
- initial response time and HTML size
|
|
108
|
+
|
|
109
|
+
## Lighthouse Requirement
|
|
110
|
+
|
|
111
|
+
`ak lighthouse` and `ak inspect` need a Chrome-family browser installed:
|
|
112
|
+
|
|
113
|
+
- Helium
|
|
114
|
+
- Google Chrome
|
|
115
|
+
- Chromium
|
|
116
|
+
- Brave
|
|
117
|
+
- Microsoft Edge
|
|
118
|
+
|
|
119
|
+
If none is installed, Lighthouse cannot run.
|
|
120
|
+
|
|
121
|
+
Audit Kit auto-detects Helium at:
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
/Applications/Helium.app/Contents/MacOS/Helium
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Override browser path when needed:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
AUDITKIT_BROWSER_PATH="/path/to/browser" ak lighthouse https://example.com
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
Run all tests:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm test
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Rust-only:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
cargo test
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Node Lighthouse helper only:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm run test:node
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Architecture notes live in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Audit Kit is a hybrid CLI.
|
|
4
|
+
|
|
5
|
+
## Rust Core
|
|
6
|
+
|
|
7
|
+
Rust owns the workflow that should be fast, predictable, and easy to inspect:
|
|
8
|
+
|
|
9
|
+
- command routing
|
|
10
|
+
- audit folder creation
|
|
11
|
+
- HTML checks
|
|
12
|
+
- security header checks
|
|
13
|
+
- report and email generation
|
|
14
|
+
- terminal output
|
|
15
|
+
|
|
16
|
+
Rust source lives in `src/`.
|
|
17
|
+
|
|
18
|
+
## Node Lighthouse Helper
|
|
19
|
+
|
|
20
|
+
Lighthouse is a Node ecosystem tool, so Audit Kit keeps that part in Node:
|
|
21
|
+
|
|
22
|
+
- `scripts/lighthouse.mjs`
|
|
23
|
+
- `scripts/auditkit/lighthouse-runner.mjs`
|
|
24
|
+
|
|
25
|
+
Rust calls the Node helper as a subprocess only when running:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
ak lighthouse latest
|
|
29
|
+
ak inspect latest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Module Map
|
|
33
|
+
|
|
34
|
+
- `src/main.rs`: command router
|
|
35
|
+
- `src/audit.rs`: audit input helpers
|
|
36
|
+
- `src/workspace.rs`: paths, generated audit files, folder lookup
|
|
37
|
+
- `src/templates.rs`: starter markdown files
|
|
38
|
+
- `src/html_check.rs`: lightweight HTML feedback
|
|
39
|
+
- `src/security.rs`: security header feedback
|
|
40
|
+
- `src/report.rs`: final report and client email
|
|
41
|
+
- `src/lighthouse.rs`: bridge to the Node Lighthouse helper
|
|
42
|
+
- `src/ui.rs`: terminal output helpers
|
|
43
|
+
|
|
44
|
+
The code is intentionally plain. Each file owns one small part of the workflow.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auditkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local hybrid CLI for agency website audits.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"auditkit": "scripts/auditkit/ak",
|
|
9
|
+
"ak": "scripts/auditkit/ak"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"Cargo.toml",
|
|
13
|
+
"Cargo.lock",
|
|
14
|
+
"src/",
|
|
15
|
+
"scripts/auditkit/",
|
|
16
|
+
"auditkit.config.json",
|
|
17
|
+
"docs/ARCHITECTURE.md"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"audit": "cargo run --quiet --",
|
|
27
|
+
"audit:new": "cargo run --quiet -- new",
|
|
28
|
+
"audit:list": "cargo run --quiet -- list",
|
|
29
|
+
"audit:check": "cargo run --quiet -- check",
|
|
30
|
+
"audit:report": "cargo run --quiet -- report",
|
|
31
|
+
"audit:security": "cargo run --quiet -- security",
|
|
32
|
+
"audit:lighthouse": "cargo run --quiet -- lighthouse",
|
|
33
|
+
"audit:inspect": "cargo run --quiet -- inspect",
|
|
34
|
+
"postinstall": "node scripts/auditkit/postinstall.mjs",
|
|
35
|
+
"site:dev": "npm --prefix site run dev",
|
|
36
|
+
"site:build": "npm --prefix site run build",
|
|
37
|
+
"site:preview": "npm --prefix site run preview",
|
|
38
|
+
"test": "npm run test:node && npm run test:rust",
|
|
39
|
+
"test:node": "node --test",
|
|
40
|
+
"test:rust": "cargo test"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chrome-launcher": "^1.2.1",
|
|
44
|
+
"lighthouse": "^13.3.0"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
SOURCE="$0"
|
|
5
|
+
while [ -L "$SOURCE" ]; do
|
|
6
|
+
DIR="$(CDPATH= cd -- "$(dirname -- "$SOURCE")" && pwd)"
|
|
7
|
+
TARGET="$(readlink "$SOURCE")"
|
|
8
|
+
case "$TARGET" in
|
|
9
|
+
/*) SOURCE="$TARGET" ;;
|
|
10
|
+
*) SOURCE="$DIR/$TARGET" ;;
|
|
11
|
+
esac
|
|
12
|
+
done
|
|
13
|
+
|
|
14
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SOURCE")" && pwd)"
|
|
15
|
+
ROOT_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
16
|
+
BINARY="$ROOT_DIR/target/debug/auditkit"
|
|
17
|
+
|
|
18
|
+
if [ ! -x "$BINARY" ]; then
|
|
19
|
+
cargo build --manifest-path "$ROOT_DIR/Cargo.toml" >/dev/null
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exec "$BINARY" "$@"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const opportunityIds = [
|
|
4
|
+
"render-blocking-resources",
|
|
5
|
+
"unused-javascript",
|
|
6
|
+
"unused-css-rules",
|
|
7
|
+
"uses-optimized-images",
|
|
8
|
+
"modern-image-formats",
|
|
9
|
+
"uses-responsive-images",
|
|
10
|
+
"unminified-javascript",
|
|
11
|
+
"unminified-css",
|
|
12
|
+
"uses-text-compression",
|
|
13
|
+
"server-response-time",
|
|
14
|
+
"redirects",
|
|
15
|
+
"bootup-time",
|
|
16
|
+
"mainthread-work-breakdown",
|
|
17
|
+
"third-party-summary",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const browserCandidates = [
|
|
21
|
+
"/Applications/Helium.app/Contents/MacOS/Helium",
|
|
22
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
23
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
24
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
25
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function section(title) {
|
|
29
|
+
return `\n== ${title} ==`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatKeyValue(label, value) {
|
|
33
|
+
return `${label}: ${value}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bullet(value) {
|
|
37
|
+
return `- ${value}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function findBrowserPath({ env = process.env, config = {}, exists = existsSync } = {}) {
|
|
41
|
+
const candidates = [
|
|
42
|
+
config.browserPath,
|
|
43
|
+
env.AUDITKIT_BROWSER_PATH,
|
|
44
|
+
env.CHROME_PATH,
|
|
45
|
+
...browserCandidates,
|
|
46
|
+
].filter(Boolean);
|
|
47
|
+
|
|
48
|
+
return candidates.find((candidate) => exists(candidate)) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function categoryScore(category) {
|
|
52
|
+
if (!category || typeof category.score !== "number") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Math.round(category.score * 100);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function auditDisplayValue(audits, id) {
|
|
60
|
+
return audits?.[id]?.displayValue ?? "n/a";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function summarizeLighthouse(lhr) {
|
|
64
|
+
const audits = lhr.audits ?? {};
|
|
65
|
+
const opportunities = opportunityIds
|
|
66
|
+
.map((id) => ({ id, ...audits[id] }))
|
|
67
|
+
.filter((audit) => audit.title && typeof audit.score === "number" && audit.score < 0.9)
|
|
68
|
+
.slice(0, 6)
|
|
69
|
+
.map((audit) => ({
|
|
70
|
+
id: audit.id,
|
|
71
|
+
title: audit.title,
|
|
72
|
+
score: Math.round(audit.score * 100),
|
|
73
|
+
displayValue: audit.displayValue ?? "",
|
|
74
|
+
description: audit.description ?? "",
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
url: lhr.finalDisplayedUrl ?? lhr.finalUrl ?? "",
|
|
79
|
+
scores: {
|
|
80
|
+
performance: categoryScore(lhr.categories?.performance),
|
|
81
|
+
accessibility: categoryScore(lhr.categories?.accessibility),
|
|
82
|
+
bestPractices: categoryScore(lhr.categories?.["best-practices"]),
|
|
83
|
+
seo: categoryScore(lhr.categories?.seo),
|
|
84
|
+
},
|
|
85
|
+
vitals: {
|
|
86
|
+
lcp: auditDisplayValue(audits, "largest-contentful-paint"),
|
|
87
|
+
cls: auditDisplayValue(audits, "cumulative-layout-shift"),
|
|
88
|
+
tbt: auditDisplayValue(audits, "total-blocking-time"),
|
|
89
|
+
},
|
|
90
|
+
opportunities,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatLighthouseCli(summary) {
|
|
95
|
+
return [
|
|
96
|
+
section("Lighthouse Check"),
|
|
97
|
+
formatKeyValue("URL", summary.url),
|
|
98
|
+
formatKeyValue("Performance", `${summary.scores.performance ?? "n/a"}/100`),
|
|
99
|
+
formatKeyValue("Accessibility", `${summary.scores.accessibility ?? "n/a"}/100`),
|
|
100
|
+
formatKeyValue("Best practices", `${summary.scores.bestPractices ?? "n/a"}/100`),
|
|
101
|
+
formatKeyValue("SEO", `${summary.scores.seo ?? "n/a"}/100`),
|
|
102
|
+
section("Vitals"),
|
|
103
|
+
formatKeyValue("LCP", summary.vitals.lcp),
|
|
104
|
+
formatKeyValue("CLS", summary.vitals.cls),
|
|
105
|
+
formatKeyValue("TBT", summary.vitals.tbt),
|
|
106
|
+
section("Top Opportunities"),
|
|
107
|
+
...(summary.opportunities.length
|
|
108
|
+
? summary.opportunities.map((item) => bullet(`${item.title}${item.displayValue ? ` — ${item.displayValue}` : ""}`))
|
|
109
|
+
: [bullet("No major Lighthouse opportunities found.")]),
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function formatLighthouseReport(summary) {
|
|
114
|
+
return `# Lighthouse Check
|
|
115
|
+
|
|
116
|
+
URL: ${summary.url}
|
|
117
|
+
|
|
118
|
+
## Scores
|
|
119
|
+
|
|
120
|
+
- Performance: ${summary.scores.performance ?? "n/a"}/100
|
|
121
|
+
- Accessibility: ${summary.scores.accessibility ?? "n/a"}/100
|
|
122
|
+
- Best practices: ${summary.scores.bestPractices ?? "n/a"}/100
|
|
123
|
+
- SEO: ${summary.scores.seo ?? "n/a"}/100
|
|
124
|
+
|
|
125
|
+
## Core Web Vitals
|
|
126
|
+
|
|
127
|
+
- LCP: ${summary.vitals.lcp}
|
|
128
|
+
- CLS: ${summary.vitals.cls}
|
|
129
|
+
- TBT: ${summary.vitals.tbt}
|
|
130
|
+
|
|
131
|
+
## Top Opportunities
|
|
132
|
+
|
|
133
|
+
${
|
|
134
|
+
summary.opportunities.length
|
|
135
|
+
? summary.opportunities
|
|
136
|
+
.map((item) => `- ${item.title}${item.displayValue ? `: ${item.displayValue}` : ""}`)
|
|
137
|
+
.join("\n")
|
|
138
|
+
: "- No major Lighthouse opportunities found."
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function runLighthouse(inputUrl, options = {}) {
|
|
144
|
+
const [{ default: lighthouse }, chromeLauncher] = await Promise.all([
|
|
145
|
+
import("lighthouse"),
|
|
146
|
+
import("chrome-launcher"),
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const browserPath = findBrowserPath({ config: options });
|
|
150
|
+
let chrome;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
chrome = await chromeLauncher.launch({
|
|
154
|
+
chromePath: browserPath ?? undefined,
|
|
155
|
+
chromeFlags: ["--headless", "--no-sandbox", "--disable-gpu"],
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (String(error.message).includes("No Chrome installations found")) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
"No Chrome-compatible browser found. Install Chrome/Chromium/Brave/Edge, or set AUDITKIT_BROWSER_PATH.",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await lighthouse(inputUrl, {
|
|
169
|
+
port: chrome.port,
|
|
170
|
+
output: "json",
|
|
171
|
+
onlyCategories: ["performance", "accessibility", "best-practices", "seo"],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const lhr = result.lhr;
|
|
175
|
+
return {
|
|
176
|
+
lhr,
|
|
177
|
+
json: JSON.stringify(lhr, null, 2),
|
|
178
|
+
summary: summarizeLighthouse(lhr),
|
|
179
|
+
};
|
|
180
|
+
} finally {
|
|
181
|
+
await chrome.kill();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
findBrowserPath,
|
|
5
|
+
formatLighthouseReport,
|
|
6
|
+
summarizeLighthouse,
|
|
7
|
+
} from "./lighthouse-runner.mjs";
|
|
8
|
+
|
|
9
|
+
const lhr = {
|
|
10
|
+
finalDisplayedUrl: "https://example.com/",
|
|
11
|
+
categories: {
|
|
12
|
+
performance: { score: 0.87 },
|
|
13
|
+
accessibility: { score: 0.92 },
|
|
14
|
+
"best-practices": { score: 0.78 },
|
|
15
|
+
seo: { score: 1 },
|
|
16
|
+
},
|
|
17
|
+
audits: {
|
|
18
|
+
"largest-contentful-paint": { displayValue: "1.8 s" },
|
|
19
|
+
"cumulative-layout-shift": { displayValue: "0.02" },
|
|
20
|
+
"total-blocking-time": { displayValue: "120 ms" },
|
|
21
|
+
"render-blocking-resources": {
|
|
22
|
+
title: "Eliminate render-blocking resources",
|
|
23
|
+
description: "Resources are blocking first paint.",
|
|
24
|
+
score: 0.4,
|
|
25
|
+
displayValue: "Potential savings of 350 ms",
|
|
26
|
+
},
|
|
27
|
+
"uses-optimized-images": {
|
|
28
|
+
title: "Efficiently encode images",
|
|
29
|
+
description: "Images could be smaller.",
|
|
30
|
+
score: 0.7,
|
|
31
|
+
displayValue: "Potential savings of 120 KiB",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
test("summarizeLighthouse extracts scores, vitals, and opportunities", () => {
|
|
37
|
+
const summary = summarizeLighthouse(lhr);
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(summary.scores, {
|
|
40
|
+
performance: 87,
|
|
41
|
+
accessibility: 92,
|
|
42
|
+
bestPractices: 78,
|
|
43
|
+
seo: 100,
|
|
44
|
+
});
|
|
45
|
+
assert.equal(summary.vitals.lcp, "1.8 s");
|
|
46
|
+
assert.equal(summary.opportunities.length, 2);
|
|
47
|
+
assert.equal(summary.opportunities[0].title, "Eliminate render-blocking resources");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("formatLighthouseReport renders markdown summary", () => {
|
|
51
|
+
const report = formatLighthouseReport(summarizeLighthouse(lhr));
|
|
52
|
+
|
|
53
|
+
assert.match(report, /# Lighthouse Check/);
|
|
54
|
+
assert.match(report, /Performance: 87\/100/);
|
|
55
|
+
assert.match(report, /Eliminate render-blocking resources/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("findBrowserPath prefers explicit config path", () => {
|
|
59
|
+
assert.equal(
|
|
60
|
+
findBrowserPath({
|
|
61
|
+
env: {},
|
|
62
|
+
config: { browserPath: "/Applications/Helium.app/Contents/MacOS/Helium" },
|
|
63
|
+
exists: () => true,
|
|
64
|
+
}),
|
|
65
|
+
"/Applications/Helium.app/Contents/MacOS/Helium",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("findBrowserPath falls back to Helium app path", () => {
|
|
70
|
+
assert.equal(
|
|
71
|
+
findBrowserPath({
|
|
72
|
+
env: {},
|
|
73
|
+
config: {},
|
|
74
|
+
exists: (value) => value === "/Applications/Helium.app/Contents/MacOS/Helium",
|
|
75
|
+
}),
|
|
76
|
+
"/Applications/Helium.app/Contents/MacOS/Helium",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
2
|
+
|
|
3
|
+
const cyan = (value) => (useColor ? `\x1b[36m${value}\x1b[0m` : value);
|
|
4
|
+
const green = (value) => (useColor ? `\x1b[32m${value}\x1b[0m` : value);
|
|
5
|
+
const bold = (value) => (useColor ? `\x1b[1m${value}\x1b[0m` : value);
|
|
6
|
+
const dim = (value) => (useColor ? `\x1b[2m${value}\x1b[0m` : value);
|
|
7
|
+
|
|
8
|
+
const width = 54;
|
|
9
|
+
const edge = cyan("│");
|
|
10
|
+
const boxLine = (value = "", format = (text) => text) => {
|
|
11
|
+
const padded = value.padEnd(width - 4, " ");
|
|
12
|
+
return `${edge} ${format(value)}${" ".repeat(padded.length - value.length)} ${edge}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const lines = [
|
|
16
|
+
"",
|
|
17
|
+
cyan(`┌${"─".repeat(width - 2)}┐`),
|
|
18
|
+
boxLine("Welcome to Audit Kit", bold),
|
|
19
|
+
boxLine("Local audits, Lighthouse, and security checks."),
|
|
20
|
+
cyan(`└${"─".repeat(width - 2)}┘`),
|
|
21
|
+
"",
|
|
22
|
+
`${green("Next steps")}`,
|
|
23
|
+
` ${bold("ak new")} create audit workspace`,
|
|
24
|
+
` ${bold("ak inspect latest")} run checks for latest audit`,
|
|
25
|
+
` ${bold("ak report latest")} generate final report`,
|
|
26
|
+
"",
|
|
27
|
+
`${dim("Tip: first run builds the Rust CLI, so it may take a few seconds.")}`,
|
|
28
|
+
"",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
console.log(lines.join("\n"));
|
package/src/audit.rs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
2
|
+
pub struct AuditInput {
|
|
3
|
+
pub client_name: String,
|
|
4
|
+
pub slug: String,
|
|
5
|
+
pub url: String,
|
|
6
|
+
pub business_type: String,
|
|
7
|
+
pub goal: String,
|
|
8
|
+
pub target_customer: String,
|
|
9
|
+
pub conversion_action: String,
|
|
10
|
+
pub pages: Vec<String>,
|
|
11
|
+
pub known_concerns: Vec<String>,
|
|
12
|
+
pub competitors: Vec<String>,
|
|
13
|
+
pub created_at: String,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
pub fn slugify(value: &str) -> String {
|
|
17
|
+
let mut slug = String::new();
|
|
18
|
+
let mut previous_dash = false;
|
|
19
|
+
|
|
20
|
+
for character in value.trim().to_lowercase().chars() {
|
|
21
|
+
if character.is_ascii_alphanumeric() {
|
|
22
|
+
slug.push(character);
|
|
23
|
+
previous_dash = false;
|
|
24
|
+
} else if !previous_dash {
|
|
25
|
+
slug.push('-');
|
|
26
|
+
previous_dash = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
slug.trim_matches('-').to_string()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn split_comma_list(value: &str) -> Vec<String> {
|
|
34
|
+
value
|
|
35
|
+
.split(',')
|
|
36
|
+
.map(str::trim)
|
|
37
|
+
.filter(|item| !item.is_empty())
|
|
38
|
+
.map(ToOwned::to_owned)
|
|
39
|
+
.collect()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#[cfg(test)]
|
|
43
|
+
mod tests {
|
|
44
|
+
use super::*;
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn slugify_makes_folder_safe_names() {
|
|
48
|
+
assert_eq!(slugify("Acme Dental Ltd"), "acme-dental-ltd");
|
|
49
|
+
assert_eq!(slugify(" My Kind_of Cruise! "), "my-kind-of-cruise");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[test]
|
|
53
|
+
fn split_comma_list_ignores_empty_items() {
|
|
54
|
+
assert_eq!(
|
|
55
|
+
split_comma_list("/, /pricing, , /contact"),
|
|
56
|
+
vec!["/", "/pricing", "/contact"]
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|