svelte-vitals 0.44.0 → 0.44.2

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/README.md CHANGED
@@ -31,14 +31,11 @@ Health: 76/100
31
31
  SEO Score: 51/100 (route avg 61 · site −10)
32
32
  Architecture Score: 100/100 (route avg 100)
33
33
 
34
- Critical (2)
34
+ Critical (1)
35
35
  ────────────────────────
36
36
  ✗ seo/title-presence Missing <title>
37
37
  /
38
38
  src/routes/+page.svelte
39
- ✗ seo/description-presence Missing <meta name="description">
40
- /
41
- src/routes/+page.svelte
42
39
 
43
40
  Warnings (10)
44
41
  ────────────────────────
package/dist/bin.js CHANGED
@@ -2,20 +2,22 @@
2
2
  import {
3
3
  CONFIG_FILENAMES,
4
4
  discoverApps,
5
- findUnknownRuleIds,
6
5
  hasDep,
7
6
  isReporterName,
8
- knownRuleIds,
9
7
  readCoreVersion,
10
8
  readPackageVersion,
11
9
  readPkg,
12
10
  run
13
- } from "./chunk-WD5TSADH.js";
11
+ } from "./chunk-EWDGS52A.js";
14
12
  import {
15
13
  consoleIO,
16
14
  parseCliArgs,
17
15
  toList
18
16
  } from "./chunk-5Q2PT47V.js";
17
+ import {
18
+ findUnknownRuleIds,
19
+ knownRuleIds
20
+ } from "./chunk-TFBLQUAC.js";
19
21
 
20
22
  // src/resolve-args.ts
21
23
  var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
