svelte-vitals 0.54.2 → 0.54.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.
@@ -1,4 +1,4 @@
1
- import { c as findUnknownRuleIds, h as readPackageVersion, i as run, l as knownRuleIds, m as readCoreVersion, p as isReporterName } from "./src-BV_3ylhu.js";
1
+ import { c as findUnknownRuleIds, h as readPackageVersion, i as run, l as knownRuleIds, m as readCoreVersion, p as isReporterName } from "./src-BhVse1Wr.js";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { terminalSafe } from "@svelte-vitals/core/internal";
@@ -53,7 +53,7 @@ async function selectAppPrompt(apps, message) {
53
53
  message,
54
54
  options: apps.map((a) => ({
55
55
  value: a,
56
- label: a
56
+ label: terminalSafe(a)
57
57
  })),
58
58
  initialValue: apps[0]
59
59
  });
@@ -77,7 +77,7 @@ function clackPrompts() {
77
77
  },
78
78
  selectApp: (apps) => selectAppPrompt(apps, "Multiple SvelteKit apps found — which one should the Vite/config targets go into?"),
79
79
  confirm: async (planText) => {
80
- const res = await p.confirm({ message: `Apply this plan?\n${planText}` });
80
+ const res = await p.confirm({ message: `Apply this plan?\n${terminalSafe(planText)}` });
81
81
  return p.isCancel(res) ? false : Boolean(res);
82
82
  }
83
83
  };
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { g as realIO, i as resolveLocale, m as consoleIO, n as runAnalyzeCliGunshi } from "./analyze-Dl-VIKsc.js";
2
+ import { g as realIO, i as resolveLocale, m as consoleIO, n as runAnalyzeCliGunshi } from "./analyze-BH6tM7-d.js";
3
3
  import { terminalSafe } from "@svelte-vitals/core/internal";
4
4
  //#region src/cli.ts
5
5
  /**
@@ -17,35 +17,35 @@ async function runCli(argv, io = consoleIO, env = process.env) {
17
17
  try {
18
18
  const locale = resolveLocale(env);
19
19
  if (argv[0] === "complete") {
20
- const { runCompleteCliGunshi } = await import("./complete-Dr0cRw1j.js");
20
+ const { runCompleteCliGunshi } = await import("./complete-DopIU_Sb.js");
21
21
  return {
22
22
  code: await runCompleteCliGunshi(argv, io),
23
23
  exit: "natural"
24
24
  };
25
25
  }
26
26
  if (argv[0] === "docs") {
27
- const { runDocsCliGunshi } = await import("./docs-DZD2UDg-.js");
27
+ const { runDocsCliGunshi } = await import("./docs-hor--7iY.js");
28
28
  return {
29
29
  code: await runDocsCliGunshi(argv.slice(1), io, locale),
30
30
  exit: "natural"
31
31
  };
32
32
  }
33
33
  if (argv[0] === "explain") {
34
- const { runExplainCliGunshi } = await import("./explain-C4eXHI7I.js");
34
+ const { runExplainCliGunshi } = await import("./explain-BQWRixs6.js");
35
35
  return {
36
36
  code: await runExplainCliGunshi(argv.slice(1), io, locale),
37
37
  exit: "natural"
38
38
  };
39
39
  }
40
40
  if (argv[0] === "install") {
41
- const { runInstallCliGunshi } = await import("./install-D-OIDsrS.js").then((n) => n.n);
41
+ const { runInstallCliGunshi } = await import("./install-BEsNEdKk.js").then((n) => n.n);
42
42
  return {
43
43
  code: await runInstallCliGunshi(argv.slice(1), io, locale),
44
44
  exit: "immediate"
45
45
  };
46
46
  }
47
47
  if (argv[0] === "ci") {
48
- const { runCiCliGunshi } = await import("./ci-DmdNtgzC.js");
48
+ const { runCiCliGunshi } = await import("./ci-DdxIti4m.js");
49
49
  return {
50
50
  code: await runCiCliGunshi(argv.slice(1), {
51
51
  ...realIO(),
@@ -1,4 +1,4 @@
1
- import { a as guardArgs, c as stripUnknownFlags, g as realIO, l as suggestClosest, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-Dl-VIKsc.js";
1
+ import { a as guardArgs, c as stripUnknownFlags, g as realIO, l as suggestClosest, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-BH6tM7-d.js";
2
2
  import { a as planWorkflowWrite, i as buildWorkflowYaml, n as ACTION_VERSION, r as WORKFLOW_PATH, t as ACTION_SHA } from "./action-pin.generated-C2XJJ3NW.js";
3
3
  import { join } from "node:path";
4
4
  import { cli } from "gunshi/bone";
@@ -1,9 +1,9 @@
1
- import { f as REPORTER_NAMES } from "./src-BV_3ylhu.js";
2
- import { d as FAIL_ON_VALUES, f as TREAT_DYNAMIC_AS_VALUES, m as consoleIO, t as ROOT_ARGS, u as CATEGORIES } from "./analyze-Dl-VIKsc.js";
3
- import { t as INSTALL_ARGS } from "./install-D-OIDsrS.js";
4
- import { CI_ARGS, CI_UPGRADE_ARGS } from "./ci-DmdNtgzC.js";
5
- import { DOCS_LIST_ARGS, DOCS_ROOT_ARGS, DOCS_SHOW_ARGS } from "./docs-DZD2UDg-.js";
6
- import { EXPLAIN_ARGS } from "./explain-C4eXHI7I.js";
1
+ import { f as REPORTER_NAMES } from "./src-BhVse1Wr.js";
2
+ import { d as FAIL_ON_VALUES, f as TREAT_DYNAMIC_AS_VALUES, m as consoleIO, t as ROOT_ARGS, u as CATEGORIES } from "./analyze-BH6tM7-d.js";
3
+ import { t as INSTALL_ARGS } from "./install-BEsNEdKk.js";
4
+ import { CI_ARGS, CI_UPGRADE_ARGS } from "./ci-DdxIti4m.js";
5
+ import { DOCS_LIST_ARGS, DOCS_ROOT_ARGS, DOCS_SHOW_ARGS } from "./docs-hor--7iY.js";
6
+ import { EXPLAIN_ARGS } from "./explain-BQWRixs6.js";
7
7
  import { cli } from "gunshi/bone";
8
8
  import { define } from "gunshi/definition";
9
9
  import { kebabnize } from "gunshi/utils";
@@ -1,5 +1,5 @@
1
- import { l as knownRuleIds } from "./src-BV_3ylhu.js";
2
- import { a as guardArgs, c as stripUnknownFlags, l as suggestClosest, m as consoleIO, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-Dl-VIKsc.js";
1
+ import { l as knownRuleIds } from "./src-BhVse1Wr.js";
2
+ import { a as guardArgs, c as stripUnknownFlags, l as suggestClosest, m as consoleIO, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-BH6tM7-d.js";
3
3
  import { docsUrlFor } from "@svelte-vitals/core/internal";
4
4
  import { cli } from "gunshi/bone";
5
5
  import { define } from "gunshi/definition";
@@ -1,5 +1,5 @@
1
- import { l as knownRuleIds } from "./src-BV_3ylhu.js";
2
- import { a as guardArgs, c as stripUnknownFlags, l as suggestClosest, m as consoleIO, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-Dl-VIKsc.js";
1
+ import { l as knownRuleIds } from "./src-BhVse1Wr.js";
2
+ import { a as guardArgs, c as stripUnknownFlags, l as suggestClosest, m as consoleIO, o as splitAtTerminator, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-BH6tM7-d.js";
3
3
  import { CATEGORIES } from "@svelte-vitals/core";
4
4
  import { allRules, explainRule } from "@svelte-vitals/core/internal";
5
5
  import { cli } from "gunshi/bone";
@@ -1,4 +1,4 @@
1
- import { t as ROOT_ARGS } from "./analyze-Dl-VIKsc.js";
2
- import { t as INSTALL_ARGS } from "./install-D-OIDsrS.js";
1
+ import { t as ROOT_ARGS } from "./analyze-BH6tM7-d.js";
2
+ import { t as INSTALL_ARGS } from "./install-BEsNEdKk.js";
3
3
  import { t as JA_ARG_DESCRIPTIONS } from "./ja-BDLP3KLG.js";
4
4
  export { INSTALL_ARGS, JA_ARG_DESCRIPTIONS, ROOT_ARGS };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Category, Config, JsonReport, Result, RuleSetting, Severity, defineConfig } from "@svelte-vitals/core";
2
- import { BranchStep, HeadTag, RuleOptionsSpec, SuppressionDirective } from "@svelte-vitals/core/internal";
2
+ import { BranchStep, HeadTag, RuleOptionsSpec, Runtime, SuppressionDirective } from "@svelte-vitals/core/internal";
3
3
  import { AST } from "svelte/compiler";
4
4
  //#region src/providers/source/imports.d.ts
5
5
  /** A resolved import binding: which module, and which export ('default' for default imports). */
