svelte-vitals 0.37.0 → 0.37.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 +1 -1
- package/dist/cli-QRSD7FSW.js +120 -0
- package/package.json +2 -2
- package/dist/cli-SOIIGBH7.js +0 -120
package/dist/bin.js
CHANGED
|
@@ -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-
|
|
1763
|
+
const { runDocsCli } = await import("./cli-QRSD7FSW.js");
|
|
1764
1764
|
process.exitCode = runDocsCli(rawArgs.slice(1));
|
|
1765
1765
|
return;
|
|
1766
1766
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import {
|
|
2
|
+
consoleIO
|
|
3
|
+
} from "./chunk-SLUMRYUD.js";
|
|
4
|
+
|
|
5
|
+
// src/docs/cli.ts
|
|
6
|
+
import mri from "mri";
|
|
7
|
+
|
|
8
|
+
// src/docs/generated.ts
|
|
9
|
+
var EMBEDDED_DOCS = [
|
|
10
|
+
{
|
|
11
|
+
name: "ci",
|
|
12
|
+
title: "Running in CI",
|
|
13
|
+
description: "Scaffold a GitHub Actions PR gate with `ci install`, what the generated workflow does, and how to gate a pipeline without the Action.",
|
|
14
|
+
body: "# Running in CI\n\n## Scaffold the GitHub Actions gate\n\n```bash\nnpx svelte-vitals@latest ci install # writes .github/workflows/svelte-vitals.yml\nnpx svelte-vitals@latest ci upgrade # bump only the pinned action ref in an existing file\n```\n\n`ci install` also exists as the `ci-workflow` target inside `svelte-vitals install`, so it can be\nset up in the same pass as everything else. Both support `--dry-run` and `--force`.\n\n## What the generated workflow does\n\nOn every `pull_request` it checks out with `fetch-depth: 0`, then calls `@svelte-vitals/action`,\nwhich runs the analysis **in-process** scoped to the PR (`diff`/`baseline` at `origin/<base>`).\nFrom that one analysis it produces:\n\n- inline annotations on the diff,\n- a job summary,\n- a sticky PR comment (a hidden `<!-- svelte-vitals-report -->` marker updates the same comment\n instead of piling up new ones).\n\nIt fails the job **after** writing those, so a failing run still leaves the feedback behind.\n\nInputs: `path` (default `.`), `diff`, `baseline`, `github-token`. No `reporter` input \u2014 the\nfan-out is fixed. It reads your committed `svelte-vitals.config.*` and\n`svelte-vitals-suppressions.json`, so rule policy stays in those files.\n\nPermissions: `contents: read`, `pull-requests: write`. On fork PRs GitHub downgrades the token\nregardless, so the action skips the comment (never failing the job); annotations and summary\nstill work.\n\n## Without the Action\n\nAny CI can run the CLI directly. On GitHub, `GITHUB_ACTIONS=true` auto-selects the `github`\nreporter, so annotations come for free \u2014 but a detected AI-agent environment outranks it, so a\njob driven by an agent gets `agent` instead. Pass `--reporter github` when you need the\nannotations regardless of who is running the job:\n\n```bash\npnpm build\nnpx svelte-vitals@latest --reporter github --fail-on warning\n```\n\nFor a PR gate that ignores a legacy backlog, pair the two scoping flags:\n\n```bash\nnpx svelte-vitals@latest --diff origin/main --baseline origin/main --fail-on warning\n```\n\nGate on the score instead of, or as well as, individual findings with `--min-health <0-100>`.\n\nExit `1` means findings gated the run; exit `2` means the run did not happen \u2014 fail the job\nloudly on `2` rather than treating it as a pass.\n\n## Related\n\n- `svelte-vitals docs show scoping` \u2014 what `--diff` / `--baseline` / suppressions each do\n- `svelte-vitals docs show output` \u2014 reporters and exit codes"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "config",
|
|
18
|
+
title: "The config file",
|
|
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"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "monorepo",
|
|
24
|
+
title: "Monorepos",
|
|
25
|
+
description: "How svelte-vitals picks which SvelteKit app to analyze, why it exits 2 instead of prompting in a non-interactive shell, and how to name the app explicitly.",
|
|
26
|
+
body: '# Monorepos\n\n## Naming the app is always safest\n\n```bash\nnpx svelte-vitals@latest apps/web\n```\n\nAn explicit `path` \u2014 or running from inside the app directory \u2014 skips detection entirely. Prefer\nit in a script, a hook, or an agent\'s shell.\n\n## What happens without a path\n\nWhen no `path` is given and the current directory is not itself a SvelteKit app, svelte-vitals\nlooks for nearby apps \u2014 a directory with `src/routes` **and** either a\n`svelte.config.{js,ts}` or a `package.json` declaring `@sveltejs/kit` (current `sv create`\noutput folds the SvelteKit config into `vite.config.ts` and emits no `svelte.config` file):\n\n| Found | Interactive terminal | Non-interactive (CI, agents, piped output) |\n| ----------- | ----------------------------------- | ---------------------------------------------------------- |\n| exactly one | analyzed, notice on stderr | same \u2014 analyzed, notice on stderr |\n| several | single-select prompt | **exit `2`** listing the apps, asking for an explicit path |\n| none | exit `2`, "not a SvelteKit project" | same |\n\n**svelte-vitals never prompts when stdout is not a TTY** \u2014 it fails fast with the list instead of\nhanging or guessing. Re-run with the path it printed. Cancelling the interactive prompt exits `0`.\n\n## `install` in a monorepo\n\n`svelte-vitals install` splits its targets by where they belong:\n\n- `vite-plugin`, `vite-hooks`, `config-file` write into the **app** directory \u2014 they resolve it\n the same way the analyzer does, and `--app <dir>` names it explicitly.\n- the agent skills and `ci-workflow` always write at the **current** directory, because the repo\n root is their correct home.\n\n```bash\nnpx svelte-vitals@latest install --client vite-plugin,config-file --app apps/web --yes\n```\n\n`--app` pointing at a directory that is not a SvelteKit app is an error (exit `2`).\n\n## Related\n\n- `svelte-vitals docs show output` \u2014 what exit `2` means versus exit `1`'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "output",
|
|
30
|
+
title: "Reading the output",
|
|
31
|
+
description: "Which reporter to use, how one is auto-selected, what goes to stdout vs stderr, and what each exit code means.",
|
|
32
|
+
body: "# Reading the output\n\n## Pick a reporter\n\n`--reporter <fmt>`: `console` (default) \xB7 `json` \xB7 `agent` \xB7 `sarif` \xB7 `github` \xB7 `html` \xB7 `md`.\n\n- **`agent`** \u2014 Markdown remediation: each failing finding with its location, a concrete fix\n (with snippet), and an acceptance check. Use this when something will act on the findings.\n- **`json`** \u2014 the full structured report (scores per route and site-wide; every finding with\n `fix`, `recommendation`, `docsUrl`). Use it to filter or count.\n- **`console`** \u2014 for a human. Grouped and capped; `--verbose` for everything.\n- **`md`** \u2014 a compact summary table for a PR comment or job summary (capped at 50 rows).\n- **`sarif`** \u2014 SARIF v2.1, for GitHub Code Scanning and other SAST tooling.\n- **`github`** \u2014 `::error` / `::warning` workflow annotations.\n- **`html`** \u2014 a self-contained report file; `--out-file <path>`, or `--out-file -` for stdout.\n\n## Auto-selection\n\nFirst match wins:\n\n1. an explicit `--reporter <fmt>`\n2. `SVELTE_VITALS_REPORTER=<fmt>`\n3. a known AI-agent environment (e.g. `CLAUDECODE` is set) \u2192 `agent`\n4. `GITHUB_ACTIONS=true` \u2192 `github`\n5. otherwise \u2192 `console`\n\nInside an agent harness you get `agent` without asking; a one-line override hint goes to stderr.\n\n## stdout vs stderr\n\nThe report goes to **stdout**; every diagnostic \u2014 hints, suppression counts, app-detection\nnotices, warnings, errors \u2014 to **stderr**. Piping stdout never mixes the two.\n\n`--reporter html` is the exception: it writes a file and prints the path to stderr, unless you\npass `--out-file -`.\n\n## Exit codes\n\n| Code | Meaning |\n| ---- | -------------------------------------------------------------------------- |\n| `0` | no failing findings |\n| `1` | a critical finding is present, or `--fail-on` / `--min-health` was reached |\n| `2` | execution error \u2014 not a SvelteKit project, bad flag, unreadable config |\n\n`2` is never a clean result \u2014 the run did not happen. `--fail-on <critical|warning|info>` lowers\nthe bar for `1`; `--min-health <0-100>` adds a score gate.\n\n## Related\n\n- `svelte-vitals docs show scoping` \u2014 report only what a change introduced\n- `svelte-vitals explain <rule-id>` \u2014 one rule's rationale, fix and options"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "scoping",
|
|
36
|
+
title: "Scoping findings to a change",
|
|
37
|
+
description: "Use --diff, --staged, --baseline and the suppressions file so only what a change introduced is reported, instead of a legacy backlog.",
|
|
38
|
+
body: "# Scoping findings to a change\n\nAn existing project usually has a backlog nobody is about to fix. Scope the report rather than\ndisabling rules.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\nReports only findings **not already present** at `ref`. Scopes by finding identity rather than by\nfile, so a pre-existing problem in a file you touched does not fail the gate. No default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nIt checks `ref` out into a temporary worktree and subtracts those findings. On failure (no git,\nbad ref) it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\nThis analyzes the whole project (`--diff`/`--staged`/`--baseline` are ignored), writes every\npenalized finding, and exits `0` without a report.\n\nThe file then applies automatically on every run, after `--diff`/`--staged` and `--baseline`.\nFixing an accepted finding leaves a **stale** entry, reported on stderr but never failing the run.\n`--no-suppressions` ignores the file for one run.\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// src/docs/cli.ts
|
|
43
|
+
var DOCS_HELP = `svelte-vitals docs \u2014 read the bundled guides without leaving the terminal
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
svelte-vitals docs list [--json] List every topic with a one-line description
|
|
47
|
+
svelte-vitals docs show <name> Print a topic
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--json Machine-readable output (list only)
|
|
51
|
+
-h, --help Show this help
|
|
52
|
+
|
|
53
|
+
The topics ship inside the CLI, so they always match the version you are running and need no
|
|
54
|
+
network. The full docs site is at https://oekazuma.github.io/svelte-vitals.
|
|
55
|
+
|
|
56
|
+
\`docs\` is a subcommand, so it wins over a directory of the same name: to analyze a directory
|
|
57
|
+
called \`docs\`, write \`svelte-vitals ./docs\`.`;
|
|
58
|
+
function knownTopicNames() {
|
|
59
|
+
return EMBEDDED_DOCS.map((d) => d.name).join(", ");
|
|
60
|
+
}
|
|
61
|
+
function renderList() {
|
|
62
|
+
const width = Math.max(...EMBEDDED_DOCS.map((d) => d.name.length));
|
|
63
|
+
const lines = EMBEDDED_DOCS.map((d) => ` ${d.name.padEnd(width)} ${d.description}`);
|
|
64
|
+
return [
|
|
65
|
+
"Topics (read one with `svelte-vitals docs show <name>`):",
|
|
66
|
+
"",
|
|
67
|
+
...lines,
|
|
68
|
+
"",
|
|
69
|
+
"Rule-level detail is a separate command: `svelte-vitals explain --list`."
|
|
70
|
+
].join("\n");
|
|
71
|
+
}
|
|
72
|
+
function runDocsCli(args, io = consoleIO) {
|
|
73
|
+
const argv = mri(args, { boolean: ["json", "help"], alias: { h: "help" } });
|
|
74
|
+
const [sub, ...rest] = argv._;
|
|
75
|
+
if (argv.help) {
|
|
76
|
+
io.log(DOCS_HELP);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (sub === void 0) {
|
|
80
|
+
io.errorLog(DOCS_HELP);
|
|
81
|
+
return 2;
|
|
82
|
+
}
|
|
83
|
+
if (sub === "list") {
|
|
84
|
+
if (rest.length > 0) {
|
|
85
|
+
io.errorLog("svelte-vitals: docs list takes no arguments; use `docs show <name>` to read one.");
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
io.log(
|
|
89
|
+
argv.json ? JSON.stringify(
|
|
90
|
+
EMBEDDED_DOCS.map((d) => ({ name: d.name, title: d.title, description: d.description })),
|
|
91
|
+
null,
|
|
92
|
+
2
|
|
93
|
+
) : renderList()
|
|
94
|
+
);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (sub === "show") {
|
|
98
|
+
if (rest.length !== 1) {
|
|
99
|
+
io.errorLog(
|
|
100
|
+
rest.length === 0 ? "svelte-vitals: docs show needs a topic name, e.g. `svelte-vitals docs show config`." : "svelte-vitals: docs show takes one topic at a time."
|
|
101
|
+
);
|
|
102
|
+
io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
const doc = EMBEDDED_DOCS.find((d) => d.name === rest[0]);
|
|
106
|
+
if (!doc) {
|
|
107
|
+
io.errorLog(`svelte-vitals: unknown docs topic '${rest[0]}'.`);
|
|
108
|
+
io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
io.log(doc.body);
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
io.errorLog(`svelte-vitals: unknown docs subcommand '${sub}'; expected list|show.`);
|
|
115
|
+
io.errorLog(DOCS_HELP);
|
|
116
|
+
return 2;
|
|
117
|
+
}
|
|
118
|
+
export {
|
|
119
|
+
runDocsCli
|
|
120
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.1",
|
|
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.
|
|
48
|
+
"@svelte-vitals/core": "0.32.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/estree": "^1.0.9",
|
package/dist/cli-SOIIGBH7.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
consoleIO
|
|
3
|
-
} from "./chunk-SLUMRYUD.js";
|
|
4
|
-
|
|
5
|
-
// src/docs/cli.ts
|
|
6
|
-
import mri from "mri";
|
|
7
|
-
|
|
8
|
-
// src/docs/generated.ts
|
|
9
|
-
var EMBEDDED_DOCS = [
|
|
10
|
-
{
|
|
11
|
-
name: "ci",
|
|
12
|
-
title: "Running in CI",
|
|
13
|
-
description: "Scaffold a GitHub Actions PR gate with `ci install`, what the generated workflow does, and how to gate a pipeline without the Action.",
|
|
14
|
-
body: "# Running in CI\n\n## Scaffold the GitHub Actions gate\n\n```bash\nnpx svelte-vitals@latest ci install # writes .github/workflows/svelte-vitals.yml\nnpx svelte-vitals@latest ci upgrade # bump only the pinned action ref in an existing file\n```\n\n`ci install` also exists as the `ci-workflow` target inside `svelte-vitals install`, so it can be\nset up in the same pass as everything else. Both support `--dry-run` and `--force`.\n\n## What the generated workflow does\n\nOn every `pull_request` it checks out with `fetch-depth: 0` (so the base ref is resolvable), then\ncalls `@svelte-vitals/action`, which runs the analysis **in-process** \u2014 no `npx`, no separate\nscan per output \u2014 scoped to the PR with `diff: origin/<base>` and `baseline: origin/<base>`. From\nthat one analysis it produces:\n\n- inline annotations on the diff,\n- a job summary,\n- a sticky PR comment (a hidden `<!-- svelte-vitals-report -->` marker updates the same comment\n instead of piling up new ones).\n\nIt fails the job **after** the summary and comment are written, so a failing run still leaves the\nfeedback behind.\n\nAction inputs: `path` (default `.`), `diff`, `baseline`, `github-token`. There is no `reporter`\ninput \u2014 the fan-out is fixed. The action reads your committed `svelte-vitals.config.*` and\n`svelte-vitals-suppressions.json` like the CLI does, so rule policy stays in those files.\n\nRequired permissions: `contents: read` and `pull-requests: write`. On PRs from forks GitHub\ndowngrades the token regardless, so the action detects that and skips the comment (never failing\nthe job); annotations and the summary still work.\n\n## Without the Action\n\nAny CI can run the CLI directly. On GitHub, `GITHUB_ACTIONS=true` auto-selects the `github`\nreporter, so annotations come for free \u2014 but a detected AI-agent environment outranks it, so a\njob driven by an agent gets `agent` instead. Pass `--reporter github` when you need the\nannotations regardless of who is running the job:\n\n```bash\npnpm build\nnpx svelte-vitals@latest --reporter github --fail-on warning\n```\n\nFor a PR gate that ignores a legacy backlog, pair the two scoping flags:\n\n```bash\nnpx svelte-vitals@latest --diff origin/main --baseline origin/main --fail-on warning\n```\n\nGate on the score instead of, or as well as, individual findings with `--min-health <0-100>`.\n\nExit `1` means findings gated the run; exit `2` means the run did not happen \u2014 fail the job\nloudly on `2` rather than treating it as a pass.\n\n## Related\n\n- `svelte-vitals docs show scoping` \u2014 what `--diff` / `--baseline` / suppressions each do\n- `svelte-vitals docs show output` \u2014 reporters and exit codes"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
name: "config",
|
|
18
|
-
title: "The config file",
|
|
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\na **runtime** import \u2014 it requires svelte-vitals to be a declared dependency, and Node 22.18+\n(or 23.6+) to load `.ts` at all. A plain `export default {}` in `.mjs` behaves identically and\nalways 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\nBefore disabling a rule, check whether it is really a **threshold disagreement** rather than a\ndefect \u2014 many rules take options. `svelte-vitals explain <rule-id>` prints each option's name,\ndefault, bounds, and how a configured value merges with the built-in default (`integer`\nreplaces it, `string-list` appends to it, `string-map` is spread over it).\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\nentries 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, an unknown category or negative weight, a malformed `overrides` entry, or an\ninvalid rule setting is a **hard error (exit `2`)** \u2014 a typo must not silently un-gate CI. An\nunrecognized `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
|
-
},
|
|
22
|
-
{
|
|
23
|
-
name: "monorepo",
|
|
24
|
-
title: "Monorepos",
|
|
25
|
-
description: "How svelte-vitals picks which SvelteKit app to analyze, why it exits 2 instead of prompting in a non-interactive shell, and how to name the app explicitly.",
|
|
26
|
-
body: '# Monorepos\n\n## Naming the app is always safest\n\n```bash\nnpx svelte-vitals@latest apps/web\n```\n\nAn explicit `path` \u2014 or running from inside the app directory \u2014 takes priority and skips\ndetection entirely. In a script, a hook, or an agent\'s shell, prefer this over relying on\ndetection.\n\n## What happens without a path\n\nWhen no `path` is given and the current directory is not itself a SvelteKit app, svelte-vitals\nlooks for nearby apps \u2014 a directory with `src/routes` **and** either a\n`svelte.config.{js,ts}` or a `package.json` declaring `@sveltejs/kit` (current `sv create`\noutput folds the SvelteKit config into `vite.config.ts` and emits no `svelte.config` file):\n\n| Found | Interactive terminal | Non-interactive (CI, agents, piped output) |\n| ----------- | ----------------------------------- | ---------------------------------------------------------- |\n| exactly one | analyzed, notice on stderr | same \u2014 analyzed, notice on stderr |\n| several | single-select prompt | **exit `2`** listing the apps, asking for an explicit path |\n| none | exit `2`, "not a SvelteKit project" | same |\n\n**svelte-vitals never prompts when stdout is not a TTY.** A non-interactive run with several apps\nfails fast with the list rather than hanging or guessing \u2014 if you hit exit `2` here, re-run with\nthe path it printed.\n\nCancelling the interactive prompt exits `0` without analyzing anything.\n\n## `install` in a monorepo\n\n`svelte-vitals install` splits its targets by where they belong:\n\n- `vite-plugin`, `vite-hooks`, `config-file` write into the **app** directory \u2014 they resolve it\n the same way the analyzer does, and `--app <dir>` names it explicitly.\n- the agent skills and `ci-workflow` always write at the **current** directory, because the repo\n root is their correct home.\n\n```bash\nnpx svelte-vitals@latest install --client vite-plugin,config-file --app apps/web --yes\n```\n\n`--app` pointing at a directory that is not a SvelteKit app is an error (exit `2`).\n\n## Related\n\n- `svelte-vitals docs show output` \u2014 what exit `2` means versus exit `1`'
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
name: "output",
|
|
30
|
-
title: "Reading the output",
|
|
31
|
-
description: "Which reporter to use, how one is auto-selected, what goes to stdout vs stderr, and what each exit code means.",
|
|
32
|
-
body: '# Reading the output\n\n## Pick a reporter\n\n`--reporter <fmt>`: `console` (default) \xB7 `json` \xB7 `agent` \xB7 `sarif` \xB7 `github` \xB7 `html` \xB7 `md`.\n\n- **`agent`** \u2014 a Markdown remediation document: every failing finding with its location, a\n concrete fix (with a code snippet), and an acceptance check. This is the one to use when\n something will act on the findings rather than read them.\n- **`json`** \u2014 the full structured report (per-route and site-wide scores, every finding with\n `fix`, `recommendation` and `docsUrl`). Use it when you need to filter or count.\n- **`console`** \u2014 for a human at a terminal. Grouped and capped; add `--verbose` for everything.\n- **`md`** \u2014 a compact summary table for a PR comment or job summary (capped at 50 rows).\n- **`sarif`** \u2014 SARIF v2.1, for GitHub Code Scanning and other SAST tooling.\n- **`github`** \u2014 `::error` / `::warning` workflow annotations.\n- **`html`** \u2014 a self-contained report file; `--out-file <path>`, or `--out-file -` for stdout.\n\n## Auto-selection\n\nFirst match wins:\n\n1. an explicit `--reporter <fmt>`\n2. `SVELTE_VITALS_REPORTER=<fmt>`\n3. a known AI-agent environment (e.g. `CLAUDECODE` is set) \u2192 `agent`\n4. `GITHUB_ACTIONS=true` \u2192 `github`\n5. otherwise \u2192 `console`\n\nSo inside an agent harness you usually get `agent` without asking. When it is auto-selected\nrather than requested, a one-line hint goes to stderr explaining how to override.\n\n## stdout vs stderr\n\nThe report goes to **stdout**. Diagnostics \u2014 auto-selection hints, suppression counts,\napp-detection notices, warnings, errors \u2014 go to **stderr**. Piping stdout is safe; you will not\nget diagnostics mixed into the report.\n\n`--reporter html` is the exception: it writes a file and prints the path to stderr, unless you\npass `--out-file -`.\n\n## Exit codes\n\n| Code | Meaning |\n| ---- | -------------------------------------------------------------------------- |\n| `0` | no failing findings |\n| `1` | a critical finding is present, or `--fail-on` / `--min-health` was reached |\n| `2` | execution error \u2014 not a SvelteKit project, bad flag, unreadable config |\n\n`1` means "the code has problems". `2` means "the run did not happen" \u2014 never treat `2` as a\nclean result. `--fail-on <critical|warning|info>` lowers the bar for `1`; `--min-health <0-100>`\nadds a score gate.\n\n## Related\n\n- `svelte-vitals docs show scoping` \u2014 report only what a change introduced\n- `svelte-vitals explain <rule-id>` \u2014 one rule\'s rationale, fix and options'
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: "scoping",
|
|
36
|
-
title: "Scoping findings to a change",
|
|
37
|
-
description: "Use --diff, --staged, --baseline and the suppressions file so only what a change introduced is reported, instead of a legacy backlog.",
|
|
38
|
-
body: "# Scoping findings to a change\n\nRunning svelte-vitals on an existing project usually surfaces a backlog nobody is about to fix.\nDo not disable rules to get a green run \u2014 scope the report instead.\n\n## Scope by file\n\n- **`--diff [ref]`** \u2014 only findings in files changed versus `ref` (default `HEAD`).\n- **`--staged`** \u2014 only findings in staged files. The pre-commit gate.\n\n```bash\nsvelte-vitals . --diff --reporter agent # after editing: what did I just break?\nsvelte-vitals . --staged # before committing\n```\n\nBoth work when the project is not at the git repo root.\n\n## Scope by finding (`--baseline <ref>`)\n\n`--baseline` reports only findings **not already present** at `ref`. It scopes by finding\nidentity rather than by file, so a pre-existing problem in a file you touched does not fail the\ngate \u2014 only what your change actually introduced does. There is no default ref.\n\n```bash\nsvelte-vitals --diff origin/main --baseline origin/main --fail-on warning # PR gate\n```\n\nInternally it checks `ref` out into a temporary git worktree and subtracts those findings. If\nthat fails (no git, bad ref), it warns and reports everything rather than failing the run.\n\n## Accept a backlog once (`svelte-vitals-suppressions.json`)\n\n`--baseline` handles the transient case. For a persistent ramp, record today's findings once:\n\n```bash\nsvelte-vitals --update-suppressions\ngit add svelte-vitals-suppressions.json\n```\n\n`--update-suppressions` analyzes the whole project (any `--diff`/`--staged`/`--baseline` scoping\nis ignored), writes every currently-penalized finding, prints a summary to stderr, and exits `0`\nwithout printing a report.\n\nOnce the file exists it applies automatically on every run, after `--diff`/`--staged` and\n`--baseline`, and reports how many findings it removed. Fixing an accepted finding leaves a\n**stale** entry \u2014 that is reported on stderr as a reminder to re-run `--update-suppressions`,\nbut never fails the run. `--no-suppressions` ignores the file for one run.\n\nA malformed suppressions file is a hard error (exit `2`), not a silent skip.\n\n## Which one\n\n| Situation | Use |\n| -------------------------------------- | ---------------------------------------- |\n| Checking an edit you just made | `--diff` |\n| Pre-commit hook | `--staged` |\n| PR gate against a base branch | `--diff <base> --baseline <base>` |\n| Adopting on a legacy project, for good | `--update-suppressions`, commit the file |\n\nMatching ignores line numbers in both `--baseline` and the suppressions file, so a second\nviolation of the same rule lower in the same file does not surface as new.\n\n## Related\n\n- `svelte-vitals docs show ci` \u2014 the generated PR gate already does the `--diff`/`--baseline` pairing\n- `svelte-vitals docs show config` \u2014 turning a rule off for good, when that is genuinely right"
|
|
39
|
-
}
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
// src/docs/cli.ts
|
|
43
|
-
var DOCS_HELP = `svelte-vitals docs \u2014 read the bundled guides without leaving the terminal
|
|
44
|
-
|
|
45
|
-
Usage:
|
|
46
|
-
svelte-vitals docs list [--json] List every topic with a one-line description
|
|
47
|
-
svelte-vitals docs show <name> Print a topic
|
|
48
|
-
|
|
49
|
-
Options:
|
|
50
|
-
--json Machine-readable output (list only)
|
|
51
|
-
-h, --help Show this help
|
|
52
|
-
|
|
53
|
-
The topics ship inside the CLI, so they always match the version you are running and need no
|
|
54
|
-
network. The full docs site is at https://oekazuma.github.io/svelte-vitals.
|
|
55
|
-
|
|
56
|
-
\`docs\` is a subcommand, so it wins over a directory of the same name: to analyze a directory
|
|
57
|
-
called \`docs\`, write \`svelte-vitals ./docs\`.`;
|
|
58
|
-
function knownTopicNames() {
|
|
59
|
-
return EMBEDDED_DOCS.map((d) => d.name).join(", ");
|
|
60
|
-
}
|
|
61
|
-
function renderList() {
|
|
62
|
-
const width = Math.max(...EMBEDDED_DOCS.map((d) => d.name.length));
|
|
63
|
-
const lines = EMBEDDED_DOCS.map((d) => ` ${d.name.padEnd(width)} ${d.description}`);
|
|
64
|
-
return [
|
|
65
|
-
"Topics (read one with `svelte-vitals docs show <name>`):",
|
|
66
|
-
"",
|
|
67
|
-
...lines,
|
|
68
|
-
"",
|
|
69
|
-
"Rule-level detail is a separate command: `svelte-vitals explain --list`."
|
|
70
|
-
].join("\n");
|
|
71
|
-
}
|
|
72
|
-
function runDocsCli(args, io = consoleIO) {
|
|
73
|
-
const argv = mri(args, { boolean: ["json", "help"], alias: { h: "help" } });
|
|
74
|
-
const [sub, ...rest] = argv._;
|
|
75
|
-
if (argv.help) {
|
|
76
|
-
io.log(DOCS_HELP);
|
|
77
|
-
return 0;
|
|
78
|
-
}
|
|
79
|
-
if (sub === void 0) {
|
|
80
|
-
io.errorLog(DOCS_HELP);
|
|
81
|
-
return 2;
|
|
82
|
-
}
|
|
83
|
-
if (sub === "list") {
|
|
84
|
-
if (rest.length > 0) {
|
|
85
|
-
io.errorLog("svelte-vitals: docs list takes no arguments; use `docs show <name>` to read one.");
|
|
86
|
-
return 2;
|
|
87
|
-
}
|
|
88
|
-
io.log(
|
|
89
|
-
argv.json ? JSON.stringify(
|
|
90
|
-
EMBEDDED_DOCS.map((d) => ({ name: d.name, title: d.title, description: d.description })),
|
|
91
|
-
null,
|
|
92
|
-
2
|
|
93
|
-
) : renderList()
|
|
94
|
-
);
|
|
95
|
-
return 0;
|
|
96
|
-
}
|
|
97
|
-
if (sub === "show") {
|
|
98
|
-
if (rest.length !== 1) {
|
|
99
|
-
io.errorLog(
|
|
100
|
-
rest.length === 0 ? "svelte-vitals: docs show needs a topic name, e.g. `svelte-vitals docs show config`." : "svelte-vitals: docs show takes one topic at a time."
|
|
101
|
-
);
|
|
102
|
-
io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
|
|
103
|
-
return 2;
|
|
104
|
-
}
|
|
105
|
-
const doc = EMBEDDED_DOCS.find((d) => d.name === rest[0]);
|
|
106
|
-
if (!doc) {
|
|
107
|
-
io.errorLog(`svelte-vitals: unknown docs topic '${rest[0]}'.`);
|
|
108
|
-
io.errorLog(`svelte-vitals: known topics: ${knownTopicNames()}.`);
|
|
109
|
-
return 2;
|
|
110
|
-
}
|
|
111
|
-
io.log(doc.body);
|
|
112
|
-
return 0;
|
|
113
|
-
}
|
|
114
|
-
io.errorLog(`svelte-vitals: unknown docs subcommand '${sub}'; expected list|show.`);
|
|
115
|
-
io.errorLog(DOCS_HELP);
|
|
116
|
-
return 2;
|
|
117
|
-
}
|
|
118
|
-
export {
|
|
119
|
-
runDocsCli
|
|
120
|
-
};
|