svelte-vitals 0.18.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 +385 -22
- package/dist/{chunk-NLQZ3CMZ.js → chunk-AAM2A7D7.js} +484 -174
- package/dist/index.d.ts +57 -5
- package/dist/index.js +5 -1
- package/package.json +4 -4
package/dist/bin.js
CHANGED
|
@@ -6,12 +6,76 @@ 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
|
|
16
|
+
var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
|
|
17
|
+
function parseWeights(raw, errors) {
|
|
18
|
+
if (typeof raw !== "string" || raw.trim() === "") return void 0;
|
|
19
|
+
const weights = {};
|
|
20
|
+
const unknownCategories = [];
|
|
21
|
+
const invalidValues = [];
|
|
22
|
+
for (const pair of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
23
|
+
const eq = pair.indexOf("=");
|
|
24
|
+
if (eq === -1) {
|
|
25
|
+
invalidValues.push(pair);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const category = pair.slice(0, eq).trim().toLowerCase();
|
|
29
|
+
const valueRaw = pair.slice(eq + 1).trim();
|
|
30
|
+
if (!CATEGORIES.includes(category)) {
|
|
31
|
+
unknownCategories.push(category);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (valueRaw === "") {
|
|
35
|
+
invalidValues.push(pair);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const value = Number(valueRaw);
|
|
39
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
40
|
+
invalidValues.push(pair);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
weights[category] = value;
|
|
44
|
+
}
|
|
45
|
+
if (unknownCategories.length > 0) {
|
|
46
|
+
errors.push(`svelte-vitals: unknown category(ies) in --weights: ${unknownCategories.join(", ")}`);
|
|
47
|
+
errors.push(`Known categories: ${CATEGORIES.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
if (invalidValues.length > 0) {
|
|
50
|
+
errors.push(
|
|
51
|
+
`svelte-vitals: invalid --weights entry(ies): ${invalidValues.join(", ")}; expected category=number with a finite number >= 0.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (unknownCategories.length === 0 && invalidValues.length === 0 && Object.keys(weights).length === 0) {
|
|
55
|
+
errors.push("svelte-vitals: --weights was passed but contains no category=number pairs.");
|
|
56
|
+
}
|
|
57
|
+
return weights;
|
|
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
|
+
}
|
|
15
79
|
var toList = (v) => typeof v === "string" ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
|
|
16
80
|
function resolveArgs(argv) {
|
|
17
81
|
const warnings = [];
|
|
@@ -28,6 +92,14 @@ function resolveArgs(argv) {
|
|
|
28
92
|
const route = typeof argv.route === "string" ? argv.route : void 0;
|
|
29
93
|
const diffBase = typeof argv.diff === "string" ? argv.diff || "HEAD" : void 0;
|
|
30
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
|
+
}
|
|
31
103
|
const allow = toList(argv.rules);
|
|
32
104
|
const ignore = toList(argv.ignore);
|
|
33
105
|
const unknown = findUnknownRuleIds([...allow, ...ignore]);
|
|
@@ -41,7 +113,7 @@ function resolveArgs(argv) {
|
|
|
41
113
|
} else if (typeof argv.reporter === "string") {
|
|
42
114
|
if (!isReporterName(argv.reporter)) {
|
|
43
115
|
errors.push(
|
|
44
|
-
`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.`
|
|
45
117
|
);
|
|
46
118
|
} else {
|
|
47
119
|
reporter = argv.reporter;
|
|
@@ -55,10 +127,21 @@ function resolveArgs(argv) {
|
|
|
55
127
|
);
|
|
56
128
|
}
|
|
57
129
|
const failOn = argv["fail-on-warning"] ? "warning" : failOnValid ? failOnRaw : void 0;
|
|
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
|
+
}
|
|
136
|
+
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
137
|
+
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
58
138
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
59
139
|
return {
|
|
60
140
|
options: {
|
|
61
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,
|
|
62
145
|
metaComponents,
|
|
63
146
|
treatDynamicAs,
|
|
64
147
|
route,
|
|
@@ -66,9 +149,13 @@ function resolveArgs(argv) {
|
|
|
66
149
|
outFile: typeof argv["out-file"] === "string" ? argv["out-file"] : void 0,
|
|
67
150
|
byRoute: Boolean(argv["by-route"]),
|
|
68
151
|
failOn,
|
|
69
|
-
rules
|
|
152
|
+
rules,
|
|
153
|
+
...weights !== void 0 ? { weights } : {},
|
|
154
|
+
...categories !== void 0 ? { categories } : {},
|
|
155
|
+
...score ? { score } : {},
|
|
70
156
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
71
|
-
...staged ? { staged } : {}
|
|
157
|
+
...staged ? { staged } : {},
|
|
158
|
+
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
72
159
|
},
|
|
73
160
|
warnings,
|
|
74
161
|
errors
|
|
@@ -189,6 +276,93 @@ function isViteTargetId(id) {
|
|
|
189
276
|
return VITE_TARGETS.some((t) => t.id === id);
|
|
190
277
|
}
|
|
191
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
|
+
|
|
192
366
|
// src/install/codemod-vite-config.ts
|
|
193
367
|
import { parseModule, generateCode, builders, MagicastError } from "magicast";
|
|
194
368
|
var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
|
|
@@ -205,7 +379,7 @@ function codemodViteConfig(existing) {
|
|
|
205
379
|
return { status: "manual", snippet: MANUAL_SNIPPET };
|
|
206
380
|
}
|
|
207
381
|
const already = configObj.plugins.find(
|
|
208
|
-
(
|
|
382
|
+
(p3) => p3?.$type === "function-call" && p3?.$callee === "svelteVitals"
|
|
209
383
|
);
|
|
210
384
|
if (already !== void 0) {
|
|
211
385
|
return { status: "exists" };
|
|
@@ -348,6 +522,13 @@ function planForDevOverlay(io) {
|
|
|
348
522
|
const result = codemodHooksServer(content);
|
|
349
523
|
return { id: "vite-dev-overlay", label: viteTargetById("vite-dev-overlay").label, path, ...result };
|
|
350
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
|
+
}
|
|
351
532
|
function indent(text) {
|
|
352
533
|
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
353
534
|
}
|
|
@@ -356,7 +537,7 @@ function rowLine(r) {
|
|
|
356
537
|
return r.status === "manual" && r.snippet ? `${head}
|
|
357
538
|
${indent(r.snippet)}` : head;
|
|
358
539
|
}
|
|
359
|
-
async function runInstall(flags, io, prompts) {
|
|
540
|
+
async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
360
541
|
let ids;
|
|
361
542
|
if (flags.client && flags.client.length > 0) {
|
|
362
543
|
ids = flags.client;
|
|
@@ -374,10 +555,21 @@ async function runInstall(flags, io, prompts) {
|
|
|
374
555
|
const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
|
|
375
556
|
(f) => configExists(join3(io.cwd, f))
|
|
376
557
|
);
|
|
377
|
-
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
|
+
];
|
|
378
569
|
const options = [
|
|
379
570
|
...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
|
|
380
|
-
...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 }))
|
|
381
573
|
];
|
|
382
574
|
const picked = await prompts.selectClients(options, detected);
|
|
383
575
|
if (picked === null) {
|
|
@@ -387,13 +579,14 @@ async function runInstall(flags, io, prompts) {
|
|
|
387
579
|
ids = picked;
|
|
388
580
|
} else {
|
|
389
581
|
io.errorLog(
|
|
390
|
-
"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."
|
|
391
583
|
);
|
|
392
584
|
return 2;
|
|
393
585
|
}
|
|
394
586
|
const clients = ids.map(clientById).filter((c) => c !== void 0);
|
|
395
587
|
const viteIds = ids.filter(isViteTargetId);
|
|
396
|
-
|
|
588
|
+
const agentIds = ids.filter(isAgentTargetId);
|
|
589
|
+
if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0) {
|
|
397
590
|
io.errorLog("svelte-vitals: no valid clients or targets selected.");
|
|
398
591
|
return 2;
|
|
399
592
|
}
|
|
@@ -427,6 +620,10 @@ async function runInstall(flags, io, prompts) {
|
|
|
427
620
|
for (const viteId of viteIds) {
|
|
428
621
|
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForDevOverlay(io));
|
|
429
622
|
}
|
|
623
|
+
for (const agentId of agentIds) {
|
|
624
|
+
const target = agentTargetById(agentId);
|
|
625
|
+
rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
|
|
626
|
+
}
|
|
430
627
|
const planText = rows.map(rowLine).join("\n");
|
|
431
628
|
io.log("Plan:");
|
|
432
629
|
io.log(planText);
|
|
@@ -483,7 +680,11 @@ ${indent(r.snippet ?? "")}`);
|
|
|
483
680
|
}
|
|
484
681
|
|
|
485
682
|
// src/install/args.ts
|
|
486
|
-
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
|
+
];
|
|
487
688
|
var EXPECTED_TARGETS = VALID_TARGETS.join("|");
|
|
488
689
|
function resolveInstallArgs(argv) {
|
|
489
690
|
const warnings = [];
|
|
@@ -521,16 +722,20 @@ function resolveInstallArgs(argv) {
|
|
|
521
722
|
}
|
|
522
723
|
|
|
523
724
|
// src/install/cli.ts
|
|
524
|
-
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
|
|
525
726
|
|
|
526
727
|
Usage:
|
|
527
728
|
svelte-vitals install [options]
|
|
528
729
|
|
|
529
730
|
Options:
|
|
530
|
-
--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)
|
|
531
733
|
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-dev-overlay
|
|
532
734
|
wires up the dev-overlay hook in src/hooks.server.{ts,js}. --force does not apply
|
|
533
|
-
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.
|
|
534
739
|
--scope <scope> project | global (applies to all selected clients; codex is always global)
|
|
535
740
|
--yes, -y Skip the confirmation prompt
|
|
536
741
|
--dry-run Print the planned changes and exit without writing
|
|
@@ -614,7 +819,143 @@ async function runInstallCli(args) {
|
|
|
614
819
|
for (const w of warnings) console.error(w);
|
|
615
820
|
for (const e of errors) console.error(e);
|
|
616
821
|
if (!flags) return 2;
|
|
617
|
-
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;
|
|
618
959
|
}
|
|
619
960
|
|
|
620
961
|
// src/bin.ts
|
|
@@ -622,7 +963,8 @@ var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (
|
|
|
622
963
|
|
|
623
964
|
Usage:
|
|
624
965
|
svelte-vitals [path] [options]
|
|
625
|
-
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)
|
|
626
968
|
|
|
627
969
|
Options:
|
|
628
970
|
--meta-components <names> Comma-separated component names that emit head metadata
|
|
@@ -630,8 +972,9 @@ Options:
|
|
|
630
972
|
--route <glob> Only analyze routes matching this glob
|
|
631
973
|
--diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
|
|
632
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)
|
|
633
976
|
--by-route Show per-route score breakdown in console output
|
|
634
|
-
--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)
|
|
635
978
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
636
979
|
--json Alias for --reporter=json
|
|
637
980
|
--fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
|
|
@@ -639,24 +982,41 @@ Options:
|
|
|
639
982
|
--min-health <0-100> Fail (exit 1) when the combined Health score is below this value
|
|
640
983
|
--rules <ids> Comma-separated rule ids to enable (all others disabled)
|
|
641
984
|
--ignore <ids> Comma-separated rule ids to disable
|
|
985
|
+
--category <cats> Comma-separated categories to analyze: seo | performance | correctness | security | architecture
|
|
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)
|
|
642
988
|
--no-color Disable ANSI color in console output
|
|
643
989
|
-h, --help Show this help
|
|
644
990
|
-v, --version Show version
|
|
645
991
|
|
|
992
|
+
Config file:
|
|
993
|
+
svelte-vitals.config.{mjs,js,ts} in the analyzed directory; flags override it.
|
|
994
|
+
|
|
646
995
|
Exit codes:
|
|
647
996
|
0 no failing findings
|
|
648
997
|
1 critical finding present (or --fail-on threshold reached)
|
|
649
998
|
2 execution error (not a SvelteKit project / internal error)`;
|
|
650
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
|
+
}
|
|
651
1007
|
async function main() {
|
|
652
1008
|
const rawArgs = process.argv.slice(2);
|
|
653
1009
|
if (rawArgs[0] === "install") {
|
|
654
1010
|
const code2 = await runInstallCli(rawArgs.slice(1));
|
|
655
1011
|
process.exit(code2);
|
|
656
1012
|
}
|
|
657
|
-
|
|
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), {
|
|
658
1018
|
alias: { h: "help", v: "version" },
|
|
659
|
-
boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color"],
|
|
1019
|
+
boolean: ["by-route", "json", "fail-on-warning", "staged", "no-color", "score"],
|
|
660
1020
|
string: [
|
|
661
1021
|
"meta-components",
|
|
662
1022
|
"treat-dynamic-as",
|
|
@@ -667,7 +1027,10 @@ async function main() {
|
|
|
667
1027
|
"ignore",
|
|
668
1028
|
"min-health",
|
|
669
1029
|
"out-file",
|
|
670
|
-
"diff"
|
|
1030
|
+
"diff",
|
|
1031
|
+
"baseline",
|
|
1032
|
+
"weights",
|
|
1033
|
+
"category"
|
|
671
1034
|
]
|
|
672
1035
|
});
|
|
673
1036
|
if (argv.help) {
|
|
@@ -692,7 +1055,7 @@ async function main() {
|
|
|
692
1055
|
}
|
|
693
1056
|
minHealth = n;
|
|
694
1057
|
}
|
|
695
|
-
const code = await run({ ...options, minHealth, noColor: argv["no-color"] });
|
|
1058
|
+
const code = await run({ ...options, minHealth, noColor: argv["no-color"], selectApp });
|
|
696
1059
|
process.exit(code);
|
|
697
1060
|
}
|
|
698
1061
|
void main();
|