@@ -135,6 +135,14 @@ declare class ProjectError extends Error {
135
135
  //#region src/route-matcher.d.ts
136
136
  declare function routeMatcher(glob: string | undefined): (route: string) => boolean;
137
137
  //#endregion
138
+ //#region src/runtime/node.d.ts
139
+ /**
140
+ * Node implementation of the core Runtime abstraction (design §8). This is the
141
+ * only place in the static-mode pipeline allowed to touch `node:` APIs; the
142
+ * provider and rules stay runtime-agnostic by going through this interface.
143
+ */
144
+ declare function createNodeRuntime(): Runtime;
145
+ //#endregion
138
146
  //#region src/rules-config.d.ts
139
147
  /** Rule ids passed to --rules/--ignore that aren't part of the built-in registry. */
140
148
  declare function findUnknownRuleIds(ids: string[]): string[];
@@ -370,4 +378,4 @@ declare function applyScope(results: Result[], opts: ApplyScopeOptions): Promise
370
378
  */
371
379
  declare function run(opts?: RunOptions): Promise<number>;
372
380
  //#endregion
373
- export { AnalyzeOptions, AnalyzeResult, ApplyScopeOptions, CONFIG_FILENAMES, type LoadedConfigFile, type ParseCache, ProjectError, RunOptions, analyzeProject, applyScope, defineConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, routeMatcher, ruleOptionsSpec, run, spinnerEnabled };
381
+ export { AnalyzeOptions, AnalyzeResult, ApplyScopeOptions, CONFIG_FILENAMES, type LoadedConfigFile, type ParseCache, ProjectError, RunOptions, analyzeProject, applyScope, createNodeRuntime, defineConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, routeMatcher, ruleOptionsSpec, run, spinnerEnabled };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as spinnerEnabled, b as ProjectError, c as findUnknownRuleIds, d as ruleOptionsSpec, i as run, l as knownRuleIds, n as applyScope, o as CONFIG_FILENAMES, r as defineConfig, s as loadConfigFile, t as analyzeProject, u as registryTag, y as routeMatcher } from "./src-BV_3ylhu.js";
2
- export { CONFIG_FILENAMES, ProjectError, analyzeProject, applyScope, defineConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, routeMatcher, ruleOptionsSpec, run, spinnerEnabled };
1
+ import { a as spinnerEnabled, b as ProjectError, c as findUnknownRuleIds, d as ruleOptionsSpec, i as run, l as knownRuleIds, n as applyScope, o as CONFIG_FILENAMES, r as defineConfig, s as loadConfigFile, t as analyzeProject, u as registryTag, x as createNodeRuntime, y as routeMatcher } from "./src-BhVse1Wr.js";
2
+ export { CONFIG_FILENAMES, ProjectError, analyzeProject, applyScope, createNodeRuntime, defineConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, routeMatcher, ruleOptionsSpec, run, spinnerEnabled };
@@ -42,8 +42,8 @@ without a judgment call of its own.
42
42
  which you delete before finishing. If asked to "just fix it", decline and
43
43
  point to \`improve-svelte execute <plan>\`, to running the plan with any
44
44
  agent, or to the \`svelte-vitals\` skill's own diff/staged gate.
45
- 2. **No mutating operations.** No \`--fix\`-style flags (svelte-vitals has
46
- none today, by design), no code edits, no commits, no formatters, no
45
+ 2. **No mutating operations.** No \`--fix\`-style flags (svelte-vitals ships
46
+ none, by design), no code edits, no commits, no formatters, no
47
47
  dependency installs. Run svelte-vitals read-only, for evidence only.
48
48
  3. **Plans must be fully self-contained.** The executor has zero context
49
49
  from this conversation. Never write "fix it like seo/title-presence above" — inline
@@ -123,7 +123,7 @@ Get the machine map before applying judgment:
123
123
  Audit against svelte-vitals' six categories: SEO, Performance, Correctness,
124
124
  Security, Architecture, Accessibility (see the rule catalog below for the
125
125
  full "hunt for" list per category, generated from svelte-vitals' own rule
126
- metadata — always in sync, never invented).
126
+ metadata).
127
127
 
128
128
  For anything beyond a small project, fan out read-only subagents — one per
129
129
  category. Each subagent prompt must include: the recon facts (stack,
@@ -167,7 +167,8 @@ Severity here is leverage-driven, **not** svelte-vitals' raw rule severity:
167
167
  - **LOW** — polish and hygiene: an \`info\`-severity finding on a low-traffic
168
168
  route, a namespace import that could be more tree-shakeable.
169
169
 
170
- After the table, list 2–4 **missed opportunities** — additive improvements
170
+ After the table, list the **missed opportunities** worth naming — additive
171
+ improvements
171
172
  svelte-vitals doesn't (and by design won't) flag, since it's a static
172
173
  analyzer, not a runtime auditor: actual Core Web Vitals measurement, a
173
174
  missing \`sitemap.xml\` entry for a new route, structured-data types beyond
@@ -198,10 +199,11 @@ order, dependencies between plans, and a status column.
198
199
 
199
200
  ## Rule catalog
200
201
 
201
- (This section is generated at install time from svelte-vitals' own rule
202
- metadata — every rule's id, title, severity, rationale, docs link and, where
203
- the rule ships one, its canonical fix — grouped by category. It is always in
204
- sync with the version of svelte-vitals you have installed.)
202
+ (This section is generated from svelte-vitals' own rule metadata — every
203
+ rule's id, title, severity, rationale, docs link and, where the rule ships
204
+ one, its canonical fix — grouped by category. It reflects the svelte-vitals
205
+ source this skill was generated from; \`npx svelte-vitals explain --list\` is
206
+ the authority for what the version installed in this project checks.)
205
207
 
206
208
  ${ruleDigest()}
207
209
 
@@ -103,4 +103,4 @@ alwaysApply: false
103
103
  ---\n\n${sharedBody(header)}`;
