@svelte-vitals/core 0.7.0 → 0.9.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 +66 -3
- package/dist/index.js +167 -17
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ interface Result {
|
|
|
47
47
|
docsUrl?: string;
|
|
48
48
|
/** Agent-actionable remediation (issue #18). */
|
|
49
49
|
fix?: Fix;
|
|
50
|
+
/** Vitals category this finding belongs to (default 'seo' when absent). */
|
|
51
|
+
category?: Category;
|
|
52
|
+
/** 1-based source line for element-level findings (e.g. a specific <img>). */
|
|
53
|
+
line?: number;
|
|
50
54
|
}
|
|
51
55
|
type Scope = 'route' | 'project';
|
|
52
56
|
type Category = 'seo' | 'performance' | 'a11y' | 'maintainability';
|
|
@@ -62,6 +66,8 @@ interface Config {
|
|
|
62
66
|
rules: Record<string, RuleSetting>;
|
|
63
67
|
/** Minimum severity that fails the run / CI (design §6). */
|
|
64
68
|
failOn: Severity;
|
|
69
|
+
/** Per-category weights for the combined Health score (default: equal, 1 each) (#10). */
|
|
70
|
+
weights?: Partial<Record<Category, number>>;
|
|
65
71
|
}
|
|
66
72
|
declare const defaultConfig: Config;
|
|
67
73
|
/** Merge user config over defaults. Identity helper for config files (design §6). */
|
|
@@ -127,6 +133,26 @@ interface HeadProvider {
|
|
|
127
133
|
collect(rt: Runtime, cwd: string, config?: Config): Promise<ResolvedHead[]>;
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
/**
|
|
137
|
+
* A normalized <img> occurrence — the mode-independent boundary for Performance
|
|
138
|
+
* rules (mirrors head.ts). Attribute presence only: a dynamically-bound attribute
|
|
139
|
+
* (width={w}) still counts as present, so dynamic values are never flagged.
|
|
140
|
+
*/
|
|
141
|
+
interface ImageInfo {
|
|
142
|
+
hasWidth: boolean;
|
|
143
|
+
hasHeight: boolean;
|
|
144
|
+
hasLoading: boolean;
|
|
145
|
+
/** 1-based source line, or 0 if unknown. */
|
|
146
|
+
line: number;
|
|
147
|
+
/** Source file the <img> came from. */
|
|
148
|
+
file: string;
|
|
149
|
+
}
|
|
150
|
+
/** Resolved <img> elements for a single route (page + layout chain). */
|
|
151
|
+
interface ResolvedImages {
|
|
152
|
+
route: string;
|
|
153
|
+
images: ImageInfo[];
|
|
154
|
+
}
|
|
155
|
+
|
|
130
156
|
/**
|
|
131
157
|
* Source-file locations that satisfy the project-scope rules, shared by every
|
|
132
158
|
* mode so the static (CLI) and rendered (plugin) collectors never drift. This
|
|
@@ -140,6 +166,8 @@ declare const SITEMAP_SOURCE_PATHS: readonly ["static/sitemap.xml", "src/routes/
|
|
|
140
166
|
/** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
|
|
141
167
|
interface RuleContext {
|
|
142
168
|
heads: ResolvedHead[];
|
|
169
|
+
/** Per-route <img> elements for Performance rules (absent in modes that don't collect them). */
|
|
170
|
+
images?: ResolvedImages[];
|
|
143
171
|
project: Project;
|
|
144
172
|
config: Config;
|
|
145
173
|
}
|
|
@@ -198,6 +226,9 @@ declare const seo006Robots: Rule;
|
|
|
198
226
|
declare const seo007Sitemap: Rule;
|
|
199
227
|
declare const seo009HtmlLang: Rule;
|
|
200
228
|
|
|
229
|
+
declare const perf001ImageDimensions: Rule;
|
|
230
|
+
declare const perf002ImageLoading: Rule;
|
|
231
|
+
|
|
201
232
|
declare const allRules: Rule[];
|
|
202
233
|
|
|
203
234
|
interface RuleInfo {
|
|
@@ -229,6 +260,21 @@ interface HeadTagRuleOptions {
|
|
|
229
260
|
/** Build a route-scope rule asserting the presence of a single head tag (design §11). */
|
|
230
261
|
declare function headTagRule(opts: HeadTagRuleOptions): Rule;
|
|
231
262
|
|
|
263
|
+
interface ImageRuleOptions {
|
|
264
|
+
id: string;
|
|
265
|
+
title: string;
|
|
266
|
+
severity: Severity;
|
|
267
|
+
/** Noun phrase for messages, e.g. '<img> width/height'. */
|
|
268
|
+
label: string;
|
|
269
|
+
recommendation: string;
|
|
270
|
+
rationale: string;
|
|
271
|
+
fix?: Fix;
|
|
272
|
+
/** Returns true when the image satisfies the rule (passes). */
|
|
273
|
+
ok: (img: ImageInfo) => boolean;
|
|
274
|
+
}
|
|
275
|
+
/** Build a route-scoped Performance rule that checks each <img> against `ok` (issue #10). */
|
|
276
|
+
declare function imageRule(opts: ImageRuleOptions): Rule;
|
|
277
|
+
|
|
232
278
|
interface Summary {
|
|
233
279
|
critical: number;
|
|
234
280
|
warning: number;
|
|
@@ -274,15 +320,28 @@ interface ScoreOptions {
|
|
|
274
320
|
}
|
|
275
321
|
/** Compute the headline score and its breakdown (design §12). */
|
|
276
322
|
declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
|
|
323
|
+
/** Compute an independent score per category present in `results` (issue #10). */
|
|
324
|
+
declare function scoresByCategory(results: Result[], config: Config): Partial<Record<Category, ScoreResult>>;
|
|
325
|
+
interface HealthResult {
|
|
326
|
+
/** Weighted overall score across present categories (0–100). */
|
|
327
|
+
health: number;
|
|
328
|
+
categories: Partial<Record<Category, ScoreResult>>;
|
|
329
|
+
/** Effective weight used per present category. */
|
|
330
|
+
weights: Partial<Record<Category, number>>;
|
|
331
|
+
}
|
|
332
|
+
/** Combined weighted Health score over the categories present in `results` (#10). */
|
|
333
|
+
declare function computeHealth(results: Result[], config: Config): HealthResult;
|
|
277
334
|
|
|
278
335
|
declare function issueOf(result: Result): {
|
|
279
336
|
fix?: Fix | undefined;
|
|
280
337
|
docsUrl?: string | undefined;
|
|
338
|
+
recommendation: string | undefined;
|
|
339
|
+
line?: number | undefined;
|
|
281
340
|
id: string;
|
|
341
|
+
category: Category;
|
|
282
342
|
title: string;
|
|
283
343
|
detection: Detection;
|
|
284
344
|
location: string | undefined;
|
|
285
|
-
recommendation: string | undefined;
|
|
286
345
|
};
|
|
287
346
|
type JsonIssue = ReturnType<typeof issueOf> & {
|
|
288
347
|
severity: ReturnType<typeof effectiveSeverity>;
|
|
@@ -290,7 +349,11 @@ type JsonIssue = ReturnType<typeof issueOf> & {
|
|
|
290
349
|
interface JsonReport {
|
|
291
350
|
version: string;
|
|
292
351
|
score: number;
|
|
293
|
-
|
|
352
|
+
weights: Partial<Record<Category, number>>;
|
|
353
|
+
categories: Record<string, {
|
|
354
|
+
score: number;
|
|
355
|
+
scoreModel: ScoreModel;
|
|
356
|
+
}>;
|
|
294
357
|
summary: Summary;
|
|
295
358
|
routes: Array<{
|
|
296
359
|
route: string;
|
|
@@ -327,4 +390,4 @@ declare function selectRules(rules: Rule[], config: Config): Rule[];
|
|
|
327
390
|
/** Apply per-rule severity overrides to results (design §6). */
|
|
328
391
|
declare function applyRuleSeverities(results: Result[], config: Config): Result[];
|
|
329
392
|
|
|
330
|
-
export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildJsonReport, classify, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
|
|
393
|
+
export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type HealthResult, type ImageInfo, type JsonReport, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedImages, type Result, type Rule, type RuleContext, type RuleInfo, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type Summary, type TreatDynamicAs, type Value, allRules, applyRuleSeverities, buildJsonReport, classify, computeHealth, computeScore, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, explainRule, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, perf001ImageDimensions, perf002ImageLoading, runRules, scoresByCategory, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
|
package/dist/index.js
CHANGED
|
@@ -74,6 +74,7 @@ var seo001Title = {
|
|
|
74
74
|
const detection = detectTitle(head);
|
|
75
75
|
return {
|
|
76
76
|
id: "SEO001",
|
|
77
|
+
category: "seo",
|
|
77
78
|
severity: "critical",
|
|
78
79
|
detection,
|
|
79
80
|
route: head.route,
|
|
@@ -108,6 +109,7 @@ function headTagRule(opts) {
|
|
|
108
109
|
const message = detection.presence === "none" ? `Missing ${opts.label}` : detection.value === "absent" ? `Empty ${opts.label}` : opts.label;
|
|
109
110
|
return {
|
|
110
111
|
id: opts.id,
|
|
112
|
+
category: "seo",
|
|
111
113
|
severity: opts.severity,
|
|
112
114
|
detection,
|
|
113
115
|
route: head.route,
|
|
@@ -220,6 +222,7 @@ var seo006Robots = {
|
|
|
220
222
|
return [
|
|
221
223
|
{
|
|
222
224
|
id: "SEO006",
|
|
225
|
+
category: "seo",
|
|
223
226
|
severity: "warning",
|
|
224
227
|
detection,
|
|
225
228
|
message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
|
|
@@ -248,6 +251,7 @@ var seo007Sitemap = {
|
|
|
248
251
|
return [
|
|
249
252
|
{
|
|
250
253
|
id: "SEO007",
|
|
254
|
+
category: "seo",
|
|
251
255
|
severity: "warning",
|
|
252
256
|
detection,
|
|
253
257
|
message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
|
|
@@ -277,6 +281,7 @@ var seo009HtmlLang = {
|
|
|
277
281
|
return [
|
|
278
282
|
{
|
|
279
283
|
id: "SEO009",
|
|
284
|
+
category: "seo",
|
|
280
285
|
severity: "warning",
|
|
281
286
|
detection,
|
|
282
287
|
message,
|
|
@@ -288,6 +293,86 @@ var seo009HtmlLang = {
|
|
|
288
293
|
}
|
|
289
294
|
};
|
|
290
295
|
|
|
296
|
+
// src/rules/perf/image-rule.ts
|
|
297
|
+
function imageRule(opts) {
|
|
298
|
+
const docsUrl = docsUrlFor(opts.id);
|
|
299
|
+
return {
|
|
300
|
+
id: opts.id,
|
|
301
|
+
title: opts.title,
|
|
302
|
+
category: "performance",
|
|
303
|
+
severity: opts.severity,
|
|
304
|
+
scope: "route",
|
|
305
|
+
rationale: opts.rationale,
|
|
306
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
307
|
+
async check(ctx) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (const route of ctx.images ?? []) {
|
|
310
|
+
if (route.images.length === 0) continue;
|
|
311
|
+
const bad = route.images.filter((img) => !opts.ok(img));
|
|
312
|
+
if (bad.length === 0) {
|
|
313
|
+
out.push({
|
|
314
|
+
id: opts.id,
|
|
315
|
+
category: "performance",
|
|
316
|
+
severity: opts.severity,
|
|
317
|
+
detection: { presence: "own", value: "static" },
|
|
318
|
+
route: route.route,
|
|
319
|
+
message: opts.label,
|
|
320
|
+
recommendation: opts.recommendation,
|
|
321
|
+
docsUrl
|
|
322
|
+
});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
for (const img of bad) {
|
|
326
|
+
out.push({
|
|
327
|
+
id: opts.id,
|
|
328
|
+
category: "performance",
|
|
329
|
+
severity: opts.severity,
|
|
330
|
+
detection: { presence: "none", value: "absent" },
|
|
331
|
+
route: route.route,
|
|
332
|
+
location: img.file,
|
|
333
|
+
...img.line > 0 ? { line: img.line } : {},
|
|
334
|
+
message: `Missing ${opts.label}`,
|
|
335
|
+
recommendation: opts.recommendation,
|
|
336
|
+
docsUrl,
|
|
337
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/rules/perf/images.ts
|
|
347
|
+
var perf001ImageDimensions = imageRule({
|
|
348
|
+
id: "PERF001",
|
|
349
|
+
title: "Image dimensions",
|
|
350
|
+
severity: "warning",
|
|
351
|
+
label: "<img> width/height",
|
|
352
|
+
recommendation: "Set explicit width and height on <img> to reserve space and avoid layout shift (CLS).",
|
|
353
|
+
rationale: "An <img> without explicit width and height triggers layout shift (CLS) as it loads, hurting Core Web Vitals and visual stability.",
|
|
354
|
+
fix: {
|
|
355
|
+
description: "Add explicit width and height attributes to the <img>.",
|
|
356
|
+
snippet: '<img src="/hero.jpg" width="1200" height="630" alt="\u2026" />',
|
|
357
|
+
lang: "svelte"
|
|
358
|
+
},
|
|
359
|
+
ok: (img) => img.hasWidth && img.hasHeight
|
|
360
|
+
});
|
|
361
|
+
var perf002ImageLoading = imageRule({
|
|
362
|
+
id: "PERF002",
|
|
363
|
+
title: "Image loading hint",
|
|
364
|
+
severity: "info",
|
|
365
|
+
label: "<img> loading attribute",
|
|
366
|
+
recommendation: 'Set loading="lazy" for offscreen images; keep the LCP image eager (consider fetchpriority="high").',
|
|
367
|
+
rationale: "A loading attribute lets the browser defer offscreen images; without it images load eagerly and can delay more important content. Static analysis cannot tell which image is the LCP, so this is advisory.",
|
|
368
|
+
fix: {
|
|
369
|
+
description: 'Add loading="lazy" to offscreen <img> elements (leave the LCP/hero image eager).',
|
|
370
|
+
snippet: '<img src="/thumb.jpg" width="320" height="240" loading="lazy" alt="\u2026" />',
|
|
371
|
+
lang: "svelte"
|
|
372
|
+
},
|
|
373
|
+
ok: (img) => img.hasLoading
|
|
374
|
+
});
|
|
375
|
+
|
|
291
376
|
// src/rules/index.ts
|
|
292
377
|
var allRules = [
|
|
293
378
|
seo001Title,
|
|
@@ -298,7 +383,9 @@ var allRules = [
|
|
|
298
383
|
seo006Robots,
|
|
299
384
|
seo007Sitemap,
|
|
300
385
|
seo008JsonLd,
|
|
301
|
-
seo009HtmlLang
|
|
386
|
+
seo009HtmlLang,
|
|
387
|
+
perf001ImageDimensions,
|
|
388
|
+
perf002ImageLoading
|
|
302
389
|
];
|
|
303
390
|
function explainRule(id) {
|
|
304
391
|
const target = id.toUpperCase();
|
|
@@ -391,6 +478,39 @@ function computeScore(results, config, options = {}) {
|
|
|
391
478
|
const score = capBinds ? CRITICAL_CAP : uncapped;
|
|
392
479
|
return { score: clamp(score), scoreModel: { routeAverage, sitePenalty, criticalCap } };
|
|
393
480
|
}
|
|
481
|
+
function scoresByCategory(results, config) {
|
|
482
|
+
const byCat = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const r of results) {
|
|
484
|
+
const cat = r.category ?? "seo";
|
|
485
|
+
let bucket = byCat.get(cat);
|
|
486
|
+
if (!bucket) byCat.set(cat, bucket = []);
|
|
487
|
+
bucket.push(r);
|
|
488
|
+
}
|
|
489
|
+
const out = {};
|
|
490
|
+
for (const [cat, rs] of byCat) out[cat] = computeScore(rs, config);
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
function computeHealth(results, config) {
|
|
494
|
+
const categories = scoresByCategory(results, config);
|
|
495
|
+
const weights = {};
|
|
496
|
+
let weighted = 0;
|
|
497
|
+
let total = 0;
|
|
498
|
+
for (const cat of Object.keys(categories)) {
|
|
499
|
+
const w = config.weights?.[cat] ?? 1;
|
|
500
|
+
if (!Number.isFinite(w) || w < 0) {
|
|
501
|
+
throw new RangeError(`invalid weight for '${cat}'; expected a finite number >= 0.`);
|
|
502
|
+
}
|
|
503
|
+
weights[cat] = w;
|
|
504
|
+
weighted += categories[cat].score * w;
|
|
505
|
+
total += w;
|
|
506
|
+
}
|
|
507
|
+
if (Object.keys(weights).length === 0) return { health: 100, categories, weights };
|
|
508
|
+
if (total === 0) {
|
|
509
|
+
throw new RangeError("Health weights sum to 0; at least one present category must have a positive weight.");
|
|
510
|
+
}
|
|
511
|
+
const health = Math.round(weighted / total);
|
|
512
|
+
return { health, categories, weights };
|
|
513
|
+
}
|
|
394
514
|
|
|
395
515
|
// src/reporter/console.ts
|
|
396
516
|
var RULE = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
|
|
@@ -399,12 +519,18 @@ var SEVERITY_TITLE = {
|
|
|
399
519
|
warning: "Warnings",
|
|
400
520
|
info: "Info"
|
|
401
521
|
};
|
|
402
|
-
|
|
403
|
-
|
|
522
|
+
var CATEGORY_LABEL = {
|
|
523
|
+
seo: "SEO",
|
|
524
|
+
performance: "Performance",
|
|
525
|
+
a11y: "Accessibility",
|
|
526
|
+
maintainability: "Maintainability"
|
|
527
|
+
};
|
|
528
|
+
var CATEGORY_ORDER = ["seo", "performance", "a11y", "maintainability"];
|
|
529
|
+
function scoreLine(label, { score, scoreModel }) {
|
|
404
530
|
const parts = [`route avg ${scoreModel.routeAverage}`];
|
|
405
531
|
if (scoreModel.sitePenalty > 0) parts.push(`site \u2212${scoreModel.sitePenalty}`);
|
|
406
532
|
if (scoreModel.criticalCap !== null) parts.push(`capped at ${scoreModel.criticalCap}: critical present`);
|
|
407
|
-
return
|
|
533
|
+
return `${label} Score: ${score}/100 (${parts.join(" \xB7 ")})`;
|
|
408
534
|
}
|
|
409
535
|
function byRouteTree(results, config) {
|
|
410
536
|
const routes = /* @__PURE__ */ new Map();
|
|
@@ -426,12 +552,13 @@ function byRouteTree(results, config) {
|
|
|
426
552
|
}
|
|
427
553
|
function formatConsoleReport(results, config, options = {}) {
|
|
428
554
|
const summary = summarize(results, config);
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
555
|
+
const { health, categories: byCat } = computeHealth(results, config);
|
|
556
|
+
const present2 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
557
|
+
const header = [`Svelte Vitals \xB7 ${options.mode ?? "static mode"}`, "", `Health: ${health}/100`];
|
|
558
|
+
for (const c of present2) {
|
|
559
|
+
header.push(scoreLine(CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
560
|
+
}
|
|
561
|
+
const lines = [...header, ""];
|
|
435
562
|
const failures = results.filter((r) => classify(r, config) === "fail");
|
|
436
563
|
for (const severity of ["critical", "warning", "info"]) {
|
|
437
564
|
const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
|
|
@@ -440,7 +567,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
440
567
|
for (const r of bucket) {
|
|
441
568
|
lines.push(`\u2717 ${r.id} ${r.message}`);
|
|
442
569
|
if (r.route) lines.push(` ${r.route}`);
|
|
443
|
-
if (r.location) lines.push(` ${r.location}`);
|
|
570
|
+
if (r.location) lines.push(` ${r.location}${r.line ? `:${r.line}` : ""}`);
|
|
444
571
|
}
|
|
445
572
|
lines.push("");
|
|
446
573
|
}
|
|
@@ -463,17 +590,22 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
463
590
|
function issueOf(result) {
|
|
464
591
|
return {
|
|
465
592
|
id: result.id,
|
|
593
|
+
category: result.category ?? "seo",
|
|
466
594
|
title: result.message,
|
|
467
595
|
detection: result.detection,
|
|
468
596
|
location: result.location,
|
|
597
|
+
...result.line !== void 0 ? { line: result.line } : {},
|
|
469
598
|
recommendation: result.recommendation,
|
|
470
599
|
...result.docsUrl ? { docsUrl: result.docsUrl } : {},
|
|
471
600
|
...result.fix ? { fix: result.fix } : {}
|
|
472
601
|
};
|
|
473
602
|
}
|
|
474
603
|
function buildJsonReport(results, config, meta) {
|
|
475
|
-
const {
|
|
604
|
+
const { health, categories: byCat, weights } = computeHealth(results, config);
|
|
476
605
|
const summary = summarize(results, config);
|
|
606
|
+
const categories = Object.fromEntries(
|
|
607
|
+
Object.entries(byCat).map(([cat, sr]) => [cat, { score: sr.score, scoreModel: sr.scoreModel }])
|
|
608
|
+
);
|
|
477
609
|
const routeMap = /* @__PURE__ */ new Map();
|
|
478
610
|
for (const r of results) {
|
|
479
611
|
if (r.route === void 0) continue;
|
|
@@ -486,7 +618,7 @@ function buildJsonReport(results, config, meta) {
|
|
|
486
618
|
issues: rs.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }))
|
|
487
619
|
}));
|
|
488
620
|
const siteIssues = results.filter((r) => r.route === void 0 && isPenalized(r.detection, config.treatDynamicAs)).map((r) => ({ ...issueOf(r), severity: effectiveSeverity(r, config) }));
|
|
489
|
-
return { version: meta.version, score,
|
|
621
|
+
return { version: meta.version, score: health, weights, categories, summary, routes, siteIssues };
|
|
490
622
|
}
|
|
491
623
|
function formatJsonReport(results, config, meta) {
|
|
492
624
|
return JSON.stringify(buildJsonReport(results, config, meta), null, 2);
|
|
@@ -499,7 +631,8 @@ function mdTags(text) {
|
|
|
499
631
|
}
|
|
500
632
|
function formatAgentReport(results, config) {
|
|
501
633
|
const failing = results.filter((r) => classify(r, config) === "fail");
|
|
502
|
-
const
|
|
634
|
+
const { health } = computeHealth(results, config);
|
|
635
|
+
const lines = ["# svelte-vitals \u2014 fixes", "", `Health: ${health}/100`, ""];
|
|
503
636
|
if (failing.length === 0) {
|
|
504
637
|
lines.push("No issues to fix.", "");
|
|
505
638
|
return lines.join("\n").replace(/\n+$/, "\n");
|
|
@@ -581,10 +714,19 @@ function formatSarifReport(results, config, meta) {
|
|
|
581
714
|
ruleIndex: ruleIndex.get(r.id),
|
|
582
715
|
level: severityToSarifLevel(effectiveSeverity(r, config)),
|
|
583
716
|
message: { text: messageText(r) },
|
|
584
|
-
partialFingerprints: {
|
|
717
|
+
partialFingerprints: {
|
|
718
|
+
"svelteVitals/v1": `${r.id}:${r.route ?? "project"}${r.line !== void 0 ? `:${r.location ?? ""}:${r.line}` : ""}`
|
|
719
|
+
}
|
|
585
720
|
};
|
|
586
721
|
if (r.location) {
|
|
587
|
-
result.locations = [
|
|
722
|
+
result.locations = [
|
|
723
|
+
{
|
|
724
|
+
physicalLocation: {
|
|
725
|
+
artifactLocation: { uri: r.location },
|
|
726
|
+
...r.line !== void 0 ? { region: { startLine: r.line } } : {}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
];
|
|
588
730
|
}
|
|
589
731
|
return result;
|
|
590
732
|
});
|
|
@@ -622,7 +764,10 @@ function formatGithubReport(results, config) {
|
|
|
622
764
|
const meta = ruleMetaById(r.id);
|
|
623
765
|
const title = meta ? `${r.id}: ${meta.title}` : r.id;
|
|
624
766
|
const props = [];
|
|
625
|
-
if (r.location)
|
|
767
|
+
if (r.location) {
|
|
768
|
+
props.push(`file=${escapeProp(r.location)}`);
|
|
769
|
+
if (r.line !== void 0) props.push(`line=${r.line}`);
|
|
770
|
+
}
|
|
626
771
|
props.push(`title=${escapeProp(title)}`);
|
|
627
772
|
return `::${level} ${props.join(",")}::${escapeData(messageText(r))}`;
|
|
628
773
|
});
|
|
@@ -646,6 +791,7 @@ export {
|
|
|
646
791
|
applyRuleSeverities,
|
|
647
792
|
buildJsonReport,
|
|
648
793
|
classify,
|
|
794
|
+
computeHealth,
|
|
649
795
|
computeScore,
|
|
650
796
|
defaultConfig,
|
|
651
797
|
defaultProject,
|
|
@@ -660,8 +806,12 @@ export {
|
|
|
660
806
|
formatSarifReport,
|
|
661
807
|
hasFailureAtOrAbove,
|
|
662
808
|
headTagRule,
|
|
809
|
+
imageRule,
|
|
663
810
|
isPenalized,
|
|
811
|
+
perf001ImageDimensions,
|
|
812
|
+
perf002ImageLoading,
|
|
664
813
|
runRules,
|
|
814
|
+
scoresByCategory,
|
|
665
815
|
selectRules,
|
|
666
816
|
seo001Title,
|
|
667
817
|
seo002Description,
|