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