104
104
  }
105
105
  //#endregion
106
- export { REPO_SKILLS_HEADER, buildCursorRules, buildSkillMarkdown, installHeader, isEmptyDefault, isInertUntilConfigured, oneLine, ruleDigest };
106
+ export { REPO_SKILLS_HEADER, buildCursorRules, buildSkillMarkdown, installHeader, isInertUntilConfigured, oneLine, ruleDigest };
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { _ as hasDep, g as discoverApps, h as readPackageVersion, o as CONFIG_FILENAMES, v as readPkg } from "./src-BV_3ylhu.js";
3
- import { a as guardArgs, c as stripUnknownFlags, g as realIO, h as clackPrompts, m as consoleIO, o as splitAtTerminator, p as toList, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-Dl-VIKsc.js";
2
+ import { _ as hasDep, g as discoverApps, h as readPackageVersion, o as CONFIG_FILENAMES, v as readPkg } from "./src-BhVse1Wr.js";
3
+ import { a as guardArgs, c as stripUnknownFlags, g as realIO, h as clackPrompts, m as consoleIO, o as splitAtTerminator, p as toList, r as localizedOptionsSection, s as stripAutoVersionLine } from "./analyze-BH6tM7-d.js";
4
4
  import { a as planWorkflowWrite, i as buildWorkflowYaml, n as ACTION_VERSION, r as WORKFLOW_PATH, t as ACTION_SHA } from "./action-pin.generated-C2XJJ3NW.js";
5
5
  import { buildCursorRules, installHeader } from "./install/skill-content.js";
6
6
  import { join } from "node:path";
@@ -1,14 +1,15 @@
1
1
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, resolve, sep } from "node:path";
3
3
  import { CATEGORIES, defineConfig, defineConfig as defineConfig$1 } from "@svelte-vitals/core";
4
- import { ANCESTRY_DEPENDENT_TAGS, ASIDE_DEMOTING_TAGS, CHILD_NODE_KEYS, IDREF_ATTRS, NAMING_ATTRS, ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, addFactsDirectives, allRules, attrText, attrTextOf, attrValue, attrValueOf, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, compileOverrides, computeHealth, decodeFragmentId, defaultConfig, docsUrlFor, findAttr, findMinifyDisabled, foldOccurrences, formatAgentReport, formatConsoleReport, formatFailedRuleWarning, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, isClassicScriptType, isPenalized, isTopFragment, lineOf, noColorPalette, parseSvelte, resolveKitAliases, resolveKitPathsBase, resolveLandmark, resolveRepoLocalPath, resolveRuleOptions, runAnalysis, scoreColor, selectRules, settingSeverity, shouldSkipRangeCheck, skippedFileWarnings, splitTokens, stripTextDirective, summarize, terminalSafe, textFromNodes, unknownDirectiveIds, validateRuleSetting, valueFromNodes, withReadLimit } from "@svelte-vitals/core/internal";
4
+ import { ANCESTRY_DEPENDENT_TAGS, ASIDE_DEMOTING_TAGS, CHILD_NODE_KEYS, IDREF_ATTRS, NAMING_ATTRS, ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, addFactsDirectives, allRules, attrText, attrTextOf, attrValue, attrValueOf, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, compileOverrides, computeHealth, decodeFragmentId, defaultConfig, docsUrlFor, findAttr, findMinifyDisabled, foldOccurrences, formatAgentReport, formatConsoleReport, formatFailedRuleWarning, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, isClassicScriptType, isPenalized, isPlainObject, isTopFragment, lineOf, noColorPalette, parseSvelte, resolveKitAliases, resolveKitPathsBase, resolveLandmark, resolveRepoLocalPath, resolveRuleOptions, runAnalysis, scoreColor, selectRules, settingSeverity, shouldSkipRangeCheck, skippedFileWarnings, splitTokens, stripTextDirective, summarize, terminalSafe, textFromNodes, unknownDirectiveIds, validateRuleSetting, valueFromNodes, withReadLimit } from "@svelte-vitals/core/internal";
5
5
  import { access, glob, readFile, stat } from "node:fs/promises";
6
6
  import { getAgentProfile } from "gunshi/agent";
7
7
  import { execFileSync } from "node:child_process";
8
8
  import { tmpdir } from "node:os";
9
- import { pathToFileURL } from "node:url";
10
9
  import { styleText } from "node:util";
11
10
  import { setTimeout } from "node:timers/promises";
11
+ import { createHash } from "node:crypto";
12
+ import { pathToFileURL } from "node:url";
12
13
  //#region src/a11y-skips.ts
13
14
  const ID_REF_RULE = "a11y/no-missing-id-ref";
14
15
  /** One entry per analyzed route whose closed world failed; sorted for stable report output. */
