svelte-vitals 0.44.0 → 0.44.1

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
@@ -10,7 +10,7 @@ import {
10
10
  readPackageVersion,
11
11
  readPkg,
12
12
  run
13
- } from "./chunk-WD5TSADH.js";
13
+ } from "./chunk-4MC7STFI.js";
14
14
  import {
15
15
  consoleIO,
16
16
  parseCliArgs,
@@ -132,6 +132,7 @@ function resolveArgs(argv) {
132
132
  const errors = [];
133
133
  for (const flag of VALUE_FLAGS) {
134
134
  const v = argv[flag];
135
+ if (flag === "out-file" && v === "-") continue;
135
136
  if (v !== void 0 && (typeof v !== "string" || v.trim() === "" || v.startsWith("-"))) {
136
137
  errors.push(`svelte-vitals: --${flag} requires a value.`);
137
138
  }
@@ -193,6 +194,15 @@ function resolveArgs(argv) {
193
194
  const failOn = failOnValid ? failOnRaw : void 0;
194
195
  const weights = parseWeights(argv.weights, errors);
195
196
  const categories = parseCategories(argv.category, errors);
197
+ if (categories !== void 0 && categories.length > 0 && allow.length > 0) {
198
+ const excluded = allow.filter((id) => !unknown.includes(id)).filter((id) => !categories.includes(id.split("/")[0]));
199
+ if (excluded.length > 0) {
200
+ errors.push(
201
+ `svelte-vitals: --rules id(s) excluded by --category ${categories.join(", ")}: ${excluded.join(", ")}`
202
+ );
203
+ errors.push("Add the rule's category to --category, or drop the rule from --rules.");
204
+ }
205
+ }
196
206
  const score = Boolean(argv.score);
197
207
  if (score && typeof argv.reporter === "string") {
198
208
  warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
@@ -395,7 +405,7 @@ ${lines}`;
395
405
  }).join("\n\n");
396
406
  }
397
407
  function sharedBody(version) {
398
- return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
408
+ return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run \`svelte-vitals install --refresh\` to regenerate. -->
399
409
 
400
410
  # svelte-vitals
401
411
 
@@ -442,7 +452,7 @@ name: improve-svelte
442
452
  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
453
  ---
444
454
 
445
- <!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
455
+ <!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run \`svelte-vitals install --refresh\` to regenerate. -->
446
456
 
447
457
  # improve-svelte
448
458
 
@@ -17,7 +17,8 @@ import {
17
17
  defineConfig,
18
18
  selectRules,
19
19
  applyRuleSeverities,
20
- applyOverrides
20
+ applyOverrides,
21
+ settingSeverity
21
22
  } from "@svelte-vitals/core";
22
23
 
23
24
  // src/runtime/node.ts
@@ -863,6 +864,7 @@ function isAutoDetectedGithub(explicit, env = process.env) {
863
864
 
864
865
  // src/changed-files.ts
865
866
  import { execFileSync } from "child_process";
867
+ import { defaultConfig as defaultConfig2, isPenalized } from "@svelte-vitals/core";
866
868
  function git(args, cwd) {
867
869
  return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\0");
868
870
  }
@@ -881,8 +883,10 @@ function getChangedFiles(cwd, opts) {
881
883
  return void 0;
882
884
  }
883
885
  }
884
- function filterToChangedFiles(results, changed) {
885
- return results.filter((r) => r.location !== void 0 && changed.has(r.location));
886
+ function filterToChangedFiles(results, changed, config = defaultConfig2) {
887
+ return results.filter(
888
+ (r) => r.location !== void 0 && changed.has(r.location) && (isPenalized(r.detection, config.treatDynamicAs) || r.route === void 0)
889
+ );
886
890
  }
887
891
 
888
892
  // src/baseline.ts
@@ -890,6 +894,7 @@ import { execFileSync as execFileSync2 } from "child_process";
890
894
  import { mkdtempSync, rmSync } from "fs";
891
895
  import { tmpdir } from "os";
892
896
  import { join as join4 } from "path";
897
+ import { defaultConfig as defaultConfig3, isPenalized as isPenalized2 } from "@svelte-vitals/core";
893
898
  function git2(args, cwd) {
894
899
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
895
900
  }
@@ -932,15 +937,16 @@ function checkoutBaseline(cwd, ref) {
932
937
  return void 0;
933
938
  }
934
939
  }
935
- function filterToNewFindings(results, baselineResults) {
936
- const baselineKeys = new Set(baselineResults.map(findingKey));
937
- return results.filter((r) => !baselineKeys.has(findingKey(r)));
940
+ function filterToNewFindings(results, baselineResults, config = defaultConfig3) {
941
+ const penalized = (rs) => rs.filter((r) => isPenalized2(r.detection, config.treatDynamicAs));
942
+ const baselineKeys = new Set(penalized(baselineResults).map(findingKey));
943
+ return penalized(results).filter((r) => !baselineKeys.has(findingKey(r)));
938
944
  }
939
945
 
940
946
  // src/suppressions.ts
941
947
  import { readFileSync as readFileSync2, writeFileSync } from "fs";
942
948
  import { join as join6 } from "path";
943
- import { isPenalized } from "@svelte-vitals/core";
949
+ import { isPenalized as isPenalized3 } from "@svelte-vitals/core";
944
950
 
945
951
  // src/config-file.ts
946
952
  import { existsSync as existsSync2 } from "fs";
@@ -948,7 +954,7 @@ import { join as join5 } from "path";
948
954
  import { pathToFileURL } from "url";
949
955
  import {
950
956
  CATEGORIES,
951
- defaultConfig as defaultConfig2,
957
+ defaultConfig as defaultConfig4,
952
958
  resolveRuleOptions,
953
959
  shouldSkipRangeCheck,
954
960
  validateRuleSetting
@@ -1072,7 +1078,7 @@ function validateConfigFile(raw, path) {
1072
1078
  }
1073
1079
  for (const [key, setting] of Object.entries(entry.rules)) {
1074
1080
  const isCategory = CATEGORIES.includes(key);
1075
- const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), { ...defaultConfig2, rules: config.rules ?? {} });
1081
+ const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), { ...defaultConfig4, rules: config.rules ?? {} });
1076
1082
  const skipRangeCheck = shouldSkipRangeCheck(rawOverrides, i, key, setting);
1077
1083
  validateSetting(path, `overrides[${i}].rules`, key, setting, !isCategory, baseline, skipRangeCheck);
1078
1084
  }
@@ -1188,7 +1194,7 @@ function writeSuppressions(cwd, results, config) {
1188
1194
  const seen = /* @__PURE__ */ new Set();
1189
1195
  const entries = [];
1190
1196
  for (const r of results) {
1191
- if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
1197
+ if (!isPenalized3(r.detection, config.treatDynamicAs)) continue;
1192
1198
  const entry = toEntry(r);
1193
1199
  const key = findingKey(entry);
1194
1200
  if (seen.has(key)) continue;
@@ -1200,20 +1206,23 @@ function writeSuppressions(cwd, results, config) {
1200
1206
  writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
1201
1207
  return entries.length;
1202
1208
  }
1203
- function applySuppressions(results, entries, config) {
1209
+ function applySuppressions(results, entries, config, allResults) {
1204
1210
  const keys = new Set(entries.map((e) => findingKey(e)));
1205
- const usedKeys = /* @__PURE__ */ new Set();
1206
1211
  const kept = [];
1207
1212
  let suppressed = 0;
1208
1213
  for (const r of results) {
1209
1214
  const key = findingKey(r);
1210
- if (keys.has(key) && isPenalized(r.detection, config.treatDynamicAs)) {
1215
+ if (keys.has(key) && isPenalized3(r.detection, config.treatDynamicAs)) {
1211
1216
  suppressed++;
1212
- usedKeys.add(key);
1213
1217
  continue;
1214
1218
  }
1215
1219
  kept.push(r);
1216
1220
  }
1221
+ const usedKeys = /* @__PURE__ */ new Set();
1222
+ for (const r of allResults ?? results) {
1223
+ const key = findingKey(r);
1224
+ if (keys.has(key) && isPenalized3(r.detection, config.treatDynamicAs)) usedKeys.add(key);
1225
+ }
1217
1226
  const stale = [...keys].filter((k) => !usedKeys.has(k)).length;
1218
1227
  return { results: kept, suppressed, stale };
1219
1228
  }
@@ -1491,6 +1500,28 @@ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
1491
1500
  function spinnerEnabled(opts) {
1492
1501
  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
1502
  }
1503
+ function formatGlob(glob2) {
1504
+ return Array.isArray(glob2) ? `[${glob2.map((g) => `'${g}'`).join(", ")}]` : `'${glob2}'`;
1505
+ }
1506
+ function overridesOffWarnings(allowRules, overrides) {
1507
+ if (!allowRules || allowRules.length === 0 || !overrides || overrides.length === 0) return [];
1508
+ const warnings = [];
1509
+ for (const ruleId of allowRules) {
1510
+ const category = ruleId.split("/")[0] ?? ruleId;
1511
+ for (const entry of overrides) {
1512
+ const severity = settingSeverity(entry.rules[ruleId]) ?? settingSeverity(entry.rules[category]);
1513
+ if (severity !== "off") continue;
1514
+ const scope = [
1515
+ entry.route !== void 0 ? `route: ${formatGlob(entry.route)}` : void 0,
1516
+ entry.files !== void 0 ? `files: ${formatGlob(entry.files)}` : void 0
1517
+ ].filter((s) => s !== void 0).join(", ");
1518
+ warnings.push(
1519
+ `--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.`
1520
+ );
1521
+ }
1522
+ }
1523
+ return warnings;
1524
+ }
1494
1525
  async function analyzeProject(opts = {}) {
1495
1526
  const cwd = opts.cwd ?? process.cwd();
1496
1527
  const rt = createNodeRuntime();
@@ -1511,7 +1542,11 @@ async function analyzeProject(opts = {}) {
1511
1542
  ...file?.overrides !== void 0 ? { overrides: file.overrides } : {}
1512
1543
  });
1513
1544
  await detectProject(rt, cwd);
1514
- const warnings = [...loaded?.warnings ?? [], ...await checkVersionFloor(rt, cwd)];
1545
+ const warnings = [
1546
+ ...loaded?.warnings ?? [],
1547
+ ...await checkVersionFloor(rt, cwd),
1548
+ ...overridesOffWarnings(opts.allowRules, config.overrides)
1549
+ ];
1515
1550
  const { heads, images, headings, project, components, kitModules, sourceFiles } = await collectAll(rt, cwd, config, {
1516
1551
  route: opts.route,
1517
1552
  parseCache: opts.parseCache
@@ -1549,7 +1584,7 @@ async function applyScope(results, opts) {
1549
1584
  "svelte-vitals: could not determine changed files (not a git repo, git unavailable, or bad ref); analyzing all."
1550
1585
  );
1551
1586
  } else {
1552
- scoped = filterToChangedFiles(scoped, changed);
1587
+ scoped = filterToChangedFiles(scoped, changed, opts.config);
1553
1588
  }
1554
1589
  }
1555
1590
  if (opts.baseline !== void 0) {
@@ -1561,7 +1596,7 @@ async function applyScope(results, opts) {
1561
1596
  } else {
1562
1597
  try {
1563
1598
  const base = await analyzeProject({ ...opts.analyzeOpts, cwd: checkout.analyzeCwd });
1564
- scoped = filterToNewFindings(scoped, base.results);
1599
+ scoped = filterToNewFindings(scoped, base.results, opts.config);
1565
1600
  } catch {
1566
1601
  errorLog(`svelte-vitals: baseline analysis of '${opts.baseline}' failed; reporting all findings.`);
1567
1602
  } finally {
@@ -1572,11 +1607,16 @@ async function applyScope(results, opts) {
1572
1607
  if (!opts.noSuppressions && opts.config) {
1573
1608
  const entries = loadSuppressions(opts.cwd);
1574
1609
  if (entries !== void 0) {
1575
- const { results: afterSuppressions, suppressed, stale } = applySuppressions(scoped, entries, opts.config);
1610
+ const {
1611
+ results: afterSuppressions,
1612
+ suppressed,
1613
+ stale
1614
+ } = applySuppressions(scoped, entries, opts.config, results);
1576
1615
  scoped = afterSuppressions;
1577
- if (suppressed > 0 || stale > 0) {
1616
+ const routeScoped = opts.analyzeOpts?.route !== void 0;
1617
+ if (suppressed > 0 || stale > 0 && !routeScoped) {
1578
1618
  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)` : "") + "."
1619
+ `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
1620
  );
1581
1621
  }
1582
1622
  }
@@ -1675,6 +1715,12 @@ async function run(opts = {}) {
1675
1715
  try {
1676
1716
  const { config, version } = analysis;
1677
1717
  if (opts.updateSuppressions) {
1718
+ if (opts.route !== void 0) {
1719
+ errorLog(
1720
+ `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.`
1721
+ );
1722
+ return 2;
1723
+ }
1678
1724
  const count = writeSuppressions(cwd, analysis.results, config);
1679
1725
  errorLog(`svelte-vitals: wrote ${count} suppression(s) to ${SUPPRESSIONS_FILE}.`);
1680
1726
  return 0;
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
@@ -10,7 +10,7 @@ import {
10
10
  ruleOptionsSpec,
11
11
  run,
12
12
  spinnerEnabled
13
- } from "./chunk-WD5TSADH.js";
13
+ } from "./chunk-4MC7STFI.js";
14
14
  export {
15
15
  ProjectError,
16
16
  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.1",
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.39.0"
48
52
  },
49
53
  "devDependencies": {
50
54
  "@types/estree": "^1.0.9",