svelte-vitals 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +332 -21
- package/dist/{chunk-ZE3M3T6U.js → chunk-AAM2A7D7.js} +205 -53
- package/dist/index.d.ts +15 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
knownRuleIds,
|
|
7
7
|
readPackageVersion,
|
|
8
8
|
run
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-AAM2A7D7.js";
|
|
10
10
|
|
|
11
11
|
// src/bin.ts
|
|
12
|
-
import
|
|
12
|
+
import mri3 from "mri";
|
|
13
|
+
import * as p2 from "@clack/prompts";
|
|
13
14
|
|
|
14
15
|
// src/resolve-args.ts
|
|
15
16
|
var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
|
|
@@ -55,6 +56,26 @@ function parseWeights(raw, errors) {
|
|
|
55
56
|
}
|
|
56
57
|
return weights;
|
|
57
58
|
}
|
|
59
|
+
function parseCategories(raw, errors) {
|
|
60
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
61
|
+
const categories = [];
|
|
62
|
+
const unknownCategories = [];
|
|
63
|
+
for (const entry of raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean)) {
|
|
64
|
+
if (!CATEGORIES.includes(entry)) {
|
|
65
|
+
unknownCategories.push(entry);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!categories.includes(entry)) categories.push(entry);
|
|
69
|
+
}
|
|
70
|
+
if (unknownCategories.length > 0) {
|
|
71
|
+
errors.push(`svelte-vitals: unknown category(ies) in --category: ${unknownCategories.join(", ")}`);
|
|
72
|
+
errors.push(`Known categories: ${CATEGORIES.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
if (unknownCategories.length === 0 && categories.length === 0) {
|
|
75
|
+
errors.push("svelte-vitals: --category was passed but contains no categories.");
|
|
76
|
+
}
|
|
77
|
+
return categories;
|
|
78
|
+
}
|
|
58
79
|
var toList = (v) => typeof v === "string" ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
59
80
|
function resolveArgs(argv) {
|
|
60
81
|
const warnings = [];
|
|
@@ -71,6 +92,14 @@ function resolveArgs(argv) {
|
|
|
71
92
|
const route = typeof argv.route === "string" ? argv.route : void 0;
|
|
72
93
|
const diffBase = typeof argv.diff === "string" ? argv.diff || "HEAD" : void 0;
|
|
73
94
|
const staged = Boolean(argv.staged);
|
|
95
|
+
let baselineRef;
|
|
96
|
+
if (typeof argv.baseline === "string") {
|
|
97
|
+
if (argv.baseline.trim() === "") {
|
|
98
|
+
errors.push("svelte-vitals: --baseline requires a git ref (e.g. --baseline origin/main).");
|
|
99
|
+
} else {
|
|
100
|
+
baselineRef = argv.baseline;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
74
103
|
const allow = toList(argv.rules);
|
|
75
104
|
const ignore = toList(argv.ignore);
|
|
76
105
|
const unknown = findUnknownRuleIds([...allow, ...ignore]);
|
|
@@ -84,7 +113,7 @@ function resolveArgs(argv) {
|
|
|
84
113
|
} else if (typeof argv.reporter === "string") {
|
|
85
114
|
if (!isReporterName(argv.reporter)) {
|
|
86
115
|
errors.push(
|
|
87
|
-
`svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html.`
|
|
116
|
+
`svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html, md.`
|
|
88
117
|
);
|
|
89
118
|
} else {
|
|
90
119
|
reporter = argv.reporter;
|
|
@@ -99,12 +128,20 @@ function resolveArgs(argv) {
|
|
|
99
128
|
}
|
|
100
129
|
const failOn = argv["fail-on-warning"] ? "warning" : failOnValid ? failOnRaw : void 0;
|
|
101
130
|
const weights = parseWeights(argv.weights, errors);
|
|
131
|
+
const categories = parseCategories(argv.category, errors);
|
|
132
|
+
const score = Boolean(argv.score);
|
|
133
|
+
if (score && (argv.json || typeof argv.reporter === "string")) {
|
|
134
|
+
warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
|
|
135
|
+
}
|
|
102
136
|
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
103
137
|
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
104
138
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
105
139
|
return {
|
|
106
140
|
options: {
|
|
107
141
|
cwd: positional ?? process.cwd(),
|
|
142
|
+
// Never reinterpret an explicit target (design doc 2026-07-08-monorepo-app-picker-design.md,
|
|
143
|
+
// decision 1): the monorepo picker in run() only triggers when this is false.
|
|
144
|
+
explicitPath: positional !== void 0,
|
|
108
145
|
metaComponents,
|
|
109
146
|
treatDynamicAs,
|
|
110
147
|
route,
|
|
@@ -114,8 +151,11 @@ function resolveArgs(argv) {
|
|
|
114
151
|
failOn,
|
|
115
152
|
rules,
|
|
116
153
|
...weights !== void 0 ? { weights } : {},
|
|
154
|
+
...categories !== void 0 ? { categories } : {},
|
|
155
|
+
...score ? { score } : {},
|
|
117
156
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
118
|
-
...staged ? { staged } : {}
|
|
157
|
+
...staged ? { staged } : {},
|
|
158
|
+
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
119
159
|
},
|
|
120
160
|
warnings,
|
|
121
161
|
errors
|
|
@@ -236,6 +276,93 @@ function isViteTargetId(id) {
|
|
|
236
276
|
return VITE_TARGETS.some((t) => t.id === id);
|
|
237
277
|
}
|
|
238
278
|
|
|
279
|
+
// src/install/agent-targets.ts
|
|
280
|
+
var AGENT_TARGETS = [
|
|
281
|
+
{
|
|
282
|
+
id: "claude-skill",
|
|
283
|
+
label: "Claude Code skill",
|
|
284
|
+
hint: "Teaches the agent svelte-vitals rules + when to run the scanner",
|
|
285
|
+
relPath: ".claude/skills/svelte-vitals/SKILL.md"
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: "cursor-rules",
|
|
289
|
+
label: "Cursor rules",
|
|
290
|
+
hint: "Project rules file so Cursor avoids flagged patterns up front",
|
|
291
|
+
relPath: ".cursor/rules/svelte-vitals.mdc"
|
|
292
|
+
}
|
|
293
|
+
];
|
|
294
|
+
function agentTargetById(id) {
|
|
295
|
+
return AGENT_TARGETS.find((t) => t.id === id);
|
|
296
|
+
}
|
|
297
|
+
function isAgentTargetId(id) {
|
|
298
|
+
return AGENT_TARGETS.some((t) => t.id === id);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/install/skill-content.ts
|
|
302
|
+
import { allRules, docsUrlFor } from "@svelte-vitals/core";
|
|
303
|
+
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
304
|
+
var CATEGORY_LABELS = {
|
|
305
|
+
seo: "SEO",
|
|
306
|
+
performance: "Performance",
|
|
307
|
+
correctness: "Correctness",
|
|
308
|
+
security: "Security",
|
|
309
|
+
architecture: "Architecture"
|
|
310
|
+
};
|
|
311
|
+
function oneLine(text) {
|
|
312
|
+
return text.replace(/\r?\n+/g, " ").trim();
|
|
313
|
+
}
|
|
314
|
+
function ruleLine(rule) {
|
|
315
|
+
const fixPart = rule.fix?.description ? ` Fix: ${oneLine(rule.fix.description)}` : "";
|
|
316
|
+
return `- **${rule.id} \u2014 ${oneLine(rule.title)}** (${rule.severity}): ${oneLine(rule.rationale)}${fixPart} ([docs](${docsUrlFor(rule.id)}))`;
|
|
317
|
+
}
|
|
318
|
+
function ruleDigest() {
|
|
319
|
+
return CATEGORY_ORDER.map((category) => {
|
|
320
|
+
const lines = allRules.filter((r) => r.category === category).map(ruleLine).join("\n");
|
|
321
|
+
return `### ${CATEGORY_LABELS[category]}
|
|
322
|
+
|
|
323
|
+
${lines}`;
|
|
324
|
+
}).join("\n\n");
|
|
325
|
+
}
|
|
326
|
+
function sharedBody(version) {
|
|
327
|
+
return `<!-- Generated by \`svelte-vitals install\` (svelte-vitals ${version}). Re-run with --force to refresh. -->
|
|
328
|
+
|
|
329
|
+
# svelte-vitals
|
|
330
|
+
|
|
331
|
+
## When to use
|
|
332
|
+
|
|
333
|
+
Use this whenever you are writing or reviewing SvelteKit route files (\`+page.svelte\`, \`+layout.svelte\`) or components in this project \u2014 svelte-vitals statically checks SEO, performance, correctness, security, and architecture patterns.
|
|
334
|
+
|
|
335
|
+
## Playbook
|
|
336
|
+
|
|
337
|
+
1. After writing or editing code, run \`npx svelte-vitals . --diff --reporter agent\` and fix any findings it reports.
|
|
338
|
+
2. Before committing, run \`npx svelte-vitals . --staged\` as a pre-commit gate.
|
|
339
|
+
3. For a rule's full rationale and fix examples, use the \`explain_rule\` MCP tool (svelte-vitals MCP server) or open its docs link below.
|
|
340
|
+
|
|
341
|
+
## Rule digest
|
|
342
|
+
|
|
343
|
+
${ruleDigest()}
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
function buildSkillMarkdown(version) {
|
|
347
|
+
const frontmatter = `---
|
|
348
|
+
name: svelte-vitals
|
|
349
|
+
description: Use when writing or reviewing SvelteKit routes/components \u2014 svelte-vitals rule knowledge (SEO, performance, correctness, security, architecture) and how to run the scanner.
|
|
350
|
+
---`;
|
|
351
|
+
return `${frontmatter}
|
|
352
|
+
|
|
353
|
+
${sharedBody(version)}`;
|
|
354
|
+
}
|
|
355
|
+
function buildCursorRules(version) {
|
|
356
|
+
const frontmatter = `---
|
|
357
|
+
description: svelte-vitals code-health rules for SvelteKit (SEO, performance, correctness, security, architecture)
|
|
358
|
+
globs: ["**/*.svelte", "src/routes/**"]
|
|
359
|
+
alwaysApply: false
|
|
360
|
+
---`;
|
|
361
|
+
return `${frontmatter}
|
|
362
|
+
|
|
363
|
+
${sharedBody(version)}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
239
366
|
// src/install/codemod-vite-config.ts
|
|
240
367
|
import { parseModule, generateCode, builders, MagicastError } from "magicast";
|
|
241
368
|
var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
|
|
@@ -252,7 +379,7 @@ function codemodViteConfig(existing) {
|
|
|
252
379
|
return { status: "manual", snippet: MANUAL_SNIPPET };
|
|
253
380
|
}
|
|
254
381
|
const already = configObj.plugins.find(
|
|
255
|
-
(
|
|
382
|
+
(p3) => p3?.$type === "function-call" && p3?.$callee === "svelteVitals"
|
|
256
383
|
);
|
|
257
384
|
if (already !== void 0) {
|
|
258
385
|
return { status: "exists" };
|
|
@@ -395,6 +522,13 @@ function planForDevOverlay(io) {
|
|
|
395
522
|
const result = codemodHooksServer(content);
|
|
396
523
|
return { id: "vite-dev-overlay", label: viteTargetById("vite-dev-overlay").label, path, ...result };
|
|
397
524
|
}
|
|
525
|
+
function planForAgentTarget(target, io, force, version) {
|
|
526
|
+
const path = join3(io.cwd, target.relPath);
|
|
527
|
+
const existing = io.readFile(path);
|
|
528
|
+
const content = target.id === "claude-skill" ? buildSkillMarkdown(version) : buildCursorRules(version);
|
|
529
|
+
const status = existing === void 0 ? "created" : force ? "updated" : "exists";
|
|
530
|
+
return { id: target.id, label: target.label, path, status, content };
|
|
531
|
+
}
|
|
398
532
|
function indent(text) {
|
|
399
533
|
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
400
534
|
}
|
|
@@ -403,7 +537,7 @@ function rowLine(r) {
|
|
|
403
537
|
return r.status === "manual" && r.snippet ? `${head}
|
|
404
538
|
${indent(r.snippet)}` : head;
|
|
405
539
|
}
|
|
406
|
-
async function runInstall(flags, io, prompts) {
|
|
540
|
+
async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
407
541
|
let ids;
|
|
408
542
|
if (flags.client && flags.client.length > 0) {
|
|
409
543
|
ids = flags.client;
|
|
@@ -421,10 +555,21 @@ async function runInstall(flags, io, prompts) {
|
|
|
421
555
|
const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
|
|
422
556
|
(f) => configExists(join3(io.cwd, f))
|
|
423
557
|
);
|
|
424
|
-
const
|
|
558
|
+
const claudeSkillDetected = configExists(join3(io.cwd, ".claude", "settings.json"));
|
|
559
|
+
const cursorRulesDetected = configExists(join3(io.cwd, ".cursor", "mcp.json"));
|
|
560
|
+
const detectedAgents = [
|
|
561
|
+
...claudeSkillDetected ? ["claude-skill"] : [],
|
|
562
|
+
...cursorRulesDetected ? ["cursor-rules"] : []
|
|
563
|
+
];
|
|
564
|
+
const detected = [
|
|
565
|
+
...detectedClients,
|
|
566
|
+
...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : [],
|
|
567
|
+
...detectedAgents
|
|
568
|
+
];
|
|
425
569
|
const options = [
|
|
426
570
|
...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
|
|
427
|
-
...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
571
|
+
...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
|
|
572
|
+
...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
428
573
|
];
|
|
429
574
|
const picked = await prompts.selectClients(options, detected);
|
|
430
575
|
if (picked === null) {
|
|
@@ -434,13 +579,14 @@ async function runInstall(flags, io, prompts) {
|
|
|
434
579
|
ids = picked;
|
|
435
580
|
} else {
|
|
436
581
|
io.errorLog(
|
|
437
|
-
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay> to install non-interactively."
|
|
582
|
+
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules> to install non-interactively."
|
|
438
583
|
);
|
|
439
584
|
return 2;
|
|
440
585
|
}
|
|
441
586
|
const clients = ids.map(clientById).filter((c) => c !== void 0);
|
|
442
587
|
const viteIds = ids.filter(isViteTargetId);
|
|
443
|
-
|
|
588
|
+
const agentIds = ids.filter(isAgentTargetId);
|
|
589
|
+
if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0) {
|
|
444
590
|
io.errorLog("svelte-vitals: no valid clients or targets selected.");
|
|
445
591
|
return 2;
|
|
446
592
|
}
|
|
@@ -474,6 +620,10 @@ async function runInstall(flags, io, prompts) {
|
|
|
474
620
|
for (const viteId of viteIds) {
|
|
475
621
|
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForDevOverlay(io));
|
|
476
622
|
}
|
|
623
|
+
for (const agentId of agentIds) {
|
|
624
|
+
const target = agentTargetById(agentId);
|
|
625
|
+
rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
|
|
626
|
+
}
|
|
477
627
|
const planText = rows.map(rowLine).join("\n");
|
|
478
628
|
io.log("Plan:");
|
|
479
629
|
io.log(planText);
|
|
@@ -530,7 +680,11 @@ ${indent(r.snippet ?? "")}`);
|
|
|
530
680
|
}
|
|
531
681
|
|
|
532
682
|
// src/install/args.ts
|
|
533
|
-
var VALID_TARGETS = [
|
|
683
|
+
var VALID_TARGETS = [
|
|
684
|
+
...CLIENTS.map((c) => c.id),
|
|
685
|
+
...VITE_TARGETS.map((t) => t.id),
|
|
686
|
+
...AGENT_TARGETS.map((t) => t.id)
|
|
687
|
+
];
|
|
534
688
|
var EXPECTED_TARGETS = VALID_TARGETS.join("|");
|
|
535
689
|
function resolveInstallArgs(argv) {
|
|
536
690
|
const warnings = [];
|
|
@@ -568,16 +722,20 @@ function resolveInstallArgs(argv) {
|
|
|
568
722
|
}
|
|
569
723
|
|
|
570
724
|
// src/install/cli.ts
|
|
571
|
-
var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server
|
|
725
|
+
var INSTALL_HELP = `svelte-vitals install \u2014 set up the svelte-vitals MCP server, Vite integration, and agent skills/rules
|
|
572
726
|
|
|
573
727
|
Usage:
|
|
574
728
|
svelte-vitals install [options]
|
|
575
729
|
|
|
576
730
|
Options:
|
|
577
|
-
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay
|
|
731
|
+
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay,claude-skill,cursor-rules
|
|
732
|
+
(skips the interactive picker)
|
|
578
733
|
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-dev-overlay
|
|
579
734
|
wires up the dev-overlay hook in src/hooks.server.{ts,js}. --force does not apply
|
|
580
|
-
to either \u2014 an existing registration is always left as-is.
|
|
735
|
+
to either of these two \u2014 an existing registration is always left as-is.
|
|
736
|
+
claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
|
|
737
|
+
writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
|
|
738
|
+
current rule set and support --force to regenerate.
|
|
581
739
|
--scope <scope> project | global (applies to all selected clients; codex is always global)
|
|
582
740
|
--yes, -y Skip the confirmation prompt
|
|
583
741
|
--dry-run Print the planned changes and exit without writing
|
|
@@ -661,7 +819,143 @@ async function runInstallCli(args) {
|
|
|
661
819
|
for (const w of warnings) console.error(w);
|
|
662
820
|
for (const e of errors) console.error(e);
|
|
663
821
|
if (!flags) return 2;
|
|
664
|
-
return runInstall(flags, realIO(), clackPrompts());
|
|
822
|
+
return runInstall(flags, realIO(), clackPrompts(), readPackageVersion());
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/ci/cli.ts
|
|
826
|
+
import { join as join4 } from "path";
|
|
827
|
+
import mri2 from "mri";
|
|
828
|
+
|
|
829
|
+
// src/ci/workflow.ts
|
|
830
|
+
var WORKFLOW_PATH = ".github/workflows/svelte-vitals.yml";
|
|
831
|
+
function planWorkflowWrite(existing, force) {
|
|
832
|
+
if (existing === void 0) return { status: "created" };
|
|
833
|
+
if (!force) return { status: "exists" };
|
|
834
|
+
return { status: "updated" };
|
|
835
|
+
}
|
|
836
|
+
function buildWorkflowYaml(opts) {
|
|
837
|
+
const v = opts.version;
|
|
838
|
+
return [
|
|
839
|
+
`# Generated by \`svelte-vitals ci install\` (svelte-vitals ${v}).`,
|
|
840
|
+
"# Re-run with --force to regenerate.",
|
|
841
|
+
"name: svelte-vitals",
|
|
842
|
+
"",
|
|
843
|
+
"on:",
|
|
844
|
+
" pull_request:",
|
|
845
|
+
"",
|
|
846
|
+
"permissions:",
|
|
847
|
+
" contents: read",
|
|
848
|
+
" pull-requests: write",
|
|
849
|
+
"",
|
|
850
|
+
"jobs:",
|
|
851
|
+
" svelte-vitals:",
|
|
852
|
+
" runs-on: ubuntu-latest",
|
|
853
|
+
" steps:",
|
|
854
|
+
" - uses: actions/checkout@v4",
|
|
855
|
+
" with:",
|
|
856
|
+
" fetch-depth: 0",
|
|
857
|
+
" - uses: actions/setup-node@v4",
|
|
858
|
+
" with:",
|
|
859
|
+
" node-version: 24",
|
|
860
|
+
" - name: Scan (inline annotations + gate)",
|
|
861
|
+
" id: scan",
|
|
862
|
+
" continue-on-error: true",
|
|
863
|
+
" run: >",
|
|
864
|
+
` npx -y svelte-vitals@${v} .`,
|
|
865
|
+
" --diff origin/${{ github.base_ref }}",
|
|
866
|
+
" --baseline origin/${{ github.base_ref }}",
|
|
867
|
+
" --reporter github",
|
|
868
|
+
" - name: Markdown summary",
|
|
869
|
+
" run: >",
|
|
870
|
+
` npx -y svelte-vitals@${v} .`,
|
|
871
|
+
" --diff origin/${{ github.base_ref }}",
|
|
872
|
+
" --baseline origin/${{ github.base_ref }}",
|
|
873
|
+
" --reporter md > svelte-vitals-report.md || true",
|
|
874
|
+
" - name: Job summary",
|
|
875
|
+
' run: cat svelte-vitals-report.md >> "$GITHUB_STEP_SUMMARY"',
|
|
876
|
+
" - name: PR comment (sticky)",
|
|
877
|
+
" if: github.event.pull_request.head.repo.full_name == github.repository",
|
|
878
|
+
" continue-on-error: true",
|
|
879
|
+
" uses: actions/github-script@v7",
|
|
880
|
+
" with:",
|
|
881
|
+
" script: |",
|
|
882
|
+
" const fs = require('fs');",
|
|
883
|
+
" const marker = '<!-- svelte-vitals-report -->';",
|
|
884
|
+
" const body = marker + '\\n' + fs.readFileSync('svelte-vitals-report.md', 'utf8');",
|
|
885
|
+
" const { data: comments } = await github.rest.issues.listComments({",
|
|
886
|
+
" ...context.repo, issue_number: context.issue.number, per_page: 100",
|
|
887
|
+
" });",
|
|
888
|
+
" const mine = comments.find(c => c.body && c.body.startsWith(marker));",
|
|
889
|
+
" if (mine) {",
|
|
890
|
+
" await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body });",
|
|
891
|
+
" } else {",
|
|
892
|
+
" await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });",
|
|
893
|
+
" }",
|
|
894
|
+
" - name: Gate",
|
|
895
|
+
" if: steps.scan.outcome == 'failure'",
|
|
896
|
+
" run: |",
|
|
897
|
+
' echo "svelte-vitals found blocking issues (see annotations above)."',
|
|
898
|
+
" exit 1",
|
|
899
|
+
""
|
|
900
|
+
].join("\n");
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/ci/cli.ts
|
|
904
|
+
var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
|
|
905
|
+
|
|
906
|
+
Usage:
|
|
907
|
+
svelte-vitals ci install [options]
|
|
908
|
+
|
|
909
|
+
Adds a GitHub Actions workflow (${WORKFLOW_PATH}) that scans pull requests, posts inline
|
|
910
|
+
annotations + a job summary, and maintains a sticky PR comment with the findings.
|
|
911
|
+
|
|
912
|
+
Options:
|
|
913
|
+
--force Overwrite an existing workflow file
|
|
914
|
+
--dry-run Print the plan and exit without writing
|
|
915
|
+
-h, --help Show this help`;
|
|
916
|
+
async function runCiCli(args, io = realIO()) {
|
|
917
|
+
const sub = args[0];
|
|
918
|
+
if (sub === "--help" || sub === "-h") {
|
|
919
|
+
io.log(CI_HELP);
|
|
920
|
+
return 0;
|
|
921
|
+
}
|
|
922
|
+
if (sub !== "install") {
|
|
923
|
+
io.log(CI_HELP);
|
|
924
|
+
return 2;
|
|
925
|
+
}
|
|
926
|
+
const argv = mri2(args.slice(1), {
|
|
927
|
+
boolean: ["force", "dry-run", "help"],
|
|
928
|
+
alias: { h: "help" }
|
|
929
|
+
});
|
|
930
|
+
if (argv.help) {
|
|
931
|
+
io.log(CI_HELP);
|
|
932
|
+
return 0;
|
|
933
|
+
}
|
|
934
|
+
const path = join4(io.cwd, WORKFLOW_PATH);
|
|
935
|
+
const existing = io.readFile(path);
|
|
936
|
+
const plan = planWorkflowWrite(existing, Boolean(argv.force));
|
|
937
|
+
io.log("Plan:");
|
|
938
|
+
io.log(` ${WORKFLOW_PATH} [${plan.status}]`);
|
|
939
|
+
if (argv["dry-run"]) {
|
|
940
|
+
io.log("Dry run \u2014 no files written.");
|
|
941
|
+
return 0;
|
|
942
|
+
}
|
|
943
|
+
if (plan.status === "exists") {
|
|
944
|
+
io.log(`= already installed (${WORKFLOW_PATH}) \u2014 use --force to regenerate.`);
|
|
945
|
+
} else {
|
|
946
|
+
const version = readPackageVersion();
|
|
947
|
+
try {
|
|
948
|
+
io.writeFile(path, buildWorkflowYaml({ version }));
|
|
949
|
+
io.log(`\u2713 ${plan.status} ${WORKFLOW_PATH}`);
|
|
950
|
+
} catch (err) {
|
|
951
|
+
io.errorLog(
|
|
952
|
+
`svelte-vitals: failed to write ${WORKFLOW_PATH}: ${err instanceof Error ? err.message : String(err)}`
|
|
953
|
+
);
|
|
954
|
+
return 2;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
io.log("Done. Commit the workflow file and open a PR to see it in action.");
|
|
958
|
+
return 0;
|
|
665
959
|
}
|
|
666
960
|
|
|
667
961
|
// src/bin.ts
|
|
@@ -669,7 +963,8 @@ var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (
|
|
|
669
963
|
|
|
670
964
|
Usage:
|
|
671
965
|
svelte-vitals [path] [options]
|
|
672
|
-
svelte-vitals install Set up the MCP server
|
|
966
|
+
svelte-vitals install Set up the MCP server, Vite integration, or agent skills/rules
|
|
967
|
+
svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
|
|
673
968
|
|
|
674
969
|
Options:
|
|
675
970
|
--meta-components <names> Comma-separated component names that emit head metadata
|
|
@@ -677,8 +972,9 @@ Options:
|
|
|
677
972
|
--route <glob> Only analyze routes matching this glob
|
|
678
973
|
--diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
|
|
679
974
|
--staged Report only findings in files staged for commit (pre-commit gate)
|
|
975
|
+
--baseline <ref> Report only findings not present at ref (compare against e.g. origin/main)
|
|
680
976
|
--by-route Show per-route score breakdown in console output
|
|
681
|
-
--reporter <fmt> console | json | agent | sarif | github | html (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
977
|
+
--reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
682
978
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
683
979
|
--json Alias for --reporter=json
|
|
684
980
|
--fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
|
|
@@ -686,7 +982,9 @@ Options:
|
|
|
686
982
|
--min-health <0-100> Fail (exit 1) when the combined Health score is below this value
|
|
687
983
|
--rules <ids> Comma-separated rule ids to enable (all others disabled)
|
|
688
984
|
--ignore <ids> Comma-separated rule ids to disable
|
|
985
|
+
--category <cats> Comma-separated categories to analyze: seo | performance | correctness | security | architecture
|
|
689
986
|
--weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
|
|
987
|
+
--score Print only the combined Health score (works with --min-health for gating)
|
|
690
988
|
--no-color Disable ANSI color in console output
|
|
691
989
|
-h, --help Show this help
|
|
692
990
|
-v, --version Show version
|
|
@@ -699,15 +997,26 @@ Exit codes:
|
|
|
699
997
|
1 critical finding present (or --fail-on threshold reached)
|
|
700
998
|
2 execution error (not a SvelteKit project / internal error)`;
|
|
701
999
|
var VERSION = readPackageVersion();
|
|
1000
|
+
async function selectApp(apps) {
|
|
1001
|
+
const res = await p2.select({
|
|
1002
|
+
message: "Multiple SvelteKit apps found \u2014 which one should svelte-vitals analyze?",
|
|
1003
|
+
options: apps.map((a) => ({ value: a, label: a }))
|
|
1004
|
+
});
|
|
1005
|
+
return p2.isCancel(res) ? null : res;
|
|
1006
|
+
}
|
|
702
1007
|
async function main() {
|
|
703
1008
|
const rawArgs = process.argv.slice(2);
|
|
704
1009
|
if (rawArgs[0] === "install") {
|
|
705
1010
|
const code2 = await runInstallCli(rawArgs.slice(1));
|
|
706
1011
|
process.exit(code2);
|
|
707
1012
|
}
|
|
708
|
-
|
|
1013
|
+
if (rawArgs[0] === "ci") {
|
|
1014
|
+
const code2 = await runCiCli(rawArgs.slice(1));
|
|
1015
|
+
process.exit(code2);
|
|
1016
|
+
}
|
|
1017
|
+
const argv = mri3(process.argv.slice(2), {
|
|
709
1018
|
alias: { h: "help", v: "version" },
|
|
710
|
-
boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color"],
|
|
1019
|
+
boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color", "score"],
|
|
711
1020
|
string: [
|
|
712
1021
|
"meta-components",
|
|
713
1022
|
"treat-dynamic-as",
|
|
@@ -719,7 +1028,9 @@ async function main() {
|
|
|
719
1028
|
"min-health",
|
|
720
1029
|
"out-file",
|
|
721
1030
|
"diff",
|
|
722
|
-
"
|
|
1031
|
+
"baseline",
|
|
1032
|
+
"weights",
|
|
1033
|
+
"category"
|
|
723
1034
|
]
|
|
724
1035
|
});
|
|
725
1036
|
if (argv.help) {
|
|
@@ -744,7 +1055,7 @@ async function main() {
|
|
|
744
1055
|
}
|
|
745
1056
|
minHealth = n;
|
|
746
1057
|
}
|
|
747
|
-
const code = await run({ ...options, minHealth, noColor: argv["no-color"] });
|
|
1058
|
+
const code = await run({ ...options, minHealth, noColor: argv["no-color"], selectApp });
|
|
748
1059
|
process.exit(code);
|
|
749
1060
|
}
|
|
750
1061
|
void main();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
-
import { dirname } from "path";
|
|
3
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
4
4
|
import {
|
|
5
5
|
allRules as allRules2,
|
|
6
6
|
runRules,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
formatSarifReport,
|
|
11
11
|
formatGithubReport,
|
|
12
12
|
formatHtmlReport,
|
|
13
|
+
formatMarkdownReport,
|
|
13
14
|
summarize,
|
|
14
15
|
hasFailureAtOrAbove,
|
|
15
16
|
computeHealth,
|
|
@@ -73,7 +74,7 @@ async function detectProject(rt, cwd) {
|
|
|
73
74
|
const hasRoutes = await rt.exists(rt.join(cwd, ROUTES_DIR));
|
|
74
75
|
if (hasKitDep || hasConfig && hasRoutes) return;
|
|
75
76
|
throw new ProjectError(
|
|
76
|
-
"No SvelteKit project found in the current directory. Run this inside a SvelteKit app, or pass
|
|
77
|
+
"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)."
|
|
77
78
|
);
|
|
78
79
|
}
|
|
79
80
|
async function enumerateRoutePages(rt, cwd) {
|
|
@@ -650,6 +651,23 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
650
651
|
// src/providers/source/components.ts
|
|
651
652
|
import { collectComponentFacts } from "@svelte-vitals/core";
|
|
652
653
|
|
|
654
|
+
// src/discover-apps.ts
|
|
655
|
+
import { existsSync } from "fs";
|
|
656
|
+
import { join as join2, dirname } from "path";
|
|
657
|
+
import { glob } from "tinyglobby";
|
|
658
|
+
async function discoverApps(cwd) {
|
|
659
|
+
const configs = await glob("**/svelte.config.{js,ts}", {
|
|
660
|
+
cwd,
|
|
661
|
+
dot: false,
|
|
662
|
+
deep: 4,
|
|
663
|
+
ignore: ["**/node_modules/**", "**/.svelte-kit/**", "**/build/**", "**/dist/**", "**/.git/**"]
|
|
664
|
+
});
|
|
665
|
+
const dirs = [...new Set(configs.map((c) => dirname(c)))].filter(
|
|
666
|
+
(d) => d !== "." && existsSync(join2(cwd, d, "src", "routes"))
|
|
667
|
+
);
|
|
668
|
+
return dirs.sort();
|
|
669
|
+
}
|
|
670
|
+
|
|
653
671
|
// src/version.ts
|
|
654
672
|
import { readFileSync } from "fs";
|
|
655
673
|
function readPackageVersion() {
|
|
@@ -664,7 +682,7 @@ function readPackageVersion() {
|
|
|
664
682
|
// src/reporter-resolve.ts
|
|
665
683
|
var AGENT_ENV_VARS = ["CLAUDECODE", "SVELTE_VITALS_AGENT"];
|
|
666
684
|
function isReporterName(value) {
|
|
667
|
-
return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html";
|
|
685
|
+
return value === "console" || value === "json" || value === "agent" || value === "sarif" || value === "github" || value === "html" || value === "md";
|
|
668
686
|
}
|
|
669
687
|
function isAgentEnv(env = process.env) {
|
|
670
688
|
return AGENT_ENV_VARS.some((key) => {
|
|
@@ -711,6 +729,58 @@ function filterToChangedFiles(results, changed) {
|
|
|
711
729
|
return results.filter((r) => r.location !== void 0 && changed.has(r.location));
|
|
712
730
|
}
|
|
713
731
|
|
|
732
|
+
// src/baseline.ts
|
|
733
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
734
|
+
import { mkdtempSync, rmSync } from "fs";
|
|
735
|
+
import { tmpdir } from "os";
|
|
736
|
+
import { join as join3 } from "path";
|
|
737
|
+
function git2(args, cwd) {
|
|
738
|
+
return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
739
|
+
}
|
|
740
|
+
function findingKey(r) {
|
|
741
|
+
return `${r.id}::${r.route ?? ""}::${r.location ?? ""}`;
|
|
742
|
+
}
|
|
743
|
+
function checkoutBaseline(cwd, ref) {
|
|
744
|
+
let tmp;
|
|
745
|
+
try {
|
|
746
|
+
const repoRoot = git2(["rev-parse", "--show-toplevel"], cwd).trim();
|
|
747
|
+
const showPrefix = git2(["rev-parse", "--show-prefix"], cwd).trim().replace(/\/+$/, "");
|
|
748
|
+
tmp = mkdtempSync(join3(tmpdir(), "svelte-vitals-baseline-"));
|
|
749
|
+
const wt = join3(tmp, "wt");
|
|
750
|
+
git2(["worktree", "add", "--detach", wt, ref], repoRoot);
|
|
751
|
+
const analyzeCwd = showPrefix ? join3(wt, showPrefix) : wt;
|
|
752
|
+
const tmpDir = tmp;
|
|
753
|
+
const cleanup = () => {
|
|
754
|
+
try {
|
|
755
|
+
execFileSync2("git", ["worktree", "remove", "--force", wt], {
|
|
756
|
+
cwd: repoRoot,
|
|
757
|
+
encoding: "utf8",
|
|
758
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
759
|
+
});
|
|
760
|
+
} catch {
|
|
761
|
+
try {
|
|
762
|
+
execFileSync2("git", ["worktree", "prune"], {
|
|
763
|
+
cwd: repoRoot,
|
|
764
|
+
encoding: "utf8",
|
|
765
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
766
|
+
});
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
} finally {
|
|
770
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
return { analyzeCwd, cleanup };
|
|
774
|
+
} catch {
|
|
775
|
+
if (tmp !== void 0) rmSync(tmp, { recursive: true, force: true });
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function filterToNewFindings(results, baselineResults) {
|
|
780
|
+
const baselineKeys = new Set(baselineResults.map(findingKey));
|
|
781
|
+
return results.filter((r) => !baselineKeys.has(findingKey(r)));
|
|
782
|
+
}
|
|
783
|
+
|
|
714
784
|
// src/color.ts
|
|
715
785
|
import { noColorPalette } from "@svelte-vitals/core";
|
|
716
786
|
var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
|
|
@@ -754,8 +824,8 @@ function startSpinner(text, opts) {
|
|
|
754
824
|
}
|
|
755
825
|
|
|
756
826
|
// src/config-file.ts
|
|
757
|
-
import { existsSync } from "fs";
|
|
758
|
-
import { join as
|
|
827
|
+
import { existsSync as existsSync2 } from "fs";
|
|
828
|
+
import { join as join4 } from "path";
|
|
759
829
|
import { pathToFileURL } from "url";
|
|
760
830
|
|
|
761
831
|
// src/rules-config.ts
|
|
@@ -852,7 +922,7 @@ function validateConfigFile(raw, path) {
|
|
|
852
922
|
return { config, warnings };
|
|
853
923
|
}
|
|
854
924
|
async function loadConfigFile(cwd) {
|
|
855
|
-
const found = CONFIG_FILENAMES.map((name) =>
|
|
925
|
+
const found = CONFIG_FILENAMES.map((name) => join4(cwd, name)).find((path) => existsSync2(path));
|
|
856
926
|
if (!found) return void 0;
|
|
857
927
|
let mod;
|
|
858
928
|
try {
|
|
@@ -879,9 +949,9 @@ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
|
|
|
879
949
|
function spinnerEnabled(opts) {
|
|
880
950
|
return opts.reporter === "console" && opts.stderrIsTTY && !isAutoDetectedAgent(opts.rawReporter, opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stderrIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
|
|
881
951
|
}
|
|
882
|
-
function routeMatcher(
|
|
883
|
-
if (!
|
|
884
|
-
const body =
|
|
952
|
+
function routeMatcher(glob2) {
|
|
953
|
+
if (!glob2) return () => true;
|
|
954
|
+
const body = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, " ").replace(/\*/g, "[^/]*").replace(/\/ $/g, "(?:/.*)?").replace(/^ \//g, "(?:.*/)?").replace(/ \//g, "(?:.*/)?").replace(/\/ /g, "(?:/.*)?").replace(/ /g, ".*");
|
|
885
955
|
const re = new RegExp(`^${body}$`);
|
|
886
956
|
return (route) => re.test(route.replace(/^\//, ""));
|
|
887
957
|
}
|
|
@@ -907,7 +977,8 @@ async function analyzeProject(opts = {}) {
|
|
|
907
977
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
908
978
|
const project = await collectProjectFacts(rt, cwd);
|
|
909
979
|
const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
|
|
910
|
-
const
|
|
980
|
+
const selected = selectRules(allRules2, config);
|
|
981
|
+
const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
|
|
911
982
|
const results = applyRuleSeverities(
|
|
912
983
|
await runRules(rules, { heads, images, headings, components, project, config }),
|
|
913
984
|
config
|
|
@@ -924,7 +995,7 @@ async function run(opts = {}) {
|
|
|
924
995
|
const env = opts.env ?? process.env;
|
|
925
996
|
const reporter = resolveReporter(opts.reporter, env);
|
|
926
997
|
const spinner = startSpinner("Analyzing\u2026", {
|
|
927
|
-
enabled: spinnerEnabled({
|
|
998
|
+
enabled: !opts.score && spinnerEnabled({
|
|
928
999
|
reporter,
|
|
929
1000
|
rawReporter: opts.reporter,
|
|
930
1001
|
stderrIsTTY: opts.stderrIsTTY ?? !!process.stderr.isTTY,
|
|
@@ -932,25 +1003,75 @@ async function run(opts = {}) {
|
|
|
932
1003
|
noColorFlag: opts.noColor
|
|
933
1004
|
})
|
|
934
1005
|
});
|
|
1006
|
+
let cwd = opts.cwd ?? process.cwd();
|
|
935
1007
|
let analysis;
|
|
936
1008
|
try {
|
|
937
1009
|
analysis = await analyzeProject({
|
|
938
|
-
cwd
|
|
1010
|
+
cwd,
|
|
939
1011
|
metaComponents: opts.metaComponents,
|
|
940
1012
|
treatDynamicAs: opts.treatDynamicAs,
|
|
941
1013
|
route: opts.route,
|
|
942
1014
|
failOn: opts.failOn,
|
|
943
1015
|
rules: opts.rules,
|
|
944
|
-
weights: opts.weights
|
|
1016
|
+
weights: opts.weights,
|
|
1017
|
+
categories: opts.categories
|
|
945
1018
|
});
|
|
946
1019
|
} catch (err) {
|
|
947
1020
|
spinner.stop();
|
|
948
1021
|
if (err instanceof ProjectError) {
|
|
949
|
-
|
|
1022
|
+
if (opts.explicitPath) {
|
|
1023
|
+
errorLog(err.message);
|
|
1024
|
+
return 2;
|
|
1025
|
+
}
|
|
1026
|
+
const apps = await discoverApps(cwd);
|
|
1027
|
+
if (apps.length === 0) {
|
|
1028
|
+
errorLog(err.message);
|
|
1029
|
+
return 2;
|
|
1030
|
+
}
|
|
1031
|
+
let chosen;
|
|
1032
|
+
if (apps.length === 1) {
|
|
1033
|
+
errorLog(`svelte-vitals: detected SvelteKit app at ${apps[0]}; analyzing it.`);
|
|
1034
|
+
chosen = apps[0];
|
|
1035
|
+
} else if (
|
|
1036
|
+
// clack reads from stdin and renders to stdout, so both must be interactive —
|
|
1037
|
+
// a piped/redirected stdin would leave the prompt hanging for input that never comes.
|
|
1038
|
+
(opts.stdinIsTTY ?? !!process.stdin.isTTY) && (opts.stdoutIsTTY ?? !!process.stdout.isTTY) && opts.selectApp
|
|
1039
|
+
) {
|
|
1040
|
+
const selection = await opts.selectApp(apps);
|
|
1041
|
+
if (selection === null) {
|
|
1042
|
+
log("Cancelled.");
|
|
1043
|
+
return 0;
|
|
1044
|
+
}
|
|
1045
|
+
chosen = selection;
|
|
1046
|
+
} else {
|
|
1047
|
+
errorLog(`svelte-vitals: multiple SvelteKit apps found: ${apps.join(", ")}.`);
|
|
1048
|
+
errorLog(`svelte-vitals: pass one as a path, e.g. \`npx svelte-vitals ${apps[0]}\`.`);
|
|
1049
|
+
return 2;
|
|
1050
|
+
}
|
|
1051
|
+
cwd = join5(cwd, chosen);
|
|
1052
|
+
try {
|
|
1053
|
+
analysis = await analyzeProject({
|
|
1054
|
+
cwd,
|
|
1055
|
+
metaComponents: opts.metaComponents,
|
|
1056
|
+
treatDynamicAs: opts.treatDynamicAs,
|
|
1057
|
+
route: opts.route,
|
|
1058
|
+
failOn: opts.failOn,
|
|
1059
|
+
rules: opts.rules,
|
|
1060
|
+
weights: opts.weights,
|
|
1061
|
+
categories: opts.categories
|
|
1062
|
+
});
|
|
1063
|
+
} catch (err2) {
|
|
1064
|
+
if (err2 instanceof ProjectError) {
|
|
1065
|
+
errorLog(err2.message);
|
|
1066
|
+
return 2;
|
|
1067
|
+
}
|
|
1068
|
+
errorLog(`svelte-vitals: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
1069
|
+
return 2;
|
|
1070
|
+
}
|
|
1071
|
+
} else {
|
|
1072
|
+
errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
|
|
950
1073
|
return 2;
|
|
951
1074
|
}
|
|
952
|
-
errorLog(`svelte-vitals: ${err instanceof Error ? err.message : String(err)}`);
|
|
953
|
-
return 2;
|
|
954
1075
|
}
|
|
955
1076
|
spinner.stop();
|
|
956
1077
|
for (const w of analysis.warnings) errorLog(`svelte-vitals: ${w}`);
|
|
@@ -958,7 +1079,6 @@ async function run(opts = {}) {
|
|
|
958
1079
|
const { config, version } = analysis;
|
|
959
1080
|
let results = analysis.results;
|
|
960
1081
|
if (opts.staged || opts.diffBase !== void 0) {
|
|
961
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
962
1082
|
const changed = opts.staged ? getChangedFiles(cwd, { staged: true }) : getChangedFiles(cwd, { base: opts.diffBase });
|
|
963
1083
|
if (changed === void 0) {
|
|
964
1084
|
errorLog(
|
|
@@ -968,46 +1088,78 @@ async function run(opts = {}) {
|
|
|
968
1088
|
results = filterToChangedFiles(results, changed);
|
|
969
1089
|
}
|
|
970
1090
|
}
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1091
|
+
if (opts.baseline !== void 0) {
|
|
1092
|
+
const checkout = checkoutBaseline(cwd, opts.baseline);
|
|
1093
|
+
if (checkout === void 0) {
|
|
1094
|
+
errorLog(
|
|
1095
|
+
`svelte-vitals: could not analyze baseline '${opts.baseline}' (not a git repo, git unavailable, or bad ref); reporting all findings.`
|
|
1096
|
+
);
|
|
1097
|
+
} else {
|
|
1098
|
+
try {
|
|
1099
|
+
const base = await analyzeProject({
|
|
1100
|
+
cwd: checkout.analyzeCwd,
|
|
1101
|
+
metaComponents: opts.metaComponents,
|
|
1102
|
+
treatDynamicAs: opts.treatDynamicAs,
|
|
1103
|
+
route: opts.route,
|
|
1104
|
+
failOn: opts.failOn,
|
|
1105
|
+
rules: opts.rules,
|
|
1106
|
+
weights: opts.weights,
|
|
1107
|
+
categories: opts.categories
|
|
1108
|
+
});
|
|
1109
|
+
results = filterToNewFindings(results, base.results);
|
|
1110
|
+
} catch {
|
|
1111
|
+
errorLog(`svelte-vitals: baseline analysis of '${opts.baseline}' failed; reporting all findings.`);
|
|
1112
|
+
} finally {
|
|
1113
|
+
checkout.cleanup();
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
980
1116
|
}
|
|
981
|
-
if (
|
|
982
|
-
log(
|
|
983
|
-
} else
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
if (
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1117
|
+
if (opts.score) {
|
|
1118
|
+
log(String(computeHealth(results, config).health));
|
|
1119
|
+
} else {
|
|
1120
|
+
if (reporter === "agent" && isAutoDetectedAgent(opts.reporter, env)) {
|
|
1121
|
+
errorLog(
|
|
1122
|
+
"svelte-vitals: agent reporter auto-selected (AI-agent env detected); override with --reporter console|json."
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (reporter === "github" && isAutoDetectedGithub(opts.reporter, env)) {
|
|
1126
|
+
errorLog(
|
|
1127
|
+
"svelte-vitals: github reporter auto-selected (GitHub Actions detected); override with --reporter console|json|sarif."
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
if (reporter === "json") {
|
|
1131
|
+
log(formatJsonReport(results, config, { version }));
|
|
1132
|
+
} else if (reporter === "agent") {
|
|
1133
|
+
log(formatAgentReport(results, config));
|
|
1134
|
+
} else if (reporter === "sarif") {
|
|
1135
|
+
log(formatSarifReport(results, config, { version }));
|
|
1136
|
+
} else if (reporter === "github") {
|
|
1137
|
+
const output = formatGithubReport(results, config);
|
|
1138
|
+
if (output) log(output);
|
|
1139
|
+
} else if (reporter === "html") {
|
|
1140
|
+
const html = formatHtmlReport(results, config, { version });
|
|
1141
|
+
if (opts.outFile === "-") {
|
|
1142
|
+
log(html);
|
|
1143
|
+
} else {
|
|
1144
|
+
const path = opts.outFile || "svelte-vitals-report.html";
|
|
1145
|
+
const write = opts.writeFile ?? ((p, c) => {
|
|
1146
|
+
mkdirSync(dirname2(p), { recursive: true });
|
|
1147
|
+
writeFileSync(p, c);
|
|
1148
|
+
});
|
|
1149
|
+
write(path, html);
|
|
1150
|
+
errorLog(`svelte-vitals: wrote report to ${path}`);
|
|
1151
|
+
}
|
|
1152
|
+
} else if (reporter === "md") {
|
|
1153
|
+
log(formatMarkdownReport(results, config, { version }));
|
|
994
1154
|
} else {
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1155
|
+
const colorOn = colorEnabled({
|
|
1156
|
+
reporter,
|
|
1157
|
+
isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
|
|
1158
|
+
env,
|
|
1159
|
+
noColorFlag: opts.noColor
|
|
999
1160
|
});
|
|
1000
|
-
|
|
1001
|
-
errorLog(`svelte-vitals: wrote report to ${path}`);
|
|
1161
|
+
log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
|
|
1002
1162
|
}
|
|
1003
|
-
} else {
|
|
1004
|
-
const colorOn = colorEnabled({
|
|
1005
|
-
reporter,
|
|
1006
|
-
isTTY: opts.stdoutIsTTY ?? !!process.stdout.isTTY,
|
|
1007
|
-
env,
|
|
1008
|
-
noColorFlag: opts.noColor
|
|
1009
|
-
});
|
|
1010
|
-
log(formatConsoleReport(results, config, { byRoute: opts.byRoute ?? false, palette: paletteFor(colorOn) }));
|
|
1011
1163
|
}
|
|
1012
1164
|
const summary = summarize(results, config);
|
|
1013
1165
|
const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
|
|
2
2
|
export { defineConfig } from '@svelte-vitals/core';
|
|
3
3
|
|
|
4
|
-
type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html';
|
|
4
|
+
type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html' | 'md';
|
|
5
5
|
|
|
6
6
|
/** Thrown when the target directory is not a SvelteKit project (CLI maps to exit 2). */
|
|
7
7
|
declare class ProjectError extends Error {
|
|
@@ -59,10 +59,14 @@ interface RunOptions {
|
|
|
59
59
|
rules?: Record<string, RuleSetting>;
|
|
60
60
|
/** Per-category weights for the combined Health score (flag > config file > default 1 each). */
|
|
61
61
|
weights?: Partial<Record<Category, number>>;
|
|
62
|
+
/** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
|
|
63
|
+
categories?: Category[];
|
|
62
64
|
/** Override process.env for reporter auto-detection (mainly useful in tests). */
|
|
63
65
|
env?: NodeJS.ProcessEnv;
|
|
64
66
|
/** Fail (exit 1) when the combined Health score is below this value (0–100). */
|
|
65
67
|
minHealth?: number;
|
|
68
|
+
/** Print only the combined Health score (integer) to stdout. */
|
|
69
|
+
score?: boolean;
|
|
66
70
|
/** Output path for --reporter html (default 'svelte-vitals-report.html'; '-' = stdout). */
|
|
67
71
|
outFile?: string;
|
|
68
72
|
/** Injected file writer for --reporter html (defaults to node:fs writeFileSync). Mainly for tests. */
|
|
@@ -71,12 +75,20 @@ interface RunOptions {
|
|
|
71
75
|
diffBase?: string;
|
|
72
76
|
/** Report only findings in files staged for commit. Takes precedence over `diffBase`. */
|
|
73
77
|
staged?: boolean;
|
|
78
|
+
/** Report only findings not present when analyzing this git ref (e.g. the PR base). */
|
|
79
|
+
baseline?: string;
|
|
74
80
|
/** Disable ANSI color in console output. */
|
|
75
81
|
noColor?: boolean;
|
|
76
82
|
/** Override stdout TTY detection (tests). */
|
|
77
83
|
stdoutIsTTY?: boolean;
|
|
78
84
|
/** Override stderr TTY detection (tests). */
|
|
79
85
|
stderrIsTTY?: boolean;
|
|
86
|
+
/** Override stdin TTY detection (tests). */
|
|
87
|
+
stdinIsTTY?: boolean;
|
|
88
|
+
/** True when the user passed a path argument — discovery must not run (design: never reinterpret an explicit target). */
|
|
89
|
+
explicitPath?: boolean;
|
|
90
|
+
/** Injected picker for the monorepo app selector (bin.ts wires a clack implementation; null = cancelled). */
|
|
91
|
+
selectApp?: (apps: string[]) => Promise<string | null>;
|
|
80
92
|
}
|
|
81
93
|
/**
|
|
82
94
|
* Whether the "Analyzing…" spinner should run. Unlike color, the spinner animates
|
|
@@ -103,6 +115,8 @@ interface AnalyzeOptions {
|
|
|
103
115
|
rules?: Record<string, RuleSetting>;
|
|
104
116
|
/** Per-category weights for the combined Health score (flag > config file > default 1 each). */
|
|
105
117
|
weights?: Partial<Record<Category, number>>;
|
|
118
|
+
/** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
|
|
119
|
+
categories?: Category[];
|
|
106
120
|
}
|
|
107
121
|
interface AnalyzeResult {
|
|
108
122
|
results: Result[];
|
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.20.0",
|
|
4
4
|
"description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"smol-toml": "^1.7.0",
|
|
46
46
|
"svelte": "^5.56.4",
|
|
47
47
|
"tinyglobby": "^0.2.17",
|
|
48
|
-
"@svelte-vitals/core": "0.
|
|
48
|
+
"@svelte-vitals/core": "0.21.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.13.2"
|