@svelte-vitals/vite 0.29.5 → 0.29.7

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.
@@ -130,12 +130,8 @@ function isLoopbackOrigin(origin) {
130
130
  return false;
131
131
  }
132
132
  }
133
- function isLoopbackHost(host) {
134
- return host !== void 0 && isLoopbackOrigin(`http://${host}`);
135
- }
136
133
 
137
134
  export {
138
135
  parseHtmlHead,
139
- isLoopbackOrigin,
140
- isLoopbackHost
136
+ isLoopbackOrigin
141
137
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  isLoopbackOrigin,
3
3
  parseHtmlHead
4
- } from "../chunk-2LDMPITM.js";
4
+ } from "../chunk-EHCC47NO.js";
5
5
 
6
6
  // src/hooks/handle.ts
7
7
  import { DEV } from "esm-env";
@@ -10,17 +10,20 @@ import {
10
10
  applyRuleSeverities,
11
11
  defineConfig,
12
12
  effectiveSeverity,
13
+ formatFailedRuleWarning,
13
14
  isPenalized,
14
15
  runRules,
15
- selectRules
16
+ selectRules,
17
+ terminalSafe
16
18
  } from "@svelte-vitals/core";
17
19
  function findingSignature(results, config) {
18
20
  return results.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => `${r.id}:${effectiveSeverity(r, config)}:${r.detection.presence}:${r.detection.value}`).sort().join("|");
19
21
  }
20
- async function postIngest(origin, route, results) {
22
+ var warn = (line) => console.warn(terminalSafe(line));
23
+ async function postIngest(origin, route, results, failedRuleIds) {
21
24
  if (!isLoopbackOrigin(origin)) {
22
25
  if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) {
23
- console.warn(
26
+ warn(
24
27
  `[svelte-vitals] live UI ingest skipped for non-loopback origin ${origin} \u2014 open the dashboard via localhost`
25
28
  );
26
29
  }
@@ -30,7 +33,9 @@ async function postIngest(origin, route, results) {
30
33
  await fetch(`${origin}/__svelte-vitals/ingest`, {
31
34
  method: "POST",
32
35
  headers: { "content-type": "application/json" },
33
- body: JSON.stringify({ route, results })
36
+ // failedRuleIds is always sent, empty array included, so a route that recovers from
37
+ // a previously-crashing rule clears its stale entry on the receiving store.
38
+ body: JSON.stringify({ route, results, failedRuleIds })
34
39
  });
35
40
  } catch {
36
41
  }
@@ -44,15 +49,25 @@ async function analyzeAndIngest(html, route, origin, rules, config, lastSignatur
44
49
  ];
45
50
  const images = [{ route, images: imgs.map((img) => ({ ...img, file: route })) }];
46
51
  const project = { hasRobotsTxt: true, hasSitemap: true, htmlLang };
47
- const { results: ruleResults } = await runRules(rules, { heads: [head], headings, images, project, config });
52
+ const { results: ruleResults, failedRules } = await runRules(rules, {
53
+ heads: [head],
54
+ headings,
55
+ images,
56
+ project,
57
+ config
58
+ });
48
59
  const results = applyRuleSeverities(ruleResults, config);
49
- const signature = findingSignature(results, config);
60
+ const failedRuleIds = failedRules.map((f) => f.id);
61
+ if (failedRules.length > 0 && globalThis.process?.env?.SVELTE_VITALS_DEBUG) {
62
+ for (const f of failedRules) warn(formatFailedRuleWarning(f));
63
+ }
64
+ const signature = `${findingSignature(results, config)}|failed:${[...failedRuleIds].sort().join(",")}`;
50
65
  if (lastSignature.get(route) === signature) return;
51
66
  lastSignature.set(route, signature);
52
- if (globalThis.process?.env?.SVELTE_VITALS_UI) void postIngest(origin, route, results);
67
+ if (globalThis.process?.env?.SVELTE_VITALS_UI) void postIngest(origin, route, results, failedRuleIds);
53
68
  } catch (err) {
54
69
  if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) {
55
- console.warn("[svelte-vitals] dev analysis failed:", err);
70
+ warn(`[svelte-vitals] dev analysis failed: ${err instanceof Error ? err.message : String(err)}`);
56
71
  }
57
72
  }
58
73
  }
