svelte-vitals 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/bin.js +219 -30
- package/dist/{chunk-HYTPZVSU.js → chunk-OCLDCX4Y.js} +323 -31
- package/dist/index.d.ts +82 -5
- package/dist/index.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
> **ESM-only** (Node 18+). Ships ES modules only; `require()` is unsupported by design.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx svelte-vitals
|
|
12
|
+
npx svelte-vitals@latest
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
> [!NOTE]
|
|
@@ -20,8 +20,8 @@ npx svelte-vitals
|
|
|
20
20
|
Run inside any SvelteKit project:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npx svelte-vitals
|
|
24
|
-
npx svelte-vitals ./apps/web # or a specific path
|
|
23
|
+
npx svelte-vitals@latest # analyze the current directory
|
|
24
|
+
npx svelte-vitals@latest ./apps/web # or a specific path
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
```
|
|
@@ -40,6 +40,8 @@ Passed (3)
|
|
|
40
40
|
|
|
41
41
|
By default, console output groups failures by rule (top 5 per severity, each with one example location and an "…and N more" count) and collapses the Passed section to a bare count, so large projects don't flood the terminal. Pass `--verbose` to see every finding uncapped and ungrouped, with each passed item listed individually. On an interactive, color-capable terminal the Health score plays a short reveal animation; pass `--no-animation` to disable it.
|
|
42
42
|
|
|
43
|
+
On an interactive terminal wide enough for the mascot (20+ columns), a small line-art face appears alongside both the analysis spinner and the Health-score reveal, reacting to the score (a perfect 100 gets a confetti flourish). On a wider terminal still (55+ columns) it also greets you with a short line in a speech bubble at startup, and again with a matching reaction line at the score reveal. `--no-animation` disables all of it, falling back to the plain spinner and plain score animation.
|
|
44
|
+
|
|
43
45
|
### Exit codes
|
|
44
46
|
|
|
45
47
|
| Code | Meaning |
|
package/dist/bin.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
readCoreVersion,
|
|
8
8
|
readPackageVersion,
|
|
9
9
|
run
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-OCLDCX4Y.js";
|
|
11
11
|
|
|
12
12
|
// src/bin.ts
|
|
13
13
|
import mri3 from "mri";
|
|
@@ -109,9 +109,7 @@ function resolveArgs(argv) {
|
|
|
109
109
|
errors.push(`Known rule ids: ${knownRuleIds().join(", ")}`);
|
|
110
110
|
}
|
|
111
111
|
let reporter;
|
|
112
|
-
if (argv.
|
|
113
|
-
reporter = "json";
|
|
114
|
-
} else if (typeof argv.reporter === "string") {
|
|
112
|
+
if (typeof argv.reporter === "string") {
|
|
115
113
|
if (!isReporterName(argv.reporter)) {
|
|
116
114
|
errors.push(
|
|
117
115
|
`svelte-vitals: unknown reporter '${argv.reporter}'. Valid values: console, json, agent, sarif, github, html, md.`
|
|
@@ -127,14 +125,21 @@ function resolveArgs(argv) {
|
|
|
127
125
|
`svelte-vitals: unknown --fail-on '${failOnRaw}'; expected critical|warning|info. No threshold applied.`
|
|
128
126
|
);
|
|
129
127
|
}
|
|
130
|
-
const failOn =
|
|
128
|
+
const failOn = failOnValid ? failOnRaw : void 0;
|
|
131
129
|
const weights = parseWeights(argv.weights, errors);
|
|
132
130
|
const categories = parseCategories(argv.category, errors);
|
|
133
131
|
const score = Boolean(argv.score);
|
|
134
|
-
if (score &&
|
|
132
|
+
if (score && typeof argv.reporter === "string") {
|
|
135
133
|
warnings.push("svelte-vitals: --score overrides --reporter; reporter output suppressed.");
|
|
136
134
|
}
|
|
137
135
|
const verbose = Boolean(argv["verbose"]);
|
|
136
|
+
const noColor = argv.color === false;
|
|
137
|
+
const noAnimation = argv.animation === false;
|
|
138
|
+
const noSuppressions = argv.suppressions === false;
|
|
139
|
+
const updateSuppressions = Boolean(argv["update-suppressions"]);
|
|
140
|
+
if (updateSuppressions && noSuppressions) {
|
|
141
|
+
errors.push("svelte-vitals: --update-suppressions and --no-suppressions cannot be used together.");
|
|
142
|
+
}
|
|
138
143
|
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
139
144
|
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
140
145
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
@@ -156,9 +161,13 @@ function resolveArgs(argv) {
|
|
|
156
161
|
...categories !== void 0 ? { categories } : {},
|
|
157
162
|
...score ? { score } : {},
|
|
158
163
|
...verbose ? { verbose } : {},
|
|
164
|
+
...noColor ? { noColor } : {},
|
|
165
|
+
...noAnimation ? { noAnimation } : {},
|
|
159
166
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
160
167
|
...staged ? { staged } : {},
|
|
161
|
-
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
168
|
+
...baselineRef !== void 0 ? { baseline: baselineRef } : {},
|
|
169
|
+
...noSuppressions ? { noSuppressions } : {},
|
|
170
|
+
...updateSuppressions ? { updateSuppressions } : {}
|
|
162
171
|
},
|
|
163
172
|
warnings,
|
|
164
173
|
errors
|
|
@@ -267,9 +276,9 @@ var VITE_TARGETS = [
|
|
|
267
276
|
hint: "Fails `vite build` when prerendered pages cross the SEO/Performance threshold"
|
|
268
277
|
},
|
|
269
278
|
{
|
|
270
|
-
id: "vite-
|
|
271
|
-
label: "
|
|
272
|
-
hint: "
|
|
279
|
+
id: "vite-hooks",
|
|
280
|
+
label: "Live dashboard accuracy",
|
|
281
|
+
hint: "Feeds real rendered results into the live dashboard as you browse \u2014 improves per-route accuracy, never fails a build"
|
|
273
282
|
}
|
|
274
283
|
];
|
|
275
284
|
function viteTargetById(id) {
|
|
@@ -301,6 +310,22 @@ function isAgentTargetId(id) {
|
|
|
301
310
|
return AGENT_TARGETS.some((t) => t.id === id);
|
|
302
311
|
}
|
|
303
312
|
|
|
313
|
+
// src/install/config-targets.ts
|
|
314
|
+
var CONFIG_TARGETS = [
|
|
315
|
+
{
|
|
316
|
+
id: "config-file",
|
|
317
|
+
label: "Config file",
|
|
318
|
+
hint: "Scaffolds svelte-vitals.config.mjs with every option commented out",
|
|
319
|
+
relPath: "svelte-vitals.config.mjs"
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
function configTargetById(id) {
|
|
323
|
+
return CONFIG_TARGETS.find((t) => t.id === id);
|
|
324
|
+
}
|
|
325
|
+
function isConfigTargetId(id) {
|
|
326
|
+
return CONFIG_TARGETS.some((t) => t.id === id);
|
|
327
|
+
}
|
|
328
|
+
|
|
304
329
|
// src/install/skill-content.ts
|
|
305
330
|
import { allRules, docsUrlFor } from "@svelte-vitals/core";
|
|
306
331
|
var CATEGORY_ORDER = ["seo", "performance", "correctness", "security", "architecture"];
|
|
@@ -366,6 +391,19 @@ alwaysApply: false
|
|
|
366
391
|
${sharedBody(version)}`;
|
|
367
392
|
}
|
|
368
393
|
|
|
394
|
+
// src/install/config-content.ts
|
|
395
|
+
function buildConfigFileTemplate() {
|
|
396
|
+
return `// svelte-vitals config file \u2014 https://oekazuma.github.io/svelte-vitals/guides/configuration/
|
|
397
|
+
export default {
|
|
398
|
+
// treatDynamicAs: 'pass', // 'pass' | 'warn' | 'fail' \u2014 how {data.title}-style dynamic values are scored
|
|
399
|
+
// metaComponents: ['Seo'], // component names that resolve SEO tags into <head>
|
|
400
|
+
// rules: {}, // e.g. { SEO001: 'off' } to disable a rule
|
|
401
|
+
// failOn: 'critical', // 'critical' | 'warning' | 'info'
|
|
402
|
+
// weights: {} // e.g. { seo: 2 } \u2014 per-category weight for the combined Health score
|
|
403
|
+
};
|
|
404
|
+
`;
|
|
405
|
+
}
|
|
406
|
+
|
|
369
407
|
// src/install/codemod-vite-config.ts
|
|
370
408
|
import { parseModule, generateCode, builders, MagicastError } from "magicast";
|
|
371
409
|
var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
|
|
@@ -529,10 +567,10 @@ function planForVitePlugin(io) {
|
|
|
529
567
|
const result = codemodViteConfig(content);
|
|
530
568
|
return { id: "vite-plugin", label: viteTargetById("vite-plugin").label, path, ...result };
|
|
531
569
|
}
|
|
532
|
-
function
|
|
570
|
+
function planForViteHooks(io) {
|
|
533
571
|
const { path, content } = resolveCandidate(io, ["src/hooks.server.ts", "src/hooks.server.js"]);
|
|
534
572
|
const result = codemodHooksServer(content);
|
|
535
|
-
return { id: "vite-
|
|
573
|
+
return { id: "vite-hooks", label: viteTargetById("vite-hooks").label, path, ...result };
|
|
536
574
|
}
|
|
537
575
|
function planForAgentTarget(target, io, force, version) {
|
|
538
576
|
const path = join3(io.cwd, target.relPath);
|
|
@@ -541,6 +579,13 @@ function planForAgentTarget(target, io, force, version) {
|
|
|
541
579
|
const status = existing === void 0 ? "created" : force ? "updated" : "exists";
|
|
542
580
|
return { id: target.id, label: target.label, path, status, content };
|
|
543
581
|
}
|
|
582
|
+
function planForConfigTarget(target, io, force) {
|
|
583
|
+
const path = join3(io.cwd, target.relPath);
|
|
584
|
+
const existing = io.readFile(path);
|
|
585
|
+
const content = buildConfigFileTemplate();
|
|
586
|
+
const status = existing === void 0 ? "created" : force ? "updated" : "exists";
|
|
587
|
+
return { id: target.id, label: target.label, path, status, content };
|
|
588
|
+
}
|
|
544
589
|
function indent(text) {
|
|
545
590
|
return text.split("\n").map((l) => ` ${l}`).join("\n");
|
|
546
591
|
}
|
|
@@ -549,7 +594,57 @@ function rowLine(r) {
|
|
|
549
594
|
return r.status === "manual" && r.snippet ? `${head}
|
|
550
595
|
${indent(r.snippet)}` : head;
|
|
551
596
|
}
|
|
597
|
+
async function runRefresh(io, flags, version) {
|
|
598
|
+
let hadFailure = false;
|
|
599
|
+
const rows = [];
|
|
600
|
+
for (const target of AGENT_TARGETS) {
|
|
601
|
+
const path = join3(io.cwd, target.relPath);
|
|
602
|
+
try {
|
|
603
|
+
if (io.readFile(path) === void 0) continue;
|
|
604
|
+
rows.push(planForAgentTarget(
|
|
605
|
+
target,
|
|
606
|
+
io,
|
|
607
|
+
/* force */
|
|
608
|
+
true,
|
|
609
|
+
version
|
|
610
|
+
));
|
|
611
|
+
} catch (err) {
|
|
612
|
+
hadFailure = true;
|
|
613
|
+
io.errorLog(`svelte-vitals: failed to read ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (rows.length === 0) {
|
|
617
|
+
if (hadFailure) return 2;
|
|
618
|
+
io.errorLog(
|
|
619
|
+
"svelte-vitals: no generated agent files found \u2014 run `svelte-vitals install --client claude-skill,cursor-rules` first."
|
|
620
|
+
);
|
|
621
|
+
return 0;
|
|
622
|
+
}
|
|
623
|
+
const planText = rows.map(rowLine).join("\n");
|
|
624
|
+
io.log("Plan:");
|
|
625
|
+
io.log(planText);
|
|
626
|
+
if (flags.dryRun) {
|
|
627
|
+
io.log("Dry run \u2014 no files written.");
|
|
628
|
+
return hadFailure ? 2 : 0;
|
|
629
|
+
}
|
|
630
|
+
for (const r of rows) {
|
|
631
|
+
try {
|
|
632
|
+
io.writeFile(r.path, r.content ?? "");
|
|
633
|
+
io.log(`\u2713 ${r.label}: ${r.status} ${r.path}`);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
hadFailure = true;
|
|
636
|
+
io.errorLog(`svelte-vitals: failed to write ${r.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (hadFailure) return 2;
|
|
640
|
+
io.log("");
|
|
641
|
+
io.log(`\u2713 refreshed ${rows.length} file(s).`);
|
|
642
|
+
return 0;
|
|
643
|
+
}
|
|
552
644
|
async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
645
|
+
if (flags.refresh) {
|
|
646
|
+
return runRefresh(io, flags, version);
|
|
647
|
+
}
|
|
553
648
|
let ids;
|
|
554
649
|
if (flags.client && flags.client.length > 0) {
|
|
555
650
|
ids = flags.client;
|
|
@@ -581,7 +676,8 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
581
676
|
const options = [
|
|
582
677
|
...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
|
|
583
678
|
...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
|
|
584
|
-
...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
679
|
+
...AGENT_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint })),
|
|
680
|
+
...CONFIG_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
|
|
585
681
|
];
|
|
586
682
|
const picked = await prompts.selectClients(options, detected);
|
|
587
683
|
if (picked === null) {
|
|
@@ -591,14 +687,15 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
591
687
|
ids = picked;
|
|
592
688
|
} else {
|
|
593
689
|
io.errorLog(
|
|
594
|
-
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-
|
|
690
|
+
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,config-file> to install non-interactively."
|
|
595
691
|
);
|
|
596
692
|
return 2;
|
|
597
693
|
}
|
|
598
694
|
const clients = ids.map(clientById).filter((c) => c !== void 0);
|
|
599
695
|
const viteIds = ids.filter(isViteTargetId);
|
|
600
696
|
const agentIds = ids.filter(isAgentTargetId);
|
|
601
|
-
|
|
697
|
+
const configIds = ids.filter(isConfigTargetId);
|
|
698
|
+
if (clients.length === 0 && viteIds.length === 0 && agentIds.length === 0 && configIds.length === 0) {
|
|
602
699
|
io.errorLog("svelte-vitals: no valid clients or targets selected.");
|
|
603
700
|
return 2;
|
|
604
701
|
}
|
|
@@ -630,12 +727,16 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
630
727
|
}
|
|
631
728
|
}
|
|
632
729
|
for (const viteId of viteIds) {
|
|
633
|
-
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) :
|
|
730
|
+
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForViteHooks(io));
|
|
634
731
|
}
|
|
635
732
|
for (const agentId of agentIds) {
|
|
636
733
|
const target = agentTargetById(agentId);
|
|
637
734
|
rows.push(planForAgentTarget(target, io, flags.force ?? false, version));
|
|
638
735
|
}
|
|
736
|
+
for (const configId of configIds) {
|
|
737
|
+
const target = configTargetById(configId);
|
|
738
|
+
rows.push(planForConfigTarget(target, io, flags.force ?? false));
|
|
739
|
+
}
|
|
639
740
|
const planText = rows.map(rowLine).join("\n");
|
|
640
741
|
io.log("Plan:");
|
|
641
742
|
io.log(planText);
|
|
@@ -724,14 +825,22 @@ function resolveInstallArgs(argv) {
|
|
|
724
825
|
if (rawScope === "project" || rawScope === "global") scope = rawScope;
|
|
725
826
|
else errors.push(`svelte-vitals: unknown --scope '${rawScope}'; expected project|global.`);
|
|
726
827
|
}
|
|
828
|
+
const refresh = Boolean(argv.refresh);
|
|
829
|
+
if (refresh && rawClients.length > 0) {
|
|
830
|
+
errors.push("svelte-vitals: --refresh regenerates existing files and cannot be combined with --client.");
|
|
831
|
+
}
|
|
727
832
|
if (errors.length > 0) return { flags: null, warnings, errors };
|
|
833
|
+
if (refresh && (scope !== void 0 || Boolean(argv.yes) || Boolean(argv.force))) {
|
|
834
|
+
warnings.push("svelte-vitals: --scope, --yes, and --force are ignored with --refresh.");
|
|
835
|
+
}
|
|
728
836
|
return {
|
|
729
837
|
flags: {
|
|
730
838
|
...client.length > 0 ? { client } : {},
|
|
731
839
|
...scope ? { scope } : {},
|
|
732
840
|
yes: Boolean(argv.yes),
|
|
733
841
|
dryRun: Boolean(argv["dry-run"]),
|
|
734
|
-
force: Boolean(argv.force)
|
|
842
|
+
force: Boolean(argv.force),
|
|
843
|
+
...refresh ? { refresh: true } : {}
|
|
735
844
|
},
|
|
736
845
|
warnings,
|
|
737
846
|
errors
|
|
@@ -745,18 +854,24 @@ Usage:
|
|
|
745
854
|
svelte-vitals install [options]
|
|
746
855
|
|
|
747
856
|
Options:
|
|
748
|
-
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-
|
|
857
|
+
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules,config-file
|
|
749
858
|
(skips the interactive picker)
|
|
750
|
-
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-
|
|
751
|
-
wires up the
|
|
859
|
+
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-hooks
|
|
860
|
+
wires up the svelteVitalsHandle hook in src/hooks.server.{ts,js}, which improves the
|
|
861
|
+
live dashboard's per-route accuracy as you browse. --force does not apply
|
|
752
862
|
to either of these two \u2014 an existing registration is always left as-is.
|
|
753
863
|
claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
|
|
754
864
|
writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
|
|
755
865
|
current rule set and support --force to regenerate.
|
|
866
|
+
config-file scaffolds svelte-vitals.config.mjs with every option commented out;
|
|
867
|
+
supports --force to regenerate.
|
|
756
868
|
--scope <scope> project | global (applies to all selected clients; codex is always global)
|
|
757
869
|
--yes, -y Skip the confirmation prompt
|
|
758
870
|
--dry-run Print the planned changes and exit without writing
|
|
759
871
|
--force Overwrite an existing svelte-vitals entry
|
|
872
|
+
--refresh Regenerate existing agent skill/rules files with the current rule set
|
|
873
|
+
(claude-skill / cursor-rules). Only regenerates files already present on
|
|
874
|
+
disk \u2014 it never creates one. Cannot be combined with --client.
|
|
760
875
|
-h, --help Show this help`;
|
|
761
876
|
function realIO() {
|
|
762
877
|
return {
|
|
@@ -824,7 +939,7 @@ ${planText}` });
|
|
|
824
939
|
}
|
|
825
940
|
async function runInstallCli(args) {
|
|
826
941
|
const argv = mri(args, {
|
|
827
|
-
boolean: ["yes", "dry-run", "force", "help"],
|
|
942
|
+
boolean: ["yes", "dry-run", "force", "refresh", "help"],
|
|
828
943
|
string: ["client", "scope"],
|
|
829
944
|
alias: { y: "yes", h: "help" }
|
|
830
945
|
});
|
|
@@ -881,22 +996,56 @@ function buildWorkflowYaml(opts) {
|
|
|
881
996
|
].join("\n");
|
|
882
997
|
}
|
|
883
998
|
|
|
999
|
+
// src/ci/upgrade.ts
|
|
1000
|
+
var ACTION_USES_LINE = /^(?<indent>\s*-\s*uses:\s*(?:&\S+\s+)?oekazuma\/svelte-vitals\/packages\/action@)(?<ref>[^\s#]+)(?<comment>\s*#.*)?$/;
|
|
1001
|
+
function upgradeActionPin(content, sha, version) {
|
|
1002
|
+
const lines = content.split("\n");
|
|
1003
|
+
let replaced = 0;
|
|
1004
|
+
let from;
|
|
1005
|
+
const next = lines.map((line) => {
|
|
1006
|
+
const eol = line.endsWith("\r") ? "\r" : "";
|
|
1007
|
+
const bare = eol ? line.slice(0, -1) : line;
|
|
1008
|
+
const match = ACTION_USES_LINE.exec(bare);
|
|
1009
|
+
if (!match || !match.groups) return line;
|
|
1010
|
+
const { indent: indent2, ref } = match.groups;
|
|
1011
|
+
if (indent2 === void 0 || ref === void 0) return line;
|
|
1012
|
+
if (ref === sha) return line;
|
|
1013
|
+
if (from === void 0) {
|
|
1014
|
+
const commentMatch = /#\s*@svelte-vitals\/action@(\S+)/.exec(match.groups.comment ?? "");
|
|
1015
|
+
from = commentMatch ? commentMatch[1] : ref.slice(0, 7);
|
|
1016
|
+
}
|
|
1017
|
+
replaced += 1;
|
|
1018
|
+
return `${indent2}${sha} # @svelte-vitals/action@${version}${eol}`;
|
|
1019
|
+
});
|
|
1020
|
+
if (replaced === 0) {
|
|
1021
|
+
const hasAnyReference = lines.some((line) => ACTION_USES_LINE.test(line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
1022
|
+
return { status: hasAnyReference ? "up-to-date" : "no-reference" };
|
|
1023
|
+
}
|
|
1024
|
+
return { status: "upgraded", content: next.join("\n"), replaced, from };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
884
1027
|
// src/ci/action-pin.generated.ts
|
|
885
|
-
var ACTION_SHA = "
|
|
886
|
-
var ACTION_VERSION = "0.
|
|
1028
|
+
var ACTION_SHA = "2c21acbad36c5360228bbb0790dbb4a06b7c773a";
|
|
1029
|
+
var ACTION_VERSION = "0.3.0";
|
|
887
1030
|
|
|
888
1031
|
// src/ci/cli.ts
|
|
889
1032
|
var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
|
|
890
1033
|
|
|
891
1034
|
Usage:
|
|
892
1035
|
svelte-vitals ci install [options]
|
|
1036
|
+
svelte-vitals ci upgrade [--dry-run]
|
|
893
1037
|
|
|
894
1038
|
Adds a GitHub Actions workflow (${WORKFLOW_PATH}) that calls the \`@svelte-vitals/action\`
|
|
895
1039
|
GitHub Action on pull requests: inline annotations, a job summary, and a sticky PR
|
|
896
1040
|
comment with the findings.
|
|
897
1041
|
|
|
1042
|
+
\`ci upgrade\` rewrites only the pinned \`@svelte-vitals/action\` reference in an existing
|
|
1043
|
+
workflow to the pin bundled with this CLI, leaving the rest of the file (and any other
|
|
1044
|
+
pins, like actions/checkout) untouched. To pick up the latest pin, run
|
|
1045
|
+
\`npx svelte-vitals@latest ci upgrade\`.
|
|
1046
|
+
|
|
898
1047
|
Options:
|
|
899
|
-
--force Overwrite an existing workflow file
|
|
1048
|
+
--force Overwrite an existing workflow file (install only)
|
|
900
1049
|
--dry-run Print the plan and exit without writing
|
|
901
1050
|
-h, --help Show this help`;
|
|
902
1051
|
async function runCiCli(args, io = realIO()) {
|
|
@@ -905,6 +1054,9 @@ async function runCiCli(args, io = realIO()) {
|
|
|
905
1054
|
io.log(CI_HELP);
|
|
906
1055
|
return 0;
|
|
907
1056
|
}
|
|
1057
|
+
if (sub === "upgrade") {
|
|
1058
|
+
return runCiUpgrade(args.slice(1), io);
|
|
1059
|
+
}
|
|
908
1060
|
if (sub !== "install") {
|
|
909
1061
|
io.log(CI_HELP);
|
|
910
1062
|
return 2;
|
|
@@ -942,6 +1094,44 @@ async function runCiCli(args, io = realIO()) {
|
|
|
942
1094
|
io.log("Done. Commit the workflow file and open a PR to see it in action.");
|
|
943
1095
|
return 0;
|
|
944
1096
|
}
|
|
1097
|
+
async function runCiUpgrade(args, io) {
|
|
1098
|
+
const argv = mri2(args, {
|
|
1099
|
+
boolean: ["dry-run", "help"],
|
|
1100
|
+
alias: { h: "help" }
|
|
1101
|
+
});
|
|
1102
|
+
if (argv.help) {
|
|
1103
|
+
io.log(CI_HELP);
|
|
1104
|
+
return 0;
|
|
1105
|
+
}
|
|
1106
|
+
const path = join4(io.cwd, WORKFLOW_PATH);
|
|
1107
|
+
const existing = io.readFile(path);
|
|
1108
|
+
if (existing === void 0) {
|
|
1109
|
+
io.errorLog(`svelte-vitals: no ${WORKFLOW_PATH} found \u2014 run \`svelte-vitals ci install\` first.`);
|
|
1110
|
+
return 2;
|
|
1111
|
+
}
|
|
1112
|
+
const outcome = upgradeActionPin(existing, ACTION_SHA, ACTION_VERSION);
|
|
1113
|
+
if (outcome.status === "no-reference") {
|
|
1114
|
+
io.errorLog(`svelte-vitals: no @svelte-vitals/action reference found in ${WORKFLOW_PATH}.`);
|
|
1115
|
+
return 2;
|
|
1116
|
+
}
|
|
1117
|
+
if (outcome.status === "up-to-date") {
|
|
1118
|
+
io.log(`= already up to date (@svelte-vitals/action@${ACTION_VERSION}).`);
|
|
1119
|
+
return 0;
|
|
1120
|
+
}
|
|
1121
|
+
if (argv["dry-run"]) {
|
|
1122
|
+
io.log(`Would upgrade @svelte-vitals/action: ${outcome.from} \u2192 ${ACTION_VERSION} (${outcome.replaced} line(s)).`);
|
|
1123
|
+
io.log("Dry run \u2014 no files written.");
|
|
1124
|
+
return 0;
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
io.writeFile(path, outcome.content ?? existing);
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
io.errorLog(`svelte-vitals: failed to write ${WORKFLOW_PATH}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1130
|
+
return 2;
|
|
1131
|
+
}
|
|
1132
|
+
io.log(`\u2713 upgraded @svelte-vitals/action: ${outcome.from} \u2192 ${ACTION_VERSION} (${outcome.replaced} line(s)).`);
|
|
1133
|
+
return 0;
|
|
1134
|
+
}
|
|
945
1135
|
|
|
946
1136
|
// src/bin.ts
|
|
947
1137
|
var HELP = `svelte-vitals \u2014 a deterministic SvelteKit code-health scanner (SEO \xB7 performance \xB7 correctness \xB7 security \xB7 architecture)
|
|
@@ -950,6 +1140,7 @@ Usage:
|
|
|
950
1140
|
svelte-vitals [path] [options]
|
|
951
1141
|
svelte-vitals install Set up the MCP server, Vite integration, or agent skills/rules
|
|
952
1142
|
svelte-vitals ci install Add a GitHub Actions PR gate (annotations + summary comment)
|
|
1143
|
+
svelte-vitals ci upgrade Refresh the pinned @svelte-vitals/action in an existing workflow
|
|
953
1144
|
|
|
954
1145
|
Options:
|
|
955
1146
|
--meta-components <names> Comma-separated component names that emit head metadata
|
|
@@ -958,12 +1149,12 @@ Options:
|
|
|
958
1149
|
--diff [ref] Report only findings in files changed vs ref (default HEAD; e.g. --diff main)
|
|
959
1150
|
--staged Report only findings in files staged for commit (pre-commit gate)
|
|
960
1151
|
--baseline <ref> Report only findings not present at ref (compare against e.g. origin/main)
|
|
1152
|
+
--update-suppressions Write svelte-vitals-suppressions.json accepting all current findings (introduce gates on legacy projects)
|
|
1153
|
+
--no-suppressions Ignore svelte-vitals-suppressions.json for this run
|
|
961
1154
|
--by-route Show per-route score breakdown in console output
|
|
962
1155
|
--reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
963
1156
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
964
|
-
--json Alias for --reporter=json
|
|
965
1157
|
--fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
|
|
966
|
-
--fail-on-warning Alias for --fail-on=warning
|
|
967
1158
|
--min-health <0-100> Fail (exit 1) when the combined Health score is below this value
|
|
968
1159
|
--rules <ids> Comma-separated rule ids to enable (all others disabled)
|
|
969
1160
|
--ignore <ids> Comma-separated rule ids to disable
|
|
@@ -971,7 +1162,7 @@ Options:
|
|
|
971
1162
|
--weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
|
|
972
1163
|
--score Print only the combined Health score (works with --min-health for gating)
|
|
973
1164
|
--no-color Disable ANSI color in console output
|
|
974
|
-
--no-animation Disable the Health-score reveal animation on an interactive terminal
|
|
1165
|
+
--no-animation Disable the Health-score reveal animation and mascot on an interactive terminal
|
|
975
1166
|
--verbose Show every finding uncapped and ungrouped (default: capped, grouped by rule)
|
|
976
1167
|
-h, --help Show this help
|
|
977
1168
|
-v, --version Show version
|
|
@@ -1003,7 +1194,7 @@ async function main() {
|
|
|
1003
1194
|
}
|
|
1004
1195
|
const argv = mri3(process.argv.slice(2), {
|
|
1005
1196
|
alias: { h: "help", v: "version" },
|
|
1006
|
-
boolean: ["by-route", "
|
|
1197
|
+
boolean: ["by-route", "staged", "score", "verbose", "update-suppressions"],
|
|
1007
1198
|
string: [
|
|
1008
1199
|
"meta-components",
|
|
1009
1200
|
"treat-dynamic-as",
|
|
@@ -1045,8 +1236,6 @@ async function main() {
|
|
|
1045
1236
|
const code = await run({
|
|
1046
1237
|
...options,
|
|
1047
1238
|
minHealth,
|
|
1048
|
-
noColor: argv["no-color"],
|
|
1049
|
-
noAnimation: argv["no-animation"],
|
|
1050
1239
|
selectApp
|
|
1051
1240
|
});
|
|
1052
1241
|
process.exit(code);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
-
import { dirname as dirname2, join as
|
|
2
|
+
import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
4
4
|
import {
|
|
5
5
|
allRules as allRules2,
|
|
6
6
|
runRules,
|
|
@@ -637,9 +637,8 @@ async function resolveRoute(rt, cwd, pageRel, config, layouts, cache) {
|
|
|
637
637
|
headings: { route, headings }
|
|
638
638
|
};
|
|
639
639
|
}
|
|
640
|
-
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
640
|
+
async function collectRoutes(rt, cwd, config = defaultConfig, cache = /* @__PURE__ */ new Map()) {
|
|
641
641
|
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
642
|
-
const cache = /* @__PURE__ */ new Map();
|
|
643
642
|
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config, layouts, cache)));
|
|
644
643
|
return {
|
|
645
644
|
heads: facts.map((f) => f.head),
|
|
@@ -793,6 +792,104 @@ function filterToNewFindings(results, baselineResults) {
|
|
|
793
792
|
return results.filter((r) => !baselineKeys.has(findingKey(r)));
|
|
794
793
|
}
|
|
795
794
|
|
|
795
|
+
// src/suppressions.ts
|
|
796
|
+
import { readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
797
|
+
import { join as join4 } from "path";
|
|
798
|
+
import { isPenalized } from "@svelte-vitals/core";
|
|
799
|
+
var SUPPRESSIONS_FILE = "svelte-vitals-suppressions.json";
|
|
800
|
+
function isPlainObject(value) {
|
|
801
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
802
|
+
}
|
|
803
|
+
function loadSuppressions(cwd) {
|
|
804
|
+
const path = join4(cwd, SUPPRESSIONS_FILE);
|
|
805
|
+
let raw;
|
|
806
|
+
try {
|
|
807
|
+
raw = readFileSync2(path, "utf8");
|
|
808
|
+
} catch {
|
|
809
|
+
return void 0;
|
|
810
|
+
}
|
|
811
|
+
let parsed;
|
|
812
|
+
try {
|
|
813
|
+
parsed = JSON.parse(raw);
|
|
814
|
+
} catch (err) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`invalid ${SUPPRESSIONS_FILE}: not valid JSON (${err instanceof Error ? err.message : String(err)}).`,
|
|
817
|
+
{ cause: err }
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (!isPlainObject(parsed)) {
|
|
821
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: expected a top-level JSON object.`);
|
|
822
|
+
}
|
|
823
|
+
if (parsed.version !== 1) {
|
|
824
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: expected "version": 1, got ${JSON.stringify(parsed.version)}.`);
|
|
825
|
+
}
|
|
826
|
+
if (!Array.isArray(parsed.suppressions)) {
|
|
827
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: "suppressions" must be an array.`);
|
|
828
|
+
}
|
|
829
|
+
const entries = [];
|
|
830
|
+
parsed.suppressions.forEach((entry, i) => {
|
|
831
|
+
if (!isPlainObject(entry) || typeof entry.id !== "string") {
|
|
832
|
+
throw new Error(`invalid ${SUPPRESSIONS_FILE}: suppressions[${i}] must be an object with a string "id".`);
|
|
833
|
+
}
|
|
834
|
+
entries.push({
|
|
835
|
+
id: entry.id,
|
|
836
|
+
...typeof entry.route === "string" ? { route: entry.route } : {},
|
|
837
|
+
...typeof entry.location === "string" ? { location: entry.location } : {}
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
return entries;
|
|
841
|
+
}
|
|
842
|
+
function toEntry(r) {
|
|
843
|
+
return {
|
|
844
|
+
id: r.id,
|
|
845
|
+
...r.route !== void 0 ? { route: r.route } : {},
|
|
846
|
+
...r.location !== void 0 ? { location: r.location } : {}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
function compareEntries(a, b) {
|
|
850
|
+
if (a.id !== b.id) return a.id < b.id ? -1 : 1;
|
|
851
|
+
const ar = a.route ?? "";
|
|
852
|
+
const br = b.route ?? "";
|
|
853
|
+
if (ar !== br) return ar < br ? -1 : 1;
|
|
854
|
+
const al = a.location ?? "";
|
|
855
|
+
const bl = b.location ?? "";
|
|
856
|
+
if (al !== bl) return al < bl ? -1 : 1;
|
|
857
|
+
return 0;
|
|
858
|
+
}
|
|
859
|
+
function writeSuppressions(cwd, results, config) {
|
|
860
|
+
const seen = /* @__PURE__ */ new Set();
|
|
861
|
+
const entries = [];
|
|
862
|
+
for (const r of results) {
|
|
863
|
+
if (!isPenalized(r.detection, config.treatDynamicAs)) continue;
|
|
864
|
+
const entry = toEntry(r);
|
|
865
|
+
const key = findingKey(entry);
|
|
866
|
+
if (seen.has(key)) continue;
|
|
867
|
+
seen.add(key);
|
|
868
|
+
entries.push(entry);
|
|
869
|
+
}
|
|
870
|
+
entries.sort(compareEntries);
|
|
871
|
+
const path = join4(cwd, SUPPRESSIONS_FILE);
|
|
872
|
+
writeFileSync(path, JSON.stringify({ version: 1, suppressions: entries }, null, 2) + "\n");
|
|
873
|
+
return entries.length;
|
|
874
|
+
}
|
|
875
|
+
function applySuppressions(results, entries, config) {
|
|
876
|
+
const keys = new Set(entries.map((e) => findingKey(e)));
|
|
877
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
878
|
+
const kept = [];
|
|
879
|
+
let suppressed = 0;
|
|
880
|
+
for (const r of results) {
|
|
881
|
+
const key = findingKey(r);
|
|
882
|
+
if (keys.has(key) && isPenalized(r.detection, config.treatDynamicAs)) {
|
|
883
|
+
suppressed++;
|
|
884
|
+
usedKeys.add(key);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
kept.push(r);
|
|
888
|
+
}
|
|
889
|
+
const stale = [...keys].filter((k) => !usedKeys.has(k)).length;
|
|
890
|
+
return { results: kept, suppressed, stale };
|
|
891
|
+
}
|
|
892
|
+
|
|
796
893
|
// src/color.ts
|
|
797
894
|
import { noColorPalette } from "@svelte-vitals/core";
|
|
798
895
|
var wrap = (open, close = 0) => (s) => `\x1B[${open}m${s}\x1B[${close}m`;
|
|
@@ -835,9 +932,160 @@ function startSpinner(text, opts) {
|
|
|
835
932
|
};
|
|
836
933
|
}
|
|
837
934
|
|
|
935
|
+
// src/mascot.ts
|
|
936
|
+
import { createLogUpdate } from "log-update";
|
|
937
|
+
function mascotStateFor(score) {
|
|
938
|
+
if (score === 100) return "ecstatic";
|
|
939
|
+
if (score >= 90) return "happy";
|
|
940
|
+
return "content";
|
|
941
|
+
}
|
|
942
|
+
var MIN_MASCOT_COLUMNS = 20;
|
|
943
|
+
function mascotFitsWidth(columns) {
|
|
944
|
+
return (columns ?? 80) >= MIN_MASCOT_COLUMNS;
|
|
945
|
+
}
|
|
946
|
+
var ORANGE_FG = "\x1B[38;2;255;62;0m";
|
|
947
|
+
var RESET = "\x1B[0m";
|
|
948
|
+
function renderFace(lines) {
|
|
949
|
+
return lines.map((line) => `${ORANGE_FG}${line}${RESET}`).join("\n");
|
|
950
|
+
}
|
|
951
|
+
var FACE_OPEN_EYES_NEUTRAL_MOUTH = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF \u25CF \u2502", "\u2502 \u2500\u2500 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
952
|
+
var FACE_CLOSED_EYES_NEUTRAL_MOUTH = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u2500 \u2500 \u2502", "\u2502 \u2500\u2500 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
953
|
+
var FACE_WINK_BOTH = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 > < \u2502", "\u2502 \u2500\u2500 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
954
|
+
var FACE_WINK_ONE = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF < \u2502", "\u2502 \u2500\u2500 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
955
|
+
var FACE_CONTENT = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF \u25CF \u2502", "\u2502 \u25E1\u25E1 \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
956
|
+
var FACE_HAPPY = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 \u25CF \u25CF \u2502", "\u2502 \u2570\u2500\u2500\u256F \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
957
|
+
var FACE_ECSTATIC = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 ^ ^ \u2502", "\u2502 \u2570\u2500\u2500\u256F \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
958
|
+
var REACTION_FACES = {
|
|
959
|
+
content: FACE_CONTENT,
|
|
960
|
+
happy: FACE_HAPPY,
|
|
961
|
+
ecstatic: FACE_ECSTATIC
|
|
962
|
+
};
|
|
963
|
+
function renderMascotReaction(state) {
|
|
964
|
+
return renderFace(REACTION_FACES[state]);
|
|
965
|
+
}
|
|
966
|
+
function renderMascotAnticipating() {
|
|
967
|
+
return renderFace(FACE_OPEN_EYES_NEUTRAL_MOUTH);
|
|
968
|
+
}
|
|
969
|
+
var IDLE_FRAME_SEQUENCE = [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 2, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 3];
|
|
970
|
+
var IDLE_FACES = {
|
|
971
|
+
0: FACE_OPEN_EYES_NEUTRAL_MOUTH,
|
|
972
|
+
1: FACE_CLOSED_EYES_NEUTRAL_MOUTH,
|
|
973
|
+
2: FACE_WINK_BOTH,
|
|
974
|
+
3: FACE_WINK_ONE
|
|
975
|
+
};
|
|
976
|
+
function renderMascotIdleFrame(frameIndex) {
|
|
977
|
+
const code = IDLE_FRAME_SEQUENCE[frameIndex % IDLE_FRAME_SEQUENCE.length];
|
|
978
|
+
return renderFace(IDLE_FACES[code]);
|
|
979
|
+
}
|
|
980
|
+
var CONFETTI_COLORS = [
|
|
981
|
+
[255, 62, 0],
|
|
982
|
+
// orange (identity accent)
|
|
983
|
+
[255, 145, 175],
|
|
984
|
+
// blush pink
|
|
985
|
+
[255, 214, 0],
|
|
986
|
+
// gold
|
|
987
|
+
[255, 255, 255]
|
|
988
|
+
// white
|
|
989
|
+
];
|
|
990
|
+
var CONFETTI_CHARS = ["*", ".", "\xB7", "+"];
|
|
991
|
+
var CONFETTI_WIDTH = 24;
|
|
992
|
+
function confettiFg(rgb) {
|
|
993
|
+
return `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
994
|
+
}
|
|
995
|
+
function confettiRow(offset) {
|
|
996
|
+
let out = "";
|
|
997
|
+
for (let col = 0; col < CONFETTI_WIDTH; col++) {
|
|
998
|
+
if ((col + offset) % 5 === 0) {
|
|
999
|
+
const glyph = CONFETTI_CHARS[(col + offset) % CONFETTI_CHARS.length];
|
|
1000
|
+
out += confettiFg(CONFETTI_COLORS[(col + offset) % CONFETTI_COLORS.length]) + glyph + RESET;
|
|
1001
|
+
} else {
|
|
1002
|
+
out += " ";
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
function renderConfettiFrame(offset, mascotBlock) {
|
|
1008
|
+
return [confettiRow(offset), mascotBlock, confettiRow(offset + 2)].join("\n");
|
|
1009
|
+
}
|
|
1010
|
+
var IDLE_TICK_MS = 160;
|
|
1011
|
+
function startMascotSpinner(text, opts) {
|
|
1012
|
+
if (!opts.enabled) return { stop() {
|
|
1013
|
+
} };
|
|
1014
|
+
const stream = opts.stream ?? process.stderr;
|
|
1015
|
+
const render = createLogUpdate(stream);
|
|
1016
|
+
let i = 0;
|
|
1017
|
+
const tick = () => {
|
|
1018
|
+
render(`${renderMascotIdleFrame(i)}
|
|
1019
|
+
${text}`);
|
|
1020
|
+
i++;
|
|
1021
|
+
};
|
|
1022
|
+
tick();
|
|
1023
|
+
const timer = setInterval(tick, IDLE_TICK_MS);
|
|
1024
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1025
|
+
return {
|
|
1026
|
+
stop() {
|
|
1027
|
+
clearInterval(timer);
|
|
1028
|
+
render.clear();
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/speech-bubble.ts
|
|
1034
|
+
import { createLogUpdate as createLogUpdate2 } from "log-update";
|
|
1035
|
+
var MIN_BUBBLE_COLUMNS = 55;
|
|
1036
|
+
function bubbleFitsWidth(columns) {
|
|
1037
|
+
return (columns ?? 80) >= MIN_BUBBLE_COLUMNS;
|
|
1038
|
+
}
|
|
1039
|
+
function renderSpeechBubble(text) {
|
|
1040
|
+
const border = "\u2500".repeat(text.length + 2);
|
|
1041
|
+
return [`\u256D${border}\u256E`, `\u2502 ${text} \u2502`, `\u2570${border}\u256F`];
|
|
1042
|
+
}
|
|
1043
|
+
function withSpeechBubble(mascotBlock, bubbleLines) {
|
|
1044
|
+
const mascotLines = mascotBlock.split("\n");
|
|
1045
|
+
const bubbleWidth = bubbleLines[0]?.length ?? 0;
|
|
1046
|
+
const blankBubbleLine = " ".repeat(bubbleWidth);
|
|
1047
|
+
const padTop = Math.floor((mascotLines.length - bubbleLines.length) / 2);
|
|
1048
|
+
const padBottom = mascotLines.length - bubbleLines.length - padTop;
|
|
1049
|
+
const paddedBubble = [
|
|
1050
|
+
...Array(Math.max(padTop, 0)).fill(blankBubbleLine),
|
|
1051
|
+
...bubbleLines,
|
|
1052
|
+
...Array(Math.max(padBottom, 0)).fill(blankBubbleLine)
|
|
1053
|
+
];
|
|
1054
|
+
return mascotLines.map((line, i) => `${line} ${paddedBubble[i] ?? blankBubbleLine}`).join("\n");
|
|
1055
|
+
}
|
|
1056
|
+
var GREETING_MESSAGES = [
|
|
1057
|
+
"Welcome to Svelte Vitals!",
|
|
1058
|
+
"Let's check your project!",
|
|
1059
|
+
"Ready when you are!",
|
|
1060
|
+
"Hi there! Let's dig in."
|
|
1061
|
+
];
|
|
1062
|
+
var REACTION_MESSAGES = {
|
|
1063
|
+
ecstatic: ["Perfect score!", "Flawless!", "You nailed it!"],
|
|
1064
|
+
happy: ["Nice work!", "Looking great!", "Almost perfect!"],
|
|
1065
|
+
content: ["Keep going!", "Room to grow!", "Let's improve this!"]
|
|
1066
|
+
};
|
|
1067
|
+
function pickMessage(pool, random = Math.random) {
|
|
1068
|
+
return pool[Math.floor(random() * pool.length)];
|
|
1069
|
+
}
|
|
1070
|
+
function renderMascotWithSpeech(mascotBlock, message) {
|
|
1071
|
+
return withSpeechBubble(mascotBlock, renderSpeechBubble(message));
|
|
1072
|
+
}
|
|
1073
|
+
function sleep(ms) {
|
|
1074
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1075
|
+
}
|
|
1076
|
+
var GREETING_HOLD_MS = 800;
|
|
1077
|
+
async function playMascotGreeting(opts) {
|
|
1078
|
+
if (!opts.enabled) return;
|
|
1079
|
+
const holdMs = opts.holdMs ?? GREETING_HOLD_MS;
|
|
1080
|
+
const render = createLogUpdate2(opts.stream);
|
|
1081
|
+
render(renderMascotWithSpeech(renderMascotAnticipating(), pickMessage(GREETING_MESSAGES)));
|
|
1082
|
+
if (holdMs > 0) await sleep(holdMs);
|
|
1083
|
+
render.clear();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
838
1086
|
// src/config-file.ts
|
|
839
1087
|
import { existsSync as existsSync2 } from "fs";
|
|
840
|
-
import { join as
|
|
1088
|
+
import { join as join5 } from "path";
|
|
841
1089
|
import { pathToFileURL } from "url";
|
|
842
1090
|
|
|
843
1091
|
// src/rules-config.ts
|
|
@@ -864,7 +1112,7 @@ var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture
|
|
|
864
1112
|
var TREAT_DYNAMIC_AS_VALUES = ["pass", "warn", "fail"];
|
|
865
1113
|
var FAIL_ON_VALUES = ["critical", "warning", "info"];
|
|
866
1114
|
var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["treatDynamicAs", "metaComponents", "rules", "failOn", "weights"]);
|
|
867
|
-
function
|
|
1115
|
+
function isPlainObject2(value) {
|
|
868
1116
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
869
1117
|
}
|
|
870
1118
|
function isMissingExtensionLoaderError(err) {
|
|
@@ -902,7 +1150,7 @@ function validateConfigFile(raw, path) {
|
|
|
902
1150
|
}
|
|
903
1151
|
}
|
|
904
1152
|
if (raw.rules !== void 0) {
|
|
905
|
-
if (!
|
|
1153
|
+
if (!isPlainObject2(raw.rules)) {
|
|
906
1154
|
throw new Error(`${path}: rules must be an object of rule-id \u2192 setting.`);
|
|
907
1155
|
}
|
|
908
1156
|
const rules = raw.rules;
|
|
@@ -915,7 +1163,7 @@ function validateConfigFile(raw, path) {
|
|
|
915
1163
|
config.rules = rules;
|
|
916
1164
|
}
|
|
917
1165
|
if (raw.weights !== void 0) {
|
|
918
|
-
if (!
|
|
1166
|
+
if (!isPlainObject2(raw.weights)) {
|
|
919
1167
|
throw new Error(`${path}: weights must be an object of category \u2192 number.`);
|
|
920
1168
|
}
|
|
921
1169
|
const weights = {};
|
|
@@ -934,7 +1182,7 @@ function validateConfigFile(raw, path) {
|
|
|
934
1182
|
return { config, warnings };
|
|
935
1183
|
}
|
|
936
1184
|
async function loadConfigFile(cwd) {
|
|
937
|
-
const found = CONFIG_FILENAMES.map((name) =>
|
|
1185
|
+
const found = CONFIG_FILENAMES.map((name) => join5(cwd, name)).find((path) => existsSync2(path));
|
|
938
1186
|
if (!found) return void 0;
|
|
939
1187
|
let mod;
|
|
940
1188
|
try {
|
|
@@ -948,7 +1196,7 @@ async function loadConfigFile(cwd) {
|
|
|
948
1196
|
}
|
|
949
1197
|
throw err;
|
|
950
1198
|
}
|
|
951
|
-
if (!
|
|
1199
|
+
if (!isPlainObject2(mod.default)) {
|
|
952
1200
|
throw new Error(
|
|
953
1201
|
`${found} must have a default export that is a plain object (e.g. \`export default defineConfig({...})\` or a plain object literal).`
|
|
954
1202
|
);
|
|
@@ -958,6 +1206,7 @@ async function loadConfigFile(cwd) {
|
|
|
958
1206
|
|
|
959
1207
|
// src/pulse-animation.ts
|
|
960
1208
|
import { scoreColor } from "@svelte-vitals/core";
|
|
1209
|
+
import { createLogUpdate as createLogUpdate3 } from "log-update";
|
|
961
1210
|
var FRAME_COUNT = 6;
|
|
962
1211
|
var FRAME_DELAY_MS = 200;
|
|
963
1212
|
var WAVE_FRAMES = [
|
|
@@ -965,26 +1214,46 @@ var WAVE_FRAMES = [
|
|
|
965
1214
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
966
1215
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
967
1216
|
"\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2572\u2500\u2500\u2500\u2500\u2500",
|
|
968
|
-
"\u2500\u2500\u2500\u2500\u2571\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2572\u2500\u2500\u2500\u2500"
|
|
969
|
-
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"
|
|
1217
|
+
"\u2500\u2500\u2500\u2500\u2571\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2572\u2500\u2500\u2500\u2500"
|
|
970
1218
|
];
|
|
971
|
-
|
|
1219
|
+
var WAVE_ORANGE_DIM = "\x1B[38;2;153;37;0m";
|
|
1220
|
+
var WAVE_RESET = "\x1B[0m";
|
|
1221
|
+
function sleep2(ms) {
|
|
972
1222
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
973
1223
|
}
|
|
1224
|
+
var REACTION_HOLD_MS = 500;
|
|
1225
|
+
var CONFETTI_FRAME_COUNT = 4;
|
|
1226
|
+
var CONFETTI_FRAME_DELAY_MS = 220;
|
|
974
1227
|
async function playScoreAnimation(opts) {
|
|
975
1228
|
const frameDelayMs = opts.frameDelayMs ?? FRAME_DELAY_MS;
|
|
1229
|
+
const holdMs = opts.frameDelayMs ?? REACTION_HOLD_MS;
|
|
1230
|
+
const confettiDelayMs = opts.frameDelayMs ?? CONFETTI_FRAME_DELAY_MS;
|
|
1231
|
+
const render = createLogUpdate3(opts.stream);
|
|
1232
|
+
const showMascot = mascotFitsWidth(opts.stream.columns);
|
|
1233
|
+
const state = mascotStateFor(opts.score);
|
|
1234
|
+
const reactionMessage = showMascot && bubbleFitsWidth(opts.stream.columns) ? pickMessage(REACTION_MESSAGES[state]) : void 0;
|
|
1235
|
+
const finalMascotBlock = reactionMessage ? renderMascotWithSpeech(renderMascotReaction(state), reactionMessage) : renderMascotReaction(state);
|
|
976
1236
|
for (let frame = 0; frame < FRAME_COUNT; frame++) {
|
|
977
1237
|
const progress = frame / (FRAME_COUNT - 1);
|
|
978
1238
|
const displayScore = Math.round(opts.score * progress);
|
|
979
1239
|
const isFinalFrame = frame === FRAME_COUNT - 1;
|
|
980
|
-
const wave = WAVE_FRAMES[frame];
|
|
981
1240
|
const scoreText = isFinalFrame ? scoreColor(opts.palette, opts.score)(`${displayScore}/100`) : opts.palette.dim(`${displayScore}/100`);
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
\
|
|
985
|
-
`);
|
|
986
|
-
if (!isFinalFrame) await
|
|
1241
|
+
const waveBlock = isFinalFrame ? `Health: ${scoreText}` : `${WAVE_ORANGE_DIM}${WAVE_FRAMES[frame]}${WAVE_RESET}
|
|
1242
|
+
Health: ${scoreText}`;
|
|
1243
|
+
const mascotBlock = showMascot ? (isFinalFrame ? finalMascotBlock : renderMascotAnticipating()) + "\n" : "";
|
|
1244
|
+
render(`${mascotBlock}${waveBlock}`);
|
|
1245
|
+
if (!isFinalFrame) await sleep2(frameDelayMs);
|
|
1246
|
+
}
|
|
1247
|
+
if (holdMs > 0) await sleep2(holdMs);
|
|
1248
|
+
if (showMascot && state === "ecstatic") {
|
|
1249
|
+
for (let i = 0; i < CONFETTI_FRAME_COUNT; i++) {
|
|
1250
|
+
const waveBlock = `Health: ${scoreColor(opts.palette, opts.score)("100/100")}`;
|
|
1251
|
+
render(`${renderConfettiFrame(i, finalMascotBlock)}
|
|
1252
|
+
${waveBlock}`);
|
|
1253
|
+
if (i < CONFETTI_FRAME_COUNT - 1 && confettiDelayMs > 0) await sleep2(confettiDelayMs);
|
|
1254
|
+
}
|
|
987
1255
|
}
|
|
1256
|
+
render.done();
|
|
988
1257
|
}
|
|
989
1258
|
function scoreAnimationEnabled(opts) {
|
|
990
1259
|
return opts.reporter === "console" && opts.stdoutIsTTY && !opts.noAnimationFlag && !isAgentEnv(opts.env) && !isCiEnv(opts.env) && colorEnabled({ reporter: opts.reporter, isTTY: opts.stdoutIsTTY, env: opts.env, noColorFlag: opts.noColorFlag });
|
|
@@ -1017,7 +1286,7 @@ async function analyzeProject(opts = {}) {
|
|
|
1017
1286
|
});
|
|
1018
1287
|
await detectProject(rt, cwd);
|
|
1019
1288
|
const matches = routeMatcher(opts.route);
|
|
1020
|
-
const collected = await collectRoutes(rt, cwd, config);
|
|
1289
|
+
const collected = await collectRoutes(rt, cwd, config, opts.parseCache);
|
|
1021
1290
|
const heads = collected.heads.filter((h) => matches(h.route));
|
|
1022
1291
|
const images = collected.images.filter((i) => matches(i.route));
|
|
1023
1292
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
@@ -1061,6 +1330,18 @@ async function applyScope(results, opts) {
|
|
|
1061
1330
|
}
|
|
1062
1331
|
}
|
|
1063
1332
|
}
|
|
1333
|
+
if (!opts.noSuppressions && opts.config) {
|
|
1334
|
+
const entries = loadSuppressions(opts.cwd);
|
|
1335
|
+
if (entries !== void 0) {
|
|
1336
|
+
const { results: afterSuppressions, suppressed, stale } = applySuppressions(scoped, entries, opts.config);
|
|
1337
|
+
scoped = afterSuppressions;
|
|
1338
|
+
if (suppressed > 0 || stale > 0) {
|
|
1339
|
+
errorLog(
|
|
1340
|
+
`svelte-vitals: ${suppressed} finding(s) suppressed by ${SUPPRESSIONS_FILE}` + (stale > 0 ? ` (${stale} stale entr${stale === 1 ? "y" : "ies"} \u2014 re-run --update-suppressions to prune)` : "") + "."
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1064
1345
|
return scoped;
|
|
1065
1346
|
}
|
|
1066
1347
|
async function run(opts = {}) {
|
|
@@ -1072,15 +1353,19 @@ async function run(opts = {}) {
|
|
|
1072
1353
|
}
|
|
1073
1354
|
const env = opts.env ?? process.env;
|
|
1074
1355
|
const reporter = resolveReporter(opts.reporter, env);
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
})
|
|
1356
|
+
const stderrStream = opts.stderrStream ?? process.stderr;
|
|
1357
|
+
const spinnerBaseEnabled = !opts.score && spinnerEnabled({
|
|
1358
|
+
reporter,
|
|
1359
|
+
rawReporter: opts.reporter,
|
|
1360
|
+
stderrIsTTY: opts.stderrIsTTY ?? !!process.stderr.isTTY,
|
|
1361
|
+
env,
|
|
1362
|
+
noColorFlag: opts.noColor
|
|
1083
1363
|
});
|
|
1364
|
+
const useMascotSpinner = spinnerBaseEnabled && !opts.noAnimation && mascotFitsWidth(stderrStream.columns);
|
|
1365
|
+
if (useMascotSpinner && bubbleFitsWidth(stderrStream.columns)) {
|
|
1366
|
+
await playMascotGreeting({ enabled: true, stream: stderrStream, holdMs: opts.animationFrameDelayMs });
|
|
1367
|
+
}
|
|
1368
|
+
const spinner = useMascotSpinner ? startMascotSpinner("Analyzing\u2026", { enabled: true, stream: stderrStream }) : startSpinner("Analyzing\u2026", { enabled: spinnerBaseEnabled, stream: stderrStream });
|
|
1084
1369
|
let cwd = opts.cwd ?? process.cwd();
|
|
1085
1370
|
let analysis;
|
|
1086
1371
|
try {
|
|
@@ -1126,7 +1411,7 @@ async function run(opts = {}) {
|
|
|
1126
1411
|
errorLog(`svelte-vitals: pass one as a path, e.g. \`npx svelte-vitals ${apps[0]}\`.`);
|
|
1127
1412
|
return 2;
|
|
1128
1413
|
}
|
|
1129
|
-
cwd =
|
|
1414
|
+
cwd = join6(cwd, chosen);
|
|
1130
1415
|
try {
|
|
1131
1416
|
analysis = await analyzeProject({
|
|
1132
1417
|
cwd,
|
|
@@ -1155,11 +1440,18 @@ async function run(opts = {}) {
|
|
|
1155
1440
|
for (const w of analysis.warnings) errorLog(`svelte-vitals: ${w}`);
|
|
1156
1441
|
try {
|
|
1157
1442
|
const { config, version } = analysis;
|
|
1443
|
+
if (opts.updateSuppressions) {
|
|
1444
|
+
const count = writeSuppressions(cwd, analysis.results, config);
|
|
1445
|
+
errorLog(`svelte-vitals: wrote ${count} suppression(s) to ${SUPPRESSIONS_FILE}.`);
|
|
1446
|
+
return 0;
|
|
1447
|
+
}
|
|
1158
1448
|
const results = await applyScope(analysis.results, {
|
|
1159
1449
|
cwd,
|
|
1450
|
+
config,
|
|
1160
1451
|
staged: opts.staged,
|
|
1161
1452
|
diffBase: opts.diffBase,
|
|
1162
1453
|
baseline: opts.baseline,
|
|
1454
|
+
noSuppressions: opts.noSuppressions,
|
|
1163
1455
|
errorLog,
|
|
1164
1456
|
analyzeOpts: {
|
|
1165
1457
|
metaComponents: opts.metaComponents,
|
|
@@ -1171,6 +1463,7 @@ async function run(opts = {}) {
|
|
|
1171
1463
|
categories: opts.categories
|
|
1172
1464
|
}
|
|
1173
1465
|
});
|
|
1466
|
+
const summary = summarize(results, config);
|
|
1174
1467
|
if (opts.score) {
|
|
1175
1468
|
log(String(computeHealth(results, config).health));
|
|
1176
1469
|
} else {
|
|
@@ -1201,7 +1494,7 @@ async function run(opts = {}) {
|
|
|
1201
1494
|
const path = opts.outFile || "svelte-vitals-report.html";
|
|
1202
1495
|
const write = opts.writeFile ?? ((p, c) => {
|
|
1203
1496
|
mkdirSync(dirname2(p), { recursive: true });
|
|
1204
|
-
|
|
1497
|
+
writeFileSync2(p, c);
|
|
1205
1498
|
});
|
|
1206
1499
|
write(path, html);
|
|
1207
1500
|
errorLog(`svelte-vitals: wrote report to ${path}`);
|
|
@@ -1242,7 +1535,6 @@ async function run(opts = {}) {
|
|
|
1242
1535
|
);
|
|
1243
1536
|
}
|
|
1244
1537
|
}
|
|
1245
|
-
const summary = summarize(results, config);
|
|
1246
1538
|
const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
|
|
1247
1539
|
const failByHealth = opts.minHealth != null && computeHealth(results, config).health < opts.minHealth;
|
|
1248
1540
|
return failBySeverity || failByHealth ? 1 : 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
import { RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
|
|
1
|
+
import { HeadTag, RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
|
|
2
2
|
export { defineConfig } from '@svelte-vitals/core';
|
|
3
3
|
|
|
4
|
+
/** A resolved import binding: which module, and which export ('default' for default imports). */
|
|
5
|
+
interface ImportInfo {
|
|
6
|
+
source: string;
|
|
7
|
+
imported: string;
|
|
8
|
+
}
|
|
9
|
+
/** local identifier -> import binding. */
|
|
10
|
+
type ImportMap = Map<string, ImportInfo>;
|
|
11
|
+
|
|
12
|
+
/** A head tag parsed from one file, before layout-chain presence is assigned. */
|
|
13
|
+
type ParsedTag = Omit<HeadTag, 'presence' | 'file'>;
|
|
14
|
+
type Node = any;
|
|
15
|
+
interface ComponentUse {
|
|
16
|
+
name: string;
|
|
17
|
+
attributes: Node[];
|
|
18
|
+
hasSpread: boolean;
|
|
19
|
+
}
|
|
20
|
+
interface ParsedImage {
|
|
21
|
+
hasWidth: boolean;
|
|
22
|
+
hasHeight: boolean;
|
|
23
|
+
hasLoading: boolean;
|
|
24
|
+
hasAlt: boolean;
|
|
25
|
+
lazy: boolean;
|
|
26
|
+
hasSrcset: boolean;
|
|
27
|
+
/** 1-based source line, or 0 if unknown. */
|
|
28
|
+
line: number;
|
|
29
|
+
}
|
|
30
|
+
/** A page-body heading (<h1>–<h6>) parsed from one file (SEO027). */
|
|
31
|
+
interface ParsedHeading {
|
|
32
|
+
/** Heading level 1–6. */
|
|
33
|
+
level: number;
|
|
34
|
+
/** 1-based source line, or 0 if unknown. */
|
|
35
|
+
line: number;
|
|
36
|
+
}
|
|
37
|
+
interface ParsedFile {
|
|
38
|
+
headTags: ParsedTag[];
|
|
39
|
+
components: ComponentUse[];
|
|
40
|
+
imports: ImportMap;
|
|
41
|
+
images: ParsedImage[];
|
|
42
|
+
headings: ParsedHeading[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Per-run read+parse memo, keyed by project-root-relative path (as normalized by
|
|
47
|
+
* chainFiles / resolveComponentPath). Shared across routes so a file imported by
|
|
48
|
+
* many pages (a root layout, a common $lib component) is only parsed once per run.
|
|
49
|
+
*/
|
|
50
|
+
type ParseCache = Map<string, Promise<ParsedFile>>;
|
|
51
|
+
|
|
4
52
|
type ReporterName = 'console' | 'json' | 'agent' | 'sarif' | 'github' | 'html' | 'md';
|
|
5
53
|
|
|
6
54
|
/** Thrown when the target directory is not a SvelteKit project (CLI maps to exit 2). */
|
|
@@ -77,6 +125,10 @@ interface RunOptions {
|
|
|
77
125
|
staged?: boolean;
|
|
78
126
|
/** Report only findings not present when analyzing this git ref (e.g. the PR base). */
|
|
79
127
|
baseline?: string;
|
|
128
|
+
/** Disable applying svelte-vitals-suppressions.json for this run. */
|
|
129
|
+
noSuppressions?: boolean;
|
|
130
|
+
/** Analyze, then (re)write svelte-vitals-suppressions.json with all currently penalized findings and exit 0. */
|
|
131
|
+
updateSuppressions?: boolean;
|
|
80
132
|
/** Disable ANSI color in console output. */
|
|
81
133
|
noColor?: boolean;
|
|
82
134
|
/** Override stdout TTY detection (tests). */
|
|
@@ -95,6 +147,8 @@ interface RunOptions {
|
|
|
95
147
|
noAnimation?: boolean;
|
|
96
148
|
/** Override the stream the score animation writes to (tests). Defaults to process.stdout. */
|
|
97
149
|
stdoutStream?: NodeJS.WriteStream;
|
|
150
|
+
/** Override the stream the analysis-phase progress indicator writes to (tests). Defaults to process.stderr. */
|
|
151
|
+
stderrStream?: NodeJS.WriteStream;
|
|
98
152
|
/** Override the animation's per-frame delay in ms (tests — 0 runs the real frame loop near-instantly). Defaults to the animation module's own constant. */
|
|
99
153
|
animationFrameDelayMs?: number;
|
|
100
154
|
}
|
|
@@ -125,6 +179,18 @@ interface AnalyzeOptions {
|
|
|
125
179
|
weights?: Partial<Record<Category, number>>;
|
|
126
180
|
/** Restrict analysis to rules in these categories (applied after rules/ignore selection). */
|
|
127
181
|
categories?: Category[];
|
|
182
|
+
/**
|
|
183
|
+
* Reuse this parse cache across multiple `analyzeProject` calls instead of
|
|
184
|
+
* starting fresh each time — the vite dev dashboard passes a long-lived cache
|
|
185
|
+
* and invalidates only the changed file's entry between re-analyses, so
|
|
186
|
+
* unchanged routes/layouts are never re-read or re-parsed. This only covers
|
|
187
|
+
* the route/layout (head-resolution) parse path via `collectRoutes` —
|
|
188
|
+
* `collectComponentFacts` (Correctness facts) is unaffected and still scans
|
|
189
|
+
* every component on each call. Callers that don't need cross-call reuse
|
|
190
|
+
* (the CLI's `run()`, MCP, the Action — each analyzes once per process) can
|
|
191
|
+
* omit this; a fresh cache is created automatically.
|
|
192
|
+
*/
|
|
193
|
+
parseCache?: ParseCache;
|
|
128
194
|
}
|
|
129
195
|
interface AnalyzeResult {
|
|
130
196
|
results: Result[];
|
|
@@ -150,14 +216,25 @@ interface ApplyScopeOptions {
|
|
|
150
216
|
staged?: boolean;
|
|
151
217
|
diffBase?: string;
|
|
152
218
|
baseline?: string;
|
|
219
|
+
/**
|
|
220
|
+
* Resolved config, needed to decide which findings count as "penalized" when
|
|
221
|
+
* applying svelte-vitals-suppressions.json (`isPenalized`). Suppression
|
|
222
|
+
* application is skipped entirely when omitted, keeping such callers'
|
|
223
|
+
* behavior unchanged (the CLI and @svelte-vitals/action both pass it).
|
|
224
|
+
*/
|
|
225
|
+
config?: Config;
|
|
226
|
+
/** Disable applying svelte-vitals-suppressions.json for this run. */
|
|
227
|
+
noSuppressions?: boolean;
|
|
153
228
|
errorLog?: (line: string) => void;
|
|
154
229
|
analyzeOpts?: AnalyzeOptions;
|
|
155
230
|
}
|
|
156
231
|
/**
|
|
157
232
|
* Narrow `results` to what a PR gate cares about: `--staged`/`--diff` restrict to
|
|
158
|
-
* changed files, `--baseline` drops findings that already existed at that ref
|
|
159
|
-
*
|
|
160
|
-
*
|
|
233
|
+
* changed files, `--baseline` drops findings that already existed at that ref,
|
|
234
|
+
* and (last) svelte-vitals-suppressions.json drops findings that were explicitly
|
|
235
|
+
* accepted via `--update-suppressions`. Shared by `run()` and
|
|
236
|
+
* `@svelte-vitals/action` (issue #154) so the git-diff/baseline orchestration
|
|
237
|
+
* lives in exactly one place.
|
|
161
238
|
*/
|
|
162
239
|
declare function applyScope(results: Result[], opts: ApplyScopeOptions): Promise<Result[]>;
|
|
163
240
|
/**
|
|
@@ -166,4 +243,4 @@ declare function applyScope(results: Result[], opts: ApplyScopeOptions): Promise
|
|
|
166
243
|
*/
|
|
167
244
|
declare function run(opts?: RunOptions): Promise<number>;
|
|
168
245
|
|
|
169
|
-
export { type AnalyzeOptions, type AnalyzeResult, type ApplyScopeOptions, type LoadedConfigFile, ProjectError, type RunOptions, analyzeProject, applyScope, buildRulesConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, routeMatcher, run, spinnerEnabled };
|
|
246
|
+
export { type AnalyzeOptions, type AnalyzeResult, type ApplyScopeOptions, type LoadedConfigFile, type ParseCache, ProjectError, type RunOptions, analyzeProject, applyScope, buildRulesConfig, findUnknownRuleIds, knownRuleIds, loadConfigFile, routeMatcher, run, spinnerEnabled };
|
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.25.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",
|
|
@@ -40,15 +40,16 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@clack/prompts": "^1.7.0",
|
|
43
|
+
"log-update": "^8.0.0",
|
|
43
44
|
"magicast": "^0.5.3",
|
|
44
45
|
"mri": "^1.2.0",
|
|
45
46
|
"smol-toml": "^1.7.0",
|
|
46
47
|
"svelte": "^5.56.4",
|
|
47
48
|
"tinyglobby": "^0.2.17",
|
|
48
|
-
"@svelte-vitals/core": "0.
|
|
49
|
+
"@svelte-vitals/core": "0.24.0"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^24.13.
|
|
52
|
+
"@types/node": "^24.13.3"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
55
|
"build": "node scripts/gen-action-pin.mjs && tsup",
|