svelte-vitals 0.40.0 → 0.42.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-J4JE5XJS.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,7 +1513,7 @@ 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
1518
  const results = applyOverrides(
1490
1519
  applyRuleSeverities(
@@ -1539,6 +1568,19 @@ async function applyScope(results, opts) {
1539
1568
  }
1540
1569
  return scoped;
1541
1570
  }
1571
+ function runAnalyzeOptions(opts) {
1572
+ return {
1573
+ metaComponents: opts.metaComponents,
1574
+ treatDynamicAs: opts.treatDynamicAs,
1575
+ route: opts.route,
1576
+ failOn: opts.failOn,
1577
+ rules: opts.rules,
1578
+ ignoreRules: opts.ignoreRules,
1579
+ allowRules: opts.allowRules,
1580
+ weights: opts.weights,
1581
+ categories: opts.categories
1582
+ };
1583
+ }
1542
1584
  async function run(opts = {}) {
1543
1585
  const log = opts.log ?? ((line) => console.log(line));
1544
1586
  const errorLog = opts.errorLog ?? ((line) => console.error(line));
@@ -1564,16 +1606,7 @@ async function run(opts = {}) {
1564
1606
  let cwd = opts.cwd ?? process.cwd();
1565
1607
  let analysis;
1566
1608
  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
- });
1609
+ analysis = await analyzeProject({ ...runAnalyzeOptions(opts), cwd });
1577
1610
  } catch (err) {
1578
1611
  spinner.stop();
1579
1612
  if (err instanceof ProjectError) {
@@ -1608,16 +1641,7 @@ async function run(opts = {}) {
1608
1641
  }
1609
1642
  cwd = join6(cwd, chosen);
1610
1643
  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
- });
1644
+ analysis = await analyzeProject({ ...runAnalyzeOptions(opts), cwd });
1621
1645
  } catch (err2) {
1622
1646
  if (err2 instanceof ProjectError) {
1623
1647
  errorLog(err2.message);
@@ -1648,15 +1672,8 @@ async function run(opts = {}) {
1648
1672
  baseline: opts.baseline,
1649
1673
  noSuppressions: opts.noSuppressions,
1650
1674
  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
- }
1675
+ // No `cwd` — applyScope analyzes the baseline in its own checkout.
1676
+ analyzeOpts: runAnalyzeOptions(opts)
1660
1677
  });
1661
1678
  const summary = summarize(results, config);
1662
1679
  if (opts.score) {
@@ -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). */
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-J4JE5XJS.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.40.0",
3
+ "version": "0.42.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.35.0"
48
+ "@svelte-vitals/core": "0.36.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/estree": "^1.0.9",