@@ -1693,35 +1694,42 @@ function isGithubActionsEnv(env = process.env) {
1693
1694
  function isCiEnv(env = process.env) {
1694
1695
  return env.CI === "true" || env.CI === "1";
1695
1696
  }
1696
- /**
1697
- * Resolve the reporter: explicit flag → SVELTE_VITALS_REPORTER → agent-env
1698
- * auto-detect → GitHub-Actions auto-detect → console.
1699
- */
1700
- function resolveReporter(explicit, env = process.env) {
1701
- if (explicit) return explicit;
1697
+ /** Precedence: explicit flag → SVELTE_VITALS_REPORTER → agent-env auto-detect → GitHub-Actions auto-detect → console. `auto` marks the two detected layers. */
1698
+ function reporterChoice(explicit, env) {
1699
+ if (explicit) return {
1700
+ name: explicit,
1701
+ auto: false
1702
+ };
1702
1703
  const fromEnv = env.SVELTE_VITALS_REPORTER;
1703
- if (isReporterName(fromEnv)) return fromEnv;
1704
- if (isAgentEnv(env)) return "agent";
1705
- if (isGithubActionsEnv(env)) return "github";
1706
- return "console";
1704
+ if (isReporterName(fromEnv)) return {
1705
+ name: fromEnv,
1706
+ auto: false
1707
+ };
1708
+ if (isAgentEnv(env)) return {
1709
+ name: "agent",
1710
+ auto: true
1711
+ };
1712
+ if (isGithubActionsEnv(env)) return {
1713
+ name: "github",
1714
+ auto: true
1715
+ };
1716
+ return {
1717
+ name: "console",
1718
+ auto: false
1719
+ };
1707
1720
  }
1708
- /**
1709
- * True when the agent reporter is chosen purely by env auto-detection — i.e. no
1710
- * explicit flag and no SVELTE_VITALS_REPORTER, but a known agent env is present.
1711
- * Used to surface a one-line "how to override" hint, since a human running the
1712
- * CLI inside an agent terminal would otherwise get Markdown unexpectedly.
1713
- */
1721
+ function resolveReporter(explicit, env = process.env) {
1722
+ return reporterChoice(explicit, env).name;
1723
+ }
1724
+ /** Auto-detected (not asked for) — the run prints a one-line "how to override" hint so a human in an agent terminal isn't surprised by Markdown. */
1714
1725
  function isAutoDetectedAgent(explicit, env = process.env) {
1715
- return !explicit && !isReporterName(env.SVELTE_VITALS_REPORTER) && isAgentEnv(env);
1726
+ const c = reporterChoice(explicit, env);
1727
+ return c.auto && c.name === "agent";
1716
1728
  }
1717
- /**
1718
- * True when the github reporter is chosen purely by GITHUB_ACTIONS auto-detection
1719
- * (no explicit flag, no SVELTE_VITALS_REPORTER, not an agent env). Used to surface
1720
- * a one-line "how to override" hint so an existing CI user isn't surprised by the
1721
- * switch from console output to workflow commands.
1722
- */
1729
+ /** Auto-detected (not asked for) — same hint, so an existing CI user isn't surprised by workflow commands replacing console output. */
1723
1730
  function isAutoDetectedGithub(explicit, env = process.env) {
1724
- return !explicit && !isReporterName(env.SVELTE_VITALS_REPORTER) && !isAgentEnv(env) && isGithubActionsEnv(env);
1731
+ const c = reporterChoice(explicit, env);
1732
+ return c.auto && c.name === "github";
1725
1733
  }
1726
1734
  //#endregion
1727
1735
  //#region src/changed-files.ts
@@ -1917,219 +1925,6 @@ function filterToNewFindings(results, baselineResults, config = defaultConfig) {
1917
1925
  return penalized(results).filter((r) => !baselineKeys.has(findingKey(r)));
1918
1926
  }
1919
1927
  //#endregion
1920
- //#region src/rules-config.ts
1921
- const KNOWN_IDS = new Set(allRules.map((r) => r.id));
1922
- const RULE_BY_ID = new Map(allRules.map((r) => [r.id, r]));
1923
- /** Rule ids passed to --rules/--ignore that aren't part of the built-in registry. */
1924
- function findUnknownRuleIds(ids) {
1925
- return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
1926
- }
1927
- /** All built-in rule ids, sorted — for help and error messages. */
1928
- function knownRuleIds() {
1929
- return [...KNOWN_IDS].sort();
1930
- }
1931
- /** The options a rule declares, or undefined when it takes none. */
1932
- function ruleOptionsSpec(id) {
1933
- return RULE_BY_ID.get(id)?.options;
1934
- }
1935
- /**
1936
- * Stamps the rule registry's provenance onto error messages that list known ids.
1937
- * Two copies of svelte-vitals can coexist in one tree (e.g. the Vite plugin's
1938
- * resolved copy and a directly installed CLI, issue #583); without the versions,
1939
- * a rule that exists in one copy but not the other reads as a mystery rather
1940
- * than a version skew.
1941
- */
1942
- function registryTag() {
1943
- return `svelte-vitals ${readPackageVersion()}, core ${readCoreVersion()}`;
1944
- }
1945
- //#endregion
1946
- //#region src/config-file.ts
1947
- /**
1948
- * Loads `svelte-vitals.config.{js,ts}` from the analyzed directory (design
1949
- * doc: docs/superpowers/specs/2026-07-05-config-file-design.md). Wired into
1950
- * `analyzeProject` (packages/cli/src/index.ts), so every caller of it inherits
1951
- * the config file.
1952
- *
1953
- * Candidate filenames, in priority order. Only `cwd` itself is searched (no
1954
- * upward directory walk — see design doc §1). Exported so the install wizard's
1955
- * config-file scaffolder (install/config-file-format.ts) detects existing
1956
- * configs against the exact same list — a second hand-maintained copy could
1957
- * drift and make the scaffolder create a duplicate config the loader then
1958
- * shadows or ignores.
1959
- */
1960
- const CONFIG_FILENAMES = ["svelte-vitals.config.js", "svelte-vitals.config.ts"];
1961
- /** The extension set `--config` accepts — derived so it cannot drift from discovery's list. */
1962
- const CONFIG_EXTENSIONS = CONFIG_FILENAMES.map((name) => name.slice(name.lastIndexOf(".")));
1963
- const TREAT_DYNAMIC_AS_VALUES = [
1964
- "pass",
1965
- "warn",
1966
- "fail"
1967
- ];
1968
- const FAIL_ON_VALUES = [
1969
- "critical",
1970
- "warning",
1971
- "info"
1972
- ];
1973
- const KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1974
- "treatDynamicAs",
1975
- "metaComponents",
1976
- "rules",
1977
- "failOn",
1978
- "weights",
1979
- "overrides"
1980
- ]);
1981
- /** Whether `value` is a plain object (not null, not an array) — usable with Object.keys/entries. */
1982
- function isPlainObject(value) {
1983
- return typeof value === "object" && value !== null && !Array.isArray(value);
1984
- }
1985
- /**
1986
- * Validate one rule setting — the bare severity string or the object form — and
1987
- * throw on the first problem, prefixed with the config file's path. The rules
1988
- * themselves live in core's `validateRuleSetting`, shared with the Vite plugin's
1989
- * `rules`/`overrides` options so a config file and the equivalent plugin option
1990
- * can't be judged differently. Everything is fatal, on the same reasoning as
1991
- * unknown rule ids — a typo that silently leaves the config inert is the failure
1992
- * being prevented.
1993
- *
1994
- * `baseline`, when given, is the already-resolved options this setting's
1995
- * `options` would be merged onto at run time — the global `rules[id].options`
1996
- * layer, for a setting that lives in `overrides[].rules` (design 2026-07-26
1997
- * review, Finding A). Omitted for the global `rules` layer itself, so its
1998
- * min/max cross-check falls back to the rule's own built-in default.
1999
- */
2000
- function validateSetting(path, where, key, setting, allowOptions, baseline, skipRangeCheck) {
2001
- const errors = validateRuleSetting(`${where}.${key}`, key, setting, ruleOptionsSpec(key), {
2002
- allowOptions,
2003
- ...baseline !== void 0 ? { baseline } : {},
2004
- ...skipRangeCheck !== void 0 ? { skipRangeCheck } : {}
2005
- });
2006
- if (errors.length > 0) throw new Error(`${path}: ${errors.join(" ")}`);
2007
- }
2008
- /**
2009
- * Validate a config file's default export (design doc §4):
2010
- *
2011
- * - **Fatal (thrown)**: unknown rule ids in `rules` (reuses `findUnknownRuleIds`
2012
- * / `knownRuleIds`, same message shape as the `--rules`/`--ignore` error);
2013
- * unknown category keys or negative/non-finite values in `weights`; an
2014
- * invalid rule setting (string or object form) or invalid rule options,
2015
- * in both `rules` and `overrides[].rules` (see `validateSetting`).
2016
- * - **Warning (returned, field dropped)**: invalid enum values for
2017
- * `treatDynamicAs` / `failOn`; unknown top-level keys (forward-compatibility).
2018
- */
2019
- function validateConfigFile(raw, path) {
2020
- const warnings = [];
2021
- const config = {};
2022
- for (const key of Object.keys(raw)) if (!KNOWN_TOP_LEVEL_KEYS.has(key)) warnings.push(`${path}: unknown config key '${key}' ignored.`);
2023
- if (raw.treatDynamicAs !== void 0) if (TREAT_DYNAMIC_AS_VALUES.includes(raw.treatDynamicAs)) config.treatDynamicAs = raw.treatDynamicAs;
2024
- else warnings.push(`${path}: unknown treatDynamicAs '${String(raw.treatDynamicAs)}'; expected pass|warn|fail. Ignoring.`);
2025
- if (raw.failOn !== void 0) if (FAIL_ON_VALUES.includes(raw.failOn)) config.failOn = raw.failOn;
2026
- else warnings.push(`${path}: unknown failOn '${String(raw.failOn)}'; expected critical|warning|info. Ignoring.`);
2027
- if (raw.metaComponents !== void 0) if (Array.isArray(raw.metaComponents) && raw.metaComponents.every((c) => typeof c === "string")) config.metaComponents = raw.metaComponents;
2028
- else warnings.push(`${path}: metaComponents must be an array of strings. Ignoring.`);
2029
- if (raw.rules !== void 0) {
2030
- if (!isPlainObject(raw.rules)) throw new Error(`${path}: rules must be an object of rule-id → setting.`);
2031
- const rules = raw.rules;
2032
- const unknown = findUnknownRuleIds(Object.keys(rules));
2033
- if (unknown.length > 0) throw new Error(`${path}: unknown rule id(s) in rules: ${unknown.join(", ")}. Known rule ids (${registryTag()}): ${knownRuleIds().join(", ")}`);
2034
- for (const [key, setting] of Object.entries(rules)) validateSetting(path, "rules", key, setting, true);
2035
- config.rules = rules;
2036
- }
2037
- if (raw.overrides !== void 0) {
2038
- if (!Array.isArray(raw.overrides)) throw new Error(`${path}: overrides must be an array of { route/files, rules } entries.`);
2039
- const isGlob = (v) => typeof v === "string" && v.length > 0;
2040
- const isGlobs = (v) => isGlob(v) || Array.isArray(v) && v.length > 0 && v.every(isGlob);
2041
- const overrides = [];
2042
- const rawOverrides = raw.overrides;
2043
- rawOverrides.forEach((entry, i) => {
2044
- if (!isPlainObject(entry)) throw new Error(`${path}: overrides[${i}] must be an object with 'route' and/or 'files', and 'rules'.`);
2045
- if (entry.route !== void 0 && !isGlobs(entry.route)) throw new Error(`${path}: overrides[${i}].route must be a non-empty string or a non-empty array of non-empty strings.`);
2046
- if (entry.files !== void 0 && !isGlobs(entry.files)) throw new Error(`${path}: overrides[${i}].files must be a non-empty string or a non-empty array of non-empty strings.`);
2047
- if (entry.route === void 0 && entry.files === void 0) throw new Error(`${path}: overrides[${i}] must set 'route' and/or 'files' to scope the override.`);
2048
- if (!isPlainObject(entry.rules)) throw new Error(`${path}: overrides[${i}].rules must be an object of rule-id/category → setting.`);
2049
- if (Object.keys(entry.rules).length === 0) throw new Error(`${path}: overrides[${i}].rules must contain at least one rule id or category.`);
2050
- const unknown = findUnknownRuleIds(Object.keys(entry.rules).filter((k) => !CATEGORIES.includes(k)));
2051
- if (unknown.length > 0) throw new Error(`${path}: unknown rule id(s) or categories in overrides[${i}].rules: ${unknown.join(", ")}. Known categories: ${CATEGORIES.join(", ")}. Known rule ids (${registryTag()}): ${knownRuleIds().join(", ")}`);
2052
- for (const [key, setting] of Object.entries(entry.rules)) {
2053
- const isCategory = CATEGORIES.includes(key);
2054
- const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), {
2055
- ...defaultConfig,
2056
- rules: config.rules ?? {}
2057
- });
2058
- const skipRangeCheck = shouldSkipRangeCheck(rawOverrides, i, key, setting);
2059
- validateSetting(path, `overrides[${i}].rules`, key, setting, !isCategory, baseline, skipRangeCheck);
2060
- }
2061
- overrides.push({
2062
- ...entry.route !== void 0 ? { route: entry.route } : {},
2063
- ...entry.files !== void 0 ? { files: entry.files } : {},
2064
- rules: entry.rules
2065
- });
2066
- });
2067
- config.overrides = overrides;
2068
- }
2069
- if (raw.weights !== void 0) {
2070
- if (!isPlainObject(raw.weights)) throw new Error(`${path}: weights must be an object of category → number.`);
2071
- const weights = {};
2072
- for (const [rawCat, w] of Object.entries(raw.weights)) {
2073
- const cat = rawCat.toLowerCase();
2074
- if (!CATEGORIES.includes(cat)) throw new Error(`${path}: unknown category '${rawCat}' in weights. Known categories: ${CATEGORIES.join(", ")}`);
2075
- if (typeof w !== "number" || !Number.isFinite(w) || w < 0) throw new Error(`${path}: invalid weight for '${cat}': ${String(w)}; expected a finite number >= 0.`);
2076
- weights[cat] = w;
2077
- }
2078
- config.weights = weights;
2079
- }
2080
- return {
2081
- config,
2082
- warnings
2083
- };
2084
- }
2085
- /**
2086
- * Find and load `svelte-vitals.config.{js,ts}` from `cwd` (only `cwd`, no
2087
- * upward search). Returns `undefined` when no candidate file exists.
2088
- *
2089
- * Loader mechanism (design doc §2): plain native `import()`. `.js` is parsed as
2090
- * ESM (the project must be `"type": "module"` — SvelteKit's default); `.ts`
2091
- * loads via native type-stripping, unflagged on every supported Node
2092
- * (engines.node >=24.16.0).
2093
- *
2094
- * Throws when: the file exists but has no usable default export, or the loaded
2095
- * config fails validation (unknown rule ids in `rules`, invalid `weights`
2096
- * entries).
2097
- */
2098
- async function loadConfigFile(cwd) {
2099
- const found = CONFIG_FILENAMES.map((name) => join(cwd, name)).find((path) => existsSync(path));
2100
- if (!found) {
2101
- const retired = join(cwd, "svelte-vitals.config.mjs");
2102
- if (existsSync(retired)) throw new Error(`${retired} is no longer read — svelte-vitals loads svelte-vitals.config.{js,ts} only. Rename the file to .js (the project must be "type": "module") or .ts.`);
2103
- return;
2104
- }
2105
- return loadFrom(found);
2106
- }
2107
- /**
2108
- * Load and validate a config file the caller named (`--config`) instead of one discovered in
2109
- * `cwd`. Same loader, same validation; the difference is what absence means — the caller chose
2110
- * this file, so a missing one is fatal where a missing discovered file is simply "no config".
2111
- * The extension is checked first, and before the disk is touched: discovery narrowed to
2112
- * `{js,ts}` and kept a loud tripwire for `.mjs`, and a by-path loader that went straight to
2113
- * `import()` would quietly accept the file that tripwire exists to reject.
2114
- */
2115
- async function loadConfigFromPath(path) {
2116
- if (!CONFIG_EXTENSIONS.some((ext) => path.endsWith(ext))) throw new Error(`${path} is not a supported config file — svelte-vitals loads ${CONFIG_EXTENSIONS.join(" and ")} only.`);
2117
- if (!existsSync(path)) throw new Error(`${path} does not exist.`);
2118
- return loadFrom(path);
2119
- }
2120
- /** Import one known-present config file and validate it. Shared by discovery and `--config`. */
2121
- async function loadFrom(path) {
2122
- let mod;
2123
- try {
2124
- mod = await import(pathToFileURL(path).href);
2125
- } catch (err) {
2126
- if (path.endsWith(".js") && err instanceof SyntaxError) throw new Error(`could not load ${path}: ${err.message} — config files are ESM, so the nearest package.json above the config file needs "type": "module" (SvelteKit's default), or use a .ts config.`, { cause: err });
2127
- throw err;
2128
- }
2129
- if (!isPlainObject(mod.default)) throw new Error(`${path} must have a default export that is a plain object (e.g. \`export default defineConfig({...})\` or a plain object literal).`);
2130
- return validateConfigFile(mod.default, path);
2131
- }
2132
- //#endregion
2133
1928
  //#region src/suppressions.ts