@@ -132,6 +134,7 @@ function resolveArgs(argv) {
132
134
  const errors = [];
133
135
  for (const flag of VALUE_FLAGS) {
134
136
  const v = argv[flag];
137
+ if (flag === "out-file" && v === "-") continue;
135
138
  if (v !== void 0 && (typeof v !== "string" || v.trim() === "" || v.startsWith("-"))) {
136
139
  errors.push(`svelte-vitals: --${flag} requires a value.`);
137
140
  }
@@ -193,6 +196,15 @@ function resolveArgs(argv) {
193
196
  const failOn = failOnValid ? failOnRaw : void 0;
194
197
  const weights = parseWeights(argv.weights, errors);
195
198
  const categories = parseCategories(argv.category, errors);
199
+ if (categories !== void 0 && categories.length > 0 && allow.length > 0) {
200
+ const excluded = allow.filter((id) => !unknown.includes(id)).filter((id) => !categories.includes(id.split("/")[0]));
201
+ if (excluded.length > 0) {
202
+ errors.push(
203
+ `svelte-vitals: --rules id(s) excluded by --category ${categories.join(", ")}: ${excluded.join(", ")}`
204
+ );
205
+ errors.push("Add the rule's category to --category, or drop the rule from --rules.");
206
+ }
207
+ }
196
208
  const score = Boolean(argv.score);
197
209
  if (score && typeof argv.reporter === "string") {
198
210
  warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
@@ -395,7 +407,7 @@ ${lines}`;
395
407
  }).join("\n\n");
396
408
  }
397
409
  function sharedBody(version) {
398
- return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
410
+ return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run \`svelte-vitals install --refresh\` to regenerate. -->
399
411
 
400
412
  # svelte-vitals
401
413
 
@@ -442,7 +454,7 @@ name: improve-svelte
442
454
  description: Survey a whole SvelteKit codebase as a senior Svelte/SvelteKit engineer, using svelte-vitals' scan as evidence, then produce a prioritized audit and self-contained implementation plans for other agents (or cheaper models) to execute. Read-only on source code \u2014 it plans improvements, it does not apply them. Use when the user asks to "improve this SvelteKit app", "audit this codebase", "make this app more SEO/performance/security solid", or wants a roadmap of fixes rather than a review of a single diff. For routine regression checks while writing code, use the \`svelte-vitals\` skill instead.
443
455
  ---
444
456
 
445
- <!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
457
+ <!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run \`svelte-vitals install --refresh\` to regenerate. -->
446
458
 
447
459
  # improve-svelte
448
460
 
@@ -1775,7 +1787,7 @@ function selectApp(apps) {
1775
1787
  async function main() {
1776
1788
  const rawArgs = process.argv.slice(2);
1777
1789
  if (rawArgs[0] === "docs") {
1778
- const { runDocsCli } = await import("./cli-LGD6NDAO.js");
1790
+ const { runDocsCli } = await import("./cli-L2HHOZDN.js");
1779
1791
  process.exitCode = runDocsCli(rawArgs.slice(1));
1780
1792
  return;
1781
1793
  }
@@ -1,8 +1,14 @@
1
+ import {
2
+ findUnknownRuleIds,
3
+ knownRuleIds,
4
+ ruleOptionsSpec
5
+ } from "./chunk-TFBLQUAC.js";
6
+
1
7
  // src/index.ts
2
8
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
3
9
  import { dirname as dirname2, join as join7 } from "path";
4
10
  import {
5
- allRules as allRules3,
11
+ allRules as allRules2,
6
12
  runRules,
7
13
  formatConsoleReport,
8
14
  formatJsonReport,
@@ -17,7 +23,8 @@ import {
17
23
  defineConfig,
18
24
  selectRules,
19
25
  applyRuleSeverities,
20
- applyOverrides
26
+ applyOverrides,
27
+ settingSeverity
21
28
  } from "@svelte-vitals/core";
22
29
 
23
30
  // src/runtime/node.ts
@@ -391,6 +398,29 @@ function collectSvelteHeads(node, acc) {
391
398
  if (key in node) collectSvelteHeads(childOf(node, key), acc);
392
399
  }
393
400
  }
401
+ var JS_MIME_TYPES = /* @__PURE__ */ new Set([
402
+ "application/ecmascript",
403
+ "application/javascript",
404
+ "application/x-ecmascript",
405
+ "application/x-javascript",
406
+ "text/ecmascript",
407
+ "text/javascript",
408
+ "text/javascript1.0",
409
+ "text/javascript1.1",
410
+ "text/javascript1.2",
411
+ "text/javascript1.3",
412
+ "text/javascript1.4",
413
+ "text/javascript1.5",
414
+ "text/jscript",
415
+ "text/livescript",
416
+ "text/x-ecmascript",
417
+ "text/x-javascript"
418
+ ]);
419
+ function isClassicScriptType(type) {
420
+ if (type === void 0) return true;
421
+ const normalized = type.trim().toLowerCase();
422
+ return normalized === "" || JS_MIME_TYPES.has(normalized);
423
+ }
394
424
  function tagsFromHead(head) {
395
425
  const tags = [];
396
426
  const children = head.fragment.nodes;
@@ -450,7 +480,7 @@ function tagsFromHead(head) {
450
480
  } else {
451
481
  const src = attrText(attributes, "src");
452
482
  if (src) {
453
- const blocking = findAttr3(attributes, "defer") === void 0 && findAttr3(attributes, "async") === void 0 && type !== "module";
483
+ const blocking = isClassicScriptType(type) && findAttr3(attributes, "defer") === void 0 && findAttr3(attributes, "async") === void 0;
454
484
  tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
455
485
  }
456
486
  }
@@ -592,6 +622,7 @@ function resolveComponentPath(source, fromFileRel) {
592
622
  }
593
623
  async function resolveFileTags(rt, cwd, fileRel, parsed, config, depth, visited, cache = /* @__PURE__ */ new Map()) {
594
624
  const tags = [...parsed.headTags];
625
+ const headings = [];
595
626
  let broad = false;
596
627
  for (const use of parsed.components) {
597
628
  const info = parsed.imports.get(use.name);
@@ -615,10 +646,12 @@ async function resolveFileTags(rt, cwd, fileRel, parsed, config, depth, visited,
615
646
  const child = await resolveFileTags(rt, cwd, childRel, childParsed, config, depth - 1, childVisited, cache);
616
647
  tags.push(...child.tags);
617
648
  broad = broad || child.broad;
649
+ for (const h of childParsed.headings) headings.push({ ...h, file: childRel });
650
+ headings.push(...child.headings);
618
651
  }
619
652
  }
620
653
  }
621
- return { tags, broad };
654
+ return { tags, broad, headings };
622
655
  }
623
656
 
624
657
  // src/providers/source/routes.ts
@@ -694,6 +727,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
694
727
  let broadInherited = false;
695
728
  const images = [];
696
729
  const headings = [];
730
+ const componentHeadings = [];
697
731
  for (const { rel, isPage } of files) {
698
732
  const parsed = await readAndParse(rt, cwd, rel, cache);
699
733
  for (const img of parsed.images) {
@@ -710,6 +744,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
710
744
  if (isPage) broadOwn = true;
711
745
  else broadInherited = true;
712
746
  }
747
+ componentHeadings.push(...resolved.headings);
713
748
  }
714
749
  if (broadOwn || broadInherited) {
715
750
  const presence = broadOwn ? "own" : "inherited";
@@ -722,7 +757,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
722
757
  return {
723
758
  head: { route, source: "static", tags: [...composed.values()], file: pageRel },
724
759
  images: { route, images },
725
- headings: { route, headings }
760
+ headings: { route, headings, componentHeadings }
726
761
  };
727
762
  }
728
763
  async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map()) {
@@ -863,6 +898,7 @@ function isAutoDetectedGithub(explicit, env = process.env) {
863
898
 
864
899
  // src/changed-files.ts
865
900
  import { execFileSync } from "child_process";
901
+ import { defaultConfig as defaultConfig2, isPenalized } from "@svelte-vitals/core";
866
902
  function git(args, cwd) {
867
903
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0");
868
904
  }
@@ -881,8 +917,10 @@ function getChangedFiles(cwd, opts) {
881
917
  return void 0;
882
918
  }
883
919
  }
884
- function filterToChangedFiles(results, changed) {
885
- return results.filter((r) => r.location !== void 0 && changed.has(r.location));
920
+ function filterToChangedFiles(results, changed, config = defaultConfig2) {
921
+ return results.filter(
922
+ (r) => r.location !== void 0 && changed.has(r.location) && (isPenalized(r.detection, config.treatDynamicAs) || r.route === void 0)
923
+ );
886
924
  }
887
925
 
888
926
  // src/baseline.ts
@@ -890,6 +928,7 @@ import { execFileSync as execFileSync2 } from "child_process";
890
928
  import { mkdtempSync, rmSync } from "fs";
891
929
  import { tmpdir } from "os";
892
930
  import { join as join4 } from "path";
931
+ import { defaultConfig as defaultConfig3, isPenalized as isPenalized2 } from "@svelte-vitals/core";
893
932
  function git2(args, cwd) {
894
933
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
895
934
  }
@@ -932,15 +971,16 @@ function checkoutBaseline(cwd, ref) {
932
971
  return void 0;
933
972
  }
934
973
  }
935
- function filterToNewFindings(results, baselineResults) {
936
- const baselineKeys = new Set(baselineResults.map(findingKey));
937
- return results.filter((r) => !baselineKeys.has(findingKey(r)));
974
+ function filterToNewFindings(results, baselineResults, config = defaultConfig3) {
975
+ const penalized = (rs) => rs.filter((r) => isPenalized2(r.detection, config.treatDynamicAs));
976
+ const baselineKeys = new Set(penalized(baselineResults).map(findingKey));
977
+ return penalized(results).filter((r) => !baselineKeys.has(findingKey(r)));
938
978
  }
939
979
 
940
980
  // src/suppressions.ts
941
981
  import { readFileSync as readFileSync2, writeFileSync } from "fs";
942
982
  import { join as join6 } from "path";
943
- import { isPenalized } from "@svelte-vitals/core";
983
+ import { isPenalized as isPenalized3 } from "@svelte-vitals/core";
944
984
 
945
985
  // src/config-file.ts
946
986
  import { existsSync as existsSync2 } from "fs";
@@ -948,27 +988,11 @@ import { join as join5 } from "path";
948
988
  import { pathToFileURL } from "url";
949
989
  import {
950
990
  CATEGORIES,
951
- defaultConfig as defaultConfig2,
991
+ defaultConfig as defaultConfig4,
952
992
  resolveRuleOptions,
953
993
  shouldSkipRangeCheck,
954
994
  validateRuleSetting
955
995
  } from "@svelte-vitals/core";
956
-
957
- // src/rules-config.ts
958
- import { allRules } from "@svelte-vitals/core";
959
- var KNOWN_IDS = new Set(allRules.map((r) => r.id));
960
- var RULE_BY_ID = new Map(allRules.map((r) => [r.id, r]));
961
- function findUnknownRuleIds(ids) {
962
- return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
963
- }
964
- function knownRuleIds() {
965
- return [...KNOWN_IDS].sort();
966
- }
967
- function ruleOptionsSpec(id) {
968
- return RULE_BY_ID.get(id)?.options;
969
- }
970
-
971
- // src/config-file.ts
972
996
  var CONFIG_FILENAMES = ["svelte-vitals.config.mjs", "svelte-vitals.config.js", "svelte-vitals.config.ts"];
973
997
  var TREAT_DYNAMIC_AS_VALUES = ["pass", "warn", "fail"];
974
998
  var FAIL_ON_VALUES = ["critical", "warning", "info"];
@@ -1072,7 +1096,7 @@ function validateConfigFile(raw, path) {
1072
1096
  }
1073
1097
  for (const [key, setting] of Object.entries(entry.rules)) {
1074
1098
  const isCategory = CATEGORIES.includes(key);
1075
- const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), { ...defaultConfig2, rules: config.rules ?? {} });
1099
+ const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), { ...defaultConfig4, rules: config.rules ?? {} });
1076
1100
  const skipRangeCheck = shouldSkipRangeCheck(rawOverrides, i, key, setting);
1077
1101
  validateSetting(path, `overrides[${i}].rules`, key, setting, !isCategory, baseline, skipRangeCheck);
1078
1102
  }
@@ -1188,7 +1212,7 @@ function writeSuppressions(cwd, results, config) {
1188
1212
  const seen = /* @__PURE__ */ new Set();
1189
1213
  const entries = [];
1190
1214
  for (const r of results) {
1191
- if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
1215
+ if (!isPenalized3(r.detection, config.treatDynamicAs)) continue;
1192
1216
  const entry = toEntry(r);
1193
1217
  const key = findingKey(entry);
1194
1218
  if (seen.has(key)) continue;
@@ -1200,20 +1224,23 @@ function writeSuppressions(cwd, results, config) {
1200
1224
  writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1201
1225
  return entries.length;
1202
1226
  }
1203
- function applySuppressions(results, entries, config) {
1227
+ function applySuppressions(results, entries, config, allResults) {
1204
1228
  const keys = new Set(entries.map((e) => findingKey(e)));
1205
- const usedKeys = /* @__PURE__ */ new Set();
1206
1229
  const kept = [];
1207
1230
  let suppressed = 0;
1208
1231
  for (const r of results) {
1209
1232
  const key = findingKey(r);
1210
- if (keys.has(key) && isPenalized(r.detection, config.treatDynamicAs)) {
1233
+ if (keys.has(key) && isPenalized3(r.detection, config.treatDynamicAs)) {
1211
1234
  suppressed++;
1212
- usedKeys.add(key);
1213
1235
  continue;
1214
1236
  }
1215
1237
  kept.push(r);
1216
1238
  }
1239
+ const usedKeys = /* @__PURE__ */ new Set();
1240
+ for (const r of allResults ?? results) {
1241
+ const key = findingKey(r);
1242
+ if (keys.has(key) && isPenalized3(r.detection, config.treatDynamicAs)) usedKeys.add(key);
1243
+ }
1217
1244
  const stale = [...keys].filter((k) => !usedKeys.has(k)).length;
1218
1245
  return { results: kept, suppressed, stale };
1219
1246
  }
@@ -1463,13 +1490,13 @@ function scoreAnimationEnabled(opts) {
1463
1490
  }
1464
1491
 
1465
1492
  // src/rule-selection.ts
1466
- import { allRules as allRules2 } from "@svelte-vitals/core";
1493
+ import { allRules } from "@svelte-vitals/core";
1467
1494
  function resolveRuleSelection(input) {
1468
1495
  const out = { ...input.rules ?? input.fileRules };
1469
1496
  const allow = input.allowRules ?? [];
1470
1497
  if (allow.length > 0) {
1471
1498
  const allowed = new Set(allow);
1472
- for (const rule of allRules2) if (!allowed.has(rule.id)) out[rule.id] = "off";
1499
+ for (const rule of allRules) if (!allowed.has(rule.id)) out[rule.id] = "off";
1473
1500
  for (const id of allowed) {
1474
1501
  const setting = out[id];
1475
1502
  if (setting === void 0) continue;
@@ -1491,6 +1518,28 @@ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
1491
1518
  function spinnerEnabled(opts) {
1492
1519
  return opts.reporter === "console" && opts.stderrIsTTY && !isAutoDetectedAgent(opts.rawReporter, opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stderrIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
1493
1520
  }
1521
+ function formatGlob(glob2) {
1522
+ return Array.isArray(glob2) ? `[${glob2.map((g) => `'${g}'`).join(", ")}]` : `'${glob2}'`;
1523
+ }
1524
+ function overridesOffWarnings(allowRules, overrides) {
1525
+ if (!allowRules || allowRules.length === 0 || !overrides || overrides.length === 0) return [];
1526
+ const warnings = [];
1527
+ for (const ruleId of allowRules) {
1528
+ const category = ruleId.split("/")[0] ?? ruleId;
1529
+ for (const entry of overrides) {
1530
+ const severity = settingSeverity(entry.rules[ruleId]) ?? settingSeverity(entry.rules[category]);
1531
+ if (severity !== "off") continue;
1532
+ const scope = [
1533
+ entry.route !== void 0 ? `route: ${formatGlob(entry.route)}` : void 0,
1534
+ entry.files !== void 0 ? `files: ${formatGlob(entry.files)}` : void 0
1535
+ ].filter((s) => s !== void 0).join(", ");
1536
+ warnings.push(
1537
+ `--rules '${ruleId}' is scoped 'off' by overrides entry { ${scope} } \u2014 findings there will not be reported. --rules overrides a global 'off' but not a scoped one.`
1538
+ );
1539
+ }
1540
+ }
1541
+ return warnings;
1542
+ }
1494
1543
  async function analyzeProject(opts = {}) {
1495
1544
  const cwd = opts.cwd ?? process.cwd();
1496
1545
  const rt = createNodeRuntime();
@@ -1511,12 +1560,16 @@ async function analyzeProject(opts = {}) {
1511
1560
  ...file?.overrides !== void 0 ? { overrides: file.overrides } : {}
1512
1561
  });
1513
1562
  await detectProject(rt, cwd);
1514
- const warnings = [...loaded?.warnings ?? [], ...await checkVersionFloor(rt, cwd)];
1563
+ const warnings = [
1564
+ ...loaded?.warnings ?? [],
1565
+ ...await checkVersionFloor(rt, cwd),
1566
+ ...overridesOffWarnings(opts.allowRules, config.overrides)
1567
+ ];
1515
1568
  const { heads, images, headings, project, components, kitModules, sourceFiles } = await collectAll(rt, cwd, config, {
1516
1569
  route: opts.route,
1517
1570
  parseCache: opts.parseCache
1518
1571
  });
1519
- const selected = selectRules(allRules3, config);
1572
+ const selected = selectRules(allRules2, config);
1520
1573
  const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
1521
1574
  const { results: rawResults, examined } = await runRules(rules, {
1522
1575
  heads,
@@ -1549,7 +1602,7 @@ async function applyScope(results, opts) {
1549
1602
  "svelte-vitals: could not determine changed files (not a git repo, git unavailable, or bad ref); analyzing all."
1550
1603
  );
1551
1604
  } else {
1552
- scoped = filterToChangedFiles(scoped, changed);
1605
+ scoped = filterToChangedFiles(scoped, changed, opts.config);
1553
1606
  }
1554
1607
  }
1555
1608
  if (opts.baseline !== void 0) {
@@ -1561,7 +1614,7 @@ async function applyScope(results, opts) {
1561
1614
  } else {
1562
1615
  try {
1563
1616
  const base = await analyzeProject({ ...opts.analyzeOpts, cwd: checkout.analyzeCwd });
1564
- scoped = filterToNewFindings(scoped, base.results);
1617
+ scoped = filterToNewFindings(scoped, base.results, opts.config);
1565
1618
  } catch {
1566
1619
  errorLog(`svelte-vitals: baseline analysis of '${opts.baseline}' failed; reporting all findings.`);
1567
1620
  } finally {
@@ -1572,11 +1625,16 @@ async function applyScope(results, opts) {
1572
1625
  if (!opts.noSuppressions && opts.config) {
1573
1626
  const entries = loadSuppressions(opts.cwd);
1574
1627
  if (entries !== void 0) {
1575
- const { results: afterSuppressions, suppressed, stale } = applySuppressions(scoped, entries, opts.config);
1628
+ const {
1629
+ results: afterSuppressions,
1630
+ suppressed,
1631
+ stale
1632
+ } = applySuppressions(scoped, entries, opts.config, results);
1576
1633
  scoped = afterSuppressions;
1577
- if (suppressed > 0 || stale > 0) {
1634
+ const routeScoped = opts.analyzeOpts?.route !== void 0;
1635
+ if (suppressed > 0 || stale > 0 && !routeScoped) {
1578
1636
  errorLog(
1579
- `svelte-vitals: ${suppressed} finding(s) suppressed by ${SUPPRESSIONS_FILE}` + (stale > 0 ? ` (${stale} stale entr${stale === 1 ? "y" : "ies"} \u2014 re-run --update-suppressions to prune)` : "") + "."
1637
+ `svelte-vitals: ${suppressed} finding(s) suppressed by ${SUPPRESSIONS_FILE}` + (stale > 0 && !routeScoped ? ` (${stale} stale entr${stale === 1 ? "y" : "ies"} \u2014 re-run --update-suppressions to prune)` : "") + "."
1580
1638
  );
1581
1639
  }
1582
1640
  }
@@ -1675,6 +1733,12 @@ async function run(opts = {}) {
1675
1733
  try {
1676
1734
  const { config, version } = analysis;
1677
1735
  if (opts.updateSuppressions) {
1736
+ if (opts.route !== void 0) {
1737
+ errorLog(
1738
+ `svelte-vitals: --update-suppressions cannot be combined with --route ('${opts.route}') \u2014 it would prune every suppression entry outside that route from ${SUPPRESSIONS_FILE}. Re-run --update-suppressions without --route.`
1739
+ );
1740
+ return 2;
1741
+ }
1678
1742
  const count = writeSuppressions(cwd, analysis.results, config);
1679
1743
  errorLog(`svelte-vitals: wrote ${count} suppression(s) to ${SUPPRESSIONS_FILE}.`);
1680
1744
  return 0;
@@ -1782,9 +1846,6 @@ export {
1782
1846
  readPackageVersion,
1783
1847
  readCoreVersion,
1784
1848
  isReporterName,
1785
- findUnknownRuleIds,
1786
- knownRuleIds,
1787
- ruleOptionsSpec,
1788
1849
  CONFIG_FILENAMES,
1789
1850
  loadConfigFile,
1790
1851
  spinnerEnabled,
@@ -0,0 +1,19 @@
1
+ // src/rules-config.ts
2
+ import { allRules } from "@svelte-vitals/core";
3
+ var KNOWN_IDS = new Set(allRules.map((r) => r.id));
4
+ var RULE_BY_ID = new Map(allRules.map((r) => [r.id, r]));
5
+ function findUnknownRuleIds(ids) {
6
+ return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
7
+ }
8
+ function knownRuleIds() {
9
+ return [...KNOWN_IDS].sort();
10
+ }
11
+ function ruleOptionsSpec(id) {
12
+ return RULE_BY_ID.get(id)?.options;
13
+ }
14
+
15
+ export {
16
+ findUnknownRuleIds,
17
+ knownRuleIds,
18
+ ruleOptionsSpec
19
+ };
@@ -2,6 +2,12 @@ import {
2
2
  consoleIO,
3
3
  parseCliArgs
4
4
  } from "./chunk-5Q2PT47V.js";
5
+ import {
6
+ knownRuleIds
7
+ } from "./chunk-TFBLQUAC.js";
8
+
9
+ // src/docs/cli.ts
10
+ import { docsUrlFor } from "@svelte-vitals/core";
5
11
 
6
12
  // src/docs/generated.ts
7
13
  var EMBEDDED_DOCS = [
@@ -100,9 +106,16 @@ function runDocsCli(args, io = consoleIO) {
100
106
  io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
101
107
  return 2;
102
108
  }
103
- const doc = EMBEDDED_DOCS.find((d) => d.name === rest[0]);
109
+ const name = rest[0];
110
+ const doc = EMBEDDED_DOCS.find((d) => d.name === name);
104
111
  if (!doc) {
105
- io.errorLog(`svelte-vitals: unknown docs topic '${rest[0]}'.`);
112
+ const ruleId = name.startsWith("rules/") ? name.slice("rules/".length) : name;
113
+ if (knownRuleIds().includes(ruleId)) {
114
+ io.errorLog(`svelte-vitals: '${ruleId}' is a rule, not a docs topic.`);
115
+ io.errorLog(`svelte-vitals: rule detail: \`svelte-vitals explain ${ruleId}\`; web: ${docsUrlFor(ruleId)}`);
116
+ return 2;
117
+ }
118
+ io.errorLog(`svelte-vitals: unknown docs topic '${name}'.`);
106
119
  io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
107
120
  return 2;
108
121
  }
package/dist/index.d.ts CHANGED
@@ -268,6 +268,12 @@ interface ApplyScopeOptions {
268
268
  /** Disable applying svelte-vitals-suppressions.json for this run. */
269
269
  noSuppressions?: boolean;
270
270
  errorLog?: (line: string) => void;
271
+ /**
272
+ * Also doubles as this call's route-scoping signal: `analyzeOpts.route !== undefined`
273
+ * tells the suppressions block that `results` (this function's first argument) was
274
+ * itself collected route-scoped, not just project-wide-then-narrowed — see the comment
275
+ * at that check.
276
+ */
271
277
  analyzeOpts?: AnalyzeOptions;
272
278
  }
273
279
  /**
package/dist/index.js CHANGED
@@ -3,14 +3,16 @@ import {
3
3
  analyzeProject,
4
4
  applyScope,
5
5
  defineConfig,
6
- findUnknownRuleIds,
7
- knownRuleIds,
8
6
  loadConfigFile,
9
7
  routeMatcher,
10
- ruleOptionsSpec,
11
8
  run,
12
9
  spinnerEnabled
13
- } from "./chunk-WD5TSADH.js";
10
+ } from "./chunk-EWDGS52A.js";
11
+ import {
12
+ findUnknownRuleIds,
13
+ knownRuleIds,
14
+ ruleOptionsSpec
15
+ } from "./chunk-TFBLQUAC.js";
14
16
  export {
15
17
  ProjectError,
16
18
  analyzeProject,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.44.0",
4
- "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
3
+ "version": "0.44.2",
4
+ "description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture) — not a runtime Web Vitals reporter.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Kazuma Oe (https://github.com/oekazuma)",
@@ -11,7 +11,11 @@
11
11
  "seo",
12
12
  "meta-tags",
13
13
  "cli",
14
- "svelte-vitals"
14
+ "svelte-vitals",
15
+ "performance",
16
+ "security",
17
+ "code-quality",
18
+ "static-analysis"
15
19
  ],
16
20
  "repository": {
17
21
  "type": "git",
@@ -44,7 +48,7 @@
44
48
  "magicast": "^0.5.4",
45
49
  "svelte": "^5.56.8",
46
50
  "tinyglobby": "^0.2.17",
47
- "@svelte-vitals/core": "0.38.0"
51
+ "@svelte-vitals/core": "0.40.0"
48
52
  },
49
53
  "devDependencies": {
50
54
  "@types/estree": "^1.0.9",