@svelte-vitals/core 0.3.0 → 0.5.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 CHANGED
@@ -23,6 +23,15 @@ interface Project {
23
23
  htmlLang: Detection;
24
24
  }
25
25
  declare const defaultProject: Project;
26
+ /** A concrete, agent-actionable remediation for a finding (design §10, issue #18). */
27
+ interface Fix {
28
+ /** One-line imperative instruction, e.g. 'Add a <meta name="description"> inside <svelte:head>.' */
29
+ description: string;
30
+ /** Concrete code to insert or a file's contents to create. */
31
+ snippet?: string;
32
+ /** Markdown fenced-code language for `snippet` (default 'svelte'). */
33
+ lang?: string;
34
+ }
26
35
  /** A single rule finding for one route (or the whole project). */
27
36
  interface Result {
28
37
  /** Rule id, e.g. 'SEO001'. */
@@ -36,6 +45,8 @@ interface Result {
36
45
  message: string;
37
46
  recommendation?: string;
38
47
  docsUrl?: string;
48
+ /** Agent-actionable remediation (issue #18). */
49
+ fix?: Fix;
39
50
  }
40
51
  type Scope = 'route' | 'project';
41
52
  type Category = 'seo' | 'performance' | 'a11y' | 'maintainability';
@@ -192,6 +203,8 @@ interface HeadTagRuleOptions {
192
203
  /** Short human label, e.g. 'description'. */
193
204
  label: string;
194
205
  recommendation: string;
206
+ /** Agent-actionable remediation attached to every finding (issue #18). */
207
+ fix?: Fix;
195
208
  }
196
209
  /** Build a route-scope rule asserting the presence of a single head tag (design §11). */
197
210
  declare function headTagRule(opts: HeadTagRuleOptions): Rule;
@@ -231,6 +244,20 @@ declare function formatJsonReport(results: Result[], config: Config, meta: {
231
244
  version: string;
232
245
  }): string;
233
246
 
247
+ /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
248
+ declare function formatAgentReport(results: Result[], config: Config): string;
249
+
250
+ /** Render penalized findings as a SARIF 2.1.0 log string (issue #18, design slice 5). */
251
+ declare function formatSarifReport(results: Result[], config: Config, meta: {
252
+ version: string;
253
+ }): string;
254
+
255
+ /**
256
+ * Render penalized findings as GitHub Actions workflow commands (issue #18, design slice 5).
257
+ * GitHub turns these into inline PR annotations and run-annotation entries. Returns '' when clean.
258
+ */
259
+ declare function formatGithubReport(results: Result[], config: Config): string;
260
+
234
261
  /** Drop rules disabled via config (design §6). */
235
262
  declare function selectRules(rules: Rule[], config: Config): Rule[];
236
263
  /** Apply per-rule severity overrides to results (design §6). */
@@ -252,4 +279,4 @@ interface ScoreOptions {
252
279
  /** Compute the headline score and its breakdown (design §12). */
253
280
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
254
281
 
255
- export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type HeadProvider, type HeadTag, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type Result, type Rule, type RuleContext, 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, classify, computeScore, defaultConfig, defaultProject, defineConfig, effectiveSeverity, formatConsoleReport, formatJsonReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
282
+ export { type Category, type Classification, type Config, type ConsoleReportOptions, type Detection, type Fix, type HeadProvider, type HeadTag, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type Result, type Rule, type RuleContext, 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, classify, computeScore, defaultConfig, defaultProject, defineConfig, effectiveSeverity, formatAgentReport, formatConsoleReport, formatGithubReport, formatJsonReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
package/dist/index.js CHANGED
@@ -71,7 +71,12 @@ var seo001Title = {
71
71
  location: head.file,
72
72
  message: messageFor(detection),
73
73
  recommendation: "Add a <title> inside <svelte:head>, e.g. <title>{data.title}</title>, or set it via your meta component.",
74
- docsUrl: DOCS_URL
74
+ docsUrl: DOCS_URL,
75
+ fix: {
76
+ description: "Add a <title> inside <svelte:head> (a dynamic title is fine).",
77
+ snippet: "<svelte:head>\n <title>{data.title}</title>\n</svelte:head>",
78
+ lang: "svelte"
79
+ }
75
80
  };
76
81
  });
77
82
  }
@@ -102,7 +107,10 @@ function headTagRule(opts) {
102
107
  location: head.file,
103
108
  message,
104
109
  recommendation: opts.recommendation,
105
- docsUrl
110
+ docsUrl,
111
+ // Copy per finding: opts.fix is a rule-level template shared across all
112
+ // results this rule emits; a fresh object keeps findings independent.
113
+ ...opts.fix ? { fix: { ...opts.fix } } : {}
106
114
  };
