svelte-vitals 0.44.2 → 0.44.4

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
@@ -8,7 +8,7 @@ import {
8
8
  readPackageVersion,
9
9
  readPkg,
10
10
  run
11
- } from "./chunk-EWDGS52A.js";
11
+ } from "./chunk-YY567AYV.js";
12
12
  import {
13
13
  consoleIO,
14
14
  parseCliArgs,
@@ -1429,7 +1429,9 @@ function realIO() {
1429
1429
  writeFileSync(path, content);
1430
1430
  },
1431
1431
  cwd: process.cwd(),
1432
- 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),
1433
1435
  nodeVersion: process.version,
1434
1436
  log: (line) => console.log(line),
1435
1437
  errorLog: (line) => console.error(line),
@@ -1484,15 +1486,15 @@ ${planText}` });
1484
1486
  }
1485
1487
  };
1486
1488
  }
1487
- async function runInstallCli(args) {
1489
+ async function runInstallCli(args, io = consoleIO) {
1488
1490
  const argv = parseInstallArgs(args);
1489
1491
  if (argv.help) {
1490
- console.log(INSTALL_HELP);
1492
+ io.log(INSTALL_HELP);
1491
1493
  return 0;
1492
1494
  }
1493
1495
  const { flags, warnings, errors } = resolveInstallArgs(argv);
1494
- for (const w of warnings) console.error(w);
1495
- for (const e of errors) console.error(e);
1496
+ for (const w of warnings) io.errorLog(w);
1497
+ for (const e of errors) io.errorLog(e);
1496
1498
  if (!flags) return 2;
1497
1499
  return runInstall(flags, realIO(), clackPrompts(), readPackageVersion());
1498
1500
  }
@@ -1720,7 +1722,7 @@ function runExplainCli(args, io = consoleIO) {
1720
1722
  return 0;
1721
1723
  }
1722
1724
 
1723
- // src/bin.ts
1725
+ // src/cli.ts
1724
1726
  var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
1725
1727
 
1726
1728
  Usage:
@@ -1784,45 +1786,53 @@ var VERSION = readPackageVersion();
1784
1786
  function selectApp(apps) {
1785
1787
  return selectAppPrompt(apps, "Multiple SvelteKit apps found \u2014 which one should svelte-vitals analyze?");
1786
1788
  }
1787
- async function main() {
1788
- const rawArgs = process.argv.slice(2);
1789
- if (rawArgs[0] === "docs") {
1790
- const { runDocsCli } = await import("./cli-L2HHOZDN.js");
1791
- process.exitCode = runDocsCli(rawArgs.slice(1));
1792
- return;
1793
- }
1794
- if (rawArgs[0] === "explain") {
1795
- process.exitCode = runExplainCli(rawArgs.slice(1));
1796
- return;
1797
- }
1798
- if (rawArgs[0] === "install") {
1799
- const code2 = await runInstallCli(rawArgs.slice(1));
1800
- process.exit(code2);
1801
- }
1802
- if (rawArgs[0] === "ci") {
1803
- const code2 = await runCiCli(rawArgs.slice(1));
1804
- process.exit(code2);
1805
- }
1806
- const argv = parseRunArgs(rawArgs);
1807
- if (argv.help) {
1808
- console.log(HELP);
1809
- return;
1810
- }
1811
- if (argv.version) {
1812
- console.log(`${VERSION} (core ${readCoreVersion()})`);
1813
- console.error("svelte-vitals: run `svelte-vitals docs list` for the bundled guides.");
1814
- return;
1815
- }
1816
- const { options, warnings, errors, minHealth } = resolveArgs(argv);
1817
- for (const w of warnings) console.error(w);
1818
- for (const e of errors) console.error(e);
1819
- if (!options) process.exit(2);
1789
+ async function runCli(argv, io = consoleIO) {
1790
+ if (argv[0] === "docs") {
1791
+ const { runDocsCli } = await import("./cli-KH6FL5V7.js");
1792
+ return { code: runDocsCli(argv.slice(1), io), exit: "natural" };
1793
+ }
1794
+ if (argv[0] === "explain") {
1795
+ return { code: runExplainCli(argv.slice(1), io), exit: "natural" };
1796
+ }
1797
+ if (argv[0] === "install") {
1798
+ return { code: await runInstallCli(argv.slice(1), io), exit: "immediate" };
1799
+ }
1800
+ if (argv[0] === "ci") {
1801
+ const code2 = await runCiCli(argv.slice(1), { ...realIO(), log: io.log, errorLog: io.errorLog });
1802
+ return { code: code2, exit: "immediate" };
1803
+ }
1804
+ const parsed = parseRunArgs(argv);
1805
+ if (parsed.help) {
1806
+ io.log(HELP);
1807
+ return { code: 0, exit: "natural" };
1808
+ }
1809
+ if (parsed.version) {
1810
+ io.log(`${VERSION} (core ${readCoreVersion()})`);
1811
+ io.errorLog("svelte-vitals: run `svelte-vitals docs list` for the bundled guides.");
1812
+ return { code: 0, exit: "natural" };
1813
+ }
1814
+ const { options, warnings, errors, minHealth } = resolveArgs(parsed);
1815
+ for (const w of warnings) io.errorLog(w);
1816
+ for (const e of errors) io.errorLog(e);
1817
+ if (!options) return { code: 2, exit: "immediate" };
1820
1818
  const code = await run({
1821
1819
  ...options,
1822
1820
  minHealth,
1823
- selectApp
1821
+ selectApp,
1822
+ log: io.log,
1823
+ errorLog: io.errorLog
1824
1824
  });
1825
1825
  await new Promise((resolve) => process.stdout.write("", resolve));
1826
- process.exit(code);
1826
+ return { code, exit: "immediate" };
1827
+ }
1828
+
1829
+ // src/bin.ts
1830
+ async function main() {
1831
+ const { code, exit } = await runCli(process.argv.slice(2));
1832
+ if (exit === "immediate") {
1833
+ process.exit(code);
1834
+ } else {
1835
+ process.exitCode = code;
1836
+ }
1827
1837
  }
1828
1838
  void main();
@@ -723,6 +723,7 @@ function chainFiles(pageRel, layouts) {
723
723
  async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
724
724
  const files = chainFiles(pageRel, layouts);
725
725
  const composed = /* @__PURE__ */ new Map();
726
+ const jsonldTags = [];
726
727
  let broadOwn = false;
727
728
  let broadInherited = false;
728
729
  const images = [];
@@ -738,7 +739,9 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
738
739
  }
739
740
  const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]), cache);
740
741
  for (const tag of resolved.tags) {
741
- composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
742
+ const stamped = { ...tag, presence: isPage ? "own" : "inherited", file: rel };
743
+ if (tag.kind === "jsonld") jsonldTags.push(stamped);
744
+ else composed.set(tagKey(tag), stamped);
742
745
  }
743
746
  if (resolved.broad) {
744
747
  if (isPage) broadOwn = true;
@@ -755,7 +758,7 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
755
758
  }
756
759
  const route = deriveRoute(pageRel);
757
760
  return {
758
- head: { route, source: "static", tags: [...composed.values()], file: pageRel },
761
+ head: { route, source: "static", tags: [...composed.values(), ...jsonldTags], file: pageRel },
759
762
  images: { route, images },
760
763
  headings: { route, headings, componentHeadings }
761
764
  };
@@ -773,7 +776,7 @@ async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE
773
776
  // src/route-matcher.ts
774
777
  function routeMatcher(glob2) {
775
778
  if (!glob2) return () => true;
776
- const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
779
+ 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, ".*");
777
780
  const re = new RegExp(`^${body}$`);
778
781
  return (route) => re.test(route.replace(/^\//, ""));
779
782
  }
@@ -781,14 +784,23 @@ function routeMatcher(glob2) {
781
784
  // src/collect-all.ts
782
785
  async function collectAll(rt, cwd, config, opts = {}) {
783
786
  const matches = routeMatcher(opts.route);
784
- const collected = await collectRoutes(rt, cwd, config, opts.parseCache);
787
+ const project = await collectProjectFacts(rt, cwd);
788
+ const [collected, components, kitModules, sourceFiles] = await Promise.all([
789
+ collectRoutes(rt, cwd, config, opts.parseCache),
790
+ // Component (Correctness) facts are file-scoped with no route attribution yet, so a
791
+ // route-filtered run skips them rather than reporting unrelated components (#68 review);
792
+ // kitModules is skipped for the same reason.
793
+ opts.route ? [] : collectComponentFacts(rt, cwd),
794
+ opts.route ? [] : collectKitModuleFacts(rt, cwd, project.kitAliases),
795
+ // Unlike its two neighbours above, the --route branch gets `undefined` here, not `[]`: an empty
796
+ // inventory would tell architecture/unit-entry-file that the declared unit directories truly do
797
+ // not exist, so it would report every declaration as inert, whereas `undefined` means the mode
798
+ // never collected the fact at all, and the rule stays silent instead of raising a false alarm.
799
+ opts.route ? void 0 : collectSourceFiles(rt, cwd)
800
+ ]);
785
801
  const heads = collected.heads.filter((h) => matches(h.route));
786
802
  const images = collected.images.filter((i) => matches(i.route));
787
803
  const headings = collected.headings.filter((h) => matches(h.route));
788
- const project = await collectProjectFacts(rt, cwd);
789
- const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
790
- const kitModules = opts.route ? [] : await collectKitModuleFacts(rt, cwd, project.kitAliases);
791
- const sourceFiles = opts.route ? void 0 : await collectSourceFiles(rt, cwd);
792
804
  return { heads, images, headings, project, components, kitModules, sourceFiles };
793
805
  }
794
806
 
@@ -978,7 +990,7 @@ function filterToNewFindings(results, baselineResults, config = defaultConfig3)
978
990
  }
979
991
 
980
992
  // src/suppressions.ts
981
- import { readFileSync as readFileSync2, writeFileSync } from "fs";
993
+ import { readFileSync as readFileSync2, renameSync, unlinkSync, writeFileSync } from "fs";
982
994
  import { join as join6 } from "path";
983
995
  import { isPenalized as isPenalized3 } from "@svelte-vitals/core";
984
996
 
@@ -1221,7 +1233,17 @@ function writeSuppressions(cwd, results, config) {
1221
1233
  }
1222
1234
  entries.sort(compareEntries);
1223
1235
  const path = join6(cwd, SUPPRESSIONS_FILE);
1224
- writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1236
+ const tmpPath = `${path}.tmp`;
1237
+ try {
1238
+ writeFileSync(tmpPath, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1239
+ renameSync(tmpPath, path);
1240
+ } catch (err) {
1241
+ try {
1242
+ unlinkSync(tmpPath);
1243
+ } catch {
1244
+ }
1245
+ throw err;
1246
+ }
1225
1247
  return entries.length;
1226
1248
  }
1227
1249
  function applySuppressions(results, entries, config, allResults) {
@@ -1540,6 +1562,16 @@ function overridesOffWarnings(allowRules, overrides) {
1540
1562
  }
1541
1563
  return warnings;
1542
1564
  }
1565
+ function skippedFileWarnings(facts) {
1566
+ const files = [...new Set(facts.filter((f) => f.parseFailed).map((f) => f.file))].sort();
1567
+ if (files.length === 0) return [];
1568
+ const shown = files.slice(0, 10);
1569
+ const list = files.length > shown.length ? `${shown.join(", ")}, \u2026 and ${files.length - shown.length} more` : shown.join(", ");
1570
+ return [
1571
+ `skipped ${files.length} file(s) that could not be parsed: ${list}`,
1572
+ "findings for these files are unavailable until they parse."
1573
+ ];
1574
+ }
1543
1575
  async function analyzeProject(opts = {}) {
1544
1576
  const cwd = opts.cwd ?? process.cwd();
1545
1577
  const rt = createNodeRuntime();
@@ -1588,7 +1620,7 @@ async function analyzeProject(opts = {}) {
1588
1620
  version: readPackageVersion(),
1589
1621
  ruleIds: rules.map((r) => r.id),
1590
1622
  examined,
1591
- warnings,
1623
+ warnings: [...warnings, ...skippedFileWarnings([...components, ...kitModules])],
1592
1624
  loadedConfig: loaded
1593
1625
  };
1594
1626
  }
@@ -39,7 +39,7 @@ var EMBEDDED_DOCS = [
39
39
  name: "scoping",
40
40
  title: "Scoping findings to a change",
41
41
  description: "Use --diff, --staged, --baseline and the suppressions file so only what a change introduced is reported, instead of a legacy backlog.",
42
- body: "# Scoping findings to a change\n\nAn existing project usually has a backlog nobody is about to fix. Scope the report rather than\ndisabling rules.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\nReports only findings **not already present** at `ref`. Scopes by finding identity rather than by\nfile, so a pre-existing problem in a file you touched does not fail the gate. No default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nIt checks `ref` out into a temporary worktree and subtracts those findings. On failure (no git,\nbad ref) it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\nThis analyzes the whole project (`--diff`/`--staged`/`--baseline` are ignored), writes every\npenalized finding, and exits `0` without a report.\n\nThe file then applies automatically on every run, after `--diff`/`--staged` and `--baseline`.\nFixing an accepted finding leaves a **stale** entry, reported on stderr but never failing the run.\n`--no-suppressions` ignores the file for one run.\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
42
+ body: "# Scoping findings to a change\n\nAn existing project usually has a backlog nobody is about to fix. Scope the report rather than\ndisabling rules.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\nReports only findings **not already present** at `ref`. Scopes by finding identity rather than by\nfile, so a pre-existing problem in a file you touched does not fail the gate. No default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nIt checks `ref` out into a temporary worktree and subtracts those findings. On failure (no git,\nbad ref) it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\nThis analyzes the whole project (`--diff`/`--staged`/`--baseline` are ignored), writes every\npenalized finding, and exits `0` without a report.\n\nThe file then applies automatically on every run, after `--diff`/`--staged` and `--baseline`.\nFixing an accepted finding leaves a **stale** entry, reported on stderr but never failing the run.\n`--no-suppressions` ignores the file for one run.\n\nAn entry covers whatever its rule reports at that route and location, not just the message\nrecorded when written \u2014 a different finding from the same rule at the same spot still matches\nand stays suppressed (and not stale).\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
43
43
  }
44
44
  ];
45
45
 
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
@@ -7,7 +7,7 @@ import {
7
7
  routeMatcher,
8
8
  run,
9
9
  spinnerEnabled
10
- } from "./chunk-EWDGS52A.js";
10
+ } from "./chunk-YY567AYV.js";
11
11
  import {
12
12
  findUnknownRuleIds,
13
13
  knownRuleIds,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.44.2",
3
+ "version": "0.44.4",
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.40.0"
51
+ "@svelte-vitals/core": "0.41.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/estree": "^1.0.9",