@svelte-vitals/core 0.2.0 → 0.4.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';
@@ -75,9 +86,10 @@ interface Runtime {
75
86
  }
76
87
 
77
88
  /**
78
- * A normalized head tag. The mode-independent boundary (design §8): both the
79
- * static SourceHeadProvider and the future RenderedHeadProvider emit these, so
80
- * rules never need to know which provider produced them.
89
+ * A normalized head tag. The mode-independent boundary (design §8): the static
90
+ * SourceHeadProvider (CLI, via the runtime-abstracted `HeadProvider` below) and
91
+ * the rendered collector (`@svelte-vitals/vite`, build-time Node) both emit
92
+ * these, so rules never need to know which mode produced them.
81
93
  */
82
94
  interface HeadTag {
83
95
  kind: 'title' | 'meta' | 'link' | 'jsonld';
@@ -105,12 +117,26 @@ interface ResolvedHead {
105
117
  /** Representative source file for the route (used for issue locations). */
106
118
  file: string;
107
119
  }
108
- /** Supplies ResolvedHead[] for a project. The only piece that differs per mode. */
120
+ /**
121
+ * Supplies ResolvedHead[] for a project through the runtime abstraction. The
122
+ * static (CLI) mode implements this; rendered mode reads prerendered HTML at
123
+ * build time and emits the same ResolvedHead[] without the runtime indirection.
124
+ */
109
125
  interface HeadProvider {
110
126
  mode: 'static' | 'rendered';
111
127
  collect(rt: Runtime, cwd: string, config?: Config): Promise<ResolvedHead[]>;
112
128
  }
113
129
 
130
+ /**
131
+ * Source-file locations that satisfy the project-scope rules, shared by every
132
+ * mode so the static (CLI) and rendered (plugin) collectors never drift. This
133
+ * module is pure data: no I/O, no `node:` imports (design §8).
134
+ */
135
+ /** Locations that satisfy the robots.txt project rule (SEO006). */
136
+ declare const ROBOTS_SOURCE_PATHS: readonly ["static/robots.txt", "src/routes/robots.txt/+server.ts", "src/routes/robots.txt/+server.js"];
137
+ /** Locations that satisfy the sitemap.xml project rule (SEO007). */
138
+ declare const SITEMAP_SOURCE_PATHS: readonly ["static/sitemap.xml", "src/routes/sitemap.xml/+server.ts", "src/routes/sitemap.xml/+server.js"];
139
+
114
140
  /** Input given to every rule. Mode-independent: rules see only ResolvedHead[] (design §8, §10). */
115
141
  interface RuleContext {
116
142
  heads: ResolvedHead[];
@@ -177,6 +203,8 @@ interface HeadTagRuleOptions {
177
203
  /** Short human label, e.g. 'description'. */
178
204
  label: string;
179
205
  recommendation: string;
206
+ /** Agent-actionable remediation attached to every finding (issue #18). */
207
+ fix?: Fix;
180
208
  }
181
209
  /** Build a route-scope rule asserting the presence of a single head tag (design §11). */
182
210
  declare function headTagRule(opts: HeadTagRuleOptions): Rule;
@@ -201,6 +229,8 @@ declare function hasFailureAtOrAbove(summary: Summary, min: Severity): boolean;
201
229
 
202
230
  interface ConsoleReportOptions {
203
231
  byRoute?: boolean;
232
+ /** Mode label shown in the header (default 'static mode'). */
233
+ mode?: string;
204
234
  }
205
235
  /**
206
236
  * Render results as a console report string (design §7). Pure: returns a string,
@@ -214,6 +244,9 @@ declare function formatJsonReport(results: Result[], config: Config, meta: {
214
244
  version: string;
215
245
  }): string;
216
246
 
247
+ /** Render failing findings as an agent-actionable Markdown remediation document (issue #18). */
248
+ declare function formatAgentReport(results: Result[], config: Config): string;
249
+
217
250
  /** Drop rules disabled via config (design §6). */
218
251
  declare function selectRules(rules: Rule[], config: Config): Rule[];
219
252
  /** Apply per-rule severity overrides to results (design §6). */
@@ -235,4 +268,4 @@ interface ScoreOptions {
235
268
  /** Compute the headline score and its breakdown (design §12). */
236
269
  declare function computeScore(results: Result[], config: Config, options?: ScoreOptions): ScoreResult;
237
270
 
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 };
271
+ 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, formatJsonReport, hasFailureAtOrAbove, headTagRule, isPenalized, runRules, selectRules, seo001Title, seo002Description, seo003Canonical, seo004OgImage, seo005OgTitle, seo006Robots, seo007Sitemap, seo008JsonLd, seo009HtmlLang, summarize };
package/dist/index.js CHANGED
@@ -14,6 +14,18 @@ function defineConfig(config = {}) {
14
14
  return { ...defaultConfig, ...config };
15
15
  }
16
16
 
17
+ // src/project-paths.ts
18
+ var ROBOTS_SOURCE_PATHS = [
19
+ "static/robots.txt",
20
+ "src/routes/robots.txt/+server.ts",
21
+ "src/routes/robots.txt/+server.js"
22
+ ];
23
+ var SITEMAP_SOURCE_PATHS = [
24
+ "static/sitemap.xml",
25
+ "src/routes/sitemap.xml/+server.ts",
26
+ "src/routes/sitemap.xml/+server.js"
27
+ ];
28
+
17
29
  // src/rule.ts
18
30
  function isPenalized(detection, treatDynamicAs) {
19
31
  if (detection.presence === "none") return true;
@@ -59,7 +71,12 @@ var seo001Title = {
59
71
  location: head.file,
60
72
  message: messageFor(detection),
61
73
  recommendation: "Add a <title> inside <svelte:head>, e.g. <title>{data.title}</title>, or set it via your meta component.",
62
- 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
+ }
63
80
  };
64
81
  });
