@svelte-vitals/core 0.1.0 → 0.2.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/dist/index.d.ts +74 -6
- package/dist/index.js +296 -14
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,14 @@ interface Detection {
|
|
|
15
15
|
presence: Presence;
|
|
16
16
|
value: Value;
|
|
17
17
|
}
|
|
18
|
+
/** Project-wide facts precomputed by the runtime layer for project-scope rules (design §10). */
|
|
19
|
+
interface Project {
|
|
20
|
+
hasRobotsTxt: boolean;
|
|
21
|
+
hasSitemap: boolean;
|
|
22
|
+
/** <html lang> from app.html: presence 'own' when the attribute exists ('none' otherwise); value 'static' if non-empty, 'absent' if empty. */
|
|
23
|
+
htmlLang: Detection;
|
|
24
|
+
}
|
|
25
|
+
declare const defaultProject: Project;
|
|
18
26
|
/** A single rule finding for one route (or the whole project). */
|
|
19
27
|
interface Result {
|
|
20
28
|
/** Rule id, e.g. 'SEO001'. */
|
|
@@ -33,10 +41,16 @@ type Scope = 'route' | 'project';
|
|
|
33
41
|
type Category = 'seo' | 'performance' | 'a11y' | 'maintainability';
|
|
34
42
|
/** How dynamic (`{data.title}`) values are treated by scoring (design §4, §12). */
|
|
35
43
|
type TreatDynamicAs = 'pass' | 'warn' | 'fail';
|
|
44
|
+
/** Per-rule override: disable, or change severity. */
|
|
45
|
+
type RuleSetting = 'off' | Severity;
|
|
36
46
|
interface Config {
|
|
37
47
|
treatDynamicAs: TreatDynamicAs;
|
|
38
48
|
/** Component names treated as meta sources of unknown content (design §11 layer 4). */
|
|
39
49
|
metaComponents: string[];
|
|
50
|
+
/** Per-rule overrides keyed by rule id (design §6). */
|
|
51
|
+
rules: Record<string, RuleSetting>;
|
|
52
|
+
/** Minimum severity that fails the run / CI (design §6). */
|
|
53
|
+
failOn: Severity;
|
|
40
54
|
}
|
|
41
55
|
declare const defaultConfig: Config;
|
|
42
56
|
/** Merge user config over defaults. Identity helper for config files (design §6). */
|
|
@@ -100,6 +114,7 @@ interface HeadProvider {
|
|
|
100
114
|
/** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
|
|
101
115
|
interface RuleContext {
|
|
102
116
|
heads: ResolvedHead[];
|
|
117
|
+
project: Project;
|
|
103
118
|
config: Config;
|
|
104
119
|
}
|
|
105
120
|
interface Rule {
|
|
@@ -122,7 +137,7 @@ interface Rule {
|
|
|
122
137
|
*
|
|
123
138
|
* presence 'none' → penalized (nothing set anywhere)
|
|
124
139
|
* value 'absent' → penalized (tag present but empty)
|
|
125
|
-
* value 'dynamic' → penalized
|
|
140
|
+
* value 'dynamic' → penalized when treatDynamicAs is not 'pass' (warn or fail)
|
|
126
141
|
* otherwise (static/inherited) → not penalized
|
|
127
142
|
*/
|
|
128
143
|
declare function isPenalized(detection: Detection, treatDynamicAs: TreatDynamicAs): boolean;
|
|
@@ -141,9 +156,31 @@ declare function runRules(rules: Rule[], ctx: RuleContext): Promise<Result[]>;
|
|
|
141
156
|
*/
|
|
142
157
|
declare const seo001Title: Rule;
|
|
143
158
|
|
|
144
|
-
|
|
159
|
+
declare const seo002Description: Rule;
|
|
160
|
+
declare const seo003Canonical: Rule;
|
|
161
|
+
declare const seo004OgImage: Rule;
|
|
162
|
+
declare const seo005OgTitle: Rule;
|
|
163
|
+
declare const seo008JsonLd: Rule;
|
|
164
|
+
|
|
165
|
+
declare const seo006Robots: Rule;
|
|
166
|
+
declare const seo007Sitemap: Rule;
|
|
167
|
+
declare const seo009HtmlLang: Rule;
|
|
168
|
+
|
|
145
169
|
declare const allRules: Rule[];
|
|
146
170
|
|
|
171
|
+
interface HeadTagRuleOptions {
|
|
172
|
+
id: string;
|
|
173
|
+
title: string;
|
|
174
|
+
severity: Severity;
|
|
175
|
+
/** Identifies the tag this rule looks for. */
|
|
176
|
+
match: (tag: HeadTag) => boolean;
|
|
177
|
+
/** Short human label, e.g. 'description'. */
|
|
178
|
+
label: string;
|
|
179
|
+
recommendation: string;
|
|
180
|
+
}
|
|
181
|
+
/** Build a route-scope rule asserting the presence of a single head tag (design §11). */
|
|
182
|
+
declare function headTagRule(opts: HeadTagRuleOptions): Rule;
|
|
183
|
+
|
|
147
184
|
interface Summary {
|
|
148
185
|
critical: number;
|
|
149
186
|
warning: number;
|
|
@@ -156,15 +193,46 @@ interface Summary {
|
|
|
156
193
|
/** Classify a single result for display/scoring (design §7, §12). */
|
|
157
194
|
type Classification = 'fail' | 'pass' | 'dynamic';
|
|
158
195
|
declare function classify(result: Result, config: Config): Classification;
|
|
196
|
+
/** A penalized dynamic finding is a warning under treatDynamicAs 'warn'; otherwise the rule's severity. */
|
|
197
|
+
declare function effectiveSeverity(result: Result, config: Config): Severity;
|
|
159
198
|
declare function summarize(results: Result[], config: Config): Summary;
|
|
160
199
|
/** Whether the run should fail the build/CI per the minimum failing severity. */
|
|
161
200
|
declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
|
|
162
201
|
|
|
202
|
+
interface ConsoleReportOptions {
|
|
203
|
+
byRoute?: boolean;
|
|
204
|
+
}
|
|
163
205
|
/**
|
|
164
206
|
* Render results as a console report string (design §7). Pure: returns a string,
|
|
165
|
-
* the caller is responsible for printing.
|
|
166
|
-
*
|
|
207
|
+
* the caller is responsible for printing. Prepends a score header; when byRoute is
|
|
208
|
+
* set, adds a per-route score tree.
|
|
167
209
|
*/
|
|
168
|
-
declare function formatConsoleReport(results: Result[], config: Config): string;
|
|
210
|
+
declare function formatConsoleReport(results: Result[], config: Config, options?: ConsoleReportOptions): string;
|
|
211
|
+
|
|
212
|
+
/** Render results as the documented JSON report string (design §7). */
|
|
213
|
+
declare function formatJsonReport(results: Result[], config: Config, meta: {
|
|
214
|
+
version: string;
|
|
215
|
+
}): string;
|
|
216
|
+
|
|
217
|
+
/** Drop rules disabled via config (design §6). */
|
|
218
|
+
declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
219
|
+
/** Apply per-rule severity overrides to results (design §6). */
|
|
220
|
+
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
221
|
+
|
|
222
|
+
interface ScoreModel {
|
|
223
|
+
routeAverage: number;
|
|
224
|
+
sitePenalty: number;
|
|
225
|
+
/** Headline cap value when it actually lowered the score, else null. */
|
|
226
|
+
criticalCap: number | null;
|
|
227
|
+
}
|
|
228
|
+
interface ScoreResult {
|
|
229
|
+
score: number;
|
|
230
|
+
scoreModel: ScoreModel;
|
|
231
|
+
}
|
|
232
|
+
interface ScoreOptions {
|
|
233
|
+
applyCriticalCap?: boolean;
|
|
234
|
+
}
|
|
235
|
+
/** Compute the headline score and its breakdown (design §12). */
|
|
236
|
+
declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
|
|
169
237
|
|
|
170
|
-
export { type Category, type Classification, type Config, type Detection, type HeadProvider, type HeadTag, type Presence, type ResolvedHead, type Result, type Rule, type RuleContext, type Runtime, type Scope, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, classify, defaultConfig, defineConfig, formatConsoleReport, hasFailureAtOrAbove, isPenalized, runRules, seo001Title, summarize };
|
|
238
|
+
export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type HeadProvider, type HeadTag, type Presence, type Project, type ResolvedHead, type Result, type Rule, type RuleContext, type RuleSetting, type Runtime, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, classify, computeScore, defaultConfig, defaultProject, defineConfig, effectiveSeverity, formatConsoleReport, formatJsonReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
// src/types.ts
|
|
2
|
+
var defaultProject = {
|
|
3
|
+
hasRobotsTxt: false,
|
|
4
|
+
hasSitemap: false,
|
|
5
|
+
htmlLang: { presence: "none", value: "absent" }
|
|
6
|
+
};
|
|
2
7
|
var defaultConfig = {
|
|
3
8
|
treatDynamicAs: "pass",
|
|
4
|
-
metaComponents: []
|
|
9
|
+
metaComponents: [],
|
|
10
|
+
rules: {},
|
|
11
|
+
failOn: "critical"
|
|
5
12
|
};
|
|
6
13
|
function defineConfig(config = {}) {
|
|
7
14
|
return { ...defaultConfig, ...config };
|
|
@@ -11,7 +18,7 @@ function defineConfig(config = {}) {
|
|
|
11
18
|
function isPenalized(detection, treatDynamicAs) {
|
|
12
19
|
if (detection.presence === "none") return true;
|
|
13
20
|
if (detection.value === "absent") return true;
|
|
14
|
-
if (detection.value === "dynamic") return treatDynamicAs
|
|
21
|
+
if (detection.value === "dynamic") return treatDynamicAs !== "pass";
|
|
15
22
|
return false;
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -58,8 +65,157 @@ var seo001Title = {
|
|
|
58
65
|
}
|
|
59
66
|
};
|
|
60
67
|
|
|
68
|
+
// src/rules/seo/head-tag-rule.ts
|
|
69
|
+
function detect(head, match) {
|
|
70
|
+
const tag = head.tags.find(match);
|
|
71
|
+
return tag ? { presence: tag.presence, value: tag.value } : { presence: "none", value: "absent" };
|
|
72
|
+
}
|
|
73
|
+
function headTagRule(opts) {
|
|
74
|
+
const docsUrl = `https://svelte-vitals.dev/rules/${opts.id}`;
|
|
75
|
+
return {
|
|
76
|
+
id: opts.id,
|
|
77
|
+
title: opts.title,
|
|
78
|
+
category: "seo",
|
|
79
|
+
severity: opts.severity,
|
|
80
|
+
scope: "route",
|
|
81
|
+
async check(ctx) {
|
|
82
|
+
return ctx.heads.map((head) => {
|
|
83
|
+
const detection = detect(head, opts.match);
|
|
84
|
+
const message = detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
|
|
85
|
+
return {
|
|
86
|
+
id: opts.id,
|
|
87
|
+
severity: opts.severity,
|
|
88
|
+
detection,
|
|
89
|
+
route: head.route,
|
|
90
|
+
location: head.file,
|
|
91
|
+
message,
|
|
92
|
+
recommendation: opts.recommendation,
|
|
93
|
+
docsUrl
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/rules/seo/seo002-005-008.ts
|
|
101
|
+
var seo002Description = headTagRule({
|
|
102
|
+
id: "SEO002",
|
|
103
|
+
title: "Description presence",
|
|
104
|
+
severity: "critical",
|
|
105
|
+
match: (t) => t.kind === "meta" && t.name === "description",
|
|
106
|
+
label: '<meta name="description">',
|
|
107
|
+
recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.'
|
|
108
|
+
});
|
|
109
|
+
var seo003Canonical = headTagRule({
|
|
110
|
+
id: "SEO003",
|
|
111
|
+
title: "Canonical URL",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
match: (t) => t.kind === "link" && t.rel === "canonical",
|
|
114
|
+
label: '<link rel="canonical">',
|
|
115
|
+
recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.'
|
|
116
|
+
});
|
|
117
|
+
var seo004OgImage = headTagRule({
|
|
118
|
+
id: "SEO004",
|
|
119
|
+
title: "Open Graph image",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
match: (t) => t.kind === "meta" && t.property === "og:image",
|
|
122
|
+
label: '<meta property="og:image">',
|
|
123
|
+
recommendation: 'Add <meta property="og:image">, or set openGraph.images on your meta component.'
|
|
124
|
+
});
|
|
125
|
+
var seo005OgTitle = headTagRule({
|
|
126
|
+
id: "SEO005",
|
|
127
|
+
title: "Open Graph title",
|
|
128
|
+
severity: "warning",
|
|
129
|
+
match: (t) => t.kind === "meta" && t.property === "og:title",
|
|
130
|
+
label: '<meta property="og:title">',
|
|
131
|
+
recommendation: 'Add <meta property="og:title">, or set openGraph.title on your meta component.'
|
|
132
|
+
});
|
|
133
|
+
var seo008JsonLd = headTagRule({
|
|
134
|
+
id: "SEO008",
|
|
135
|
+
title: "JSON-LD structured data",
|
|
136
|
+
severity: "info",
|
|
137
|
+
match: (t) => t.kind === "jsonld",
|
|
138
|
+
label: 'JSON-LD (<script type="application/ld+json">)',
|
|
139
|
+
recommendation: "Add JSON-LD structured data, e.g. via <svelte:head> or a JsonLd component."
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// src/rules/seo/project-rules.ts
|
|
143
|
+
var present = { presence: "own", value: "static" };
|
|
144
|
+
var absent = { presence: "none", value: "absent" };
|
|
145
|
+
var seo006Robots = {
|
|
146
|
+
id: "SEO006",
|
|
147
|
+
title: "robots.txt",
|
|
148
|
+
category: "seo",
|
|
149
|
+
severity: "warning",
|
|
150
|
+
scope: "project",
|
|
151
|
+
async check(ctx) {
|
|
152
|
+
const detection = ctx.project.hasRobotsTxt ? present : absent;
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
id: "SEO006",
|
|
156
|
+
severity: "warning",
|
|
157
|
+
detection,
|
|
158
|
+
message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
|
|
159
|
+
recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
|
|
160
|
+
docsUrl: "https://svelte-vitals.dev/rules/SEO006"
|
|
161
|
+
}
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
var seo007Sitemap = {
|
|
166
|
+
id: "SEO007",
|
|
167
|
+
title: "sitemap.xml",
|
|
168
|
+
category: "seo",
|
|
169
|
+
severity: "warning",
|
|
170
|
+
scope: "project",
|
|
171
|
+
async check(ctx) {
|
|
172
|
+
const detection = ctx.project.hasSitemap ? present : absent;
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
id: "SEO007",
|
|
176
|
+
severity: "warning",
|
|
177
|
+
detection,
|
|
178
|
+
message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
|
|
179
|
+
recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
|
|
180
|
+
docsUrl: "https://svelte-vitals.dev/rules/SEO007"
|
|
181
|
+
}
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var seo009HtmlLang = {
|
|
186
|
+
id: "SEO009",
|
|
187
|
+
title: "<html lang>",
|
|
188
|
+
category: "seo",
|
|
189
|
+
severity: "warning",
|
|
190
|
+
scope: "project",
|
|
191
|
+
async check(ctx) {
|
|
192
|
+
const detection = ctx.project.htmlLang;
|
|
193
|
+
const message = detection.presence === "none" ? "Missing <html lang>" : detection.value === "absent" ? "Empty <html lang>" : "<html lang>";
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
id: "SEO009",
|
|
197
|
+
severity: "warning",
|
|
198
|
+
detection,
|
|
199
|
+
message,
|
|
200
|
+
recommendation: 'Set <html lang="..."> in src/app.html.',
|
|
201
|
+
docsUrl: "https://svelte-vitals.dev/rules/SEO009"
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
61
207
|
// src/rules/index.ts
|
|
62
|
-
var allRules = [
|
|
208
|
+
var allRules = [
|
|
209
|
+
seo001Title,
|
|
210
|
+
seo002Description,
|
|
211
|
+
seo003Canonical,
|
|
212
|
+
seo004OgImage,
|
|
213
|
+
seo005OgTitle,
|
|
214
|
+
seo006Robots,
|
|
215
|
+
seo007Sitemap,
|
|
216
|
+
seo008JsonLd,
|
|
217
|
+
seo009HtmlLang
|
|
218
|
+
];
|
|
63
219
|
|
|
64
220
|
// src/summary.ts
|
|
65
221
|
function classify(result, config) {
|
|
@@ -67,12 +223,16 @@ function classify(result, config) {
|
|
|
67
223
|
if (result.detection.value === "dynamic") return "dynamic";
|
|
68
224
|
return "pass";
|
|
69
225
|
}
|
|
226
|
+
function effectiveSeverity(result, config) {
|
|
227
|
+
if (result.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
|
|
228
|
+
return result.severity;
|
|
229
|
+
}
|
|
70
230
|
function summarize(results, config) {
|
|
71
231
|
const summary = { critical: 0, warning: 0, info: 0, passed: 0, dynamic: 0 };
|
|
72
232
|
for (const result of results) {
|
|
73
233
|
const cls = classify(result, config);
|
|
74
234
|
if (cls === "fail") {
|
|
75
|
-
summary[
|
|
235
|
+
summary[effectiveSeverity(result, config)] += 1;
|
|
76
236
|
} else {
|
|
77
237
|
summary.passed += 1;
|
|
78
238
|
if (cls === "dynamic") summary.dynamic += 1;
|
|
@@ -83,10 +243,55 @@ function summarize(results, config) {
|
|
|
83
243
|
function hasFailureAtOrAbove(summary, min) {
|
|
84
244
|
const order = ["info", "warning", "critical"];
|
|
85
245
|
const threshold = order.indexOf(min);
|
|
86
|
-
return order.some((sev, idx) => idx >= threshold && summary[
|
|
246
|
+
return order.some((sev, idx) => idx >= threshold && summary[sev] > 0);
|
|
87
247
|
}
|
|
88
|
-
|
|
89
|
-
|
|
248
|
+
|
|
249
|
+
// src/scoring/score.ts
|
|
250
|
+
var DEDUCTION = { critical: 15, warning: 5, info: 1 };
|
|
251
|
+
var CRITICAL_CAP = 79;
|
|
252
|
+
function clamp(n) {
|
|
253
|
+
return Math.max(0, Math.min(100, n));
|
|
254
|
+
}
|
|
255
|
+
function computeScore(results, config, options = {}) {
|
|
256
|
+
const routeResults = results.filter((r) => r.route !== void 0);
|
|
257
|
+
const projectResults = results.filter((r) => r.route === void 0);
|
|
258
|
+
const routeScores = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const r of routeResults) if (!routeScores.has(r.route)) routeScores.set(r.route, 100);
|
|
260
|
+
let anyCritical = false;
|
|
261
|
+
const routeRuleMax = /* @__PURE__ */ new Map();
|
|
262
|
+
for (const r of routeResults) {
|
|
263
|
+
if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
|
|
264
|
+
const sev = effectiveSeverity(r, config);
|
|
265
|
+
if (sev === "critical") anyCritical = true;
|
|
266
|
+
const route = r.route;
|
|
267
|
+
let perRule = routeRuleMax.get(route);
|
|
268
|
+
if (!perRule) routeRuleMax.set(route, perRule = /* @__PURE__ */ new Map());
|
|
269
|
+
const prev = perRule.get(r.id) ?? 0;
|
|
270
|
+
if (DEDUCTION[sev] > prev) perRule.set(r.id, DEDUCTION[sev]);
|
|
271
|
+
}
|
|
272
|
+
for (const [route, perRule] of routeRuleMax) {
|
|
273
|
+
let deduction = 0;
|
|
274
|
+
for (const d of perRule.values()) deduction += d;
|
|
275
|
+
routeScores.set(route, routeScores.get(route) - deduction);
|
|
276
|
+
}
|
|
277
|
+
const scores = [...routeScores.values()].map(clamp);
|
|
278
|
+
const routeAverage = scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : 100;
|
|
279
|
+
const projectRuleMax = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const r of projectResults) {
|
|
281
|
+
if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
|
|
282
|
+
const sev = effectiveSeverity(r, config);
|
|
283
|
+
if (sev === "critical") anyCritical = true;
|
|
284
|
+
const prev = projectRuleMax.get(r.id) ?? 0;
|
|
285
|
+
if (DEDUCTION[sev] > prev) projectRuleMax.set(r.id, DEDUCTION[sev]);
|
|
286
|
+
}
|
|
287
|
+
let sitePenalty = 0;
|
|
288
|
+
for (const deduction of projectRuleMax.values()) sitePenalty += deduction;
|
|
289
|
+
const applyCap = options.applyCriticalCap ?? true;
|
|
290
|
+
const uncapped = routeAverage - sitePenalty;
|
|
291
|
+
const capBinds = applyCap && anyCritical && uncapped > CRITICAL_CAP;
|
|
292
|
+
const criticalCap = capBinds ? CRITICAL_CAP : null;
|
|
293
|
+
const score = capBinds ? CRITICAL_CAP : uncapped;
|
|
294
|
+
return { score: clamp(score), scoreModel: { routeAverage, sitePenalty, criticalCap } };
|
|
90
295
|
}
|
|
91
296
|
|
|
92
297
|
// src/reporter/console.ts
|
|
@@ -96,13 +301,37 @@ var SEVERITY_TITLE = {
|
|
|
96
301
|
warning: "Warnings",
|
|
97
302
|
info: "Info"
|
|
98
303
|
};
|
|
99
|
-
function
|
|
304
|
+
function scoreHeader(results, config) {
|
|
305
|
+
const { score, scoreModel } = computeScore(results, config);
|
|
306
|
+
const parts = [`route avg ${scoreModel.routeAverage}`];
|
|
307
|
+
if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
|
|
308
|
+
if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
|
|
309
|
+
return `SEO Score: ${score}/100 (${parts.join(" \xB7 ")})`;
|
|
310
|
+
}
|
|
311
|
+
function byRouteTree(results, config) {
|
|
312
|
+
const routes = /* @__PURE__ */ new Map();
|
|
313
|
+
for (const r of results) {
|
|
314
|
+
if (r.route === void 0) continue;
|
|
315
|
+
if (!routes.has(r.route)) routes.set(r.route, []);
|
|
316
|
+
routes.get(r.route).push(r);
|
|
317
|
+
}
|
|
318
|
+
const lines = ["By route", RULE];
|
|
319
|
+
for (const [route, rs] of [...routes.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
320
|
+
const { score } = computeScore(rs, config, { applyCriticalCap: false });
|
|
321
|
+
lines.push(`${route.padEnd(28)} ${score}`);
|
|
322
|
+
for (const r of rs.filter((x) => classify(x, config) === "fail")) {
|
|
323
|
+
lines.push(` \u2717 ${r.id} ${r.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
lines.push("");
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
function formatConsoleReport(results, config, options = {}) {
|
|
100
330
|
const summary = summarize(results, config);
|
|
101
|
-
const lines = [];
|
|
102
|
-
lines.push("Svelte Vitals \xB7 SEO (static mode)", "");
|
|
331
|
+
const lines = ["Svelte Vitals \xB7 SEO (static mode)", "", scoreHeader(results, config), ""];
|
|
103
332
|
const failures = results.filter((r) => classify(r, config) === "fail");
|
|
104
333
|
for (const severity of ["critical", "warning", "info"]) {
|
|
105
|
-
const bucket = failures.filter((r) => r
|
|
334
|
+
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
106
335
|
if (bucket.length === 0) continue;
|
|
107
336
|
lines.push(`${SEVERITY_TITLE[severity]} (${bucket.length})`, RULE);
|
|
108
337
|
for (const r of bucket) {
|
|
@@ -122,20 +351,73 @@ function formatConsoleReport(results, config) {
|
|
|
122
351
|
}
|
|
123
352
|
lines.push("");
|
|
124
353
|
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
}
|
|
354
|
+
if (options.byRoute) lines.push(...byRouteTree(results, config));
|
|
355
|
+
if (summary.dynamic > 0) lines.push("\u21AF = set dynamically (verified at runtime).");
|
|
128
356
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
129
357
|
}
|
|
358
|
+
|
|
359
|
+
// src/reporter/json.ts
|
|
360
|
+
function issueOf(result) {
|
|
361
|
+
return {
|
|
362
|
+
id: result.id,
|
|
363
|
+
title: result.message,
|
|
364
|
+
detection: result.detection,
|
|
365
|
+
location: result.location,
|
|
366
|
+
recommendation: result.recommendation
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function formatJsonReport(results, config, meta) {
|
|
370
|
+
const { score, scoreModel } = computeScore(results, config);
|
|
371
|
+
const summary = summarize(results, config);
|
|
372
|
+
const routeMap = /* @__PURE__ */ new Map();
|
|
373
|
+
for (const r of results) {
|
|
374
|
+
if (r.route === void 0) continue;
|
|
375
|
+
if (!routeMap.has(r.route)) routeMap.set(r.route, { route: r.route, results: [] });
|
|
376
|
+
routeMap.get(r.route).results.push(r);
|
|
377
|
+
}
|
|
378
|
+
const routes = [...routeMap.values()].sort((a, b) => a.route.localeCompare(b.route)).map(({ route, results: rs }) => ({
|
|
379
|
+
route,
|
|
380
|
+
score: computeScore(rs, config, { applyCriticalCap: false }).score,
|
|
381
|
+
issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
|
|
382
|
+
}));
|
|
383
|
+
const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
|
|
384
|
+
return JSON.stringify({ version: meta.version, score, scoreModel, summary, routes, siteIssues }, null, 2);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/config-apply.ts
|
|
388
|
+
function selectRules(rules, config) {
|
|
389
|
+
return rules.filter((rule) => config.rules[rule.id] !== "off");
|
|
390
|
+
}
|
|
391
|
+
function applyRuleSeverities(results, config) {
|
|
392
|
+
return results.map((result) => {
|
|
393
|
+
const setting = config.rules[result.id];
|
|
394
|
+
return setting && setting !== "off" ? { ...result, severity: setting } : result;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
130
397
|
export {
|
|
131
398
|
allRules,
|
|
399
|
+
applyRuleSeverities,
|
|
132
400
|
classify,
|
|
401
|
+
computeScore,
|
|
133
402
|
defaultConfig,
|
|
403
|
+
defaultProject,
|
|
134
404
|
defineConfig,
|
|
405
|
+
effectiveSeverity,
|
|
135
406
|
formatConsoleReport,
|
|
407
|
+
formatJsonReport,
|
|
136
408
|
hasFailureAtOrAbove,
|
|
409
|
+
headTagRule,
|
|
137
410
|
isPenalized,
|
|
138
411
|
runRules,
|
|
412
|
+
selectRules,
|
|
139
413
|
seo001Title,
|
|
414
|
+
seo002Description,
|
|
415
|
+
seo003Canonical,
|
|
416
|
+
seo004OgImage,
|
|
417
|
+
seo005OgTitle,
|
|
418
|
+
seo006Robots,
|
|
419
|
+
seo007Sitemap,
|
|
420
|
+
seo008JsonLd,
|
|
421
|
+
seo009HtmlLang,
|
|
140
422
|
summarize
|
|
141
423
|
};
|