svelte-vitals 0.19.0 → 0.20.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/bin.js CHANGED
@@ -6,10 +6,11 @@ import {
6
6
  knownRuleIds,
7
7
  readPackageVersion,
8
8
  run
9
- } from "./chunk-ZE3M3T6U.js";
9
+ } from "./chunk-AAM2A7D7.js";
10
10
 
11
11
  // src/bin.ts
12
- import mri2 from "mri";
12
+ import mri3 from "mri";
13
+ import * as p2 from "@clack/prompts";
13
14
 
14
15
  // src/resolve-args.ts
15
16
  var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
@@ -55,6 +56,26 @@ function parseWeights(raw, errors) {
55
56
  }
56
57
  return weights;
57
58
  }
59
+ function parseCategories(raw, errors) {
60
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
61
+ const categories = [];
62
+ const unknownCategories = [];
63
+ for (const entry of raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)) {
64
+ if (!CATEGORIES.includes(entry)) {
65
+ unknownCategories.push(entry);
66
+ continue;
67
+ }
68
+ if (!categories.includes(entry)) categories.push(entry);
69
+ }
70
+ if (unknownCategories.length > 0) {
71
+ errors.push(`svelte-vitals: unknown category(ies) in --category: ${unknownCategories.join(", ")}`);
72
+ errors.push(`Known categories: ${CATEGORIES.join(", ")}`);
73
+ }
74
+ if (unknownCategories.length === 0 && categories.length === 0) {
75
+ errors.push("svelte-vitals: --category was passed but contains no categories.");
76
+ }
77
+ return categories;
78
+ }
58
79
  var toList = (v) => typeof v === "string" ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
