svelte-vitals 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -6,12 +6,55 @@ import {
6
6
  knownRuleIds,
7
7
  readPackageVersion,
8
8
  run
9
- } from "./chunk-VG647TPJ.js";
9
+ } from "./chunk-ZE3M3T6U.js";
10
10
 
11
11
  // src/bin.ts
12
12
  import mri2 from "mri";
13
13
 
14
14
  // src/resolve-args.ts
15
+ var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
16
+ function parseWeights(raw, errors) {
17
+ if (typeof raw !== "string" || raw.trim() === "") return void 0;
18
+ const weights = {};
19
+ const unknownCategories = [];
20
+ const invalidValues = [];
21
+ for (const pair of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
22
+ const eq = pair.indexOf("=");
23
+ if (eq === -1) {
24
+ invalidValues.push(pair);
25
+ continue;
26
+ }
27
+ const category = pair.slice(0, eq).trim().toLowerCase();
28
+ const valueRaw = pair.slice(eq + 1).trim();
29
+ if (!CATEGORIES.includes(category)) {
30
+ unknownCategories.push(category);
31
+ continue;
32
+ }
33
+ if (valueRaw === "") {
34
+ invalidValues.push(pair);
35
+ continue;
36
+ }
37
+ const value = Number(valueRaw);
38
+ if (!Number.isFinite(value) || value < 0) {
39
+ invalidValues.push(pair);
40
+ continue;
41
+ }
42
+ weights[category] = value;
43
+ }
44
+ if (unknownCategories.length > 0) {
45
+ errors.push(`svelte-vitals: unknown category(ies) in --weights: ${unknownCategories.join(", ")}`);
46
+ errors.push(`Known categories: ${CATEGORIES.join(", ")}`);
47
+ }
48
+ if (invalidValues.length > 0) {
49
+ errors.push(
50
+ `svelte-vitals: invalid --weights entry(ies): ${invalidValues.join(", ")}; expected category=number with a finite number >= 0.`
51
+ );
52
+ }
53
+ if (unknownCategories.length === 0 && invalidValues.length === 0 && Object.keys(weights).length === 0) {
54
+ errors.push("svelte-vitals: --weights was passed but contains no category=number pairs.");
55
+ }
56
+ return weights;
57
+ }
15
58
  var toList = (v) => typeof v === "string" ? v.split(",").map((s) => s.trim()).filter(Boolean) : [];
16
59
  function resolveArgs(argv) {
17
60
  const warnings = [];
@@ -55,6 +98,9 @@ function resolveArgs(argv) {
55
98
  );
56
99
  }
57
100
  const failOn = argv["fail-on-warning"] ? "warning" : failOnValid ? failOnRaw : void 0;
101
+ const weights = parseWeights(argv.weights, errors);
102
+ const rulesConfig = buildRulesConfig(allow, ignore);
103
+ const rules = Object.keys(rulesConfig).length > 0 ? rulesConfig : void 0;
58
104
  if (errors.length > 0) return { options: null, warnings, errors };