2134
1929
  /**
2135
1930
  * Persistent adoption ramp (design doc 2026-07-13-suppressions-file-design.md):
@@ -2659,6 +2454,221 @@ async function playMascotGreeting(opts) {
2659
2454
  render.clear();
2660
2455
  }
2661
2456
  //#endregion
2457
+ //#region src/rules-config.ts
2458
+ const KNOWN_IDS = new Set(allRules.map((r) => r.id));
2459
+ const RULE_BY_ID = new Map(allRules.map((r) => [r.id, r]));
2460
+ /** Rule ids passed to --rules/--ignore that aren't part of the built-in registry. */
2461
+ function findUnknownRuleIds(ids) {
2462
+ return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
2463
+ }
2464
+ /** All built-in rule ids, sorted — for help and error messages. */
2465
+ function knownRuleIds() {
2466
+ return [...KNOWN_IDS].sort();
2467
+ }
2468
+ /** The options a rule declares, or undefined when it takes none. */
2469
+ function ruleOptionsSpec(id) {
2470
+ return RULE_BY_ID.get(id)?.options;
2471
+ }
2472
+ /**
2473
+ * Stamps the rule registry's provenance onto error messages that list known ids.
2474
+ * Two copies of svelte-vitals can coexist in one tree (e.g. the Vite plugin's
2475
+ * resolved copy and a directly installed CLI, issue #583); without the versions,
2476
+ * a rule that exists in one copy but not the other reads as a mystery rather
2477
+ * than a version skew.
2478
+ */
2479
+ function registryTag() {
2480
+ return `svelte-vitals ${readPackageVersion()}, core ${readCoreVersion()}`;
2481
+ }
2482
+ //#endregion
2483
+ //#region src/config-file.ts
2484
+ /**
2485
+ * Loads `svelte-vitals.config.{js,ts}` from the analyzed directory (design
2486
+ * doc: docs/superpowers/specs/2026-07-05-config-file-design.md). Wired into
2487
+ * `analyzeProject` (packages/cli/src/index.ts), so every caller of it inherits
2488
+ * the config file.
2489
+ *
2490
+ * Candidate filenames, in priority order. Only `cwd` itself is searched (no
2491
+ * upward directory walk — see design doc §1). Exported so the install wizard's
2492
+ * config-file scaffolder (install/config-file-format.ts) detects existing
2493
+ * configs against the exact same list — a second hand-maintained copy could
2494
+ * drift and make the scaffolder create a duplicate config the loader then
2495
+ * shadows or ignores.
2496
+ */
2497
+ const CONFIG_FILENAMES = ["svelte-vitals.config.js", "svelte-vitals.config.ts"];
2498
+ /** The extension set `--config` accepts — derived so it cannot drift from discovery's list. */
2499
+ const CONFIG_EXTENSIONS = CONFIG_FILENAMES.map((name) => name.slice(name.lastIndexOf(".")));
2500
+ const TREAT_DYNAMIC_AS_VALUES = [
2501
+ "pass",
2502
+ "warn",
2503
+ "fail"
2504
+ ];
2505
+ const FAIL_ON_VALUES = [
2506
+ "critical",
2507
+ "warning",
2508
+ "info"
2509
+ ];
2510
+ const KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
2511
+ "treatDynamicAs",
2512
+ "metaComponents",
2513
+ "rules",
2514
+ "failOn",
2515
+ "weights",
2516
+ "overrides"
2517
+ ]);
2518
+ /**
2519
+ * Validate one rule setting — the bare severity string or the object form — and
2520
+ * throw on the first problem, prefixed with the config file's path. The rules
2521
+ * themselves live in core's `validateRuleSetting`, shared with the Vite plugin's
2522
+ * `rules`/`overrides` options so a config file and the equivalent plugin option
2523
+ * can't be judged differently. Everything is fatal, on the same reasoning as
2524
+ * unknown rule ids — a typo that silently leaves the config inert is the failure
2525
+ * being prevented.
2526
+ *
2527
+ * `baseline`, when given, is the already-resolved options this setting's
2528
+ * `options` would be merged onto at run time — the global `rules[id].options`
2529
+ * layer, for a setting that lives in `overrides[].rules` (design 2026-07-26
2530
+ * review, Finding A). Omitted for the global `rules` layer itself, so its
2531
+ * min/max cross-check falls back to the rule's own built-in default.
2532
+ */
2533
+ function validateSetting(path, where, key, setting, allowOptions, baseline, skipRangeCheck) {
2534
+ const errors = validateRuleSetting(`${where}.${key}`, key, setting, ruleOptionsSpec(key), {
2535
+ allowOptions,
2536
+ ...baseline !== void 0 ? { baseline } : {},
2537
+ ...skipRangeCheck !== void 0 ? { skipRangeCheck } : {}
2538
+ });
2539
+ if (errors.length > 0) throw new Error(`${path}: ${errors.join(" ")}`);
2540
+ }
2541
+ /**
2542
+ * Validate a config file's default export (design doc §4):
2543
+ *
2544
+ * - **Fatal (thrown)**: unknown rule ids in `rules` (reuses `findUnknownRuleIds`
2545
+ * / `knownRuleIds`, same message shape as the `--rules`/`--ignore` error);
2546
+ * unknown category keys or negative/non-finite values in `weights`; an
2547
+ * invalid rule setting (string or object form) or invalid rule options,
2548
+ * in both `rules` and `overrides[].rules` (see `validateSetting`).
2549
+ * - **Warning (returned, field dropped)**: invalid enum values for
2550
+ * `treatDynamicAs` / `failOn`; unknown top-level keys (forward-compatibility).
2551
+ */
2552
+ function validateConfigFile(raw, path) {
2553
+ const warnings = [];
2554
+ const config = {};
2555
+ for (const key of Object.keys(raw)) if (!KNOWN_TOP_LEVEL_KEYS.has(key)) warnings.push(`${path}: unknown config key '${key}' ignored.`);
2556
+ if (raw.treatDynamicAs !== void 0) if (TREAT_DYNAMIC_AS_VALUES.includes(raw.treatDynamicAs)) config.treatDynamicAs = raw.treatDynamicAs;
2557
+ else warnings.push(`${path}: unknown treatDynamicAs '${String(raw.treatDynamicAs)}'; expected pass|warn|fail. Ignoring.`);
2558
+ if (raw.failOn !== void 0) if (FAIL_ON_VALUES.includes(raw.failOn)) config.failOn = raw.failOn;
2559
+ else warnings.push(`${path}: unknown failOn '${String(raw.failOn)}'; expected critical|warning|info. Ignoring.`);
2560
+ if (raw.metaComponents !== void 0) if (Array.isArray(raw.metaComponents) && raw.metaComponents.every((c) => typeof c === "string")) config.metaComponents = raw.metaComponents;
2561
+ else warnings.push(`${path}: metaComponents must be an array of strings. Ignoring.`);
2562
+ if (raw.rules !== void 0) {
2563
+ if (!isPlainObject(raw.rules)) throw new Error(`${path}: rules must be an object of rule-id → setting.`);
2564
+ const rules = raw.rules;
2565
+ const unknown = findUnknownRuleIds(Object.keys(rules));
2566
+ if (unknown.length > 0) throw new Error(`${path}: unknown rule id(s) in rules: ${unknown.join(", ")}. Known rule ids (${registryTag()}): ${knownRuleIds().join(", ")}`);
2567
+ for (const [key, setting] of Object.entries(rules)) validateSetting(path, "rules", key, setting, true);
2568
+ config.rules = rules;
2569
+ }
2570
+ if (raw.overrides !== void 0) {
2571
+ if (!Array.isArray(raw.overrides)) throw new Error(`${path}: overrides must be an array of { route/files, rules } entries.`);
2572
+ const isGlob = (v) => typeof v === "string" && v.length > 0;
2573
+ const isGlobs = (v) => isGlob(v) || Array.isArray(v) && v.length > 0 && v.every(isGlob);
2574
+ const overrides = [];
2575
+ const rawOverrides = raw.overrides;
2576
+ rawOverrides.forEach((entry, i) => {
2577
+ if (!isPlainObject(entry)) throw new Error(`${path}: overrides[${i}] must be an object with 'route' and/or 'files', and 'rules'.`);
2578
+ if (entry.route !== void 0 && !isGlobs(entry.route)) throw new Error(`${path}: overrides[${i}].route must be a non-empty string or a non-empty array of non-empty strings.`);
2579
+ if (entry.files !== void 0 && !isGlobs(entry.files)) throw new Error(`${path}: overrides[${i}].files must be a non-empty string or a non-empty array of non-empty strings.`);
2580
+ if (entry.route === void 0 && entry.files === void 0) throw new Error(`${path}: overrides[${i}] must set 'route' and/or 'files' to scope the override.`);
2581
+ if (!isPlainObject(entry.rules)) throw new Error(`${path}: overrides[${i}].rules must be an object of rule-id/category → setting.`);
2582
+ if (Object.keys(entry.rules).length === 0) throw new Error(`${path}: overrides[${i}].rules must contain at least one rule id or category.`);
2583
+ const unknown = findUnknownRuleIds(Object.keys(entry.rules).filter((k) => !CATEGORIES.includes(k)));
2584
+ if (unknown.length > 0) throw new Error(`${path}: unknown rule id(s) or categories in overrides[${i}].rules: ${unknown.join(", ")}. Known categories: ${CATEGORIES.join(", ")}. Known rule ids (${registryTag()}): ${knownRuleIds().join(", ")}`);
2585
+ for (const [key, setting] of Object.entries(entry.rules)) {
2586
+ const isCategory = CATEGORIES.includes(key);
2587
+ const baseline = isCategory ? void 0 : resolveRuleOptions(key, ruleOptionsSpec(key), {
2588
+ ...defaultConfig,
2589
+ rules: config.rules ?? {}
2590
+ });
2591
+ const skipRangeCheck = shouldSkipRangeCheck(rawOverrides, i, key, setting);
2592
+ validateSetting(path, `overrides[${i}].rules`, key, setting, !isCategory, baseline, skipRangeCheck);
2593
+ }
2594
+ overrides.push({
2595
+ ...entry.route !== void 0 ? { route: entry.route } : {},
2596
+ ...entry.files !== void 0 ? { files: entry.files } : {},
2597
+ rules: entry.rules
2598
+ });
2599
+ });
2600
+ config.overrides = overrides;
2601
+ }
2602
+ if (raw.weights !== void 0) {
2603
+ if (!isPlainObject(raw.weights)) throw new Error(`${path}: weights must be an object of category → number.`);
2604
+ const weights = {};
2605
+ for (const [rawCat, w] of Object.entries(raw.weights)) {
2606
+ const cat = rawCat.toLowerCase();
2607
+ if (!CATEGORIES.includes(cat)) throw new Error(`${path}: unknown category '${rawCat}' in weights. Known categories: ${CATEGORIES.join(", ")}`);
2608
+ if (typeof w !== "number" || !Number.isFinite(w) || w < 0) throw new Error(`${path}: invalid weight for '${cat}': ${String(w)}; expected a finite number >= 0.`);
2609
+ weights[cat] = w;
2610
+ }
2611
+ config.weights = weights;
2612
+ }
2613
+ return {
2614
+ config,
2615
+ warnings
2616
+ };
2617
+ }
2618
+ /**
2619
+ * Find and load `svelte-vitals.config.{js,ts}` from `cwd` (only `cwd`, no
2620
+ * upward search). Returns `undefined` when no candidate file exists.
2621
+ *
2622
+ * Loader mechanism (design doc §2): plain native `import()`. `.js` is parsed as
2623
+ * ESM (the project must be `"type": "module"` — SvelteKit's default); `.ts`
2624
+ * loads via native type-stripping, unflagged on every supported Node
2625
+ * (engines.node >=24.16.0).
2626
+ *
2627
+ * Throws when: the file exists but has no usable default export, or the loaded
2628
+ * config fails validation (unknown rule ids in `rules`, invalid `weights`
2629
+ * entries).
2630
+ */
2631
+ async function loadConfigFile(cwd) {
2632
+ const found = CONFIG_FILENAMES.map((name) => join(cwd, name)).find((path) => existsSync(path));
2633
+ if (!found) {
2634
+ const retired = join(cwd, "svelte-vitals.config.mjs");
2635
+ if (existsSync(retired)) throw new Error(`${retired} is no longer read — svelte-vitals loads svelte-vitals.config.{js,ts} only. Rename the file to .js (the project must be "type": "module") or .ts.`);
2636
+ return;
2637
+ }
2638
+ return loadFrom(found);
2639
+ }
2640
+ /**
2641
+ * Load and validate a config file the caller named (`--config`) instead of one discovered in
2642
+ * `cwd`. Same loader, same validation; the difference is what absence means — the caller chose
2643
+ * this file, so a missing one is fatal where a missing discovered file is simply "no config".
2644
+ * The extension is checked first, and before the disk is touched: discovery narrowed to
2645
+ * `{js,ts}` and kept a loud tripwire for `.mjs`, and a by-path loader that went straight to
2646
+ * `import()` would quietly accept the file that tripwire exists to reject.
2647
+ */
2648
+ async function loadConfigFromPath(path) {
2649
+ if (!CONFIG_EXTENSIONS.some((ext) => path.endsWith(ext))) throw new Error(`${path} is not a supported config file — svelte-vitals loads ${CONFIG_EXTENSIONS.join(" and ")} only.`);
2650
+ if (!existsSync(path)) throw new Error(`${path} does not exist.`);
2651
+ return loadFrom(path);
2652
+ }
2653
+ /** Import one known-present config file and validate it. Shared by discovery and `--config`. */
2654
+ async function loadFrom(path) {
2655
+ let mod;
2656
+ try {
2657
+ let digest = createHash("sha1").update(readFileSync(path)).digest("hex").slice(0, 16);
2658
+ for (let attempt = 0;; attempt++) {
2659
+ mod = await import(`${pathToFileURL(path).href}?v=${digest}`);
2660
+ const digestAfter = createHash("sha1").update(readFileSync(path)).digest("hex").slice(0, 16);
2661
+ if (digestAfter === digest || attempt >= 2) break;
2662
+ digest = digestAfter;
2663
+ }
2664
+ } catch (err) {
2665
+ if (path.endsWith(".js") && err instanceof SyntaxError) throw new Error(`could not load ${path}: ${err.message} — config files are ESM, so the nearest package.json above the config file needs "type": "module" (SvelteKit's default), or use a .ts config.`, { cause: err });
2666
+ throw err;
2667
+ }
2668
+ if (!isPlainObject(mod.default)) throw new Error(`${path} must have a default export that is a plain object (e.g. \`export default defineConfig({...})\` or a plain object literal).`);
2669
+ return validateConfigFile(mod.default, path);
2670
+ }
2671
+ //#endregion
2662
2672
  //#region src/pulse-animation.ts