59
80
  function resolveArgs(argv) {
60
81
  const warnings = [];
@@ -71,6 +92,14 @@ function resolveArgs(argv) {
71
92
  const route = typeof argv.route === "string" ? argv.route : void 0;
72
93
  const diffBase = typeof argv.diff === "string" ? argv.diff || "HEAD" : void 0;
73
94
  const staged = Boolean(argv.staged);
95
+ let baselineRef;
96
+ if (typeof argv.baseline === "string") {
97
+ if (argv.baseline.trim() === "") {
98
+ errors.push("svelte-vitals: --baseline requires a git ref (e.g. --baseline origin/main).");
99
+ } else {
100
+ baselineRef = argv.baseline;
101
+ }
102
+ }
74
103
  const allow = toList(argv.rules);
75
104
  const ignore = toList(argv.ignore);
76
105
  const unknown = findUnknownRuleIds([...allow, ...ignore]);
@@ -84,7 +113,7 @@ function resolveArgs(argv) {
84
113
  } else if (typeof argv.reporter === "string") {
85
114
  if (!isReporterName(argv.reporter)) {
86
115
  errors.push(
87
- `svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html.`
116
+ `svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html, md.`
88
117
  );
89
118
  } else {
90
119
  reporter = argv.reporter;
@@ -99,12 +128,20 @@ function resolveArgs(argv) {
99
128
  }
100
129
  const failOn = argv["fail-on-warning"] ? "warning" : failOnValid ? failOnRaw : void 0;
101
130
  const weights = parseWeights(argv.weights, errors);
131
+ const categories = parseCategories(argv.category, errors);
132
+ const score = Boolean(argv.score);
133
+ if (score && (argv.json || typeof argv.reporter === "string")) {
134
+ warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
135
+ }
102
136
  const rulesConfig = buildRulesConfig(allow, ignore);
103
137
  const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
104
138
  if (errors.length > 0) return { options: null, warnings, errors };
105
139
  return {
106
140
  options: {
107
141
  cwd: positional ?? process.cwd(),
142
+ // Never reinterpret an explicit target (design doc 2026-07-08-monorepo-app-picker-design.md,
143
+ // decision 1): the monorepo picker in run() only triggers when this is false.
144
+ explicitPath: positional !== void 0,
108
145
  metaComponents,
109
146
  treatDynamicAs,
110
147
  route,
@@ -114,8 +151,11 @@ function resolveArgs(argv) {
114
151
  failOn,
115
152
  rules,
116
153
  ...weights !== void 0 ? { weights } : {},
154
+ ...categories !== void 0 ? { categories } : {},
155
+ ...score ? { score } : {},
117
156
  ...diffBase !== void 0 ? { diffBase } : {},
118
- ...staged ? { staged } : {}
157
+ ...staged ? { staged } : {},
158
+ ...baselineRef !== void 0 ? { baseline: baselineRef } : {}
119
159
  },
120
160
  warnings,
121
161
  errors
@@ -236,6 +276,93 @@ function isViteTargetId(id) {
236
276
  return VITE_TARGETS.some((t) => t.id === id);
237
277
  }
238
278
 
279
+ // src/install/agent-targets.ts
280
+ var AGENT_TARGETS = [
281
+ {
282
+ id: "claude-skill",
283
+ label: "Claude Code skill",
284
+ hint: "Teaches the agent svelte-vitals rules + when to run the scanner",
285
+ relPath: ".claude/skills/svelte-vitals/SKILL.md"
286
+ },
287
+ {
288
+ id: "cursor-rules",
289
+ label: "Cursor rules",
290
+ hint: "Project rules file so Cursor avoids flagged patterns up front",
291
+ relPath: ".cursor/rules/svelte-vitals.mdc"
292
+ }
293
+ ];
294
+ function agentTargetById(id) {
295
+ return AGENT_TARGETS.find((t) => t.id === id);
296
+ }
297
+ function isAgentTargetId(id) {
298
+ return AGENT_TARGETS.some((t) => t.id === id);
299
+ }
300
+
301
+ // src/install/skill-content.ts
302
+ import { allRules, docsUrlFor } from "@svelte-vitals/core";
303
+ var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
304
+ var CATEGORY_LABELS = {
305
+ seo: "SEO",
306
+ performance: "Performance",
307
+ correctness: "Correctness",
308
+ security: "Security",
309
+ architecture: "Architecture"
310
+ };
311
+ function oneLine(text) {
312
+ return text.replace(/\r?\n+/g, " ").trim();
313
+ }
314
+ function ruleLine(rule) {
315
+ const fixPart = rule.fix?.description ? ` Fix: ${oneLine(rule.fix.description)}` : "";
316
+ return `- **${rule.id} \u2014 ${oneLine(rule.title)}** (${rule.severity}): ${oneLine(rule.rationale)}${fixPart} ([docs](${docsUrlFor(rule.id)}))`;
317
+ }
318
+ function ruleDigest() {
319
+ return CATEGORY_ORDER.map((category) => {
320
+ const lines = allRules.filter((r) => r.category === category).map(ruleLine).join("\n");
321
+ return `### ${CATEGORY_LABELS[category]}
322
+
323
+ ${lines}`;
324
+ }).join("\n\n");
325
+ }
326
+ function sharedBody(version) {
327
+ return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
328
+
329
+ # svelte-vitals
330
+
331
+ ## When to use
332
+
333
+ Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, and architecture patterns.
334
+
335
+ ## Playbook
336
+
337
+ 1. After writing or editing code, run \`npx svelte-vitals . --diff --reporter agent\` and fix any findings it reports.
338
+ 2. Before committing, run \`npx svelte-vitals . --staged\` as a pre-commit gate.
339
+ 3. For a rule's full rationale and fix examples, use the \`explain_rule\` MCP tool (svelte-vitals MCP server) or open its docs link below.
340
+
341
+ ## Rule digest
342
+
343
+ ${ruleDigest()}
344
+ `;
345
+ }
346
+ function buildSkillMarkdown(version) {
347
+ const frontmatter = `---
348
+ name: svelte-vitals
349
+ description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture) and how to run the scanner.
350
+ ---`;
351
+ return `${frontmatter}
352
+
353
+ ${sharedBody(version)}`;
354
+ }
355
+ function buildCursorRules(version) {
356
+ const frontmatter = `---
357
+ description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture)
358
+ globs: ["**/*.svelte", "src/routes/**"]
359
+ alwaysApply: false
360
+ ---`;
361
+ return `${frontmatter}
362
+
363
+ ${sharedBody(version)}`;
364
+ }
365
+
239
366
  // src/install/codemod-vite-config.ts
240
367
  import { parseModule, generateCode, builders, MagicastError } from "magicast";
241
368
  var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
@@ -252,7 +379,7 @@ function codemodViteConfig(existing) {
252
379
  return { status: "manual", snippet: MANUAL_SNIPPET };
253
380
  }
254
381
  const already = configObj.plugins.find(
255
- (p2) => p2?.$type === "function-call" && p2?.$callee === "svelteVitals"
382
+ (p3) => p3?.$type === "function-call" && p3?.$callee === "svelteVitals"
256
383
  );
257
384
  if (already !== void 0) {
258
385
  return { status: "exists" };
@@ -395,6 +522,13 @@ function planForDevOverlay(io) {
395
522
  const result = codemodHooksServer(content);
396
523
  return { id: "vite-dev-overlay", label: viteTargetById("vite-dev-overlay").label, path, ...result };
397
524
  }
525
+ function planForAgentTarget(target, io, force, version) {
526
+ const path = join3(io.cwd, target.relPath);
527
+ const existing = io.readFile(path);
528
+ const content = target.id === "claude-skill" ? buildSkillMarkdown(version) : buildCursorRules(version);
529
+ const status = existing === void 0 ? "created" : force ? "updated" : "exists";
530
+ return { id: target.id, label: target.label, path, status, content };
531
+ }
398
532
  function indent(text) {
399
533
  return text.split("\n").map((l) => ` ${l}`).join("\n");
400
534
  }
@@ -403,7 +537,7 @@ function rowLine(r) {
403
537
  return r.status === "manual" && r.snippet ? `${head}
404
538
  ${indent(r.snippet)}` : head;
405
539
  }
406
- async function runInstall(flags, io, prompts) {
540
+ async function runInstall(flags, io, prompts, version = "0.0.0") {
407
541
  let ids;
408
542
  if (flags.client && flags.client.length > 0) {
409
543
  ids = flags.client;
@@ -421,10 +555,21 @@ async function runInstall(flags, io, prompts) {
421
555
  const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
422
556
  (f) => configExists(join3(io.cwd, f))
423
557
  );
424
- const detected = [...detectedClients, ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : []];
558
+ const claudeSkillDetected = configExists(join3(io.cwd, ".claude", "settings.json"));
559
+ const cursorRulesDetected = configExists(join3(io.cwd, ".cursor", "mcp.json"));
560
+ const detectedAgents = [
561
+ ...claudeSkillDetected ? ["claude-skill"] : [],
562
+ ...cursorRulesDetected ? ["cursor-rules"] : []
563
+ ];
564
+ const detected = [
565
+ ...detectedClients,
566
+ ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : [],
567
+ ...detectedAgents
568
+ ];
425
569
  const options = [
426
570
  ...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
427
- ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
571
+ ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
572
+ ...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
428
573
  ];
429
574
  const picked = await prompts.selectClients(options, detected);
430
575
  if (picked === null) {
@@ -434,13 +579,14 @@ async function runInstall(flags, io, prompts) {
434
579
  ids = picked;
435
580
  } else {
436
581
  io.errorLog(
437
- "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay> to install non-interactively."
582
+ "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules> to install non-interactively."
438
583
  );
439
584
  return 2;
440
585
  }
441
586
  const clients = ids.map(clientById).filter((c) => c !== void 0);
442
587
  const viteIds = ids.filter(isViteTargetId);
443
- if (clients.length === 0 && viteIds.length === 0) {
588
+ const agentIds = ids.filter(isAgentTargetId);
589
+ if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0) {
444
590
  io.errorLog("svelte-vitals: no valid clients or targets selected.");
445
591
  return 2;
446
592
  }
@@ -474,6 +620,10 @@ async function runInstall(flags, io, prompts) {
474
620
  for (const viteId of viteIds) {
475
621
  rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForDevOverlay(io));
476
622
  }
623
+ for (const agentId of agentIds) {
624
+ const target = agentTargetById(agentId);
625
+ rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
626
+ }
477
627
  const planText = rows.map(rowLine).join("\n");
478
628
  io.log("Plan:");
479
629
  io.log(planText);
@@ -530,7 +680,11 @@ ${indent(r.snippet ?? "")}`);
530
680
  }
531
681
 
532
682
  // src/install/args.ts
533
- var VALID_TARGETS = [...CLIENTS.map((c) => c.id), ...VITE_TARGETS.map((t) => t.id)];
683
+ var VALID_TARGETS = [
684
+ ...CLIENTS.map((c) => c.id),
685
+ ...VITE_TARGETS.map((t) => t.id),
686
+ ...AGENT_TARGETS.map((t) => t.id)
687
+ ];
534
688
  var EXPECTED_TARGETS = VALID_TARGETS.join("|");
535
689
  function resolveInstallArgs(argv) {
536
690
  const warnings = [];
@@ -568,16 +722,20 @@ function resolveInstallArgs(argv) {
568
722
  }
569
723
 
570
724
  // src/install/cli.ts
571
- var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server for your AI-agent clients
725
+ var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server, Vite integration, and agent skills/rules
572
726
 
573
727
  Usage:
574
728
  svelte-vitals install [options]
575
729
 
576
730
  Options:
577
- --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay (skips the interactive picker)
731
+ --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules
732
+ (skips the interactive picker)
578
733
  vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-dev-overlay
579
734
  wires up the dev-overlay hook in src/hooks.server.{ts,js}. --force does not apply
580
- to either \u2014 an existing registration is always left as-is.
735
+ to either of these two \u2014 an existing registration is always left as-is.
736
+ claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
737
+ writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
738
+ current rule set and support --force to regenerate.
581
739
  --scope <scope> project | global (applies to all selected clients; codex is always global)
582
740
  --yes, -y Skip the confirmation prompt
583
741
  --dry-run Print the planned changes and exit without writing
@@ -661,7 +819,143 @@ async function runInstallCli(args) {
661
819
  for (const w of warnings) console.error(w);
662
820
  for (const e of errors) console.error(e);
663
821
  if (!flags) return 2;
664
- return runInstall(flags, realIO(), clackPrompts());
822
+ return runInstall(flags, realIO(), clackPrompts(), readPackageVersion());
823
+ }
824
+
825
+ // src/ci/cli.ts
826
+ import { join as join4 } from "path";
827
+ import mri2 from "mri";
828
+
829
+ // src/ci/workflow.ts
830
+ var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
831
+ function planWorkflowWrite(existing, force) {
832
+ if (existing === void 0) return { status: "created" };
833
+ if (!force) return { status: "exists" };
834
+ return { status: "updated" };
835
+ }
836
+ function buildWorkflowYaml(opts) {
837
+ const v = opts.version;
838
+ return [
839
+ `# Generated by \`svelte-vitals ci install\` (svelte-vitals ${v}).`,
840
+ "# Re-run with --force to regenerate.",
841
+ "name: svelte-vitals",
842
+ "",
843
+ "on:",
844
+ " pull_request:",
845
+ "",
846
+ "permissions:",
847
+ " contents: read",
848
+ " pull-requests: write",
849
+ "",
850
+ "jobs:",
851
+ " svelte-vitals:",
852
+ " runs-on: ubuntu-latest",
853
+ " steps:",
854
+ " - uses: actions/checkout@v4",
855
+ " with:",
856
+ " fetch-depth: 0",
857
+ " - uses: actions/setup-node@v4",
858
+ " with:",
859
+ " node-version: 24",
860
+ " - name: Scan (inline annotations + gate)",
861
+ " id: scan",
862
+ " continue-on-error: true",
863
+ " run: >",
864
+ ` npx -y svelte-vitals@${v} .`,
865
+ " --diff origin/${{ github.base_ref }}",
866
+ " --baseline origin/${{ github.base_ref }}",
867
+ " --reporter github",
868
+ " - name: Markdown summary",
869
+ " run: >",
870
+ ` npx -y svelte-vitals@${v} .`,
871
+ " --diff origin/${{ github.base_ref }}",
872
+ " --baseline origin/${{ github.base_ref }}",
873
+ " --reporter md > svelte-vitals-report.md || true",
874
+ " - name: Job summary",
875
+ ' run: cat svelte-vitals-report.md >> "$GITHUB_STEP_SUMMARY"',
876
+ " - name: PR comment (sticky)",
877
+ " if: github.event.pull_request.head.repo.full_name == github.repository",
878
+ " continue-on-error: true",
879
+ " uses: actions/github-script@v7",
880
+ " with:",
881
+ " script: |",
882
+ " const fs = require('fs');",
883
+ " const marker = '<!-- svelte-vitals-report -->';",
884
+ " const body = marker + '\\n' + fs.readFileSync('svelte-vitals-report.md', 'utf8');",
885
+ " const { data: comments } = await github.rest.issues.listComments({",
886
+ " ...context.repo, issue_number: context.issue.number, per_page: 100",
887
+ " });",
888
+ " const mine = comments.find(c => c.body && c.body.startsWith(marker));",
889
+ " if (mine) {",
890
+ " await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body });",
891
+ " } else {",
892
+ " await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });",
893
+ " }",
894
+ " - name: Gate",
895
+ " if: steps.scan.outcome == 'failure'",
896
+ " run: |",
897
+ ' echo "svelte-vitals found blocking issues (see annotations above)."',
898
+ " exit 1",
899
+ ""
900
+ ].join("\n");
901
+ }
902
+
903
+ // src/ci/cli.ts
904
+ var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
905
+
906
+ Usage:
907
+ svelte-vitals ci install [options]
908
+
909
+ Adds a GitHub Actions workflow (${WORKFLOW_PATH}) that scans pull requests, posts inline
910
+ annotations + a job summary, and maintains a sticky PR comment with the findings.
911
+
912
+ Options:
913
+ --force Overwrite an existing workflow file
914
+ --dry-run Print the plan and exit without writing
915
+ -h, --help Show this help`;
916
+ async function runCiCli(args, io = realIO()) {
917
+ const sub = args[0];
918
+ if (sub === "--help" || sub === "-h") {
919
+ io.log(CI_HELP);
920
+ return 0;
921
+ }
922
+ if (sub !== "install") {
923
+ io.log(CI_HELP);
924
+ return 2;
925
+ }
926
+ const argv = mri2(args.slice(1), {
927
+ boolean: ["force", "dry-run", "help"],
928
+ alias: { h: "help" }
929
+ });
930
+ if (argv.help) {
931
+ io.log(CI_HELP);
932
+ return 0;
933
+ }
934
+ const path = join4(io.cwd, WORKFLOW_PATH);
935
+ const existing = io.readFile(path);
936
+ const plan = planWorkflowWrite(existing, Boolean(argv.force));
937
+ io.log("Plan:");
938
+ io.log(` ${WORKFLOW_PATH} [${plan.status}]`);
939
+ if (argv["dry-run"]) {
940
+ io.log("Dry run \u2014 no files written.");
941
+ return 0;
942
+ }
943
+ if (plan.status === "exists") {
944
+ io.log(`= already installed (${WORKFLOW_PATH}) \u2014 use --force to regenerate.`);
945
+ } else {
946
+ const version = readPackageVersion();
947
+ try {
948
+ io.writeFile(path, buildWorkflowYaml({ version }));
949
+ io.log(`\u2713 ${plan.status} ${WORKFLOW_PATH}`);
950
+ } catch (err) {
951
+ io.errorLog(
952
+ `svelte-vitals: failed to write ${WORKFLOW_PATH}: ${err instanceof Error ? err.message : String(err)}`
953
+ );
954
+ return 2;
955
+ }
956
+ }
957
+ io.log("Done. Commit the workflow file and open a PR to see it in action.");
958
+ return 0;
665
959
  }
666
960
 
667
961
  // src/bin.ts
@@ -669,7 +963,8 @@ var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (
669
963
 
670
964
  Usage:
671
965
  svelte-vitals [path] [options]
672
- svelte-vitals install Set up the MCP server for Claude Code / Cursor / Codex
966
+ svelte-vitals install Set up the MCP server, Vite integration, or agent skills/rules
967
+ svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
673
968
 
674
969
  Options:
675
970
  --meta-components <names> Comma-separated component names that emit head metadata
@@ -677,8 +972,9 @@ Options:
677
972
  --route <glob> Only analyze routes matching this glob
678
973
  --diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
679
974
  --staged Report only findings in files staged for commit (pre-commit gate)
975
+ --baseline <ref> Report only findings not present at ref (compare against e.g. origin/main)
680
976
  --by-route Show per-route score breakdown in console output
681
- --reporter <fmt> console | json | agent | sarif | github | html (auto: agent under AI-agent envs, github under GitHub Actions)
977
+ --reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
682
978
  --out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
683
979
  --json Alias for --reporter=json
684
980
  --fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
@@ -686,7 +982,9 @@ Options:
686
982
  --min-health <0-100> Fail (exit 1) when the combined Health score is below this value
687
983
  --rules <ids> Comma-separated rule ids to enable (all others disabled)
688
984
  --ignore <ids> Comma-separated rule ids to disable
985
+ --category <cats> Comma-separated categories to analyze: seo | performance | correctness | security | architecture
689
986
  --weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
987
+ --score Print only the combined Health score (works with --min-health for gating)
690
988
  --no-color Disable ANSI color in console output
691
989
  -h, --help Show this help
692
990
  -v, --version Show version
@@ -699,15 +997,26 @@ Exit codes:
699
997
  1 critical finding present (or --fail-on threshold reached)
700
998
  2 execution error (not a SvelteKit project / internal error)`;
701
999
  var VERSION = readPackageVersion();
1000
+ async function selectApp(apps) {
1001
+ const res = await p2.select({
1002
+ message: "Multiple SvelteKit apps found \u2014 which one should svelte-vitals analyze?",
1003
+ options: apps.map((a) => ({ value: a, label: a }))
1004
+ });
1005
+ return p2.isCancel(res) ? null : res;
1006
+ }
702
1007
  async function main() {
703
1008
  const rawArgs = process.argv.slice(2);
704
1009
  if (rawArgs[0] === "install") {
705
1010
  const code2 = await runInstallCli(rawArgs.slice(1));
706
1011
  process.exit(code2);
707
1012
  }
708
- const argv = mri2(process.argv.slice(2), {
1013
+ if (rawArgs[0] === "ci") {
1014
+ const code2 = await runCiCli(rawArgs.slice(1));
1015
+ process.exit(code2);
1016
+ }
1017
+ const argv = mri3(process.argv.slice(2), {
709
1018
  alias: { h: "help", v: "version" },
710
- boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color"],
1019
+ boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color", "score"],
711
1020
  string: [
712
1021
  "meta-components",
713
1022
  "treat-dynamic-as",
@@ -719,7 +1028,9 @@ async function main() {
719
1028
  "min-health",
720
1029
  "out-file",
721
1030
  "diff",
722
- "weights"
1031
+ "baseline",
1032
+ "weights",
1033
+ "category"
723
1034
  ]
724
1035
  });
725
1036
  if (argv.help) {
@@ -744,7 +1055,7 @@ async function main() {
744
1055
  }
745
1056
  minHealth = n;
746
1057
  }
747
- const code = await run({ ...options, minHealth, noColor: argv["no-color"] });
1058
+ const code = await run({ ...options, minHealth, noColor: argv["no-color"], selectApp });
748
1059
  process.exit(code);
749
1060
  }
750
1061
  void main();
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { mkdirSync, writeFileSync } from "fs";
3
- import { dirname } from "path";
3
+ import { dirname as dirname2, join as join5 } from "path";
4
4
  import {
5
5
  allRules as allRules2,
6
6
  runRules,
@@ -10,6 +10,7 @@ import {
10
10
  formatSarifReport,
11
11
  formatGithubReport,
12
12
  formatHtmlReport,
13
+ formatMarkdownReport,
13
14
  summarize,
14
15
  hasFailureAtOrAbove,
15
16
  computeHealth,
@@ -73,7 +74,7 @@ async function detectProject(rt, cwd) {
73
74
  const hasRoutes = await rt.exists(rt.join(cwd, ROUTES_DIR));
74
75
  if (hasKitDep || hasConfig && hasRoutes) return;
75
76
  throw new ProjectError(
76
- "No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass --config."
77
+ "No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass a path (e.g. npx svelte-vitals apps/web)."
77
78
  );
78
79
  }
79
80
  async function enumerateRoutePages(rt, cwd) {
@@ -650,6 +651,23 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
650
651
  // src/providers/source/components.ts
651
652
  import { collectComponentFacts } from "@svelte-vitals/core";
652
653
 
654
+ // src/discover-apps.ts
655
+ import { existsSync } from "fs";
656
+ import { join as join2, dirname } from "path";
657
+ import { glob } from "tinyglobby";
658
+ async function discoverApps(cwd) {
659
+ const configs = await glob("**/svelte.config.{js,ts}", {
660
+ cwd,
661
+ dot: false,
662
+ deep: 4,
663
+ ignore: ["**/node_modules/**", "**/.svelte-kit/**", "**/build/**", "**/dist/**", "**/.git/**"]
664
+ });
665
+ const dirs = [...new Set(configs.map((c) => dirname(c)))].filter(
666
+ (d) => d !== "." && existsSync(join2(cwd, d, "src", "routes"))
667
+ );
668
+ return dirs.sort();
669
+ }
670
+
653
671
  // src/version.ts
654
672
  import { readFileSync } from "fs";
655
673
  function readPackageVersion() {
@@ -664,7 +682,7 @@ function readPackageVersion() {
664
682
  // src/reporter-resolve.ts
665
683
  var AGENT_ENV_VARS = ["CLAUDECODE", "SVELTE_VITALS_AGENT"];
666
684
  function isReporterName(value) {
667
- return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html";
685
+ return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html" || value === "md";
668
686
  }
669
687
  function isAgentEnv(env = process.env) {
670
688
  return AGENT_ENV_VARS.some((key) => {
@@ -711,6 +729,58 @@ function filterToChangedFiles(results, changed) {
711
729
  return results.filter((r) => r.location !== void 0 && changed.has(r.location));
712
730
  }
713
731
 
732
+ // src/baseline.ts
733
+ import { execFileSync as execFileSync2 } from "child_process";
734
+ import { mkdtempSync, rmSync } from "fs";
735
+ import { tmpdir } from "os";
736
+ import { join as join3 } from "path";
737
+ function git2(args, cwd) {
738
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
739
+ }
740
+ function findingKey(r) {
741
+ return `${r.id}::${r.route ?? ""}::${r.location ?? ""}`;
742
+ }
743
+ function checkoutBaseline(cwd, ref) {
744
+ let tmp;
745
+ try {
746
+ const repoRoot = git2(["rev-parse", "--show-toplevel"], cwd).trim();
747
+ const showPrefix = git2(["rev-parse", "--show-prefix"], cwd).trim().replace(/\/+$/, "");
748
+ tmp = mkdtempSync(join3(tmpdir(), "svelte-vitals-baseline-"));
749
+ const wt = join3(tmp, "wt");
750
+ git2(["worktree", "add", "--detach", wt, ref], repoRoot);
751
+ const analyzeCwd = showPrefix ? join3(wt, showPrefix) : wt;
752
+ const tmpDir = tmp;
753
+ const cleanup = () => {
754
+ try {
755
+ execFileSync2("git", ["worktree", "remove", "--force", wt], {
756
+ cwd: repoRoot,
757
+ encoding: "utf8",
758
+ stdio: ["ignore", "pipe", "ignore"]
759
+ });
760
+ } catch {
761
+ try {
762
+ execFileSync2("git", ["worktree", "prune"], {
763
+ cwd: repoRoot,
764
+ encoding: "utf8",
765
+ stdio: ["ignore", "pipe", "ignore"]
766
+ });
767
+ } catch {
768
+ }
769
+ } finally {
770
+ rmSync(tmpDir, { recursive: true, force: true });
771
+ }
772
+ };
773
+ return { analyzeCwd, cleanup };
774
+ } catch {
775
+ if (tmp !== void 0) rmSync(tmp, { recursive: true, force: true });
776
+ return void 0;
777
+ }
778
+ }
779
+ function filterToNewFindings(results, baselineResults) {
780
+ const baselineKeys = new Set(baselineResults.map(findingKey));
781
+ return results.filter((r) => !baselineKeys.has(findingKey(r)));
782
+ }
783
+
714
784
  // src/color.ts
715
785
  import { noColorPalette } from "@svelte-vitals/core";
716
786
  var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
@@ -754,8 +824,8 @@ function startSpinner(text, opts) {
754
824
  }
755
825
 
756
826
  // src/config-file.ts
757
- import { existsSync } from "fs";
758
- import { join as join2 } from "path";
827
+ import { existsSync as existsSync2 } from "fs";
828
+ import { join as join4 } from "path";
759
829
  import { pathToFileURL } from "url";
760
830
 
761
831
  // src/rules-config.ts
@@ -852,7 +922,7 @@ function validateConfigFile(raw, path) {
852
922
  return { config, warnings };
853
923
  }
854
924
  async function loadConfigFile(cwd) {
855
- const found = CONFIG_FILENAMES.map((name) => join2(cwd, name)).find((path) => existsSync(path));
925
+ const found = CONFIG_FILENAMES.map((name) => join4(cwd, name)).find((path) => existsSync2(path));
856
926
  if (!found) return void 0;
857
927
  let mod;
858
928
  try {
@@ -879,9 +949,9 @@ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
879
949
  function spinnerEnabled(opts) {
880
950
  return opts.reporter === "console" && opts.stderrIsTTY && !isAutoDetectedAgent(opts.rawReporter, opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stderrIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
881
951
  }
882
- function routeMatcher(glob) {
883
- if (!glob) return () => true;
884
- const body = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
952
+ function routeMatcher(glob2) {
953
+ if (!glob2) return () => true;
954
+ const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
885
955
  const re = new RegExp(`^${body}$`);
886
956
  return (route) => re.test(route.replace(/^\//, ""));
887
957
  }
@@ -907,7 +977,8 @@ async function analyzeProject(opts = {}) {
907
977
  const headings = collected.headings.filter((h) => matches(h.route));
908
978
  const project = await collectProjectFacts(rt, cwd);
909
979
  const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
910
- const rules = selectRules(allRules2, config);
980
+ const selected = selectRules(allRules2, config);
981
+ const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
911
982
  const results = applyRuleSeverities(
912
983
  await runRules(rules, { heads, images, headings, components, project, config }),
913
984
  config
@@ -924,7 +995,7 @@ async function run(opts = {}) {
924
995
  const env = opts.env ?? process.env;
925
996
  const reporter = resolveReporter(opts.reporter, env);
926
997
  const spinner = startSpinner("Analyzing\u2026", {
927
- enabled: spinnerEnabled({
998
+ enabled: !opts.score && spinnerEnabled({
928
999
  reporter,
929
1000
  rawReporter: opts.reporter,
930
1001
  stderrIsTTY: opts.stderrIsTTY ?? !!process.stderr.isTTY,
@@ -932,25 +1003,75 @@ async function run(opts = {}) {
932
1003
  noColorFlag: opts.noColor
933
1004
  })
934
1005
  });
1006
+ let cwd = opts.cwd ?? process.cwd();
935
1007
  let analysis;
936
1008
  try {
937
1009
  analysis = await analyzeProject({
938
- cwd: opts.cwd ?? process.cwd(),
1010
+ cwd,
939
1011
  metaComponents: opts.metaComponents,
940
1012
  treatDynamicAs: opts.treatDynamicAs,
941
1013
  route: opts.route,
942
1014
  failOn: opts.failOn,
943
1015
  rules: opts.rules,
944
- weights: opts.weights
1016
+ weights: opts.weights,
1017
+ categories: opts.categories
945
1018
  });
946
1019
  } catch (err) {
947
1020
  spinner.stop();
948
1021
  if (err instanceof ProjectError) {
949
- errorLog(err.message);
1022
+ if (opts.explicitPath) {
1023
+ errorLog(err.message);
1024
+ return 2;
1025
+ }
1026
+ const apps = await discoverApps(cwd);
1027
+ if (apps.length === 0) {
1028
+ errorLog(err.message);
1029
+ return 2;
1030
+ }
1031
+ let chosen;
1032
+ if (apps.length === 1) {
1033
+ errorLog(`svelte-vitals: detected SvelteKit app at ${apps[0]}; analyzing it.`);
1034
+ chosen = apps[0];
1035
+ } else if (
1036
+ // clack reads from stdin and renders to stdout, so both must be interactive —
1037
+ // a piped/redirected stdin would leave the prompt hanging for input that never comes.
1038
+ (opts.stdinIsTTY ?? !!process.stdin.isTTY) && (opts.stdoutIsTTY ?? !!process.stdout.isTTY) && opts.selectApp
1039
+ ) {
1040
+ const selection = await opts.selectApp(apps);
1041
+ if (selection === null) {
1042
+ log("Cancelled.");
1043
+ return 0;
1044
+ }
1045
+ chosen = selection;
1046
+ } else {
1047
+ errorLog(`svelte-vitals: multiple SvelteKit apps found: ${apps.join(", ")}.`);
1048
+ errorLog(`svelte-vitals: pass one as a path, e.g. \`npx svelte-vitals ${apps[0]}\`.`);
1049
+ return 2;
1050
+ }
1051
+ cwd = join5(cwd, chosen);
1052
+ try {
1053
+ analysis = await analyzeProject({
1054
+ cwd,
1055
+ metaComponents: opts.metaComponents,
1056
+ treatDynamicAs: opts.treatDynamicAs,
1057
+ route: opts.route,
1058
+ failOn: opts.failOn,
1059
+ rules: opts.rules,
1060
+ weights: opts.weights,
1061
+ categories: opts.categories
1062
+ });
1063
+ } catch (err2) {
1064
+ if (err2 instanceof ProjectError) {
1065
+ errorLog(err2.message);
1066
+ return 2;
1067
+ }
1068
+ errorLog(`svelte-vitals: ${err2 instanceof Error ? err2.message : String(err2)}`);
1069
+ return 2;
1070
+ }
1071
+ } else {
1072
+ errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
950
1073
  return 2;
951
1074
  }
952
- errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
953
- return 2;
954
1075
  }
955
1076
  spinner.stop();
956
1077
  for (const w of analysis.warnings) errorLog(`svelte-vitals: ${w}`);
@@ -958,7 +1079,6 @@ async function run(opts = {}) {
958
1079
  const { config, version } = analysis;
959
1080
  let results = analysis.results;
960
1081
  if (opts.staged || opts.diffBase !== void 0) {
961
- const cwd = opts.cwd ?? process.cwd();
962
1082
  const changed = opts.staged ? getChangedFiles(cwd, { staged: true }) : getChangedFiles(cwd, { base: opts.diffBase });
963
1083
  if (changed === void 0) {
964
1084
  errorLog(
@@ -968,46 +1088,78 @@ async function run(opts = {}) {
968
1088
  results = filterToChangedFiles(results, changed);
969
1089
  }
970
1090
  }
971
- if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
972
- errorLog(
973
- "svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
974
- );
975
- }
976
- if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
977
- errorLog(
978
- "svelte-vitals: github reporter auto-selected (GitHub Actions detected); override with --reporter console|json|sarif."
979
- );
1091
+ if (opts.baseline !== void 0) {
1092
+ const checkout = checkoutBaseline(cwd, opts.baseline);
1093
+ if (checkout === void 0) {
1094
+ errorLog(
1095
+ `svelte-vitals: could not analyze baseline '${opts.baseline}' (not a git repo, git unavailable, or bad ref); reporting all findings.`
1096
+ );
1097
+ } else {
1098
+ try {
1099
+ const base = await analyzeProject({
1100
+ cwd: checkout.analyzeCwd,
1101
+ metaComponents: opts.metaComponents,
1102
+ treatDynamicAs: opts.treatDynamicAs,
1103
+ route: opts.route,
1104
+ failOn: opts.failOn,
1105
+ rules: opts.rules,
1106
+ weights: opts.weights,
1107
+ categories: opts.categories
1108
+ });
1109
+ results = filterToNewFindings(results, base.results);
1110
+ } catch {
1111
+ errorLog(`svelte-vitals: baseline analysis of '${opts.baseline}' failed; reporting all findings.`);
1112
+ } finally {
1113
+ checkout.cleanup();
1114
+ }
1115
+ }
980
1116
  }
981
- if (reporter === "json") {
982
- log(formatJsonReport(results, config, { version }));
983
- } else if (reporter === "agent") {
984
- log(formatAgentReport(results, config));
985
- } else if (reporter === "sarif") {
986
- log(formatSarifReport(results, config, { version }));
987
- } else if (reporter === "github") {
988
- const output = formatGithubReport(results, config);
989
- if (output) log(output);
990
- } else if (reporter === "html") {
991
- const html = formatHtmlReport(results, config, { version });
992
- if (opts.outFile === "-") {
993
- log(html);
1117
+ if (opts.score) {
1118
+ log(String(computeHealth(results, config).health));
1119
+ } else {
1120
+ if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
1121
+ errorLog(
1122
+ "svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
1123
+ );
1124
+ }
1125
+ if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
1126
+ errorLog(
1127
+ "svelte-vitals: github reporter auto-selected (GitHub Actions detected); override with --reporter console|json|sarif."
1128
+ );
1129
+ }
1130
+ if (reporter === "json") {
1131
+ log(formatJsonReport(results, config, { version }));
1132
+ } else if (reporter === "agent") {
1133
+ log(formatAgentReport(results, config));
1134
+ } else if (reporter === "sarif") {
1135
+ log(formatSarifReport(results, config, { version }));
1136
+ } else if (reporter === "github") {
1137
+ const output = formatGithubReport(results, config);
1138
+ if (output) log(output);
1139
+ } else if (reporter === "html") {
1140
+ const html = formatHtmlReport(results, config, { version });
1141
+ if (opts.outFile === "-") {
1142
+ log(html);
1143
+ } else {
1144
+ const path = opts.outFile || "svelte-vitals-report.html";
1145
+ const write = opts.writeFile ?? ((p, c) => {
1146
+ mkdirSync(dirname2(p), { recursive: true });
1147
+ writeFileSync(p, c);
1148
+ });
1149
+ write(path, html);
1150
+ errorLog(`svelte-vitals: wrote report to ${path}`);
1151
+ }
1152
+ } else if (reporter === "md") {
1153
+ log(formatMarkdownReport(results, config, { version }));
994
1154
  } else {
995
- const path = opts.outFile || "svelte-vitals-report.html";
996
- const write = opts.writeFile ?? ((p, c) => {
997
- mkdirSync(dirname(p), { recursive: true });
998
- writeFileSync(p, c);
1155
+ const colorOn = colorEnabled({
1156
+ reporter,
1157
+ isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
1158
+ env,
1159
+ noColorFlag: opts.noColor
999
1160
  });
1000
- write(path, html);
1001
- errorLog(`svelte-vitals: wrote report to ${path}`);
1161
+ log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
1002
1162
  }
1003
- } else {
1004
- const colorOn = colorEnabled({
1005
- reporter,
1006
- isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
1007
- env,
1008
- noColorFlag: opts.noColor
1009
- });
1010
- log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
1011
1163
  }
1012
1164
  const summary = summarize(results, config);
1013
1165
  const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
2
2
  export { defineConfig } from '@svelte-vitals/core';
3
3
 
4
- type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html';
4
+ type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html' | 'md';
5
5
 
6
6
  /** Thrown when the target directory is not a SvelteKit project (CLI maps to exit 2). */
7
7
  declare class ProjectError extends Error {
@@ -59,10 +59,14 @@ interface RunOptions {
59
59
  rules?: Record<string, RuleSetting>;
60
60
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
61
61
  weights?: Partial<Record<Category, number>>;
62
+ /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
63
+ categories?: Category[];
62
64
  /** Override process.env for reporter auto-detection (mainly useful in tests). */
63
65
  env?: NodeJS.ProcessEnv;
64
66
  /** Fail (exit 1) when the combined Health score is below this value (0–100). */
65
67
  minHealth?: number;
68
+ /** Print only the combined Health score (integer) to stdout. */
69
+ score?: boolean;
66
70
  /** Output path for --reporter html (default 'svelte-vitals-report.html'; '-' = stdout). */
67
71
  outFile?: string;
68
72
  /** Injected file writer for --reporter html (defaults to node:fs writeFileSync). Mainly for tests. */
@@ -71,12 +75,20 @@ interface RunOptions {
71
75
  diffBase?: string;
72
76
  /** Report only findings in files staged for commit. Takes precedence over `diffBase`. */
73
77
  staged?: boolean;
78
+ /** Report only findings not present when analyzing this git ref (e.g. the PR base). */
79
+ baseline?: string;
74
80
  /** Disable ANSI color in console output. */
75
81
  noColor?: boolean;
76
82
  /** Override stdout TTY detection (tests). */
77
83
  stdoutIsTTY?: boolean;
78
84
  /** Override stderr TTY detection (tests). */
79
85
  stderrIsTTY?: boolean;
86
+ /** Override stdin TTY detection (tests). */
87
+ stdinIsTTY?: boolean;
88
+ /** True when the user passed a path argument — discovery must not run (design: never reinterpret an explicit target). */
89
+ explicitPath?: boolean;
90
+ /** Injected picker for the monorepo app selector (bin.ts wires a clack implementation; null = cancelled). */
91
+ selectApp?: (apps: string[]) => Promise<string | null>;
80
92
  }
81
93
  /**
82
94
  * Whether the "Analyzing…" spinner should run. Unlike color, the spinner animates
@@ -103,6 +115,8 @@ interface AnalyzeOptions {
103
115
  rules?: Record<string, RuleSetting>;
104
116
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
105
117
  weights?: Partial<Record<Category, number>>;
118
+ /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
119
+ categories?: Category[];
106
120
  }
107
121
  interface AnalyzeResult {
108
122
  results: Result[];
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  routeMatcher,
10
10
  run,
11
11
  spinnerEnabled
12
- } from "./chunk-ZE3M3T6U.js";
12
+ } from "./chunk-AAM2A7D7.js";
13
13
  export {
14
14
  ProjectError,
15
15
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,7 +45,7 @@
45
45
  "smol-toml": "^1.7.0",
46
46
  "svelte": "^5.56.4",
47
47
  "tinyglobby": "^0.2.17",
48
- "@svelte-vitals/core": "0.20.0"
48
+ "@svelte-vitals/core": "0.21.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^24.13.2"