svelte-vitals 0.22.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -7
- package/dist/bin.js +31 -22
- package/dist/{chunk-KSJZLF5U.js → chunk-TCZ6OF6J.js} +248 -11
- package/dist/index.d.ts +10 -0
- 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
|
```
|
|
@@ -34,14 +34,14 @@ Critical (1)
|
|
|
34
34
|
src/routes/none/+page.svelte
|
|
35
35
|
|
|
36
36
|
Passed (3)
|
|
37
|
-
────────────────────────
|
|
38
|
-
✓ SEO001 <title> /blog
|
|
39
|
-
✓ SEO001 <title> ↯ dynamic /dynamic
|
|
40
|
-
✓ SEO001 <title> /static
|
|
41
37
|
|
|
42
38
|
↯ = set dynamically (verified at runtime).
|
|
43
39
|
```
|
|
44
40
|
|
|
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
|
+
|
|
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
|
+
|
|
45
45
|
### Exit codes
|
|
46
46
|
|
|
47
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-TCZ6OF6J.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,13 +125,16 @@ 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
|
}
|
|
135
|
+
const verbose = Boolean(argv["verbose"]);
|
|
136
|
+
const noColor = argv.color === false;
|
|
137
|
+
const noAnimation = argv.animation === false;
|
|
137
138
|
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
138
139
|
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
139
140
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
@@ -154,6 +155,9 @@ function resolveArgs(argv) {
|
|
|
154
155
|
...weights !== void 0 ? { weights } : {},
|
|
155
156
|
...categories !== void 0 ? { categories } : {},
|
|
156
157
|
...score ? { score } : {},
|
|
158
|
+
...verbose ? { verbose } : {},
|
|
159
|
+
...noColor ? { noColor } : {},
|
|
160
|
+
...noAnimation ? { noAnimation } : {},
|
|
157
161
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
158
162
|
...staged ? { staged } : {},
|
|
159
163
|
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
@@ -265,9 +269,9 @@ var VITE_TARGETS = [
|
|
|
265
269
|
hint: "Fails `vite build` when prerendered pages cross the SEO/Performance threshold"
|
|
266
270
|
},
|
|
267
271
|
{
|
|
268
|
-
id: "vite-
|
|
269
|
-
label: "
|
|
270
|
-
hint: "
|
|
272
|
+
id: "vite-hooks",
|
|
273
|
+
label: "Live dashboard accuracy",
|
|
274
|
+
hint: "Feeds real rendered results into the live dashboard as you browse \u2014 improves per-route accuracy, never fails a build"
|
|
271
275
|
}
|
|
272
276
|
];
|
|
273
277
|
function viteTargetById(id) {
|
|
@@ -527,10 +531,10 @@ function planForVitePlugin(io) {
|
|
|
527
531
|
const result = codemodViteConfig(content);
|
|
528
532
|
return { id: "vite-plugin", label: viteTargetById("vite-plugin").label, path, ...result };
|
|
529
533
|
}
|
|
530
|
-
function
|
|
534
|
+
function planForViteHooks(io) {
|
|
531
535
|
const { path, content } = resolveCandidate(io, ["src/hooks.server.ts", "src/hooks.server.js"]);
|
|
532
536
|
const result = codemodHooksServer(content);
|
|
533
|
-
return { id: "vite-
|
|
537
|
+
return { id: "vite-hooks", label: viteTargetById("vite-hooks").label, path, ...result };
|
|
534
538
|
}
|
|
535
539
|
function planForAgentTarget(target, io, force, version) {
|
|
536
540
|
const path = join3(io.cwd, target.relPath);
|
|
@@ -589,7 +593,7 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
589
593
|
ids = picked;
|
|
590
594
|
} else {
|
|
591
595
|
io.errorLog(
|
|
592
|
-
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-
|
|
596
|
+
"svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules> to install non-interactively."
|
|
593
597
|
);
|
|
594
598
|
return 2;
|
|
595
599
|
}
|
|
@@ -628,7 +632,7 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
628
632
|
}
|
|
629
633
|
}
|
|
630
634
|
for (const viteId of viteIds) {
|
|
631
|
-
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) :
|
|
635
|
+
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForViteHooks(io));
|
|
632
636
|
}
|
|
633
637
|
for (const agentId of agentIds) {
|
|
634
638
|
const target = agentTargetById(agentId);
|
|
@@ -743,10 +747,11 @@ Usage:
|
|
|
743
747
|
svelte-vitals install [options]
|
|
744
748
|
|
|
745
749
|
Options:
|
|
746
|
-
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-
|
|
750
|
+
--client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-hooks,claude-skill,cursor-rules
|
|
747
751
|
(skips the interactive picker)
|
|
748
|
-
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-
|
|
749
|
-
wires up the
|
|
752
|
+
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-hooks
|
|
753
|
+
wires up the svelteVitalsHandle hook in src/hooks.server.{ts,js}, which improves the
|
|
754
|
+
live dashboard's per-route accuracy as you browse. --force does not apply
|
|
750
755
|
to either of these two \u2014 an existing registration is always left as-is.
|
|
751
756
|
claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
|
|
752
757
|
writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
|
|
@@ -880,8 +885,8 @@ function buildWorkflowYaml(opts) {
|
|
|
880
885
|
}
|
|
881
886
|
|
|
882
887
|
// src/ci/action-pin.generated.ts
|
|
883
|
-
var ACTION_SHA = "
|
|
884
|
-
var ACTION_VERSION = "0.2.
|
|
888
|
+
var ACTION_SHA = "c9257b68ce2c4006f3631345f66c05342c715fdd";
|
|
889
|
+
var ACTION_VERSION = "0.2.3";
|
|
885
890
|
|
|
886
891
|
// src/ci/cli.ts
|
|
887
892
|
var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
|
|
@@ -959,9 +964,7 @@ Options:
|
|
|
959
964
|
--by-route Show per-route score breakdown in console output
|
|
960
965
|
--reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
961
966
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
962
|
-
--json Alias for --reporter=json
|
|
963
967
|
--fail-on <severity> Fail (exit 1) when any finding reaches this severity: critical | warning | info
|
|
964
|
-
--fail-on-warning Alias for --fail-on=warning
|
|
965
968
|
--min-health <0-100> Fail (exit 1) when the combined Health score is below this value
|
|
966
969
|
--rules <ids> Comma-separated rule ids to enable (all others disabled)
|
|
967
970
|
--ignore <ids> Comma-separated rule ids to disable
|
|
@@ -969,6 +972,8 @@ Options:
|
|
|
969
972
|
--weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
|
|
970
973
|
--score Print only the combined Health score (works with --min-health for gating)
|
|
971
974
|
--no-color Disable ANSI color in console output
|
|
975
|
+
--no-animation Disable the Health-score reveal animation and mascot on an interactive terminal
|
|
976
|
+
--verbose Show every finding uncapped and ungrouped (default: capped, grouped by rule)
|
|
972
977
|
-h, --help Show this help
|
|
973
978
|
-v, --version Show version
|
|
974
979
|
|
|
@@ -999,7 +1004,7 @@ async function main() {
|
|
|
999
1004
|
}
|
|
1000
1005
|
const argv = mri3(process.argv.slice(2), {
|
|
1001
1006
|
alias: { h: "help", v: "version" },
|
|
1002
|
-
boolean: ["by-route", "
|
|
1007
|
+
boolean: ["by-route", "staged", "score", "verbose"],
|
|
1003
1008
|
string: [
|
|
1004
1009
|
"meta-components",
|
|
1005
1010
|
"treat-dynamic-as",
|
|
@@ -1038,7 +1043,11 @@ async function main() {
|
|
|
1038
1043
|
}
|
|
1039
1044
|
minHealth = n;
|
|
1040
1045
|
}
|
|
1041
|
-
const code = await run({
|
|
1046
|
+
const code = await run({
|
|
1047
|
+
...options,
|
|
1048
|
+
minHealth,
|
|
1049
|
+
selectApp
|
|
1050
|
+
});
|
|
1042
1051
|
process.exit(code);
|
|
1043
1052
|
}
|
|
1044
1053
|
void main();
|
|
@@ -702,6 +702,9 @@ function isAgentEnv(env = process.env) {
|
|
|
702
702
|
function isGithubActionsEnv(env = process.env) {
|
|
703
703
|
return env.GITHUB_ACTIONS === "true";
|
|
704
704
|
}
|
|
705
|
+
function isCiEnv(env = process.env) {
|
|
706
|
+
return env.CI === "true" || env.CI === "1";
|
|
707
|
+
}
|
|
705
708
|
function resolveReporter(explicit, env = process.env) {
|
|
706
709
|
if (explicit) return explicit;
|
|
707
710
|
const fromEnv = env.SVELTE_VITALS_REPORTER;
|
|
@@ -832,6 +835,157 @@ function startSpinner(text, opts) {
|
|
|
832
835
|
};
|
|
833
836
|
}
|
|
834
837
|
|
|
838
|
+
// src/mascot.ts
|
|
839
|
+
import { createLogUpdate } from "log-update";
|
|
840
|
+
function mascotStateFor(score) {
|
|
841
|
+
if (score === 100) return "ecstatic";
|
|
842
|
+
if (score >= 90) return "happy";
|
|
843
|
+
return "content";
|
|
844
|
+
}
|
|
845
|
+
var MIN_MASCOT_COLUMNS = 20;
|
|
846
|
+
function mascotFitsWidth(columns) {
|
|
847
|
+
return (columns ?? 80) >= MIN_MASCOT_COLUMNS;
|
|
848
|
+
}
|
|
849
|
+
var ORANGE_FG = "\x1B[38;2;255;62;0m";
|
|
850
|
+
var RESET = "\x1B[0m";
|
|
851
|
+
function renderFace(lines) {
|
|
852
|
+
return lines.map((line) => `${ORANGE_FG}${line}${RESET}`).join("\n");
|
|
853
|
+
}
|
|
854
|
+
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"];
|
|
855
|
+
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"];
|
|
856
|
+
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"];
|
|
857
|
+
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"];
|
|
858
|
+
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"];
|
|
859
|
+
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"];
|
|
860
|
+
var FACE_ECSTATIC = ["\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E", "\u2502 ^ ^ \u2502", "\u2502 \u2570\u2500\u2500\u2500\u256F \u2502", "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F"];
|
|
861
|
+
var REACTION_FACES = {
|
|
862
|
+
content: FACE_CONTENT,
|
|
863
|
+
happy: FACE_HAPPY,
|
|
864
|
+
ecstatic: FACE_ECSTATIC
|
|
865
|
+
};
|
|
866
|
+
function renderMascotReaction(state) {
|
|
867
|
+
return renderFace(REACTION_FACES[state]);
|
|
868
|
+
}
|
|
869
|
+
function renderMascotAnticipating() {
|
|
870
|
+
return renderFace(FACE_OPEN_EYES_NEUTRAL_MOUTH);
|
|
871
|
+
}
|
|
872
|
+
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];
|
|
873
|
+
var IDLE_FACES = {
|
|
874
|
+
0: FACE_OPEN_EYES_NEUTRAL_MOUTH,
|
|
875
|
+
1: FACE_CLOSED_EYES_NEUTRAL_MOUTH,
|
|
876
|
+
2: FACE_WINK_BOTH,
|
|
877
|
+
3: FACE_WINK_ONE
|
|
878
|
+
};
|
|
879
|
+
function renderMascotIdleFrame(frameIndex) {
|
|
880
|
+
const code = IDLE_FRAME_SEQUENCE[frameIndex % IDLE_FRAME_SEQUENCE.length];
|
|
881
|
+
return renderFace(IDLE_FACES[code]);
|
|
882
|
+
}
|
|
883
|
+
var CONFETTI_COLORS = [
|
|
884
|
+
[255, 62, 0],
|
|
885
|
+
// orange (identity accent)
|
|
886
|
+
[255, 145, 175],
|
|
887
|
+
// blush pink
|
|
888
|
+
[255, 214, 0],
|
|
889
|
+
// gold
|
|
890
|
+
[255, 255, 255]
|
|
891
|
+
// white
|
|
892
|
+
];
|
|
893
|
+
var CONFETTI_CHARS = ["*", ".", "\xB7", "+"];
|
|
894
|
+
var CONFETTI_WIDTH = 24;
|
|
895
|
+
function confettiFg(rgb) {
|
|
896
|
+
return `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
|
|
897
|
+
}
|
|
898
|
+
function confettiRow(offset) {
|
|
899
|
+
let out = "";
|
|
900
|
+
for (let col = 0; col < CONFETTI_WIDTH; col++) {
|
|
901
|
+
if ((col + offset) % 5 === 0) {
|
|
902
|
+
const glyph = CONFETTI_CHARS[(col + offset) % CONFETTI_CHARS.length];
|
|
903
|
+
out += confettiFg(CONFETTI_COLORS[(col + offset) % CONFETTI_COLORS.length]) + glyph + RESET;
|
|
904
|
+
} else {
|
|
905
|
+
out += " ";
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return out;
|
|
909
|
+
}
|
|
910
|
+
function renderConfettiFrame(offset, mascotBlock) {
|
|
911
|
+
return [confettiRow(offset), mascotBlock, confettiRow(offset + 2)].join("\n");
|
|
912
|
+
}
|
|
913
|
+
var IDLE_TICK_MS = 160;
|
|
914
|
+
function startMascotSpinner(text, opts) {
|
|
915
|
+
if (!opts.enabled) return { stop() {
|
|
916
|
+
} };
|
|
917
|
+
const stream = opts.stream ?? process.stderr;
|
|
918
|
+
const render = createLogUpdate(stream);
|
|
919
|
+
let i = 0;
|
|
920
|
+
const tick = () => {
|
|
921
|
+
render(`${renderMascotIdleFrame(i)}
|
|
922
|
+
${text}`);
|
|
923
|
+
i++;
|
|
924
|
+
};
|
|
925
|
+
tick();
|
|
926
|
+
const timer = setInterval(tick, IDLE_TICK_MS);
|
|
927
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
928
|
+
return {
|
|
929
|
+
stop() {
|
|
930
|
+
clearInterval(timer);
|
|
931
|
+
render.clear();
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// src/speech-bubble.ts
|
|
937
|
+
import { createLogUpdate as createLogUpdate2 } from "log-update";
|
|
938
|
+
var MIN_BUBBLE_COLUMNS = 55;
|
|
939
|
+
function bubbleFitsWidth(columns) {
|
|
940
|
+
return (columns ?? 80) >= MIN_BUBBLE_COLUMNS;
|
|
941
|
+
}
|
|
942
|
+
function renderSpeechBubble(text) {
|
|
943
|
+
const border = "\u2500".repeat(text.length + 2);
|
|
944
|
+
return [`\u256D${border}\u256E`, `\u2502 ${text} \u2502`, `\u2570${border}\u256F`];
|
|
945
|
+
}
|
|
946
|
+
function withSpeechBubble(mascotBlock, bubbleLines) {
|
|
947
|
+
const mascotLines = mascotBlock.split("\n");
|
|
948
|
+
const bubbleWidth = bubbleLines[0]?.length ?? 0;
|
|
949
|
+
const blankBubbleLine = " ".repeat(bubbleWidth);
|
|
950
|
+
const padTop = Math.floor((mascotLines.length - bubbleLines.length) / 2);
|
|
951
|
+
const padBottom = mascotLines.length - bubbleLines.length - padTop;
|
|
952
|
+
const paddedBubble = [
|
|
953
|
+
...Array(Math.max(padTop, 0)).fill(blankBubbleLine),
|
|
954
|
+
...bubbleLines,
|
|
955
|
+
...Array(Math.max(padBottom, 0)).fill(blankBubbleLine)
|
|
956
|
+
];
|
|
957
|
+
return mascotLines.map((line, i) => `${line} ${paddedBubble[i] ?? blankBubbleLine}`).join("\n");
|
|
958
|
+
}
|
|
959
|
+
var GREETING_MESSAGES = [
|
|
960
|
+
"Welcome to Svelte Vitals!",
|
|
961
|
+
"Let's check your project!",
|
|
962
|
+
"Ready when you are!",
|
|
963
|
+
"Hi there! Let's dig in."
|
|
964
|
+
];
|
|
965
|
+
var REACTION_MESSAGES = {
|
|
966
|
+
ecstatic: ["Perfect score!", "Flawless!", "You nailed it!"],
|
|
967
|
+
happy: ["Nice work!", "Looking great!", "Almost perfect!"],
|
|
968
|
+
content: ["Keep going!", "Room to grow!", "Let's improve this!"]
|
|
969
|
+
};
|
|
970
|
+
function pickMessage(pool, random = Math.random) {
|
|
971
|
+
return pool[Math.floor(random() * pool.length)];
|
|
972
|
+
}
|
|
973
|
+
function renderMascotWithSpeech(mascotBlock, message) {
|
|
974
|
+
return withSpeechBubble(mascotBlock, renderSpeechBubble(message));
|
|
975
|
+
}
|
|
976
|
+
function sleep(ms) {
|
|
977
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
978
|
+
}
|
|
979
|
+
var GREETING_HOLD_MS = 800;
|
|
980
|
+
async function playMascotGreeting(opts) {
|
|
981
|
+
if (!opts.enabled) return;
|
|
982
|
+
const holdMs = opts.holdMs ?? GREETING_HOLD_MS;
|
|
983
|
+
const render = createLogUpdate2(opts.stream);
|
|
984
|
+
render(renderMascotWithSpeech(renderMascotAnticipating(), pickMessage(GREETING_MESSAGES)));
|
|
985
|
+
if (holdMs > 0) await sleep(holdMs);
|
|
986
|
+
render.clear();
|
|
987
|
+
}
|
|
988
|
+
|
|
835
989
|
// src/config-file.ts
|
|
836
990
|
import { existsSync as existsSync2 } from "fs";
|
|
837
991
|
import { join as join4 } from "path";
|
|
@@ -953,6 +1107,61 @@ async function loadConfigFile(cwd) {
|
|
|
953
1107
|
return validateConfigFile(mod.default, found);
|
|
954
1108
|
}
|
|
955
1109
|
|
|
1110
|
+
// src/pulse-animation.ts
|
|
1111
|
+
import { scoreColor } from "@svelte-vitals/core";
|
|
1112
|
+
import { createLogUpdate as createLogUpdate3 } from "log-update";
|
|
1113
|
+
var FRAME_COUNT = 6;
|
|
1114
|
+
var FRAME_DELAY_MS = 200;
|
|
1115
|
+
var WAVE_FRAMES = [
|
|
1116
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1117
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1118
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
1119
|
+
"\u2500\u2500\u2500\u2500\u2500\u2500\u2571\u2572\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2572\u2500\u2500\u2500\u2500\u2500",
|
|
1120
|
+
"\u2500\u2500\u2500\u2500\u2571\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2572\u2500\u2500\u2500\u2500"
|
|
1121
|
+
];
|
|
1122
|
+
var WAVE_ORANGE_DIM = "\x1B[38;2;153;37;0m";
|
|
1123
|
+
var WAVE_RESET = "\x1B[0m";
|
|
1124
|
+
function sleep2(ms) {
|
|
1125
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1126
|
+
}
|
|
1127
|
+
var REACTION_HOLD_MS = 500;
|
|
1128
|
+
var CONFETTI_FRAME_COUNT = 4;
|
|
1129
|
+
var CONFETTI_FRAME_DELAY_MS = 220;
|
|
1130
|
+
async function playScoreAnimation(opts) {
|
|
1131
|
+
const frameDelayMs = opts.frameDelayMs ?? FRAME_DELAY_MS;
|
|
1132
|
+
const holdMs = opts.frameDelayMs ?? REACTION_HOLD_MS;
|
|
1133
|
+
const confettiDelayMs = opts.frameDelayMs ?? CONFETTI_FRAME_DELAY_MS;
|
|
1134
|
+
const render = createLogUpdate3(opts.stream);
|
|
1135
|
+
const showMascot = mascotFitsWidth(opts.stream.columns);
|
|
1136
|
+
const state = mascotStateFor(opts.score);
|
|
1137
|
+
const reactionMessage = showMascot && bubbleFitsWidth(opts.stream.columns) ? pickMessage(REACTION_MESSAGES[state]) : void 0;
|
|
1138
|
+
const finalMascotBlock = reactionMessage ? renderMascotWithSpeech(renderMascotReaction(state), reactionMessage) : renderMascotReaction(state);
|
|
1139
|
+
for (let frame = 0; frame < FRAME_COUNT; frame++) {
|
|
1140
|
+
const progress = frame / (FRAME_COUNT - 1);
|
|
1141
|
+
const displayScore = Math.round(opts.score * progress);
|
|
1142
|
+
const isFinalFrame = frame === FRAME_COUNT - 1;
|
|
1143
|
+
const scoreText = isFinalFrame ? scoreColor(opts.palette, opts.score)(`${displayScore}/100`) : opts.palette.dim(`${displayScore}/100`);
|
|
1144
|
+
const waveBlock = isFinalFrame ? `Health: ${scoreText}` : `${WAVE_ORANGE_DIM}${WAVE_FRAMES[frame]}${WAVE_RESET}
|
|
1145
|
+
Health: ${scoreText}`;
|
|
1146
|
+
const mascotBlock = showMascot ? (isFinalFrame ? finalMascotBlock : renderMascotAnticipating()) + "\n" : "";
|
|
1147
|
+
render(`${mascotBlock}${waveBlock}`);
|
|
1148
|
+
if (!isFinalFrame) await sleep2(frameDelayMs);
|
|
1149
|
+
}
|
|
1150
|
+
if (holdMs > 0) await sleep2(holdMs);
|
|
1151
|
+
if (showMascot && state === "ecstatic") {
|
|
1152
|
+
for (let i = 0; i < CONFETTI_FRAME_COUNT; i++) {
|
|
1153
|
+
const waveBlock = `Health: ${scoreColor(opts.palette, opts.score)("100/100")}`;
|
|
1154
|
+
render(`${renderConfettiFrame(i, finalMascotBlock)}
|
|
1155
|
+
${waveBlock}`);
|
|
1156
|
+
if (i < CONFETTI_FRAME_COUNT - 1 && confettiDelayMs > 0) await sleep2(confettiDelayMs);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
render.done();
|
|
1160
|
+
}
|
|
1161
|
+
function scoreAnimationEnabled(opts) {
|
|
1162
|
+
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 });
|
|
1163
|
+
}
|
|
1164
|
+
|
|
956
1165
|
// src/index.ts
|
|
957
1166
|
import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
|
|
958
1167
|
function spinnerEnabled(opts) {
|
|
@@ -1035,15 +1244,19 @@ async function run(opts = {}) {
|
|
|
1035
1244
|
}
|
|
1036
1245
|
const env = opts.env ?? process.env;
|
|
1037
1246
|
const reporter = resolveReporter(opts.reporter, env);
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
})
|
|
1247
|
+
const stderrStream = opts.stderrStream ?? process.stderr;
|
|
1248
|
+
const spinnerBaseEnabled = !opts.score && spinnerEnabled({
|
|
1249
|
+
reporter,
|
|
1250
|
+
rawReporter: opts.reporter,
|
|
1251
|
+
stderrIsTTY: opts.stderrIsTTY ?? !!process.stderr.isTTY,
|
|
1252
|
+
env,
|
|
1253
|
+
noColorFlag: opts.noColor
|
|
1046
1254
|
});
|
|
1255
|
+
const useMascotSpinner = spinnerBaseEnabled && !opts.noAnimation && mascotFitsWidth(stderrStream.columns);
|
|
1256
|
+
if (useMascotSpinner && bubbleFitsWidth(stderrStream.columns)) {
|
|
1257
|
+
await playMascotGreeting({ enabled: true, stream: stderrStream, holdMs: opts.animationFrameDelayMs });
|
|
1258
|
+
}
|
|
1259
|
+
const spinner = useMascotSpinner ? startMascotSpinner("Analyzing\u2026", { enabled: true, stream: stderrStream }) : startSpinner("Analyzing\u2026", { enabled: spinnerBaseEnabled, stream: stderrStream });
|
|
1047
1260
|
let cwd = opts.cwd ?? process.cwd();
|
|
1048
1261
|
let analysis;
|
|
1049
1262
|
try {
|
|
@@ -1134,6 +1347,7 @@ async function run(opts = {}) {
|
|
|
1134
1347
|
categories: opts.categories
|
|
1135
1348
|
}
|
|
1136
1349
|
});
|
|
1350
|
+
const summary = summarize(results, config);
|
|
1137
1351
|
if (opts.score) {
|
|
1138
1352
|
log(String(computeHealth(results, config).health));
|
|
1139
1353
|
} else {
|
|
@@ -1172,16 +1386,39 @@ async function run(opts = {}) {
|
|
|
1172
1386
|
} else if (reporter === "md") {
|
|
1173
1387
|
log(formatMarkdownReport(results, config, { version }));
|
|
1174
1388
|
} else {
|
|
1389
|
+
const stdoutIsTTY = opts.stdoutIsTTY ?? !!process.stdout.isTTY;
|
|
1175
1390
|
const colorOn = colorEnabled({
|
|
1176
1391
|
reporter,
|
|
1177
|
-
isTTY:
|
|
1392
|
+
isTTY: stdoutIsTTY,
|
|
1178
1393
|
env,
|
|
1179
1394
|
noColorFlag: opts.noColor
|
|
1180
1395
|
});
|
|
1181
|
-
|
|
1396
|
+
const palette = paletteFor(colorOn);
|
|
1397
|
+
const animate = scoreAnimationEnabled({
|
|
1398
|
+
reporter,
|
|
1399
|
+
stdoutIsTTY,
|
|
1400
|
+
env,
|
|
1401
|
+
noColorFlag: opts.noColor,
|
|
1402
|
+
noAnimationFlag: opts.noAnimation
|
|
1403
|
+
});
|
|
1404
|
+
if (animate) {
|
|
1405
|
+
await playScoreAnimation({
|
|
1406
|
+
score: computeHealth(results, config).health,
|
|
1407
|
+
palette,
|
|
1408
|
+
stream: opts.stdoutStream ?? process.stdout,
|
|
1409
|
+
frameDelayMs: opts.animationFrameDelayMs
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
log(
|
|
1413
|
+
formatConsoleReport(results, config, {
|
|
1414
|
+
byRoute: opts.byRoute ?? false,
|
|
1415
|
+
verbose: opts.verbose ?? false,
|
|
1416
|
+
palette,
|
|
1417
|
+
omitHeader: animate
|
|
1418
|
+
})
|
|
1419
|
+
);
|
|
1182
1420
|
}
|
|
1183
1421
|
}
|
|
1184
|
-
const summary = summarize(results, config);
|
|
1185
1422
|
const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
|
|
1186
1423
|
const failByHealth = opts.minHealth != null && computeHealth(results, config).health < opts.minHealth;
|
|
1187
1424
|
return failBySeverity || failByHealth ? 1 : 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -89,6 +89,16 @@ interface RunOptions {
|
|
|
89
89
|
explicitPath?: boolean;
|
|
90
90
|
/** Injected picker for the monorepo app selector (bin.ts wires a clack implementation; null = cancelled). */
|
|
91
91
|
selectApp?: (apps: string[]) => Promise<string | null>;
|
|
92
|
+
/** Show every finding uncapped and ungrouped in console output (default false — capped, grouped by rule). */
|
|
93
|
+
verbose?: boolean;
|
|
94
|
+
/** Disable the Health-score reveal animation even on an interactive stdout. */
|
|
95
|
+
noAnimation?: boolean;
|
|
96
|
+
/** Override the stream the score animation writes to (tests). Defaults to process.stdout. */
|
|
97
|
+
stdoutStream?: NodeJS.WriteStream;
|
|
98
|
+
/** Override the stream the analysis-phase progress indicator writes to (tests). Defaults to process.stderr. */
|
|
99
|
+
stderrStream?: NodeJS.WriteStream;
|
|
100
|
+
/** 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. */
|
|
101
|
+
animationFrameDelayMs?: number;
|
|
92
102
|
}
|
|
93
103
|
/**
|
|
94
104
|
* Whether the "Analyzing…" spinner should run. Unlike color, the spinner animates
|
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.24.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.23.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",
|