svelte-vitals 0.44.1 → 0.44.3

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-4MC7STFI.js";
11
+ } from "./chunk-NIUYLAJK.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"];
@@ -1427,7 +1429,9 @@ function realIO() {
1427
1429
  writeFileSync(path, content);
1428
1430
  },
1429
1431
  cwd: process.cwd(),
1430
- isTTY: Boolean(process.stdout.isTTY),
1432
+ // clack reads from stdin and renders to stdout, so both must be interactive —
1433
+ // a piped/redirected stdin would leave the prompt hanging for input that never comes.
1434
+ isTTY: Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY),
1431
1435
  nodeVersion: process.version,
1432
1436
  log: (line) => console.log(line),
1433
1437
  errorLog: (line) => console.error(line),
@@ -1785,7 +1789,7 @@ function selectApp(apps) {
1785
1789
  async function main() {
1786
1790
  const rawArgs = process.argv.slice(2);
1787
1791
  if (rawArgs[0] === "docs") {
1788
- const { runDocsCli } = await import("./cli-LGD6NDAO.js");
1792
+ const { runDocsCli } = await import("./cli-L2HHOZDN.js");
1789
1793
  process.exitCode = runDocsCli(rawArgs.slice(1));
1790
1794
  return;
1791
1795
  }
@@ -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,
@@ -392,6 +398,29 @@ function collectSvelteHeads(node, acc) {
392
398
  if (key in node) collectSvelteHeads(childOf(node, key), acc);
393
399
  }
394
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
+ }
395
424
  function tagsFromHead(head) {
396
425
  const tags = [];
397
426
  const children = head.fragment.nodes;
@@ -451,7 +480,7 @@ function tagsFromHead(head) {
451
480
  } else {
452
481
  const src = attrText(attributes, "src");
453
482
  if (src) {
454
- 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;
455
484
  tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
456
485
  }
457
486
  }
@@ -593,6 +622,7 @@ function resolveComponentPath(source, fromFileRel) {
593
622
  }
594
623
  async function resolveFileTags(rt, cwd, fileRel, parsed, config, depth, visited, cache = /* @__PURE__ */ new Map()) {
595
624
  const tags = [...parsed.headTags];
625
+ const headings = [];
596
626
  let broad = false;
597
627
  for (const use of parsed.components) {
598
628
  const info = parsed.imports.get(use.name);
@@ -616,10 +646,12 @@ async function resolveFileTags(rt, cwd, fileRel, parsed, config, depth, visited,
616
646
  const child = await resolveFileTags(rt, cwd, childRel, childParsed, config, depth - 1, childVisited, cache);
617
647
  tags.push(...child.tags);
618
648
  broad = broad || child.broad;
649
+ for (const h of childParsed.headings) headings.push({ ...h, file: childRel });
650
+ headings.push(...child.headings);
619
651
  }
620
652
  }
621
653
  }
622
- return { tags, broad };
654
+ return { tags, broad, headings };
623
655
  }
624
656
 
625
657
  // src/providers/source/routes.ts
@@ -695,6 +727,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
695
727
  let broadInherited = false;
696
728
  const images = [];
697
729
  const headings = [];
730
+ const componentHeadings = [];
698
731
  for (const { rel, isPage } of files) {
699
732
  const parsed = await readAndParse(rt, cwd, rel, cache);
700
733
  for (const img of parsed.images) {
@@ -711,6 +744,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
711
744
  if (isPage) broadOwn = true;
712
745
  else broadInherited = true;
713
746
  }
747
+ componentHeadings.push(...resolved.headings);
714
748
  }
715
749
  if (broadOwn || broadInherited) {
716
750
  const presence = broadOwn ? "own" : "inherited";
@@ -723,7 +757,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
723
757
  return {
724
758
  head: { route, source: "static", tags: [...composed.values()], file: pageRel },
725
759
  images: { route, images },
726
- headings: { route, headings }
760
+ headings: { route, headings, componentHeadings }
727
761
  };
728
762
  }
729
763
  async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map()) {
@@ -739,7 +773,7 @@ async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE
739
773
  // src/route-matcher.ts
740
774
  function routeMatcher(glob2) {
741
775
  if (!glob2) return () => true;
742
- const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
776
+ const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(/\/\0$/g, "(?:/.*)?").replace(/^\0\//g, "(?:.*/)?").replace(/\0\//g, "(?:.*/)?").replace(/\/\0/g, "(?:/.*)?").replace(/\0/g, ".*");
743
777
  const re = new RegExp(`^${body}$`);
744
778
  return (route) => re.test(route.replace(/^\//, ""));
745
779
  }
@@ -747,14 +781,23 @@ function routeMatcher(glob2) {
747
781
  // src/collect-all.ts
748
782
  async function collectAll(rt, cwd, config, opts = {}) {
749
783
  const matches = routeMatcher(opts.route);
750
- const collected = await collectRoutes(rt, cwd, config, opts.parseCache);
784
+ const project = await collectProjectFacts(rt, cwd);
785
+ const [collected, components, kitModules, sourceFiles] = await Promise.all([
786
+ collectRoutes(rt, cwd, config, opts.parseCache),
787
+ // Component (Correctness) facts are file-scoped with no route attribution yet, so a
788
+ // route-filtered run skips them rather than reporting unrelated components (#68 review);
789
+ // kitModules is skipped for the same reason.
790
+ opts.route ? [] : collectComponentFacts(rt, cwd),
791
+ opts.route ? [] : collectKitModuleFacts(rt, cwd, project.kitAliases),
792
+ // Unlike its two neighbours above, the --route branch gets `undefined` here, not `[]`: an empty
793
+ // inventory would tell architecture/unit-entry-file that the declared unit directories truly do
794
+ // not exist, so it would report every declaration as inert, whereas `undefined` means the mode
795
+ // never collected the fact at all, and the rule stays silent instead of raising a false alarm.
796
+ opts.route ? void 0 : collectSourceFiles(rt, cwd)
797
+ ]);
751
798
  const heads = collected.heads.filter((h) => matches(h.route));
752
799
  const images = collected.images.filter((i) => matches(i.route));
753
800
  const headings = collected.headings.filter((h) => matches(h.route));
754
- const project = await collectProjectFacts(rt, cwd);
755
- const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
756
- const kitModules = opts.route ? [] : await collectKitModuleFacts(rt, cwd, project.kitAliases);
757
- const sourceFiles = opts.route ? void 0 : await collectSourceFiles(rt, cwd);
758
801
  return { heads, images, headings, project, components, kitModules, sourceFiles };
759
802
  }
760
803
 
@@ -944,7 +987,7 @@ function filterToNewFindings(results, baselineResults, config = defaultConfig3)
944
987
  }
945
988
 
946
989
  // src/suppressions.ts
947
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
990
+ import { readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
948
991
  import { join as join6 } from "path";
949
992
  import { isPenalized as isPenalized3 } from "@svelte-vitals/core";
950
993
 
@@ -959,22 +1002,6 @@ import {
959
1002
  shouldSkipRangeCheck,
960
1003
  validateRuleSetting
961
1004
  } from "@svelte-vitals/core";
962
-
963
- // src/rules-config.ts
964
- import { allRules } from "@svelte-vitals/core";
965
- var KNOWN_IDS = new Set(allRules.map((r) => r.id));
966
- var RULE_BY_ID = new Map(allRules.map((r) => [r.id, r]));
967
- function findUnknownRuleIds(ids) {
968
- return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
969
- }
970
- function knownRuleIds() {
971
- return [...KNOWN_IDS].sort();
972
- }
973
- function ruleOptionsSpec(id) {
974
- return RULE_BY_ID.get(id)?.options;
975
- }
976
-
977
- // src/config-file.ts
978
1005
  var CONFIG_FILENAMES = ["svelte-vitals.config.mjs", "svelte-vitals.config.js", "svelte-vitals.config.ts"];
979
1006
  var TREAT_DYNAMIC_AS_VALUES = ["pass", "warn", "fail"];
980
1007
  var FAIL_ON_VALUES = ["critical", "warning", "info"];
@@ -1203,7 +1230,17 @@ function writeSuppressions(cwd, results, config) {
1203
1230
  }
1204
1231
  entries.sort(compareEntries);
1205
1232
  const path = join6(cwd, SUPPRESSIONS_FILE);
1206
- writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1233
+ const tmpPath = `${path}.tmp`;
1234
+ try {
1235
+ writeFileSync(tmpPath, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1236
+ renameSync(tmpPath, path);
1237
+ } catch (err) {
1238
+ try {
1239
+ unlinkSync(tmpPath);
1240
+ } catch {
1241
+ }
1242
+ throw err;
1243
+ }
1207
1244
  return entries.length;
1208
1245
  }
1209
1246
  function applySuppressions(results, entries, config, allResults) {
@@ -1472,13 +1509,13 @@ function scoreAnimationEnabled(opts) {
1472
1509
  }
1473
1510
 
1474
1511
  // src/rule-selection.ts
1475
- import { allRules as allRules2 } from "@svelte-vitals/core";
1512
+ import { allRules } from "@svelte-vitals/core";
1476
1513
  function resolveRuleSelection(input) {
1477
1514
  const out = { ...input.rules ?? input.fileRules };
1478
1515
  const allow = input.allowRules ?? [];
1479
1516
  if (allow.length > 0) {
1480
1517
  const allowed = new Set(allow);
1481
- for (const rule of allRules2) if (!allowed.has(rule.id)) out[rule.id] = "off";
1518
+ for (const rule of allRules) if (!allowed.has(rule.id)) out[rule.id] = "off";
1482
1519
  for (const id of allowed) {
1483
1520
  const setting = out[id];
1484
1521
  if (setting === void 0) continue;
@@ -1522,6 +1559,16 @@ function overridesOffWarnings(allowRules, overrides) {
1522
1559
  }
1523
1560
  return warnings;
1524
1561
  }
1562
+ function skippedFileWarnings(facts) {
1563
+ const files = [...new Set(facts.filter((f) => f.parseFailed).map((f) => f.file))].sort();
1564
+ if (files.length === 0) return [];
1565
+ const shown = files.slice(0, 10);
1566
+ const list = files.length > shown.length ? `${shown.join(", ")}, \u2026 and ${files.length - shown.length} more` : shown.join(", ");
1567
+ return [
1568
+ `skipped ${files.length} file(s) that could not be parsed: ${list}`,
1569
+ "findings for these files are unavailable until they parse."
1570
+ ];
1571
+ }
1525
1572
  async function analyzeProject(opts = {}) {
1526
1573
  const cwd = opts.cwd ?? process.cwd();
1527
1574
  const rt = createNodeRuntime();
@@ -1551,7 +1598,7 @@ async function analyzeProject(opts = {}) {
1551
1598
  route: opts.route,
1552
1599
  parseCache: opts.parseCache
1553
1600
  });
1554
- const selected = selectRules(allRules3, config);
1601
+ const selected = selectRules(allRules2, config);
1555
1602
  const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
1556
1603
  const { results: rawResults, examined } = await runRules(rules, {
1557
1604
  heads,
@@ -1570,7 +1617,7 @@ async function analyzeProject(opts = {}) {
1570
1617
  version: readPackageVersion(),
1571
1618
  ruleIds: rules.map((r) => r.id),
1572
1619
  examined,
1573
- warnings,
1620
+ warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules])],
1574
1621
  loadedConfig: loaded
1575
1622
  };
1576
1623
  }
@@ -1828,9 +1875,6 @@ export {
1828
1875
  readPackageVersion,
1829
1876
  readCoreVersion,
1830
1877
  isReporterName,
1831
- findUnknownRuleIds,
1832
- knownRuleIds,
1833
- ruleOptionsSpec,
1834
1878
  CONFIG_FILENAMES,
1835
1879
  loadConfigFile,
1836
1880
  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
@@ -231,7 +231,7 @@ interface AnalyzeResult {
231
231
  ruleIds: string[];
232
232
  /** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
233
233
  examined: Record<string, Record<string, number>>;
234
- /** Non-fatal config-file issues (unknown top-level keys, invalid enum values). Empty when no config file or none found. */
234
+ /** Non-fatal issues surfaced during analysis: config-file problems (unknown top-level keys, invalid enum values), version-floor notices, `--rules`/overrides conflicts, and skipped-file notices. Empty when none apply. */
235
235
  warnings: string[];
236
236
  /**
237
237
  * This analysis's config-file load result (`undefined` when no config file exists at its
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-4MC7STFI.js";
10
+ } from "./chunk-NIUYLAJK.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,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.44.1",
3
+ "version": "0.44.3",
4
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",
@@ -48,7 +48,7 @@
48
48
  "magicast": "^0.5.4",
49
49
  "svelte": "^5.56.8",
50
50
  "tinyglobby": "^0.2.17",
51
- "@svelte-vitals/core": "0.39.0"
51
+ "@svelte-vitals/core": "0.40.1"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/estree": "^1.0.9",