59
105
  return {
60
106
  options: {
@@ -66,7 +112,8 @@ function resolveArgs(argv) {
66
112
  outFile: typeof argv["out-file"] === "string" ? argv["out-file"] : void 0,
67
113
  byRoute: Boolean(argv["by-route"]),
68
114
  failOn,
69
- rules: buildRulesConfig(allow, ignore),
115
+ rules,
116
+ ...weights !== void 0 ? { weights } : {},
70
117
  ...diffBase !== void 0 ? { diffBase } : {},
71
118
  ...staged ? { staged } : {}
72
119
  },
@@ -79,9 +126,13 @@ function resolveArgs(argv) {
79
126
  import { mkdirSync, readFileSync, writeFileSync } from "fs";
80
127
  import { dirname } from "path";
81
128
  import { homedir } from "os";
129
+ import { spawnSync } from "child_process";
82
130
  import mri from "mri";
83
131
  import * as p from "@clack/prompts";
84
132
 
133
+ // src/install/index.ts
134
+ import { join as join3 } from "path";
135
+
85
136
  // src/install/clients.ts
86
137
  import { join } from "path";
87
138
  var MCP_ENTRY = { command: "npx", args: ["-y", "@svelte-vitals/mcp"] };
@@ -165,12 +216,192 @@ function mergeToml(existing, entry, force) {
165
216
  return { content: stringifyToml(root), status };
166
217
  }
167
218
 
219
+ // src/install/vite-targets.ts
220
+ var VITE_TARGETS = [
221
+ {
222
+ id: "vite-plugin",
223
+ label: "Vite plugin (build gate)",
224
+ hint: "Fails `vite build` when prerendered pages cross the SEO/Performance threshold"
225
+ },
226
+ {
227
+ id: "vite-dev-overlay",
228
+ label: "Dev overlay",
229
+ hint: "Live warnings in `vite dev` only \u2014 never fails a build or CI"
230
+ }
231
+ ];
232
+ function viteTargetById(id) {
233
+ return VITE_TARGETS.find((t) => t.id === id);
234
+ }
235
+ function isViteTargetId(id) {
236
+ return VITE_TARGETS.some((t) => t.id === id);
237
+ }
238
+
239
+ // src/install/codemod-vite-config.ts
240
+ import { parseModule, generateCode, builders, MagicastError } from "magicast";
241
+ var MANUAL_SNIPPET = `import { svelteVitals } from '@svelte-vitals/vite';
242
+ // add svelteVitals() to your \`plugins\` array`;
243
+ function codemodViteConfig(existing) {
244
+ if (existing === void 0) {
245
+ return { status: "manual", snippet: MANUAL_SNIPPET };
246
+ }
247
+ try {
248
+ const mod = parseModule(existing);
249
+ const def = mod.exports.default;
250
+ const configObj = def?.$type === "function-call" ? def.$args[0] : def;
251
+ if (!configObj || configObj.$type !== "object" || configObj.plugins?.$type !== "array") {
252
+ return { status: "manual", snippet: MANUAL_SNIPPET };
253
+ }
254
+ const already = configObj.plugins.find(
255
+ (p2) => p2?.$type === "function-call" && p2?.$callee === "svelteVitals"
256
+ );
257
+ if (already !== void 0) {
258
+ return { status: "exists" };
259
+ }
260
+ if (!mod.imports.svelteVitals) {
261
+ mod.imports.$append({ imported: "svelteVitals", local: "svelteVitals", from: "@svelte-vitals/vite" });
262
+ }
263
+ configObj.plugins.unshift(builders.functionCall("svelteVitals"));
264
+ return { status: "added", content: generateCode(mod, { format: { objectCurlySpacing: true } }).code };
265
+ } catch (err) {
266
+ if (err instanceof MagicastError) {
267
+ return { status: "manual", snippet: MANUAL_SNIPPET };
268
+ }
269
+ throw err;
270
+ }
271
+ }
272
+
273
+ // src/install/codemod-hooks.ts
274
+ import { parseModule as parseModule2, generateCode as generateCode2, builders as builders2, MagicastError as MagicastError2 } from "magicast";
275
+ var FRESH_HANDLE = `import { svelteVitalsHandle } from '@svelte-vitals/vite/hooks';
276
+ import { sequence } from '@sveltejs/kit/hooks';
277
+
278
+ export const handle = sequence(svelteVitalsHandle());
279
+ `;
280
+ var MANUAL_SNIPPET2 = `import { svelteVitalsHandle } from '@svelte-vitals/vite/hooks';
281
+ import { sequence } from '@sveltejs/kit/hooks';
282
+ // wrap your existing \`handle\` in sequence(yourHandle, svelteVitalsHandle())`;
283
+ function addImports(mod) {
284
+ if (!mod.imports.sequence) {
285
+ mod.imports.$append({ imported: "sequence", local: "sequence", from: "@sveltejs/kit/hooks" });
286
+ }
287
+ if (!mod.imports.svelteVitalsHandle) {
288
+ mod.imports.$append({
289
+ imported: "svelteVitalsHandle",
290
+ local: "svelteVitalsHandle",
291
+ from: "@svelte-vitals/vite/hooks"
292
+ });
293
+ }
294
+ }
295
+ function codemodHooksServer(existing) {
296
+ if (existing === void 0) {
297
+ return { status: "created", content: FRESH_HANDLE };
298
+ }
299
+ try {
300
+ const mod = parseModule2(existing);
301
+ const handle = mod.exports.handle;
302
+ if (handle === void 0) {
303
+ addImports(mod);
304
+ mod.exports.handle = builders2.functionCall("sequence", builders2.functionCall("svelteVitalsHandle"));
305
+ return {
306
+ status: "added",
307
+ content: generateCode2(mod, { format: { objectCurlySpacing: true, quote: "single" } }).code
308
+ };
309
+ }
310
+ if (handle.$type === "function-call" && handle.$callee === "sequence") {
311
+ const already = handle.$args.find(
312
+ (a) => a?.$type === "function-call" && a?.$callee === "svelteVitalsHandle"
313
+ );
314
+ if (already !== void 0) {
315
+ return { status: "exists" };
316
+ }
317
+ if (!mod.imports.svelteVitalsHandle) {
318
+ mod.imports.$append({
319
+ imported: "svelteVitalsHandle",
320
+ local: "svelteVitalsHandle",
321
+ from: "@svelte-vitals/vite/hooks"
322
+ });
323
+ }
324
+ handle.$args.push(builders2.functionCall("svelteVitalsHandle"));
325
+ return {
326
+ status: "added",
327
+ content: generateCode2(mod, { format: { objectCurlySpacing: true, quote: "single" } }).code
328
+ };
329
+ }
330
+ addImports(mod);
331
+ mod.exports.handle = builders2.functionCall("sequence", handle, builders2.functionCall("svelteVitalsHandle"));
332
+ return {
333
+ status: "updated",
334
+ content: generateCode2(mod, { format: { objectCurlySpacing: true, quote: "single" } }).code
335
+ };
336
+ } catch (err) {
337
+ if (err instanceof MagicastError2) {
338
+ return { status: "manual", snippet: MANUAL_SNIPPET2 };
339
+ }
340
+ throw err;
341
+ }
342
+ }
343
+
344
+ // src/install/package-manager.ts
345
+ import { join as join2 } from "path";
346
+ var LOCKFILE_TO_PM = {
347
+ "pnpm-lock.yaml": "pnpm",
348
+ "yarn.lock": "yarn",
349
+ "bun.lock": "bun",
350
+ "bun.lockb": "bun"
351
+ };
352
+ function detectPackageManager(io) {
353
+ for (const [file, pm] of Object.entries(LOCKFILE_TO_PM)) {
354
+ if (io.readFile(join2(io.cwd, file)) !== void 0) return pm;
355
+ }
356
+ return "npm";
357
+ }
358
+ function hasVitePackage(io) {
359
+ const raw = io.readFile(join2(io.cwd, "package.json"));
360
+ if (raw === void 0) return false;
361
+ try {
362
+ const pkg = JSON.parse(raw);
363
+ return Boolean(pkg.dependencies?.["@svelte-vitals/vite"] || pkg.devDependencies?.["@svelte-vitals/vite"]);
364
+ } catch {
365
+ return false;
366
+ }
367
+ }
368
+ function installCommand(pm) {
369
+ const action = pm === "npm" ? "install" : "add";
370
+ return { command: pm, args: [action, "-D", "@svelte-vitals/vite"] };
371
+ }
372
+
168
373
  // src/install/index.ts
169
- function planFor(client, scope, io, force) {
374
+ function planForClient(client, scope, io, force) {
170
375
  const path = client.resolvePath(scope, io.cwd, io.home);
171
376
  const existing = io.readFile(path);
172
377
  const merged = client.format === "toml" ? mergeToml(existing, MCP_ENTRY, force) : mergeJson(existing, MCP_ENTRY, force);
173
- return { client, scope, path, status: merged.status, content: merged.content };
378
+ return { id: client.id, label: client.label, scope, path, status: merged.status, content: merged.content };
379
+ }
380
+ function resolveCandidate(io, candidates) {
381
+ for (const rel of candidates) {
382
+ const path = join3(io.cwd, rel);
383
+ const content = io.readFile(path);
384
+ if (content !== void 0) return { path, content };
385
+ }
386
+ return { path: join3(io.cwd, candidates[0]), content: void 0 };
387
+ }
388
+ function planForVitePlugin(io) {
389
+ const { path, content } = resolveCandidate(io, ["vite.config.ts", "vite.config.js", "vite.config.mjs"]);
390
+ const result = codemodViteConfig(content);
391
+ return { id: "vite-plugin", label: viteTargetById("vite-plugin").label, path, ...result };
392
+ }
393
+ function planForDevOverlay(io) {
394
+ const { path, content } = resolveCandidate(io, ["src/hooks.server.ts", "src/hooks.server.js"]);
395
+ const result = codemodHooksServer(content);
396
+ return { id: "vite-dev-overlay", label: viteTargetById("vite-dev-overlay").label, path, ...result };
397
+ }
398
+ function indent(text) {
399
+ return text.split("\n").map((l) => ` ${l}`).join("\n");
400
+ }
401
+ function rowLine(r) {
402
+ const head = ` ${r.label}${r.scope ? ` (${r.scope})` : ""} \u2192 ${r.path} [${r.status}]`;
403
+ return r.status === "manual" && r.snippet ? `${head}
404
+ ${indent(r.snippet)}` : head;
174
405
  }
175
406
  async function runInstall(flags, io, prompts) {
176
407
  let ids;
@@ -184,22 +415,33 @@ async function runInstall(flags, io, prompts) {
184
415
  return false;
185
416
  }
186
417
  };
187
- const detected = CLIENTS.filter((c) => c.scopes.some((s) => configExists(c.resolvePath(s, io.cwd, io.home)))).map(
188
- (c) => c.id
418
+ const detectedClients = CLIENTS.filter(
419
+ (c) => c.scopes.some((s) => configExists(c.resolvePath(s, io.cwd, io.home)))
420
+ ).map((c) => c.id);
421
+ const viteConfigExists = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].some(
422
+ (f) => configExists(join3(io.cwd, f))
189
423
  );
190
- const picked = await prompts.selectClients(CLIENTS, detected);
424
+ const detected = [...detectedClients, ...viteConfigExists ? VITE_TARGETS.map((t) => t.id) : []];
425
+ const options = [
426
+ ...CLIENTS.map((c) => ({ id: c.id, label: c.label })),
427
+ ...VITE_TARGETS.map((t) => ({ id: t.id, label: t.label, hint: t.hint }))
428
+ ];
429
+ const picked = await prompts.selectClients(options, detected);
191
430
  if (picked === null) {
192
431
  io.log("Cancelled.");
193
432
  return 0;
194
433
  }
195
434
  ids = picked;
196
435
  } else {
197
- io.errorLog("svelte-vitals: no TTY; pass --client <claude-code,cursor,codex> to install non-interactively.");
436
+ io.errorLog(
437
+ "svelte-vitals: no TTY; pass --client <claude-code,cursor,codex,vite-plugin,vite-dev-overlay> to install non-interactively."
438
+ );
198
439
  return 2;
199
440
  }
200
441
  const clients = ids.map(clientById).filter((c) => c !== void 0);
201
- if (clients.length === 0) {
202
- io.errorLog("svelte-vitals: no valid clients selected.");
442
+ const viteIds = ids.filter(isViteTargetId);
443
+ if (clients.length === 0 && viteIds.length === 0) {
444
+ io.errorLog("svelte-vitals: no valid clients or targets selected.");
203
445
  return 2;
204
446
  }
205
447
  const rows = [];
@@ -220,7 +462,7 @@ async function runInstall(flags, io, prompts) {
220
462
  scope = "project";
221
463
  }
222
464
  try {
223
- rows.push(planFor(client, scope, io, flags.force ?? false));
465
+ rows.push(planForClient(client, scope, io, flags.force ?? false));
224
466
  } catch (err) {
225
467
  const path = client.resolvePath(scope, io.cwd, io.home);
226
468
  io.errorLog(
@@ -229,7 +471,10 @@ async function runInstall(flags, io, prompts) {
229
471
  return 2;
230
472
  }
231
473
  }
232
- const planText = rows.map((r) => ` ${r.client.label} (${r.scope}) \u2192 ${r.path} [${r.status}]`).join("\n");
474
+ for (const viteId of viteIds) {
475
+ rows.push(viteId === "vite-plugin" ? planForVitePlugin(io) : planForDevOverlay(io));
476
+ }
477
+ const planText = rows.map(rowLine).join("\n");
233
478
  io.log("Plan:");
234
479
  io.log(planText);
235
480
  if (flags.dryRun) {
@@ -244,41 +489,63 @@ async function runInstall(flags, io, prompts) {
244
489
  }
245
490
  }
246
491
  let hadFailure = false;
492
+ let viteWasWritten = false;
247
493
  for (const r of rows) {
248
494
  if (r.status === "exists") {
249
- io.log(`= ${r.client.label}: already configured (${r.path}) \u2014 use --force to overwrite.`);
495
+ const hint = isViteTargetId(r.id) ? "" : " \u2014 use --force to overwrite";
496
+ io.log(`= ${r.label}: already configured (${r.path})${hint}.`);
497
+ continue;
498
+ }
499
+ if (r.status === "manual") {
500
+ io.log(`! ${r.label}: couldn't safely modify ${r.path} \u2014 add this by hand:
501
+ ${indent(r.snippet ?? "")}`);
250
502
  continue;
251
503
  }
252
504
  try {
253
- io.writeFile(r.path, r.content);
254
- io.log(`\u2713 ${r.client.label}: ${r.status} ${r.path}`);
505
+ io.writeFile(r.path, r.content ?? "");
506
+ io.log(`\u2713 ${r.label}: ${r.status} ${r.path}`);
507
+ if (isViteTargetId(r.id)) viteWasWritten = true;
255
508
  } catch (err) {
256
509
  hadFailure = true;
257
510
  io.errorLog(`svelte-vitals: failed to write ${r.path}: ${err instanceof Error ? err.message : String(err)}`);
258
511
  }
259
512
  }
513
+ if (viteWasWritten && io.runCommand && !hasVitePackage(io)) {
514
+ const pm = detectPackageManager(io);
515
+ const { command, args } = installCommand(pm);
516
+ io.log(`Installing @svelte-vitals/vite via ${pm}...`);
517
+ const code = io.runCommand(command, args, io.cwd);
518
+ if (code !== 0) {
519
+ io.errorLog(
520
+ `svelte-vitals: failed to install @svelte-vitals/vite (${command} ${args.join(" ")} exited ${code}). Install it manually.`
521
+ );
522
+ }
523
+ }
260
524
  if (hadFailure) return 2;
261
525
  io.log("");
262
- io.log("Done. Restart your client to load the svelte-vitals MCP server.");
526
+ if (clients.length > 0) io.log("Restart your client to load the svelte-vitals MCP server.");
527
+ if (viteWasWritten) io.log("Restart `vite dev` (or your build) to pick up the change.");
528
+ io.log("Done.");
263
529
  return 0;
264
530
  }
265
531
 
266
532
  // src/install/args.ts
267
- var VALID_CLIENTS = CLIENTS.map((c) => c.id);
533
+ var VALID_TARGETS = [...CLIENTS.map((c) => c.id), ...VITE_TARGETS.map((t) => t.id)];
534
+ var EXPECTED_TARGETS = VALID_TARGETS.join("|");
268
535
  function resolveInstallArgs(argv) {
269
536
  const warnings = [];
270
537
  const errors = [];
271
538
  const rawClients = typeof argv.client === "string" ? argv.client.split(",").map((s) => s.trim()).filter(Boolean) : [];
272
539
  const client = [];
273
540
  for (const c of rawClients) {
274
- if (VALID_CLIENTS.includes(c)) {
541
+ if (VALID_TARGETS.includes(c)) {
275
542
  if (!client.includes(c)) client.push(c);
276
543
  } else {
277
- warnings.push(`svelte-vitals: unknown --client '${c}'; expected claude-code|cursor|codex. Skipping.`);
544
+ warnings.push(`svelte-vitals: unknown --client '${c}'; expected ${EXPECTED_TARGETS}. Skipping.`);
278
545
  }
279
546
  }
280
547
  if (rawClients.length > 0 && client.length === 0) {
281
- errors.push("svelte-vitals: no valid --client values; expected claude-code|cursor|codex.");
548
+ errors.push(`svelte-vitals: no valid --client values; expected ${EXPECTED_TARGETS}.`);
282
549
  }
283
550
  let scope;
284
551
  const rawScope = argv.scope;
@@ -307,7 +574,10 @@ Usage:
307
574
  svelte-vitals install [options]
308
575
 
309
576
  Options:
310
- --client <ids> Comma-separated: claude-code,cursor,codex (skips the interactive picker)
577
+ --client <ids> Comma-separated: claude-code,cursor,codex,vite-plugin,vite-dev-overlay (skips the interactive picker)
578
+ vite-plugin registers the build-mode plugin in vite.config.{ts,js,mjs}; vite-dev-overlay
579
+ wires up the dev-overlay hook in src/hooks.server.{ts,js}. --force does not apply
580
+ to either \u2014 an existing registration is always left as-is.
311
581
  --scope <scope> project | global (applies to all selected clients; codex is always global)
312
582
  --yes, -y Skip the confirmation prompt
313
583
  --dry-run Print the planned changes and exit without writing
@@ -331,15 +601,32 @@ function realIO() {
331
601
  home: homedir(),
332
602
  isTTY: Boolean(process.stdout.isTTY),
333
603
  log: (line) => console.log(line),
334
- errorLog: (line) => console.error(line)
604
+ errorLog: (line) => console.error(line),
605
+ runCommand: (command, args, cwd) => {
606
+ const result = spawnSync(command, args, {
607
+ cwd,
608
+ stdio: "inherit",
609
+ shell: process.platform === "win32",
610
+ timeout: 12e4
611
+ });
612
+ if (result.error) {
613
+ console.error(`svelte-vitals: ${command} failed to start: ${result.error.message}`);
614
+ return 1;
615
+ }
616
+ if (result.signal) {
617
+ console.error(`svelte-vitals: ${command} was terminated (${result.signal}) \u2014 it may have timed out.`);
618
+ return 1;
619
+ }
620
+ return result.status ?? 1;
621
+ }
335
622
  };
336
623
  }
337
624
  function clackPrompts() {
338
625
  return {
339
626
  selectClients: async (all, defaults) => {
340
627
  const res = await p.multiselect({
341
- message: "Which clients should svelte-vitals be installed for?",
342
- options: all.map((c) => ({ value: c.id, label: c.label })),
628
+ message: "Which clients/targets should svelte-vitals be installed for?",
629
+ options: all.map((o) => ({ value: o.id, label: o.label, hint: o.hint })),
343
630
  initialValues: defaults,
344
631
  required: true
345
632
  });
@@ -399,10 +686,14 @@ Options:
399
686
  --min-health <0-100> Fail (exit 1) when the combined Health score is below this value
400
687
  --rules <ids> Comma-separated rule ids to enable (all others disabled)
401
688
  --ignore <ids> Comma-separated rule ids to disable
689
+ --weights <pairs> Per-category Health weight overrides, e.g. seo=2,performance=1 (unlisted categories default to 1)
402
690
  --no-color Disable ANSI color in console output
403
691
  -h, --help Show this help
404
692
  -v, --version Show version
405
693
 
694
+ Config file:
695
+ svelte-vitals.config.{mjs,js,ts} in the analyzed directory; flags override it.
696
+
406
697
  Exit codes:
407
698
  0 no failing findings
408
699
  1 critical finding present (or --fail-on threshold reached)
@@ -427,7 +718,8 @@ async function main() {
427
718
  "ignore",
428
719
  "min-health",
429
720
  "out-file",
430
- "diff"
721
+ "diff",
722
+ "weights"
431
723
  ]
432
724
  });
433
725
  if (argv.help) {