svelte-vitals 0.36.0 → 0.37.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/README.md +7 -2
- package/dist/bin.js +71 -17
- package/dist/{chunk-D6AUX2GC.js → chunk-I2MKPWWT.js} +2 -2
- package/dist/chunk-SLUMRYUD.js +9 -0
- package/dist/cli-SOIIGBH7.js +120 -0
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -84,14 +84,19 @@ An interactive wizard that wires up the [Vite plugin](https://www.npmjs.com/pack
|
|
|
84
84
|
npx svelte-vitals@latest install
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
### `svelte-vitals
|
|
87
|
+
### `svelte-vitals docs` / `svelte-vitals explain`
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
Both read out of the CLI itself, so the answer always matches the installed version and needs no network — the thing an AI agent otherwise guesses at or fetches from a page describing a different release.
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
+
npx svelte-vitals@latest docs list # every bundled topic, with a one-line description
|
|
93
|
+
npx svelte-vitals@latest docs show scoping
|
|
94
|
+
npx svelte-vitals@latest explain --list # every rule, by category
|
|
92
95
|
npx svelte-vitals@latest explain performance/heavy-import
|
|
93
96
|
```
|
|
94
97
|
|
|
98
|
+
`explain <rule-id>` prints that rule's rationale, docs link, fix template, and — for a configurable rule — every option's default, bounds, and how a configured value merges with the built-in default: the detail needed to decide whether a finding is a defect or a threshold disagreement. `docs list` and both forms of `explain` take `--json`; `docs show` prints the topic as Markdown.
|
|
99
|
+
|
|
95
100
|
### CI integration
|
|
96
101
|
|
|
97
102
|
`svelte-vitals ci install` scaffolds a GitHub Actions workflow around `@svelte-vitals/action` — inline PR annotations, a job summary, and a sticky PR comment, no YAML to hand-write. The same workflow is also a selectable `ci-workflow` target inside `svelte-vitals install`, so it can be set up in the same pass as everything else. See [CI integration](https://oekazuma.github.io/svelte-vitals/guides/ci/).
|
package/dist/bin.js
CHANGED
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
readCoreVersion,
|
|
10
10
|
readPackageVersion,
|
|
11
11
|
run
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-I2MKPWWT.js";
|
|
13
|
+
import {
|
|
14
|
+
consoleIO
|
|
15
|
+
} from "./chunk-SLUMRYUD.js";
|
|
13
16
|
|
|
14
17
|
// src/bin.ts
|
|
15
18
|
import mri4 from "mri";
|
|
@@ -361,6 +364,7 @@ Use this whenever you are writing or reviewing SvelteKit route files (\`+page.sv
|
|
|
361
364
|
1. After writing or editing code, run \`npx svelte-vitals . --diff --reporter agent\` and fix any findings it reports.
|
|
362
365
|
2. Before committing, run \`npx svelte-vitals . --staged\` as a pre-commit gate.
|
|
363
366
|
3. For a rule's full rationale, configurable options and fix examples, run \`npx svelte-vitals explain <rule-id>\` (add \`--json\` for a structured object) or open its docs link below.
|
|
367
|
+
4. For anything else \u2014 reporters, the config file, scoping to a change, CI, monorepos \u2014 run \`npx svelte-vitals docs list\` and then \`npx svelte-vitals docs show <name>\`. Those guides ship inside the CLI, so they match the version installed here; prefer them over searching the web.
|
|
364
368
|
|
|
365
369
|
## Rule digest
|
|
366
370
|
|
|
@@ -1599,21 +1603,19 @@ async function runCiUpgrade(args, io) {
|
|
|
1599
1603
|
|
|
1600
1604
|
// src/explain.ts
|
|
1601
1605
|
import mri3 from "mri";
|
|
1602
|
-
import { explainRule } from "@svelte-vitals/core";
|
|
1606
|
+
import { allRules as allRules2, CATEGORIES as CATEGORIES2, explainRule } from "@svelte-vitals/core";
|
|
1603
1607
|
var EXPLAIN_HELP = `svelte-vitals explain \u2014 print a rule's rationale, fix, and configurable options
|
|
1604
1608
|
|
|
1605
1609
|
Usage:
|
|
1606
|
-
svelte-vitals explain
|
|
1610
|
+
svelte-vitals explain --list List every rule id, grouped by category
|
|
1611
|
+
svelte-vitals explain <rule-id> Explain one rule
|
|
1607
1612
|
|
|
1608
1613
|
Options:
|
|
1609
|
-
--
|
|
1614
|
+
--list List every rule instead of explaining one
|
|
1615
|
+
--json Machine-readable output (works with --list and with a rule id)
|
|
1610
1616
|
-h, --help Show this help
|
|
1611
1617
|
|
|
1612
1618
|
Rule ids are category/kebab-case and matched exactly, e.g. \`svelte-vitals explain seo/ssr-disabled\`.`;
|
|
1613
|
-
var realIO2 = {
|
|
1614
|
-
log: (line) => console.log(line),
|
|
1615
|
-
errorLog: (line) => console.error(line)
|
|
1616
|
-
};
|
|
1617
1619
|
function describeOptions(id, options) {
|
|
1618
1620
|
const MERGE = {
|
|
1619
1621
|
integer: "replaces the default",
|
|
@@ -1638,19 +1640,46 @@ Fix: ${info.fix.description}` : "") + (info.options ? `
|
|
|
1638
1640
|
|
|
1639
1641
|
Configurable: ${describeOptions(info.id, info.options)}` : "");
|
|
1640
1642
|
}
|
|
1641
|
-
function
|
|
1642
|
-
const
|
|
1643
|
+
function renderRuleList() {
|
|
1644
|
+
const sections = CATEGORIES2.map((category) => {
|
|
1645
|
+
const rules = allRules2.filter((r) => r.category === category);
|
|
1646
|
+
const width = Math.max(...rules.map((r) => r.id.length));
|
|
1647
|
+
const lines = rules.map((r) => ` ${r.id.padEnd(width)} ${r.severity.padEnd(8)} ${r.title}`);
|
|
1648
|
+
return [`${category} (${rules.length})`, ...lines].join("\n");
|
|
1649
|
+
});
|
|
1650
|
+
return [...sections, "", `${allRules2.length} rules. Explain one with \`svelte-vitals explain <rule-id>\`.`].join(
|
|
1651
|
+
"\n\n"
|
|
1652
|
+
);
|
|
1653
|
+
}
|
|
1654
|
+
function runExplainCli(args, io = consoleIO) {
|
|
1655
|
+
const argv = mri3(args, { boolean: ["json", "list", "help"], alias: { h: "help" } });
|
|
1643
1656
|
if (argv.help) {
|
|
1644
1657
|
io.log(EXPLAIN_HELP);
|
|
1645
1658
|
return 0;
|
|
1646
1659
|
}
|
|
1660
|
+
if (argv.list) {
|
|
1661
|
+
if (argv._.length > 0) {
|
|
1662
|
+
io.errorLog("svelte-vitals: explain --list takes no rule id; drop --list to explain one.");
|
|
1663
|
+
return 2;
|
|
1664
|
+
}
|
|
1665
|
+
io.log(
|
|
1666
|
+
argv.json ? JSON.stringify(
|
|
1667
|
+
allRules2.map((r) => ({ id: r.id, category: r.category, severity: r.severity, title: r.title })),
|
|
1668
|
+
null,
|
|
1669
|
+
2
|
|
1670
|
+
) : renderRuleList()
|
|
1671
|
+
);
|
|
1672
|
+
return 0;
|
|
1673
|
+
}
|
|
1647
1674
|
const id = argv._[0];
|
|
1648
1675
|
if (id === void 0) {
|
|
1649
|
-
io.errorLog(
|
|
1676
|
+
io.errorLog(
|
|
1677
|
+
"svelte-vitals: explain needs a rule id, e.g. `svelte-vitals explain seo/ssr-disabled`; `--list` shows them all."
|
|
1678
|
+
);
|
|
1650
1679
|
io.errorLog(`svelte-vitals: known rule ids: ${knownRuleIds().join(", ")}.`);
|
|
1651
1680
|
return 2;
|
|
1652
1681
|
}
|
|
1653
|
-
const info = explainRule(
|
|
1682
|
+
const info = explainRule(id);
|
|
1654
1683
|
if (!info) {
|
|
1655
1684
|
io.errorLog(`svelte-vitals: unknown rule id '${id}'.`);
|
|
1656
1685
|
io.errorLog(`svelte-vitals: known rule ids: ${knownRuleIds().join(", ")}.`);
|
|
@@ -1665,7 +1694,8 @@ var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (
|
|
|
1665
1694
|
|
|
1666
1695
|
Usage:
|
|
1667
1696
|
svelte-vitals [path] [options]
|
|
1668
|
-
svelte-vitals
|
|
1697
|
+
svelte-vitals docs list List the bundled guides (docs show <name> prints one)
|
|
1698
|
+
svelte-vitals explain --list List every rule (explain <rule-id> explains one)
|
|
1669
1699
|
svelte-vitals install Set up the Vite integration, agent skills/rules, config file, or CI
|
|
1670
1700
|
svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
|
|
1671
1701
|
svelte-vitals ci upgrade Refresh the pinned @svelte-vitals/action in an existing workflow
|
|
@@ -1701,7 +1731,24 @@ Config file:
|
|
|
1701
1731
|
Exit codes:
|
|
1702
1732
|
0 no failing findings
|
|
1703
1733
|
1 critical finding present (or --fail-on threshold reached)
|
|
1704
|
-
2 execution error (not a SvelteKit project / internal error)
|
|
1734
|
+
2 execution error (not a SvelteKit project / internal error)
|
|
1735
|
+
|
|
1736
|
+
If you are an AI agent:
|
|
1737
|
+
- \`svelte-vitals docs list\` then \`docs show <name>\` \u2014 the guides ship inside this CLI, so
|
|
1738
|
+
they match this exact version and need no network. Read those before searching the web.
|
|
1739
|
+
- \`--reporter agent\` gives every failing finding a location, a concrete fix and an acceptance
|
|
1740
|
+
check; it is auto-selected when an agent environment is detected. \`--reporter json\` is the
|
|
1741
|
+
structured form.
|
|
1742
|
+
- \`--diff\` scopes the report to what you just changed; \`--staged\` is the pre-commit gate.
|
|
1743
|
+
- \`svelte-vitals explain <rule-id>\` says why a rule exists and which options it takes, before
|
|
1744
|
+
you decide to turn it off.
|
|
1745
|
+
- Do NOT reach for \`--update-suppressions\` to make a run pass: it accepts every current
|
|
1746
|
+
finding into a committed file and un-gates CI for all of them. Fix the findings, or scope
|
|
1747
|
+
the run with \`--diff\`. Only a human should decide to accept a backlog.
|
|
1748
|
+
- Exit 2 is never a pass \u2014 it means the analysis did not run. Read stderr.
|
|
1749
|
+
- Analysis never prompts when stdout is not a TTY: where it would have asked, it exits 2
|
|
1750
|
+
naming the flag to pass. \`install\` is the exception \u2014 non-interactively it skips its
|
|
1751
|
+
confirmation and writes, so pass \`--dry-run\` first if you need to see the plan.`;
|
|
1705
1752
|
var VERSION = readPackageVersion();
|
|
1706
1753
|
async function selectApp(apps) {
|
|
1707
1754
|
const res = await p2.select({
|
|
@@ -1712,8 +1759,14 @@ async function selectApp(apps) {
|
|
|
1712
1759
|
}
|
|
1713
1760
|
async function main() {
|
|
1714
1761
|
const rawArgs = process.argv.slice(2);
|
|
1762
|
+
if (rawArgs[0] === "docs") {
|
|
1763
|
+
const { runDocsCli } = await import("./cli-SOIIGBH7.js");
|
|
1764
|
+
process.exitCode = runDocsCli(rawArgs.slice(1));
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1715
1767
|
if (rawArgs[0] === "explain") {
|
|
1716
|
-
process.
|
|
1768
|
+
process.exitCode = runExplainCli(rawArgs.slice(1));
|
|
1769
|
+
return;
|
|
1717
1770
|
}
|
|
1718
1771
|
if (rawArgs[0] === "install") {
|
|
1719
1772
|
const code2 = await runInstallCli(rawArgs.slice(1));
|
|
@@ -1744,11 +1797,12 @@ async function main() {
|
|
|
1744
1797
|
});
|
|
1745
1798
|
if (argv.help) {
|
|
1746
1799
|
console.log(HELP);
|
|
1747
|
-
|
|
1800
|
+
return;
|
|
1748
1801
|
}
|
|
1749
1802
|
if (argv.version) {
|
|
1750
1803
|
console.log(`${VERSION} (core ${readCoreVersion()})`);
|
|
1751
|
-
|
|
1804
|
+
console.error("svelte-vitals: run `svelte-vitals docs list` for the bundled guides.");
|
|
1805
|
+
return;
|
|
1752
1806
|
}
|
|
1753
1807
|
const { options, warnings, errors } = resolveArgs(argv);
|
|
1754
1808
|
for (const w of warnings) console.error(w);
|
|
@@ -109,7 +109,7 @@ async function detectProject(rt, cwd) {
|
|
|
109
109
|
const hasRoutes = await rt.exists(rt.join(cwd, ROUTES_DIR));
|
|
110
110
|
if (hasKitDep || hasConfig && hasRoutes) return;
|
|
111
111
|
throw new ProjectError(
|
|
112
|
-
"No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass a path (e.g. npx svelte-vitals apps/web)."
|
|
112
|
+
"No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass a path (e.g. npx svelte-vitals apps/web). See `svelte-vitals docs show monorepo` for how the app is resolved."
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
async function enumerateRoutePages(rt, cwd) {
|
|
@@ -1664,7 +1664,7 @@ async function run(opts = {}) {
|
|
|
1664
1664
|
} else {
|
|
1665
1665
|
if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
|
|
1666
1666
|
errorLog(
|
|
1667
|
-
"svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
|
|
1667
|
+
"svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json. Run `svelte-vitals docs list` for the bundled guides."
|
|
1668
1668
|
);
|
|
1669
1669
|
}
|
|
1670
1670
|
if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
|
|
@@ -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` (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
|
+
};
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.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",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"typecheck": "tsc --noEmit",
|
|
57
57
|
"test": "vitest run",
|
|
58
58
|
"gen:rules-index": "pnpm --filter @svelte-vitals/core build && node scripts/gen-rules-index.mjs",
|
|
59
|
-
"update-action-pin": "node scripts/gen-action-pin.mjs"
|
|
59
|
+
"update-action-pin": "node scripts/gen-action-pin.mjs",
|
|
60
|
+
"gen:docs": "node scripts/gen-docs.mjs"
|
|
60
61
|
}
|
|
61
62
|
}
|