svelte-vitals 0.23.0 → 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 +5 -3
- package/dist/bin.js +23 -24
- package/dist/{chunk-HYTPZVSU.js → chunk-TCZ6OF6J.js} +194 -18
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/package.json +3 -2
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-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,14 +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
|
}
|
|
137
135
|
const verbose = Boolean(argv["verbose"]);
|
|
136
|
+
const noColor = argv.color === false;
|
|
137
|
+
const noAnimation = argv.animation === false;
|
|
138
138
|
const rulesConfig = buildRulesConfig(allow, ignore);
|
|
139
139
|
const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
|
|
140
140
|
if (errors.length > 0) return { options: null, warnings, errors };
|
|
@@ -156,6 +156,8 @@ function resolveArgs(argv) {
|
|
|
156
156
|
...categories !== void 0 ? { categories } : {},
|
|
157
157
|
...score ? { score } : {},
|
|
158
158
|
...verbose ? { verbose } : {},
|
|
159
|
+
...noColor ? { noColor } : {},
|
|
160
|
+
...noAnimation ? { noAnimation } : {},
|
|
159
161
|
...diffBase !== void 0 ? { diffBase } : {},
|
|
160
162
|
...staged ? { staged } : {},
|
|
161
163
|
...baselineRef !== void 0 ? { baseline: baselineRef } : {}
|
|
@@ -267,9 +269,9 @@ var VITE_TARGETS = [
|
|
|
267
269
|
hint: "Fails `vite build` when prerendered pages cross the SEO/Performance threshold"
|
|
268
270
|
},
|
|
269
271
|
{
|
|
270
|
-
id: "vite-
|
|
271
|
-
label: "
|
|
272
|
-
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"
|
|
273
275
|
}
|
|
274
276
|
];
|
|
275
277
|
function viteTargetById(id) {
|
|
@@ -529,10 +531,10 @@ function planForVitePlugin(io) {
|
|
|
529
531
|
const result = codemodViteConfig(content);
|
|
530
532
|
return { id: "vite-plugin", label: viteTargetById("vite-plugin").label, path, ...result };
|
|
531
533
|
}
|
|
532
|
-
function
|
|
534
|
+
function planForViteHooks(io) {
|
|
533
535
|
const { path, content } = resolveCandidate(io, ["src/hooks.server.ts", "src/hooks.server.js"]);
|
|
534
536
|
const result = codemodHooksServer(content);
|
|
535
|
-
return { id: "vite-
|
|
537
|
+
return { id: "vite-hooks", label: viteTargetById("vite-hooks").label, path, ...result };
|
|
536
538
|
}
|
|
537
539
|
function planForAgentTarget(target, io, force, version) {
|
|
538
540
|
const path = join3(io.cwd, target.relPath);
|
|
@@ -591,7 +593,7 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
591
593
|
ids = picked;
|
|
592
594
|
} else {
|
|
593
595
|
io.errorLog(
|
|
594
|
-
"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."
|
|
595
597
|
);
|
|
596
598
|
return 2;
|
|
597
599
|
}
|
|
@@ -630,7 +632,7 @@ async function runInstall(flags, io, prompts, version = "0.0.0") {
|
|
|
630
632
|
}
|
|
631
633
|
}
|
|
632
634
|
for (const viteId of viteIds) {
|
|
633
|
-
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) :
|
|
635
|
+
rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForViteHooks(io));
|
|
634
636
|
}
|
|
635
637
|
for (const agentId of agentIds) {
|
|
636
638
|
const target = agentTargetById(agentId);
|
|
@@ -745,10 +747,11 @@ Usage:
|
|
|
745
747
|
svelte-vitals install [options]
|
|
746
748
|
|
|
747
749
|
Options:
|
|
748
|
-
--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
|
|
749
751
|
(skips the interactive picker)
|
|
750
|
-
vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-
|
|
751
|
-
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
|
|
752
755
|
to either of these two \u2014 an existing registration is always left as-is.
|
|
753
756
|
claude-skill writes a Claude Code skill (.claude/skills/svelte-vitals/SKILL.md); cursor-rules
|
|
754
757
|
writes a Cursor rules file (.cursor/rules/svelte-vitals.mdc). Both are generated from the
|
|
@@ -882,8 +885,8 @@ function buildWorkflowYaml(opts) {
|
|
|
882
885
|
}
|
|
883
886
|
|
|
884
887
|
// src/ci/action-pin.generated.ts
|
|
885
|
-
var ACTION_SHA = "
|
|
886
|
-
var ACTION_VERSION = "0.2.
|
|
888
|
+
var ACTION_SHA = "c9257b68ce2c4006f3631345f66c05342c715fdd";
|
|
889
|
+
var ACTION_VERSION = "0.2.3";
|
|
887
890
|
|
|
888
891
|
// src/ci/cli.ts
|
|
889
892
|
var CI_HELP = `svelte-vitals ci \u2014 scaffold CI integration
|
|
@@ -961,9 +964,7 @@ Options:
|
|
|
961
964
|
--by-route Show per-route score breakdown in console output
|
|
962
965
|
--reporter <fmt> console | json | agent | sarif | github | html | md (auto: agent under AI-agent envs, github under GitHub Actions)
|
|
963
966
|
--out-file <path> Output path for --reporter html (default: svelte-vitals-report.html; '-' for stdout)
|
|
964
|
-
--json Alias for --reporter=json
|
|
965
967
|
--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
968
|
--min-health <0-100> Fail (exit 1) when the combined Health score is below this value
|
|
968
969
|
--rules <ids> Comma-separated rule ids to enable (all others disabled)
|
|
969
970
|
--ignore <ids> Comma-separated rule ids to disable
|
|
@@ -971,7 +972,7 @@ Options:
|
|
|
971
972
|
--weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
|
|
972
973
|
--score Print only the combined Health score (works with --min-health for gating)
|
|
973
974
|
--no-color Disable ANSI color in console output
|
|
974
|
-
--no-animation Disable the Health-score reveal animation on an interactive terminal
|
|
975
|
+
--no-animation Disable the Health-score reveal animation and mascot on an interactive terminal
|
|
975
976
|
--verbose Show every finding uncapped and ungrouped (default: capped, grouped by rule)
|
|
976
977
|
-h, --help Show this help
|
|
977
978
|
-v, --version Show version
|
|
@@ -1003,7 +1004,7 @@ async function main() {
|
|
|
1003
1004
|
}
|
|
1004
1005
|
const argv = mri3(process.argv.slice(2), {
|
|
1005
1006
|
alias: { h: "help", v: "version" },
|
|
1006
|
-
boolean: ["by-route", "
|
|
1007
|
+
boolean: ["by-route", "staged", "score", "verbose"],
|
|
1007
1008
|
string: [
|
|
1008
1009
|
"meta-components",
|
|
1009
1010
|
"treat-dynamic-as",
|
|
@@ -1045,8 +1046,6 @@ async function main() {
|
|
|
1045
1046
|
const code = await run({
|
|
1046
1047
|
...options,
|
|
1047
1048
|
minHealth,
|
|
1048
|
-
noColor: argv["no-color"],
|
|
1049
|
-
noAnimation: argv["no-animation"],
|
|
1050
1049
|
selectApp
|
|
1051
1050
|
});
|
|
1052
1051
|
process.exit(code);
|
|
@@ -835,6 +835,157 @@ function startSpinner(text, opts) {
|
|
|
835
835
|
};
|
|
836
836
|
}
|
|
837
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
|
+
|
|
838
989
|
// src/config-file.ts
|
|
839
990
|
import { existsSync as existsSync2 } from "fs";
|
|
840
991
|
import { join as join4 } from "path";
|
|
@@ -958,6 +1109,7 @@ async function loadConfigFile(cwd) {
|
|
|
958
1109
|
|
|
959
1110
|
// src/pulse-animation.ts
|
|
960
1111
|
import { scoreColor } from "@svelte-vitals/core";
|
|
1112
|
+
import { createLogUpdate as createLogUpdate3 } from "log-update";
|
|
961
1113
|
var FRAME_COUNT = 6;
|
|
962
1114
|
var FRAME_DELAY_MS = 200;
|
|
963
1115
|
var WAVE_FRAMES = [
|
|
@@ -965,26 +1117,46 @@ var WAVE_FRAMES = [
|
|
|
965
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",
|
|
966
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",
|
|
967
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",
|
|
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"
|
|
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"
|
|
970
1121
|
];
|
|
971
|
-
|
|
1122
|
+
var WAVE_ORANGE_DIM = "\x1B[38;2;153;37;0m";
|
|
1123
|
+
var WAVE_RESET = "\x1B[0m";
|
|
1124
|
+
function sleep2(ms) {
|
|
972
1125
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
973
1126
|
}
|
|
1127
|
+
var REACTION_HOLD_MS = 500;
|
|
1128
|
+
var CONFETTI_FRAME_COUNT = 4;
|
|
1129
|
+
var CONFETTI_FRAME_DELAY_MS = 220;
|
|
974
1130
|
async function playScoreAnimation(opts) {
|
|
975
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);
|
|
976
1139
|
for (let frame = 0; frame < FRAME_COUNT; frame++) {
|
|
977
1140
|
const progress = frame / (FRAME_COUNT - 1);
|
|
978
1141
|
const displayScore = Math.round(opts.score * progress);
|
|
979
1142
|
const isFinalFrame = frame === FRAME_COUNT - 1;
|
|
980
|
-
const wave = WAVE_FRAMES[frame];
|
|
981
1143
|
const scoreText = isFinalFrame ? scoreColor(opts.palette, opts.score)(`${displayScore}/100`) : opts.palette.dim(`${displayScore}/100`);
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
\
|
|
985
|
-
`);
|
|
986
|
-
if (!isFinalFrame) await
|
|
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);
|
|
987
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();
|
|
988
1160
|
}
|
|
989
1161
|
function scoreAnimationEnabled(opts) {
|
|
990
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 });
|
|
@@ -1072,15 +1244,19 @@ async function run(opts = {}) {
|
|
|
1072
1244
|
}
|
|
1073
1245
|
const env = opts.env ?? process.env;
|
|
1074
1246
|
const reporter = resolveReporter(opts.reporter, env);
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
})
|
|
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
|
|
1083
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 });
|
|
1084
1260
|
let cwd = opts.cwd ?? process.cwd();
|
|
1085
1261
|
let analysis;
|
|
1086
1262
|
try {
|
|
@@ -1171,6 +1347,7 @@ async function run(opts = {}) {
|
|
|
1171
1347
|
categories: opts.categories
|
|
1172
1348
|
}
|
|
1173
1349
|
});
|
|
1350
|
+
const summary = summarize(results, config);
|
|
1174
1351
|
if (opts.score) {
|
|
1175
1352
|
log(String(computeHealth(results, config).health));
|
|
1176
1353
|
} else {
|
|
@@ -1242,7 +1419,6 @@ async function run(opts = {}) {
|
|
|
1242
1419
|
);
|
|
1243
1420
|
}
|
|
1244
1421
|
}
|
|
1245
|
-
const summary = summarize(results, config);
|
|
1246
1422
|
const failBySeverity = hasFailureAtOrAbove(summary, config.failOn);
|
|
1247
1423
|
const failByHealth = opts.minHealth != null && computeHealth(results, config).health < opts.minHealth;
|
|
1248
1424
|
return failBySeverity || failByHealth ? 1 : 0;
|
package/dist/index.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ interface RunOptions {
|
|
|
95
95
|
noAnimation?: boolean;
|
|
96
96
|
/** Override the stream the score animation writes to (tests). Defaults to process.stdout. */
|
|
97
97
|
stdoutStream?: NodeJS.WriteStream;
|
|
98
|
+
/** Override the stream the analysis-phase progress indicator writes to (tests). Defaults to process.stderr. */
|
|
99
|
+
stderrStream?: NodeJS.WriteStream;
|
|
98
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. */
|
|
99
101
|
animationFrameDelayMs?: number;
|
|
100
102
|
}
|
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,6 +40,7 @@
|
|
|
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",
|
|
@@ -48,7 +49,7 @@
|
|
|
48
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",
|