package/dist/index.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import {
2
- isLoopbackHost,
3
2
  isLoopbackOrigin,
4
3
  parseHtmlHead
5
- } from "./chunk-2LDMPITM.js";
4
+ } from "./chunk-EHCC47NO.js";
6
5
 
7
6
  // src/plugin.ts
8
7
  import { existsSync } from "fs";
@@ -13,6 +12,7 @@ import {
13
12
  defaultConfig,
14
13
  resolveRuleOptions,
15
14
  shouldSkipRangeCheck,
15
+ terminalSafe,
16
16
  validateRuleSetting
17
17
  } from "@svelte-vitals/core";
18
18
  import { findUnknownRuleIds, knownRuleIds, ruleOptionsSpec } from "svelte-vitals";
@@ -24,6 +24,8 @@ import {
24
24
  applyRuleSeverities,
25
25
  applyOverrides,
26
26
  runRules,
27
+ withFailedRulesOff,
28
+ formatFailedRuleWarning,
27
29
  computeScore,
28
30
  summarize,
29
31
  hasFailureAtOrAbove,
@@ -216,7 +218,11 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
216
218
  };
217
219
  const kitModules = await collectKitModuleFacts(cwd, project.kitAliases);
218
220
  const selected = selectRules(allRules, config);
219
- const { results: rawResults, examined } = await runRules(selected, {
221
+ const {
222
+ results: rawResults,
223
+ examined,
224
+ failedRules
225
+ } = await runRules(selected, {
220
226
  heads,
221
227
  headings,
222
228
  images,
@@ -227,15 +233,20 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
227
233
  sourceFiles
228
234
  });
229
235
  const results = applyOverrides(applyRuleSeverities(rawResults, config), config);
230
- const { score } = computeScore(results, config);
231
- const summary = summarize(results, config);
232
- const failed = hasFailureAtOrAbove(summary, config.failOn);
236
+ for (const f of failedRules) warnings.push(formatFailedRuleWarning(f));
237
+ const scoringConfig = withFailedRulesOff(
238
+ config,
239
+ failedRules.map((f) => f.id)
240
+ );
241
+ const { score } = computeScore(results, scoringConfig);
242
+ const summary = summarize(results, scoringConfig);
243
+ const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
233
244
  const coverageNote = `Analyzed ${heads.length} prerendered route(s). SSR/dynamic routes are not covered \u2014 run \`npx svelte-vitals\` for those.
234
245
  Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Bundle findings.`;
235
- const consoleReport = formatConsoleReport(results, config, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
246
+ const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
236
247
  const jsonReport = formatJsonReport(
237
248
  results,
238
- config,
249
+ scoringConfig,
239
250
  { version: readPackageVersion() },
240
251
  selected.map((r) => r.id),
241
252
  examined
@@ -248,7 +259,7 @@ Scanned ${components.length} component(s) under src/ for Correctness/Security/Ar
248
259
  jsonReport,
249
260
  routeCount: heads.length,
250
261
  failed,
251
- failOn: config.failOn,
262
+ failOn: scoringConfig.failOn,
252
263
  warnings
253
264
  };
254
265
  }
@@ -274,7 +285,11 @@ async function resolveMinifyDisabled(minify, configFile, root) {
274
285
  import { CATEGORIES, renderAppShell } from "@svelte-vitals/core";
275
286
 
276
287
  // src/ui/snapshot.ts
277
- import { buildJsonReport, safeHref } from "@svelte-vitals/core";
288
+ import {
289
+ buildJsonReport,
290
+ safeHref,
291
+ withFailedRulesOff as withFailedRulesOff2
292
+ } from "@svelte-vitals/core";
278
293
  function sanitizeDocsUrl(issue) {
279
294
  if (issue.docsUrl === void 0) return issue;
280
295
  if (safeHref(issue.docsUrl) !== null) return issue;
@@ -287,12 +302,14 @@ function sanitizeReport(report) {
287
302
  siteIssues: report.siteIssues.map(sanitizeDocsUrl)
288
303
  };
289
304
  }
290
- function buildSnapshot(store, config, meta) {
305
+ function buildSnapshot(store, config, meta, staticFailedRuleIds) {
306
+ const failedRuleIds = [.../* @__PURE__ */ new Set([...staticFailedRuleIds ?? [], ...store.failedRuleIds()])];
307
+ const scoringConfig = withFailedRulesOff2(config, failedRuleIds);
291
308
  return {
292
309
  // No rule-id list threaded through: `report.rules` is seeded from `store.snapshot()`
293
310
  // alone here, so presence means "produced a result", not "was selected" — unlike the
294
311
  // `json` reporter (design doc 2026-08-03-json-rule-evidence-design.md, Not in scope).
295
- report: sanitizeReport(buildJsonReport(store.snapshot(), config, meta)),
312
+ report: sanitizeReport(buildJsonReport(store.snapshot(), scoringConfig, meta)),
296
313
  badges: store.badges(),
297
314
  analyzing: store.isAnalyzing(),
298
315
  sequence: store.sequence(),
@@ -314,7 +331,7 @@ function isResultLike(x) {
314
331
  const f = r.fix;
315
332
  return typeof r.id === "string" && typeof r.message === "string" && typeof r.severity === "string" && SEVERITIES.has(r.severity) && typeof d === "object" && d !== null && typeof d.presence === "string" && typeof d.value === "string" && (r.category === void 0 || typeof r.category === "string" && CATEGORY_SET.has(r.category)) && isOptionalString(r.location) && isOptionalString(r.recommendation) && isOptionalString(r.docsUrl) && (f === void 0 || typeof f === "object" && f !== null && typeof f.description === "string" && isOptionalString(f.snippet) && isOptionalString(f.lang)) && (r.line === void 0 || typeof r.line === "number") && isOptionalString(r.route);
316
333
  }
317
- function installUiMiddleware(server, config, version, store, coreVersion) {
334
+ function installUiMiddleware(server, config, version, store, coreVersion, getStaticFailedRuleIds) {
318
335
  const clients = /* @__PURE__ */ new Set();
319
336
  store.subscribe(() => {
320
337
  for (const res of clients) {
@@ -333,7 +350,7 @@ function installUiMiddleware(server, config, version, store, coreVersion) {
333
350
  const url = req.url ?? "/";
334
351
  const origin = req.headers.origin;
335
352
  const host = req.headers.host;
336
- if (typeof origin === "string" && !isLoopbackOrigin(origin) || !isLoopbackHost(host)) {
353
+ if (typeof origin === "string" && !isLoopbackOrigin(origin) || host === void 0 || !isLoopbackOrigin(`http://${host}`)) {
337
354
  req.resume();
338
355
  res.statusCode = 403;
339
356
  res.end("svelte-vitals dev UI is only available from localhost");
@@ -344,9 +361,10 @@ function installUiMiddleware(server, config, version, store, coreVersion) {
344
361
  req.on("data", (c) => chunks.push(c));
345
362
  req.on("end", () => {
346
363
  try {
347
- const { route, results } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
364
+ const { route, results, failedRuleIds } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
348
365
  if (typeof route === "string" && Array.isArray(results)) {
349
- store.set(route, results.filter(isResultLike));
366
+ const failedIds = Array.isArray(failedRuleIds) ? failedRuleIds.filter((id) => typeof id === "string") : [];
367
+ store.set(route, results.filter(isResultLike), failedIds);
350
368
  }
351
369
  } catch {
352
370
  }
@@ -368,7 +386,7 @@ function installUiMiddleware(server, config, version, store, coreVersion) {
368
386
  }
369
387
  if (url.startsWith("/data.json")) {
370
388
  try {
371
- const snapshot = buildSnapshot(store, config, { version, coreVersion });
389
+ const snapshot = buildSnapshot(store, config, { version, coreVersion }, getStaticFailedRuleIds?.());
372
390
  res.setHeader("Content-Type", "application/json");
373
391
  res.end(JSON.stringify(snapshot));
374
392
  } catch {
@@ -378,7 +396,7 @@ function installUiMiddleware(server, config, version, store, coreVersion) {
378
396
  return;
379
397
  }
380
398
  try {
381
- const html = renderAppShell(buildSnapshot(store, config, { version, coreVersion }));
399
+ const html = renderAppShell(buildSnapshot(store, config, { version, coreVersion }, getStaticFailedRuleIds?.()));
382
400
  res.setHeader("Content-Type", "text/html");
383
401
  res.end(html);
384
402
  } catch {
@@ -422,6 +440,7 @@ function composeBadges(staticResults, liveByRoute) {
422
440
  function createStore() {
423
441
  let staticResults = [];
424
442
  const liveByRoute = /* @__PURE__ */ new Map();
443
+ const liveFailedByRoute = /* @__PURE__ */ new Map();
425
444
  const subs = /* @__PURE__ */ new Set();
426
445
  let analyzing = false;
427
446
  let seq = 0;
@@ -430,11 +449,13 @@ function createStore() {
430
449
  for (const fn of subs) fn();
431
450
  }
432
451
  return {
433
- set(route, results) {
452
+ set(route, results, failedRuleIds) {
434
453
  liveByRoute.set(
435
454
  route,
436
455
  results.map((r) => r.route ? r : { ...r, route })
437
456
  );
457
+ if (failedRuleIds && failedRuleIds.length > 0) liveFailedByRoute.set(route, failedRuleIds);
458
+ else liveFailedByRoute.delete(route);
438
459
  notify();
439
460
  },
440
461
  setStatic(results) {
@@ -454,6 +475,9 @@ function createStore() {
454
475
  badges() {
455
476
  return composeBadges(staticResults, liveByRoute);
456
477
  },
478
+ failedRuleIds() {
479
+ return [...new Set([...liveFailedByRoute.values()].flat())].sort();
480
+ },
457
481
  sequence() {
458
482
  return seq;
459
483
  },
@@ -466,28 +490,21 @@ function createStore() {
466
490
 
467
491
  // src/ui/analysis.ts
468
492
  import { relative as relative2, sep } from "path";
493
+ import { analyzeProject } from "svelte-vitals";
469
494
  function createAnalysisRunner(opts) {
470
495
  const debounceMs = opts.debounceMs ?? 500;
471
- let cachedAnalyze = opts.analyze;
496
+ const analyze2 = opts.analyze ?? analyzeProject;
472
497
  const parseCache = /* @__PURE__ */ new Map();
473
498
  let stopped = false;
474
499
  let running = false;
475
500
  let pending = false;
476
501
  let timer;
477
- async function getAnalyze() {
478
- if (cachedAnalyze) return cachedAnalyze;
479
- const mod = await import("svelte-vitals");
480
- const fn = (o) => mod.analyzeProject(o);
481
- cachedAnalyze = fn;
482
- return fn;
483
- }
484
502
  async function runOnce() {
485
503
  if (stopped) return;
486
504
  running = true;
487
505
  opts.onStatusChange?.(true);
488
506
  try {
489
- const analyze2 = await getAnalyze();
490
- const { results } = await analyze2({
507
+ const { results, failedRuleIds } = await analyze2({
491
508
  cwd: opts.root,
492
509
  treatDynamicAs: opts.treatDynamicAs,
493
510
  metaComponents: opts.metaComponents,
@@ -495,7 +512,10 @@ function createAnalysisRunner(opts) {
495
512
  failOn: opts.failOn,
496
513
  parseCache
497
514
  });
498
- if (!stopped) opts.onResults(results);
515
+ if (!stopped) {
516
+ if (failedRuleIds !== void 0) opts.onResults(results, failedRuleIds);
517
+ else opts.onResults(results);
518
+ }
499
519
  } catch (err) {
500
520
  if (!stopped) opts.onError(err);
501
521
  } finally {
@@ -550,6 +570,7 @@ var CONFIG_BASENAMES = /* @__PURE__ */ new Set([
550
570
  "vite.config.cts"
551
571
  ]);
552
572
  var IGNORED_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", ".svelte-kit", "build", "dist"]);
573
+ var warn = (line) => console.warn(terminalSafe(line));
553
574
  function isRelevant(file, root) {
554
575
  const rel = relative3(root, file);
555
576
  if (rel === "" || rel.startsWith("..")) return false;
@@ -630,10 +651,10 @@ function svelteVitals(options = {}) {
630
651
  resolvedConfig
631
652
  );
632
653
  } catch (err) {
633
- console.warn(`svelte-vitals: skipped \u2014 analysis failed: ${err instanceof Error ? err.message : String(err)}`);
654
+ warn(`svelte-vitals: skipped \u2014 analysis failed: ${err instanceof Error ? err.message : String(err)}`);
634
655
  return;
635
656
  }
636
- for (const w of result.warnings) console.warn(`svelte-vitals: ${w}`);
657
+ for (const w of result.warnings) warn(`svelte-vitals: ${w}`);
637
658
  if (result.routeCount === 0) return;
638
659
  if (options.report !== false) {
639
660
  const out = options.report === "json" ? result.jsonReport : result.consoleReport;
@@ -661,22 +682,26 @@ function svelteVitals(options = {}) {
661
682
  try {
662
683
  ({ config, warnings } = await resolveConfig(uiRoot, options));
663
684
  } catch (err) {
664
- console.warn(
685
+ warn(
665
686
  `svelte-vitals: config file invalid \u2014 dashboard using plugin options/defaults: ${err instanceof Error ? err.message : String(err)}`
666
687
  );
667
688
  config = mergeConfig(options, void 0);
668
689
  warnings = [];
669
690
  }
670
- for (const w of warnings) console.warn(`svelte-vitals: ${w}`);
691
+ for (const w of warnings) warn(`svelte-vitals: ${w}`);
671
692
  const store = createStore();
693
+ let staticFailedRuleIds = [];
672
694
  const runner = createAnalysisRunner({
673
695
  root: uiRoot,
674
696
  treatDynamicAs: options.treatDynamicAs,
675
697
  metaComponents: options.metaComponents,
676
698
  rules: options.rules,
677
699
  failOn: options.failOn,
678
- onResults: (results) => store.setStatic(results),
679
- onError: (err) => console.warn("[svelte-vitals] dev analysis failed:", err),
700
+ onResults: (results, failedRuleIds) => {
701
+ store.setStatic(results);
702
+ staticFailedRuleIds = failedRuleIds ?? [];
703
+ },
704
+ onError: (err) => warn(`[svelte-vitals] dev analysis failed: ${err instanceof Error ? err.message : String(err)}`),
680
705
  onStatusChange: (analyzing) => store.setAnalyzing(analyzing)
681
706
  });
682
707
  runner.start();
@@ -687,7 +712,7 @@ function svelteVitals(options = {}) {
687
712
  delete process.env.SVELTE_VITALS_UI;
688
713
  runner.stop();
689
714
  });
690
- installUiMiddleware(server, config, readPackageVersion(), store, readCoreVersion());
715
+ installUiMiddleware(server, config, readPackageVersion(), store, readCoreVersion(), () => staticFailedRuleIds);
691
716
  if (typeof server.printUrls === "function") {
692
717
  const printUrls = server.printUrls.bind(server);
693
718
  server.printUrls = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/vite",
3
- "version": "0.29.5",
3
+ "version": "0.29.7",
4
4
  "description": "Vite/SvelteKit plugin for svelte-vitals — analyzes prerendered HTML during vite build, plus a live dev dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,8 +42,8 @@
42
42
  "esm-env": "^1.2.2",
43
43
  "node-html-parser": "^9.0.1",
44
44
  "tinyglobby": "^0.2.17",
45
- "@svelte-vitals/core": "0.41.0",
46
- "svelte-vitals": "0.45.0"
45
+ "@svelte-vitals/core": "0.42.0",
46
+ "svelte-vitals": "0.46.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@sveltejs/kit": "^2.70.2",