@svelte-vitals/core 0.0.1 → 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 +77 -7
- package/dist/index.js +297 -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,8 +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;
|
|
48
|
+
/** Component names treated as meta sources of unknown content (design §11 layer 4). */
|
|
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;
|
|
38
54
|
}
|
|
39
55
|
declare const defaultConfig: Config;
|
|
40
56
|
/** Merge user config over defaults. Identity helper for config files (design §6). */
|
|
@@ -92,12 +108,13 @@ interface ResolvedHead {
|
|
|
92
108
|
/** Supplies ResolvedHead[] for a project. The only piece that differs per mode. */
|
|
93
109
|
interface HeadProvider {
|
|
94
110
|
mode: 'static' | 'rendered';
|
|
95
|
-
collect(rt: Runtime, cwd: string): Promise<ResolvedHead[]>;
|
|
111
|
+
collect(rt: Runtime, cwd: string, config?: Config): Promise<ResolvedHead[]>;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
/** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
|
|
99
115
|
interface RuleContext {
|
|
100
116
|
heads: ResolvedHead[];
|
|
117
|
+
project: Project;
|
|
101
118
|
config: Config;
|
|
102
119
|
}
|
|
103
120
|
interface Rule {
|
|
@@ -120,7 +137,7 @@ interface Rule {
|
|
|
120
137
|
*
|
|
121
138
|
* presence 'none' → penalized (nothing set anywhere)
|
|
122
139
|
* value 'absent' → penalized (tag present but empty)
|
|
123
|
-
* value 'dynamic' → penalized
|
|
140
|
+
* value 'dynamic' → penalized when treatDynamicAs is not 'pass' (warn or fail)
|
|
124
141
|
* otherwise (static/inherited) → not penalized
|
|
125
142
|
*/
|
|
126
143
|
declare function isPenalized(detection: Detection, treatDynamicAs: TreatDynamicAs): boolean;
|
|
@@ -139,9 +156,31 @@ declare function runRules(rules: Rule[], ctx: RuleContext): Promise<Result[]>;
|
|
|
139
156
|
*/
|
|
140
157
|
declare const seo001Title: Rule;
|
|
141
158
|
|
|
142
|
-
|
|
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
|
+
|
|
143
169
|
declare const allRules: Rule[];
|
|
144
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
|
+
|
|
145
184
|
interface Summary {
|
|
146
185
|
critical: number;
|
|
147
186
|
warning: number;
|
|
@@ -154,15 +193,46 @@ interface Summary {
|
|
|
154
193
|
/** Classify a single result for display/scoring (design §7, §12). */
|
|
155
194
|
type Classification = 'fail' | 'pass' | 'dynamic';
|
|
156
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;
|
|
157
198
|
declare function summarize(results: Result[], config: Config): Summary;
|
|
158
199
|
/** Whether the run should fail the build/CI per the minimum failing severity. */
|
|
159
200
|
declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
|
|
160
201
|
|
|
202
|
+
interface ConsoleReportOptions {
|
|
203
|
+
byRoute?: boolean;
|
|
204
|
+
}
|
|
161
205
|
/**
|
|
162
206
|
* Render results as a console report string (design §7). Pure: returns a string,
|
|
163
|
-
* the caller is responsible for printing.
|
|
164
|
-
*
|
|
207
|
+
* the caller is responsible for printing. Prepends a score header; when byRoute is
|
|
208
|
+
* set, adds a per-route score tree.
|
|
165
209
|
*/
|
|
166
|
-
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;
|
|
167
237
|
|
|
168
|
-
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,6 +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
|
-
treatDynamicAs: "pass"
|
|
8
|
+
treatDynamicAs: "pass",
|
|
9
|
+
metaComponents: [],
|
|
10
|
+
rules: {},
|
|
11
|
+
failOn: "critical"
|
|
4
12
|
};
|
|
5
13
|
function defineConfig(config = {}) {
|
|
6
14
|
return { ...defaultConfig, ...config };
|
|
@@ -10,7 +18,7 @@ function defineConfig(config = {}) {
|
|
|
10
18
|
function isPenalized(detection, treatDynamicAs) {
|
|
11
19
|
if (detection.presence === "none") return true;
|
|
12
20
|
if (detection.value === "absent") return true;
|
|
13
|
-
if (detection.value === "dynamic") return treatDynamicAs
|
|
21
|
+
if (detection.value === "dynamic") return treatDynamicAs !== "pass";
|
|
14
22
|
return false;
|
|
15
23
|
}
|
|
16
24
|
|
|
@@ -57,8 +65,157 @@ var seo001Title = {
|
|
|
57
65
|
}
|
|
58
66
|
};
|
|
59
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
|
+
|
|
60
207
|
// src/rules/index.ts
|
|
61
|
-
var allRules = [
|
|
208
|
+
var allRules = [
|
|
209
|
+
seo001Title,
|
|
210
|
+
seo002Description,
|
|
211
|
+
seo003Canonical,
|
|
212
|
+
seo004OgImage,
|
|
213
|
+
seo005OgTitle,
|
|
214
|
+
seo006Robots,
|
|
215
|
+
seo007Sitemap,
|
|
216
|
+
seo008JsonLd,
|
|
217
|
+
seo009HtmlLang
|
|
218
|
+
];
|
|
62
219
|
|
|
63
220
|
// src/summary.ts
|
|
64
221
|
function classify(result, config) {
|
|
@@ -66,12 +223,16 @@ function classify(result, config) {
|
|
|
66
223
|
if (result.detection.value === "dynamic") return "dynamic";
|
|
67
224
|
return "pass";
|
|
68
225
|
}
|
|
226
|
+
function effectiveSeverity(result, config) {
|
|
227
|
+
if (result.detection.value === "dynamic" && config.treatDynamicAs === "warn") return "warning";
|
|
228
|
+
return result.severity;
|
|
229
|
+
}
|
|
69
230
|
function summarize(results, config) {
|
|
70
231
|
const summary = { critical: 0, warning: 0, info: 0, passed: 0, dynamic: 0 };
|
|
71
232
|
for (const result of results) {
|
|
72
233
|
const cls = classify(result, config);
|
|
73
234
|
if (cls === "fail") {
|
|
74
|
-
summary[
|
|
235
|
+
summary[effectiveSeverity(result, config)] += 1;
|
|
75
236
|
} else {
|
|
76
237
|
summary.passed += 1;
|
|
77
238
|
if (cls === "dynamic") summary.dynamic += 1;
|
|
@@ -82,10 +243,55 @@ function summarize(results, config) {
|
|
|
82
243
|
function hasFailureAtOrAbove(summary, min) {
|
|
83
244
|
const order = ["info", "warning", "critical"];
|
|
84
245
|
const threshold = order.indexOf(min);
|
|
85
|
-
return order.some((sev, idx) => idx >= threshold && summary[
|
|
246
|
+
return order.some((sev, idx) => idx >= threshold && summary[sev] > 0);
|
|
86
247
|
}
|
|
87
|
-
|
|
88
|
-
|
|
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 } };
|
|
89
295
|
}
|
|
90
296
|
|
|
91
297
|
// src/reporter/console.ts
|
|
@@ -95,13 +301,37 @@ var SEVERITY_TITLE = {
|
|
|
95
301
|
warning: "Warnings",
|
|
96
302
|
info: "Info"
|
|
97
303
|
};
|
|
98
|
-
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 = {}) {
|
|
99
330
|
const summary = summarize(results, config);
|
|
100
|
-
const lines = [];
|
|
101
|
-
lines.push("Svelte Vitals \xB7 SEO (static mode)", "");
|
|
331
|
+
const lines = ["Svelte Vitals \xB7 SEO (static mode)", "", scoreHeader(results, config), ""];
|
|
102
332
|
const failures = results.filter((r) => classify(r, config) === "fail");
|
|
103
333
|
for (const severity of ["critical", "warning", "info"]) {
|
|
104
|
-
const bucket = failures.filter((r) => r
|
|
334
|
+
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
105
335
|
if (bucket.length === 0) continue;
|
|
106
336
|
lines.push(`${SEVERITY_TITLE[severity]} (${bucket.length})`, RULE);
|
|
107
337
|
for (const r of bucket) {
|
|
@@ -121,20 +351,73 @@ function formatConsoleReport(results, config) {
|
|
|
121
351
|
}
|
|
122
352
|
lines.push("");
|
|
123
353
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
}
|
|
354
|
+
if (options.byRoute) lines.push(...byRouteTree(results, config));
|
|
355
|
+
if (summary.dynamic > 0) lines.push("\u21AF = set dynamically (verified at runtime).");
|
|
127
356
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
128
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
|
+
}
|
|
129
397
|
export {
|
|
130
398
|
allRules,
|
|
399
|
+
applyRuleSeverities,
|
|
131
400
|
classify,
|
|
401
|
+
computeScore,
|
|
132
402
|
defaultConfig,
|
|
403
|
+
defaultProject,
|
|
133
404
|
defineConfig,
|
|
405
|
+
effectiveSeverity,
|
|
134
406
|
formatConsoleReport,
|
|
407
|
+
formatJsonReport,
|
|
135
408
|
hasFailureAtOrAbove,
|
|
409
|
+
headTagRule,
|
|
136
410
|
isPenalized,
|
|
137
411
|
runRules,
|
|
412
|
+
selectRules,
|
|
138
413
|
seo001Title,
|
|
414
|
+
seo002Description,
|
|
415
|
+
seo003Canonical,
|
|
416
|
+
seo004OgImage,
|
|
417
|
+
seo005OgTitle,
|
|
418
|
+
seo006Robots,
|
|
419
|
+
seo007Sitemap,
|
|
420
|
+
seo008JsonLd,
|
|
421
|
+
seo009HtmlLang,
|
|
139
422
|
summarize
|
|
140
423
|
};
|