2663
2673
  const FRAME_COUNT = 6;
2664
2674
  const FRAME_DELAY_MS = 200;
@@ -3136,4 +3146,4 @@ async function run(opts = {}) {
3136
3146
  }
3137
3147
  }
3138
3148
  //#endregion
3139
- export { hasDep as _, spinnerEnabled as a, ProjectError as b, findUnknownRuleIds as c, ruleOptionsSpec as d, REPORTER_NAMES as f, discoverApps as g, readPackageVersion as h, run as i, knownRuleIds as l, readCoreVersion as m, applyScope as n, CONFIG_FILENAMES as o, isReporterName as p, defineConfig$1 as r, loadConfigFile as s, analyzeProject as t, registryTag as u, readPkg as v, routeMatcher as y };
3149
+ export { hasDep as _, spinnerEnabled as a, ProjectError as b, findUnknownRuleIds as c, ruleOptionsSpec as d, REPORTER_NAMES as f, discoverApps as g, readPackageVersion as h, run as i, knownRuleIds as l, readCoreVersion as m, applyScope as n, CONFIG_FILENAMES as o, isReporterName as p, defineConfig$1 as r, loadConfigFile as s, analyzeProject as t, registryTag as u, readPkg as v, createNodeRuntime as x, routeMatcher as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.54.2",
3
+ "version": "0.54.4",
4
4
  "description": "A deterministic SvelteKit code-health scanner (SEO, performance, correctness, security, architecture, accessibility).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "dist"
44
44
  ],
45
45
  "dependencies": {
46
- "@svelte-vitals/core": "0.51.0",
46
+ "@svelte-vitals/core": "0.51.2",
47
47
  "@clack/prompts": "^1.7.0",
48
48
  "@gunshi/plugin-completion": "0.37.1",
49
49
  "@gunshi/plugin-i18n": "0.37.1",