svelte-vitals 0.41.0 → 0.43.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
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CONFIG_FILENAMES,
4
- buildRulesConfig,
5
4
  discoverApps,
6
5
  findUnknownRuleIds,
7
6
  isReporterName,
@@ -9,7 +8,7 @@ import {
9
8
  readCoreVersion,
10
9
  readPackageVersion,
11
10
  run
12
- } from "./chunk-2WDZJQGD.js";
11
+ } from "./chunk-6Y2E7QCQ.js";
13
12
  import {
14
13
  consoleIO
15
14
  } from "./chunk-SLUMRYUD.js";
@@ -145,8 +144,8 @@ function resolveArgs(argv) {
145
144
  if (updateSuppressions && noSuppressions) {
146
145
  errors.push("svelte-vitals: --update-suppressions and --no-suppressions cannot be used together.");
147
146
  }
148
- const rulesConfig = buildRulesConfig(allow, ignore);
149
- const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
147
+ const allowRules = allow.length > 0 ? allow : void 0;
148
+ const ignoreRules = ignore.length > 0 ? ignore : void 0;
150
149
  if (errors.length > 0) return { options: null, warnings, errors };
151
150
  return {
152
151
  options: {
@@ -161,7 +160,8 @@ function resolveArgs(argv) {
161
160
  outFile: typeof argv["out-file"] === "string" ? argv["out-file"] : void 0,
162
161
  byRoute: Boolean(argv["by-route"]),
163
162
  failOn,
164
- rules,
163
+ ...allowRules !== void 0 ? { allowRules } : {},
164
+ ...ignoreRules !== void 0 ? { ignoreRules } : {},
165
165
  ...weights !== void 0 ? { weights } : {},
166
166
  ...categories !== void 0 ? { categories } : {},
167
167
  ...score ? { score } : {},
@@ -1760,7 +1760,7 @@ async function selectApp(apps) {
1760
1760
  async function main() {
1761
1761
  const rawArgs = process.argv.slice(2);
1762
1762
  if (rawArgs[0] === "docs") {
1763
- const { runDocsCli } = await import("./cli-QRSD7FSW.js");
1763
+ const { runDocsCli } = await import("./cli-J5V65YIN.js");
1764
1764
  process.exitCode = runDocsCli(rawArgs.slice(1));
1765
1765
  return;
1766
1766
  }
@@ -2,7 +2,7 @@
2
2
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
3
3
  import { dirname as dirname2, join as join6 } from "path";
4
4
  import {
5
- allRules as allRules2,
5
+ allRules as allRules3,
6
6
  runRules,
7
7
  formatConsoleReport,
8
8
  formatJsonReport,
@@ -1459,6 +1459,30 @@ function scoreAnimationEnabled(opts) {
1459
1459
  return opts.reporter === "console" && opts.stdoutIsTTY && !opts.noAnimationFlag && !isAgentEnv(opts.env) && !isCiEnv(opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stdoutIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
1460
1460
  }
1461
1461
 
1462
+ // src/rule-selection.ts
1463
+ import { allRules as allRules2 } from "@svelte-vitals/core";
1464
+ function resolveRuleSelection(input) {
1465
+ const out = { ...input.rules ?? input.fileRules };
1466
+ const allow = input.allowRules ?? [];
1467
+ if (allow.length > 0) {
1468
+ const allowed = new Set(allow);
1469
+ for (const rule of allRules2) if (!allowed.has(rule.id)) out[rule.id] = "off";
1470
+ for (const id of allowed) {
1471
+ const setting = out[id];
1472
+ if (setting === void 0) continue;
1473
+ if (setting === "off") {
1474
+ delete out[id];
1475
+ } else if (typeof setting === "object" && setting.severity === "off") {
1476
+ const { severity: _forceEnabled, ...rest } = setting;
1477
+ if (Object.keys(rest).length === 0) delete out[id];
1478
+ else out[id] = rest;
1479
+ }
1480
+ }
1481
+ }
1482
+ for (const id of input.ignoreRules ?? []) out[id] = "off";
1483
+ return out;
1484
+ }
1485
+
1462
1486
  // src/index.ts
1463
1487
  import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
1464
1488
  function spinnerEnabled(opts) {
@@ -1473,7 +1497,12 @@ async function analyzeProject(opts = {}) {
1473
1497
  const config = defineConfig({
1474
1498
  treatDynamicAs: opts.treatDynamicAs ?? file?.treatDynamicAs ?? "pass",
1475
1499
  metaComponents: opts.metaComponents ?? file?.metaComponents ?? [],
1476
- rules: opts.rules ?? file?.rules ?? {},
1500
+ rules: resolveRuleSelection({
1501
+ fileRules: file?.rules,
1502
+ rules: opts.rules,
1503
+ allowRules: opts.allowRules,
1504
+ ignoreRules: opts.ignoreRules
1505
+ }),
1477
1506
  failOn: opts.failOn ?? file?.failOn ?? "critical",
1478
1507
  ...weights !== void 0 ? { weights } : {},
1479
1508
  ...file?.overrides !== void 0 ? { overrides: file.overrides } : {}
@@ -1484,16 +1513,20 @@ async function analyzeProject(opts = {}) {
1484
1513
  route: opts.route,
1485
1514
  parseCache: opts.parseCache
1486
1515
  });
1487
- const selected = selectRules(allRules2, config);
1516
+ const selected = selectRules(allRules3, config);
1488
1517
  const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
1489
- const results = applyOverrides(
1490
- applyRuleSeverities(
1491
- await runRules(rules, { heads, images, headings, components, project, config, kitModules, sourceFiles }),
1492
- config
1493
- ),
1494
- config
1495
- );
1496
- return { results, config, version: readPackageVersion(), ruleIds: rules.map((r) => r.id), warnings };
1518
+ const { results: rawResults, examined } = await runRules(rules, {
1519
+ heads,
1520
+ images,
1521
+ headings,
1522
+ components,
1523
+ project,
1524
+ config,
1525
+ kitModules,
1526
+ sourceFiles
1527
+ });
1528
+ const results = applyOverrides(applyRuleSeverities(rawResults, config), config);
1529
+ return { results, config, version: readPackageVersion(), ruleIds: rules.map((r) => r.id), examined, warnings };
1497
1530
  }
1498
1531
  async function applyScope(results, opts) {
1499
1532
  const errorLog = opts.errorLog ?? ((line) => console.error(line));
@@ -1539,6 +1572,19 @@ async function applyScope(results, opts) {
1539
1572
  }
1540
1573
  return scoped;
1541
1574
  }
1575
+ function runAnalyzeOptions(opts) {
1576
+ return {
1577
+ metaComponents: opts.metaComponents,
1578
+ treatDynamicAs: opts.treatDynamicAs,
1579
+ route: opts.route,
1580
+ failOn: opts.failOn,
1581
+ rules: opts.rules,
1582
+ ignoreRules: opts.ignoreRules,
1583
+ allowRules: opts.allowRules,
1584
+ weights: opts.weights,
1585
+ categories: opts.categories
1586
+ };
1587
+ }
1542
1588
  async function run(opts = {}) {
1543
1589
  const log = opts.log ?? ((line) => console.log(line));
1544
1590
  const errorLog = opts.errorLog ?? ((line) => console.error(line));
@@ -1564,16 +1610,7 @@ async function run(opts = {}) {
1564
1610
  let cwd = opts.cwd ?? process.cwd();
1565
1611
  let analysis;
1566
1612
  try {
1567
- analysis = await analyzeProject({
1568
- cwd,
1569
- metaComponents: opts.metaComponents,
1570
- treatDynamicAs: opts.treatDynamicAs,
1571
- route: opts.route,
1572
- failOn: opts.failOn,
1573
- rules: opts.rules,
1574
- weights: opts.weights,
1575
- categories: opts.categories
1576
- });
1613
+ analysis = await analyzeProject({ ...runAnalyzeOptions(opts), cwd });
1577
1614
  } catch (err) {
1578
1615
  spinner.stop();
1579
1616
  if (err instanceof ProjectError) {
@@ -1608,16 +1645,7 @@ async function run(opts = {}) {
1608
1645
  }
1609
1646
  cwd = join6(cwd, chosen);
1610
1647
  try {
1611
- analysis = await analyzeProject({
1612
- cwd,
1613
- metaComponents: opts.metaComponents,
1614
- treatDynamicAs: opts.treatDynamicAs,
1615
- route: opts.route,
1616
- failOn: opts.failOn,
1617
- rules: opts.rules,
1618
- weights: opts.weights,
1619
- categories: opts.categories
1620
- });
1648
+ analysis = await analyzeProject({ ...runAnalyzeOptions(opts), cwd });
1621
1649
  } catch (err2) {
1622
1650
  if (err2 instanceof ProjectError) {
1623
1651
  errorLog(err2.message);
@@ -1648,15 +1676,8 @@ async function run(opts = {}) {
1648
1676
  baseline: opts.baseline,
1649
1677
  noSuppressions: opts.noSuppressions,
1650
1678
  errorLog,
1651
- analyzeOpts: {
1652
- metaComponents: opts.metaComponents,
1653
- treatDynamicAs: opts.treatDynamicAs,
1654
- route: opts.route,
1655
- failOn: opts.failOn,
1656
- rules: opts.rules,
1657
- weights: opts.weights,
1658
- categories: opts.categories
1659
- }
1679
+ // No `cwd` — applyScope analyzes the baseline in its own checkout.
1680
+ analyzeOpts: runAnalyzeOptions(opts)
1660
1681
  });
1661
1682
  const summary = summarize(results, config);
1662
1683
  if (opts.score) {
@@ -1673,7 +1694,7 @@ async function run(opts = {}) {
1673
1694
  );
1674
1695
  }
1675
1696
  if (reporter === "json") {
1676
- log(formatJsonReport(results, config, { version }, analysis.ruleIds));
1697
+ log(formatJsonReport(results, config, { version }, analysis.ruleIds, analysis.examined));
1677
1698
  } else if (reporter === "agent") {
1678
1699
  log(formatAgentReport(results, config));
1679
1700
  } else if (reporter === "sarif") {
@@ -17,7 +17,7 @@ var EMBEDDED_DOCS = [
17
17
  name: "config",
18
18
  title: "The config file",
19
19
  description: "Where svelte-vitals.config lives, every top-level option, how to disable or re-grade a rule, and how to scope rules to routes or files.",
20
- body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules`/`--ignore`\nreplace the config file's `rules` wholesale for that run rather than merging.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
20
+ body: "# The config file\n\n## Where it lives\n\nIn the **analyzed directory only** \u2014 no upward search. First match wins:\n\n1. `svelte-vitals.config.mjs`\n2. `svelte-vitals.config.js`\n3. `svelte-vitals.config.ts`\n\nNo file means built-in defaults. `svelte-vitals install --client config-file` scaffolds one with\nevery option commented out.\n\n```js\n// svelte-vitals.config.mjs\nexport default {\n treatDynamicAs: 'warn',\n metaComponents: ['Seo'],\n rules: { 'seo/json-ld': 'off' },\n failOn: 'warning',\n weights: { seo: 2 }\n};\n```\n\nA `.ts` config can `import { defineConfig } from 'svelte-vitals'` for type-checking, but that is a\n**runtime** import: it needs svelte-vitals as a declared dependency and Node 22.18+ (or 23.6+).\nA plain `export default {}` in `.mjs` behaves identically and always works.\n\n## Options\n\n| Option | Type | Default |\n| ---------------- | -------------------------------------------------------------- | ------------------ |\n| `treatDynamicAs` | `'pass' \\| 'warn' \\| 'fail'` | `'pass'` |\n| `metaComponents` | `string[]` | `[]` |\n| `rules` | `Record<ruleId, 'off' \\| Severity \\| { severity?, options? }>` | `{}` |\n| `failOn` | `'critical' \\| 'warning' \\| 'info'` | `'critical'` |\n| `weights` | `Partial<Record<Category, number>>` | every category `1` |\n| `overrides` | `RuleOverride[]` | (none) |\n\n`Severity` is `'critical' | 'warning' | 'info'`. `Category` is `'seo' | 'performance' |\n'correctness' | 'security' | 'architecture'`. A weight of `0` drops a category from the Health\naverage; setting every category to `0` is an error (exit `2`).\n\n## Turning a rule off or down\n\n```js\nexport default {\n rules: {\n 'seo/json-ld': 'off', // remove its findings entirely\n 'architecture/prop-count': 'info' // keep it, stop it failing the build\n }\n};\n```\n\nMany rules take options, so check whether the finding is a **threshold disagreement** rather than\na defect first. `svelte-vitals explain <rule-id>` prints each option's name, default, bounds, and\nmerge semantics (`integer` replaces, `string-list` appends, `string-map` is spread over).\n\n```js\nexport default {\n rules: {\n 'architecture/prop-count': { options: { max: 12 } }\n }\n};\n```\n\n## Scoping to routes or files (`overrides`)\n\n`rules` applies everywhere; `overrides` applies only where it matches \u2014 typically routes that\nare deliberately not public.\n\n```js\nexport default {\n overrides: [\n { files: 'src/routes/(app)/**', rules: { seo: 'off' } },\n { route: '/admin/**', rules: { 'seo/title-presence': 'info' } }\n ]\n};\n```\n\nEach entry needs `rules` (keys are rule ids **or** category names) plus at least one of:\n\n- **`route`** \u2014 glob(s) against the route id as reported (`/blog/[slug]`). SvelteKit `(group)`\n segments are **not** in the route id, so use `files` to target a group.\n- **`files`** \u2014 glob(s) against the source path.\n\nGlobs are deliberately small: `*` within a segment, `**` across segments, a trailing `/**` also\nmatches the bare prefix. Everything else \u2014 including `(`, `)`, `[`, `]` \u2014 is literal. Later entries win.\n\n## Precedence\n\nPer field: **CLI flag > config file > built-in default**. One exception \u2014 `--rules` and `--ignore`\nare selection, not configuration: `--rules` narrows the run to the ids it names and overrides a\nconfig-file `off` for them, but keeps their declared severity and options; `--ignore` adds `off`\nentries for the ids it names, layered on top of whatever `rules` resolved to, and beats `--rules`\nwhen both name the same rule.\n\n`overrides` has no CLI flag; route policy belongs in a committed file.\n\n## Validation\n\nAn unknown rule id or category, a negative weight, a malformed `overrides` entry, or an invalid\nrule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An unrecognized\n`treatDynamicAs`/`failOn` value, or an unknown top-level key, only warns.\n\n## Related\n\n- `svelte-vitals explain --list` \u2014 every rule id\n- `svelte-vitals docs show scoping` \u2014 accepting an existing backlog instead of disabling rules"
21
21
  },
22
22
  {
23
23
  name: "monorepo",
package/dist/index.d.ts CHANGED
@@ -69,6 +69,15 @@ declare function ruleOptionsSpec(id: string): RuleOptionsSpec | undefined;
69
69
  * (--ignore). An allow-list disables every rule not listed; deny always wins.
70
70
  * Callers should reject unknown ids first (see findUnknownRuleIds) so a typo in
71
71
  * --rules can't silently disable every rule.
72
+ *
73
+ * No longer used by the CLI: `resolve-args` passes `--rules`/`--ignore` as id lists and
74
+ * `rule-selection.ts` composes the map (design 2026-08-06-rule-selection-design.md). The CLI
75
+ * used to call this with `ignore` empty and hand the result to `AnalyzeOptions.rules`, which
76
+ * replaces a config file's `rules` map as a whole (design 2026-07-05-config-file-design.md §3)
77
+ * — correct for --rules's allow-list semantics under that encoding, but not for --ignore, which
78
+ * names only the rule(s) it silences and must layer onto the file's map instead. Kept as
79
+ * exported API: a direct caller building a `rules` value from both an allow- and a deny-list on
80
+ * purpose gets exactly that whole-field replacement.
72
81
  */
73
82
  declare function buildRulesConfig(allow: string[], ignore: string[]): Record<string, RuleSetting>;
74
83
 
@@ -108,7 +117,21 @@ interface RunOptions {
108
117
  reporter?: ReporterName;
109
118
  byRoute?: boolean;
110
119
  failOn?: Severity;
120
+ /**
121
+ * A complete replacement for the config file's `rules` map — what the Vite plugin and
122
+ * programmatic callers pass. Whole-field, per the per-field precedence every other config
123
+ * field follows.
124
+ */
111
125
  rules?: Record<string, RuleSetting>;
126
+ /**
127
+ * Rule ids to silence on top of `rules`/the config file (--ignore). Unlike `rules`, this
128
+ * never replaces anything — it only ever adds `'off'` entries for the ids listed, so a
129
+ * rule not named here keeps whatever `rules`/the file said for it (design:
130
+ * rules-flag-clobbers-config-options).
131
+ */
132
+ ignoreRules?: string[];
133
+ /** `--rules`: run only these rule ids. Selection; the config file still supplies their options. */
134
+ allowRules?: string[];
112
135
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
113
136
  weights?: Partial<Record<Category, number>>;
114
137
  /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
@@ -177,7 +200,21 @@ interface AnalyzeOptions {
177
200
  /** Restrict analysis to routes whose path matches this glob (matched against the route path without leading slash). */
178
201
  route?: string;
179
202
  failOn?: Severity;
203
+ /**
204
+ * A complete replacement for the config file's `rules` map — what the Vite plugin and
205
+ * programmatic callers pass. Whole-field, per the per-field precedence every other config
206
+ * field follows.
207
+ */
180
208
  rules?: Record<string, RuleSetting>;
209
+ /**
210
+ * Rule ids to silence on top of `rules`/the config file (--ignore). Unlike `rules`, this
211
+ * never replaces anything — it only ever adds `'off'` entries for the ids listed, so a
212
+ * rule not named here keeps whatever `rules`/the file said for it (design:
213
+ * rules-flag-clobbers-config-options).
214
+ */
215
+ ignoreRules?: string[];
216
+ /** `--rules`: run only these rule ids. Selection; the config file still supplies their options. */
217
+ allowRules?: string[];
181
218
  /** Per-category weights for the combined Health score (flag > config file > default 1 each). */
182
219
  weights?: Partial<Record<Category, number>>;
183
220
  /** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
@@ -201,6 +238,8 @@ interface AnalyzeResult {
201
238
  version: string;
202
239
  /** Ids of the rules that ran, after `--category` narrowing. The JSON report lists these so a rule that found nothing stays distinguishable from one that was never selected. */
203
240
  ruleIds: string[];
241
+ /** Per-rule, per-declaration counts of places examined, unfiltered by `--diff`/`--baseline`/suppressions. */
242
+ examined: Record<string, Record<string, number>>;
204
243
  /** Non-fatal config-file issues (unknown top-level keys, invalid enum values). Empty when no config file or none found. */
205
244
  warnings: string[];
206
245
  }
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  ruleOptionsSpec,
12
12
  run,
13
13
  spinnerEnabled
14
- } from "./chunk-2WDZJQGD.js";
14
+ } from "./chunk-6Y2E7QCQ.js";
15
15
  export {
16
16
  ProjectError,
17
17
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.41.0",
3
+ "version": "0.43.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
  "mri": "^1.2.0",
46
46
  "svelte": "^5.56.8",
47
47
  "tinyglobby": "^0.2.17",
48
- "@svelte-vitals/core": "0.36.0"
48
+ "@svelte-vitals/core": "0.37.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/estree": "^1.0.9",