107
115
  });
108
116
  }
@@ -116,7 +124,12 @@ var seo002Description = headTagRule({
116
124
  severity: "critical",
117
125
  match: (t) => t.kind === "meta" && t.name === "description",
118
126
  label: '<meta name="description">',
119
- recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.'
127
+ recommendation: 'Add a <meta name="description"> in <svelte:head>, or set the description on your meta component.',
128
+ fix: {
129
+ description: 'Add a <meta name="description"> inside <svelte:head>, or set description on your meta component.',
130
+ snippet: '<svelte:head>\n <meta name="description" content="A concise page summary." />\n</svelte:head>',
131
+ lang: "svelte"
132
+ }
120
133
  });
121
134
  var seo003Canonical = headTagRule({
122
135
  id: "SEO003",
@@ -124,7 +137,12 @@ var seo003Canonical = headTagRule({
124
137
  severity: "warning",
125
138
  match: (t) => t.kind === "link" && t.rel === "canonical",
126
139
  label: '<link rel="canonical">',
127
- recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.'
140
+ recommendation: 'Add <link rel="canonical"> in <svelte:head>, or set the canonical prop on your meta component.',
141
+ fix: {
142
+ description: 'Add <link rel="canonical"> inside <svelte:head>, or set the canonical prop on your meta component.',
143
+ snippet: '<svelte:head>\n <link rel="canonical" href="https://example.com/this-page" />\n</svelte:head>',
144
+ lang: "svelte"
145
+ }
128
146
  });
129
147
  var seo004OgImage = headTagRule({
130
148
  id: "SEO004",
@@ -132,7 +150,12 @@ var seo004OgImage = headTagRule({
132
150
  severity: "warning",
133
151
  match: (t) => t.kind === "meta" && t.property === "og:image",
134
152
  label: '<meta property="og:image">',
135
- recommendation: 'Add <meta property="og:image">, or set openGraph.images on your meta component.'
153
+ recommendation: 'Add <meta property="og:image">, or set openGraph.images on your meta component.',
154
+ fix: {
155
+ description: 'Add <meta property="og:image">, or set openGraph.images on your meta component.',
156
+ snippet: '<svelte:head>\n <meta property="og:image" content="https://example.com/og.png" />\n</svelte:head>',
157
+ lang: "svelte"
158
+ }
136
159
  });
137
160
  var seo005OgTitle = headTagRule({
138
161
  id: "SEO005",
@@ -140,7 +163,12 @@ var seo005OgTitle = headTagRule({
140
163
  severity: "warning",
141
164
  match: (t) => t.kind === "meta" && t.property === "og:title",
142
165
  label: '<meta property="og:title">',
143
- recommendation: 'Add <meta property="og:title">, or set openGraph.title on your meta component.'
166
+ recommendation: 'Add <meta property="og:title">, or set openGraph.title on your meta component.',
167
+ fix: {
168
+ description: 'Add <meta property="og:title">, or set openGraph.title on your meta component.',
169
+ snippet: '<svelte:head>\n <meta property="og:title" content="Page title" />\n</svelte:head>',
170
+ lang: "svelte"
171
+ }
144
172
  });
145
173
  var seo008JsonLd = headTagRule({
146
174
  id: "SEO008",
@@ -148,7 +176,15 @@ var seo008JsonLd = headTagRule({
148
176
  severity: "info",
149
177
  match: (t) => t.kind === "jsonld",
150
178
  label: 'JSON-LD (<script type="application/ld+json">)',
151
- recommendation: "Add JSON-LD structured data, e.g. via <svelte:head> or a JsonLd component."
179
+ recommendation: "Add JSON-LD structured data, e.g. via <svelte:head> or a JsonLd component.",
180
+ fix: {
181
+ // Svelte ships <script> contents verbatim (the body is raw text, not Svelte
182
+ // markup), so use literal JSON here — an interpolation like {JSON.stringify(...)}
183
+ // would be emitted as that literal string and produce invalid JSON-LD.
184
+ description: "Add a JSON-LD <script> inside <svelte:head> with literal JSON (Svelte emits the script body as-is).",
185
+ snippet: '<svelte:head>\n <script type="application/ld+json">\n {\n "@context": "https://schema.org",\n "@type": "WebPage",\n "name": "Page title"\n }\n </script>\n</svelte:head>',
186
+ lang: "svelte"
187
+ }
152
188
  });
153
189
 
154
190
  // src/rules/seo/project-rules.ts
@@ -169,7 +205,12 @@ var seo006Robots = {
169
205
  detection,
170
206
  message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
171
207
  recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
172
- docsUrl: "https://svelte-vitals.dev/rules/SEO006"
208
+ docsUrl: "https://svelte-vitals.dev/rules/SEO006",
209
+ fix: {
210
+ description: "Create static/robots.txt (or a src/routes/robots.txt/+server endpoint).",
211
+ snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
212
+ lang: "text"
213
+ }
173
214
  }
174
215
  ];
175
216
  }
@@ -189,7 +230,12 @@ var seo007Sitemap = {
189
230
  detection,
190
231
  message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
191
232
  recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
192
- docsUrl: "https://svelte-vitals.dev/rules/SEO007"
233
+ docsUrl: "https://svelte-vitals.dev/rules/SEO007",
234
+ fix: {
235
+ description: "Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint).",
236
+ snippet: '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://example.com/</loc></url>\n</urlset>',
237
+ lang: "xml"
238
+ }
193
239
  }
194
240
  ];
195
241
  }
@@ -210,7 +256,12 @@ var seo009HtmlLang = {
210
256
  detection,
211
257
  message,
212
258
  recommendation: 'Set <html lang="..."> in src/app.html.',
213
- docsUrl: "https://svelte-vitals.dev/rules/SEO009"
259
+ docsUrl: "https://svelte-vitals.dev/rules/SEO009",
260
+ fix: {
261
+ description: "Set the lang attribute on <html> in src/app.html.",
262
+ snippet: '<html lang="en">',
263
+ lang: "html"
264
+ }
214
265
  }
215
266
  ];
216
267
  }
@@ -380,7 +431,8 @@ function issueOf(result) {
380
431
  title: result.message,
381
432
  detection: result.detection,
382
433
  location: result.location,
383
- recommendation: result.recommendation
434
+ recommendation: result.recommendation,
435
+ ...result.fix ? { fix: result.fix } : {}
384
436
  };
385
437
  }
386
438
  function formatJsonReport(results, config, meta) {
@@ -401,6 +453,143 @@ function formatJsonReport(results, config, meta) {
401
453
  return JSON.stringify({ version: meta.version, score, scoreModel, summary, routes, siteIssues }, null, 2);
402
454
  }
403
455
 
456
+ // src/reporter/agent.ts
457
+ var SEVERITY_RANK = { critical: 0, warning: 1, info: 2 };
458
+ function mdTags(text) {
459
+ return text.replace(/<[^>]+>/g, (tag) => `\`${tag}\``);
460
+ }
461
+ function formatAgentReport(results, config) {
462
+ const failing = results.filter((r) => classify(r, config) === "fail");
463
+ const lines = ["# svelte-vitals \u2014 SEO fixes", ""];
464
+ if (failing.length === 0) {
465
+ lines.push("No issues to fix.", "");
466
+ return lines.join("\n").replace(/\n+$/, "\n");
467
+ }
468
+ lines.push(
469
+ `${failing.length} issue(s) to fix, ordered most-severe first. Fix critical issues first; warning and info items improve SEO but do not fail the default build. Apply each fix below, then re-run \`svelte-vitals\` (or the build) to confirm each rule passes.`,
470
+ ""
471
+ );
472
+ const groups = /* @__PURE__ */ new Map();
473
+ for (const r of failing) {
474
+ const key = r.location ?? r.route ?? "(project)";
475
+ if (!groups.has(key)) groups.set(key, []);
476
+ groups.get(key).push(r);
477
+ }
478
+ const groupSeverity = (rs) => Math.min(...rs.map((r) => SEVERITY_RANK[effectiveSeverity(r, config)]));
479
+ const orderedGroups = [...groups.entries()].sort(
480
+ (a, b) => groupSeverity(a[1]) - groupSeverity(b[1]) || a[0].localeCompare(b[0])
481
+ );
482
+ for (const [loc, rs] of orderedGroups) {
483
+ rs.sort(
484
+ (x, y) => SEVERITY_RANK[effectiveSeverity(x, config)] - SEVERITY_RANK[effectiveSeverity(y, config)] || x.id.localeCompare(y.id)
485
+ );
486
+ lines.push(`## ${loc}`, "");
487
+ for (const r of rs) {
488
+ lines.push(`### ${r.id} \xB7 ${mdTags(r.message)} (${effectiveSeverity(r, config)})`);
489
+ if (r.fix) {
490
+ lines.push(`- Fix: ${mdTags(r.fix.description)}`);
491
+ if (r.fix.snippet) lines.push("", "```" + (r.fix.lang ?? "svelte"), r.fix.snippet, "```");
492
+ } else if (r.recommendation) {
493
+ lines.push(`- Fix: ${mdTags(r.recommendation)}`);
494
+ }
495
+ if (r.docsUrl) lines.push(`- Docs: ${r.docsUrl}`);
496
+ lines.push(`- Accept: re-run svelte-vitals; ${r.id} passes${r.route ? ` for ${r.route}` : ""}.`, "");
497
+ }
498
+ }
499
+ return lines.join("\n").replace(/\n+$/, "\n");
500
+ }
501
+
502
+ // src/reporter/shared.ts
503
+ function severityToSarifLevel(sev) {
504
+ return sev === "critical" ? "error" : sev === "warning" ? "warning" : "note";
505
+ }
506
+ function severityToGithubLevel(sev) {
507
+ return sev === "critical" ? "error" : sev === "warning" ? "warning" : "notice";
508
+ }
509
+ function messageText(result) {
510
+ return result.recommendation ? `${result.message} ${result.recommendation}` : result.message;
511
+ }
512
+ function docsUrlFor(id) {
513
+ return `https://svelte-vitals.dev/rules/${id}`;
514
+ }
515
+ var RULE_META = new Map(
516
+ allRules.map((r) => [r.id, { title: r.title, severity: r.severity, docsUrl: docsUrlFor(r.id) }])
517
+ );
518
+ function ruleMetaById(id) {
519
+ return RULE_META.get(id);
520
+ }
521
+
522
+ // src/reporter/sarif.ts
523
+ function formatSarifReport(results, config, meta) {
524
+ const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
525
+ const rules = [];
526
+ const ruleIndex = /* @__PURE__ */ new Map();
527
+ const sarifResults = penalized.map((r) => {
528
+ if (!ruleIndex.has(r.id)) {
529
+ const m = ruleMetaById(r.id);
530
+ const name = m?.title ?? r.id;
531
+ ruleIndex.set(r.id, rules.length);
532
+ rules.push({
533
+ id: r.id,
534
+ name,
535
+ shortDescription: { text: name },
536
+ helpUri: r.docsUrl ?? m?.docsUrl ?? docsUrlFor(r.id),
537
+ defaultConfiguration: { level: severityToSarifLevel(m?.severity ?? r.severity) }
538
+ });
539
+ }
540
+ const result = {
541
+ ruleId: r.id,
542
+ ruleIndex: ruleIndex.get(r.id),
543
+ level: severityToSarifLevel(effectiveSeverity(r, config)),
544
+ message: { text: messageText(r) },
545
+ partialFingerprints: { "svelteVitals/v1": `${r.id}:${r.route ?? "project"}` }
546
+ };
547
+ if (r.location) {
548
+ result.locations = [{ physicalLocation: { artifactLocation: { uri: r.location } } }];
549
+ }
550
+ return result;
551
+ });
552
+ const log = {
553
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
554
+ version: "2.1.0",
555
+ runs: [
556
+ {
557
+ tool: {
558
+ driver: {
559
+ name: "svelte-vitals",
560
+ informationUri: "https://svelte-vitals.dev",
561
+ version: meta.version,
562
+ rules
563
+ }
564
+ },
565
+ results: sarifResults
566
+ }
567
+ ]
568
+ };
569
+ return JSON.stringify(log, null, 2);
570
+ }
571
+
572
+ // src/reporter/github.ts
573
+ function escapeData(s) {
574
+ return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
575
+ }
576
+ function escapeProp(s) {
577
+ return escapeData(s).replace(/:/g, "%3A").replace(/,/g, "%2C");
578
+ }
579
+ function formatGithubReport(results, config) {
580
+ const penalized = results.filter((r) => isPenalized(r.detection, config.treatDynamicAs));
581
+ const lines = penalized.map((r) => {
582
+ const level = severityToGithubLevel(effectiveSeverity(r, config));
583
+ const meta = ruleMetaById(r.id);
584
+ const title = meta ? `${r.id}: ${meta.title}` : r.id;
585
+ const props = [];
586
+ if (r.location) props.push(`file=${escapeProp(r.location)}`);
587
+ props.push(`title=${escapeProp(title)}`);
588
+ return `::${level} ${props.join(",")}::${escapeData(messageText(r))}`;
589
+ });
590
+ return lines.join("\n");
591
+ }
592
+
404
593
  // src/config-apply.ts
405
594
  function selectRules(rules, config) {
406
595
  return rules.filter((rule) => config.rules[rule.id] !== "off");
@@ -422,8 +611,11 @@ export {
422
611
  defaultProject,
423
612
  defineConfig,
424
613
  effectiveSeverity,
614
+ formatAgentReport,
425
615
  formatConsoleReport,
616
+ formatGithubReport,
426
617
  formatJsonReport,
618
+ formatSarifReport,
427
619
  hasFailureAtOrAbove,
428
620
  headTagRule,
429
621
  isPenalized,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",