65
82
  }
@@ -90,7 +107,10 @@ function headTagRule(opts) {
90
107
  location: head.file,
91
108
  message,
92
109
  recommendation: opts.recommendation,
93
- 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 } } : {}
94
114
  };
95
115
  });
96
116
  }
@@ -104,7 +124,12 @@ var seo002Description = headTagRule({
104
124
  severity: "critical",
105
125
  match: (t) => t.kind === "meta" && t.name === "description",
106
126
  label: '<meta name="description">',
107
- 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
+ }
108
133
  });
109
134
  var seo003Canonical = headTagRule({
110
135
  id: "SEO003",
@@ -112,7 +137,12 @@ var seo003Canonical = headTagRule({
112
137
  severity: "warning",
113
138
  match: (t) => t.kind === "link" && t.rel === "canonical",
114
139
  label: '<link rel="canonical">',
115
- 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
+ }
116
146
  });
117
147
  var seo004OgImage = headTagRule({
118
148
  id: "SEO004",
@@ -120,7 +150,12 @@ var seo004OgImage = headTagRule({
120
150
  severity: "warning",
121
151
  match: (t) => t.kind === "meta" && t.property === "og:image",
122
152
  label: '<meta property="og:image">',
123
- 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
+ }
124
159
  });
125
160
  var seo005OgTitle = headTagRule({
126
161
  id: "SEO005",
@@ -128,7 +163,12 @@ var seo005OgTitle = headTagRule({
128
163
  severity: "warning",
129
164
  match: (t) => t.kind === "meta" && t.property === "og:title",
130
165
  label: '<meta property="og:title">',
131
- 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
+ }
132
172
  });
133
173
  var seo008JsonLd = headTagRule({
134
174
  id: "SEO008",
@@ -136,7 +176,15 @@ var seo008JsonLd = headTagRule({
136
176
  severity: "info",
137
177
  match: (t) => t.kind === "jsonld",
138
178
  label: 'JSON-LD (<script type="application/ld+json">)',
139
- 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
+ }
140
188
  });
141
189
 
142
190
  // src/rules/seo/project-rules.ts
@@ -157,7 +205,12 @@ var seo006Robots = {
157
205
  detection,
158
206
  message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
159
207
  recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
160
- 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
+ }
161
214
  }
162
215
  ];
163
216
  }
@@ -177,7 +230,12 @@ var seo007Sitemap = {
177
230
  detection,
178
231
  message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
179
232
  recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
180
- 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
+ }
181
239
  }
182
240
  ];
183
241
  }
@@ -198,7 +256,12 @@ var seo009HtmlLang = {
198
256
  detection,
199
257
  message,
200
258
  recommendation: 'Set <html lang="..."> in src/app.html.',
201
- 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
+ }
202
265
  }
203
266
  ];
204
267
  }
@@ -328,7 +391,12 @@ function byRouteTree(results, config) {
328
391
  }
329
392
  function formatConsoleReport(results, config, options = {}) {
330
393
  const summary = summarize(results, config);
331
- const lines = ["Svelte Vitals \xB7 SEO (static mode)", "", scoreHeader(results, config), ""];
394
+ const lines = [
395
+ `Svelte Vitals \xB7 SEO (${options.mode ?? "static mode"})`,
396
+ "",
397
+ scoreHeader(results, config),
398
+ ""
399
+ ];
332
400
  const failures = results.filter((r) => classify(r, config) === "fail");
333
401
  for (const severity of ["critical", "warning", "info"]) {
334
402
  const bucket = failures.filter((r) => effectiveSeverity(r, config) === severity);
@@ -363,7 +431,8 @@ function issueOf(result) {
363
431
  title: result.message,
364
432
  detection: result.detection,
365
433
  location: result.location,
366
- recommendation: result.recommendation
434
+ recommendation: result.recommendation,
435
+ ...result.fix ? { fix: result.fix } : {}
367
436
  };
368
437
  }
369
438
  function formatJsonReport(results, config, meta) {
@@ -384,6 +453,52 @@ function formatJsonReport(results, config, meta) {
384
453
  return JSON.stringify({ version: meta.version, score, scoreModel, summary, routes, siteIssues }, null, 2);
385
454
  }
386
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
+
387
502
  // src/config-apply.ts
388
503
  function selectRules(rules, config) {
389
504
  return rules.filter((rule) => config.rules[rule.id] !== "off");
@@ -395,6 +510,8 @@ function applyRuleSeverities(results, config) {
395
510
  });
396
511
  }
397
512
  export {
513
+ ROBOTS_SOURCE_PATHS,
514
+ SITEMAP_SOURCE_PATHS,
398
515
  allRules,
399
516
  applyRuleSeverities,
400
517
  classify,
@@ -403,6 +520,7 @@ export {
403
520
  defaultProject,
404
521
  defineConfig,
405
522
  effectiveSeverity,
523
+ formatAgentReport,
406
524
  formatConsoleReport,
407
525
  formatJsonReport,
408
526
  hasFailureAtOrAbove,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",