@svelte-vitals/vite 0.54.6 → 0.55.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 CHANGED
@@ -34,6 +34,8 @@ export default {
34
34
  - `cwd`: project root (defaults to the Vite config root).
35
35
  - `ui`: serve the live dev dashboard (default `true`); pass `false` to keep only the build-time gate.
36
36
 
37
+ A `svelte-vitals.config.*` and a `svelte-vitals-suppressions.json` in the project root are read automatically (explicit options win over the config file). Only source-scan suppression entries apply; route-level entries are skipped on every plugin surface (rendered route findings anchor to the built HTML) and the plugin says how many.
38
+
37
39
  At build time the HTML check covers prerendered routes only; the source scan applies project-wide. Routes that are not prerendered (server-rendered, or dynamic routes without prerendered entries) have no build output, so nothing checks their rendered HTML here. A prerendered route with `ssr = false` has build output, but only the empty app shell, so the HTML check skips it too (a `+layout` with `ssr = false` skips its subtree) and prints one warning listing the skipped routes. The `svelte-vitals` CLI covers them by source analysis, and browsing them during `vite dev` gets the live dashboard a rendered reading.
38
40
 
39
41
  See [Live dashboard](https://oekazuma.github.io/svelte-vitals/guides/dev-dashboard/) and [Plugin mode](https://oekazuma.github.io/svelte-vitals/guides/plugin-mode/) for the full picture.
@@ -1,4 +1,4 @@
1
- import { RuleSetting } from "@svelte-vitals/core";
1
+ import { Config, RuleSetting } from "@svelte-vitals/core";
2
2
  import { Handle } from "@sveltejs/kit";
3
3
  //#region src/hooks/handle.d.ts
4
4
  /** Options for the dev-time SvelteKit handle. A focused subset of the plugin options. */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Category, RuleOverride, RuleSetting, Severity, TreatDynamicAs } from "@svelte-vitals/core";
1
+ import { Category, Config, RuleOverride, RuleSetting, Severity, TreatDynamicAs } from "@svelte-vitals/core";
2
2
  import { Plugin } from "vite";
3
3
  //#region src/plugin.d.ts
4
4
  interface SvelteVitalsOptions {
@@ -13,6 +13,8 @@ interface SvelteVitalsOptions {
13
13
  failOn?: Severity;
14
14
  /** Per-category weights for the combined Health score shown in the JSON/console report (flag > config file > default 1 each). */
15
15
  weights?: Partial<Record<Category, number>>;
16
+ /** SEO-wide switches (option > config file). `indexable: false` turns off the search-result rules. */
17
+ seo?: Config['seo'];
16
18
  /** Report output (default: 'console'). */
17
19
  report?: 'console' | 'json' | false;
18
20
  /** Write the JSON report to this path. */
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
5
5
  import { CATEGORIES, defineConfig } from "@svelte-vitals/core";
6
- import { ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, addFactsDirectives, allRules, buildJsonReport, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, computeScore, defaultConfig, findMinifyDisabled, formatConsoleReport, formatFailedRuleWarning, formatJsonReport, hasFailureAtOrAbove, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, runAnalysis, safeHref, selectRules, shouldSkipRangeCheck, skippedFileWarnings, summarize, terminalSafe, unknownDirectiveIds, validateRuleSetting, withFailedRulesOff } from "@svelte-vitals/core/internal";
7
- import { CONFIG_FILENAMES, analyzeProject, createNodeRuntime, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, ruleOptionsSpec } from "svelte-vitals";
6
+ import { ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, addFactsDirectives, allRules, buildJsonReport, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, computeScore, defaultConfig, findMinifyDisabled, formatConsoleReport, formatFailedRuleWarning, formatJsonReport, hasFailureAtOrAbove, isPlainObject, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, runAnalysis, safeHref, selectRules, shouldSkipRangeCheck, skippedFileWarnings, summarize, terminalSafe, unknownDirectiveIds, validateRuleSetting, withFailedRulesOff, withIndexableOff } from "@svelte-vitals/core/internal";
7
+ import { CONFIG_FILENAMES, SUPPRESSIONS_FILE, analyzeProject, applySuppressions, createNodeRuntime, findUnknownRuleIds, knownRuleIds, loadConfigFile, loadSuppressions, registryTag, ruleOptionsSpec } from "svelte-vitals";
8
8
  //#region src/providers/rendered/collect.ts
9
9
  /** Map a prerendered HTML path (relative to pages/, POSIX) to its route. */
10
10
  function deriveRouteFromHtmlPath(relPath) {
@@ -157,6 +157,29 @@ function collectSourceFiles$1(root) {
157
157
  return collectSourceFiles(nodeRuntime, root);
158
158
  }
159
159
  //#endregion
160
+ //#region src/suppressions.ts
161
+ /**
162
+ * Apply `svelte-vitals-suppressions.json` the way every plugin surface must: source-scan entries
163
+ * only. A route-level entry (route starting with `/`) is written by the CLI against
164
+ * `src/routes/…/+page.svelte`, while the build gate's rendered findings anchor to the built HTML
165
+ * and the dashboard's live layer to the route itself — the gate could never match it, and the
166
+ * dashboard's static layer would drop it only until the route was browsed and the live result
167
+ * re-surfaced it. Skipping them everywhere keeps Health identical across surfaces; the caller
168
+ * gets one line per fact to print. Stale entries are not counted: pruning stays with the CLI.
169
+ */
170
+ function applySourceSuppressions(results, entries, config) {
171
+ const notices = [];
172
+ const source = entries.filter((e) => !e.route?.startsWith("/"));
173
+ const routeLevel = entries.length - source.length;
174
+ const applied = applySuppressions(results, source, config);
175
+ if (applied.suppressed > 0) notices.push(`${applied.suppressed} finding(s) suppressed by ${SUPPRESSIONS_FILE}.`);
176
+ if (routeLevel > 0) notices.push(`${routeLevel} route-level ${routeLevel === 1 ? "entry does" : "entries do"} not apply to the plugin (${SUPPRESSIONS_FILE}) — rendered route findings anchor to the built HTML; use \`overrides\` for those.`);
177
+ return {
178
+ results: applied.results,
179
+ notices
180
+ };
181
+ }
182
+ //#endregion
160
183
  //#region src/version.ts
161
184
  /** Read this package's version from its own package.json so the report never drifts. */
162
185
  function readPackageVersion() {
@@ -197,13 +220,15 @@ function readCoreVersion() {
197
220
  function mergeConfig(options, fileConfig) {
198
221
  const weights = options.weights ?? fileConfig?.weights;
199
222
  const overrides = options.overrides ?? fileConfig?.overrides;
223
+ const seo = options.seo ?? fileConfig?.seo;
200
224
  return defineConfig({
201
225
  treatDynamicAs: options.treatDynamicAs ?? fileConfig?.treatDynamicAs ?? "pass",
202
226
  metaComponents: options.metaComponents ?? fileConfig?.metaComponents ?? [],
203
- rules: options.rules ?? fileConfig?.rules ?? {},
227
+ rules: withIndexableOff(options.rules ?? fileConfig?.rules, seo) ?? {},
204
228
  failOn: options.failOn ?? fileConfig?.failOn ?? "critical",
205
229
  ...weights !== void 0 ? { weights } : {},
206
- ...overrides !== void 0 ? { overrides } : {}
230
+ ...overrides !== void 0 ? { overrides } : {},
231
+ ...seo !== void 0 ? { seo } : {}
207
232
  });
208
233
  }
209
234
  /**
@@ -261,11 +286,13 @@ function ssrDisabledRouteMatcher(kitModules) {
261
286
  /**
262
287
  * Collect prerendered heads + project facts + component facts, run the core pipeline, and
263
288
  * format reports. Config precedence: see `resolveConfig`. Pass `resolved` when the caller
264
- * already resolved config itself (build mode resolves it outside `analyze`'s try/catch so a
265
- * config-file validation error fails the build instead of being caught as an analysis error).
289
+ * already resolved config and loaded `svelte-vitals-suppressions.json` itself (build mode does
290
+ * both outside `analyze`'s try/catch so a validation error fails the build instead of being
291
+ * caught as an analysis error); `suppressions: undefined` means the file does not exist.
266
292
  */
267
293
  async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resolved) {
268
294
  const { config, warnings } = resolved ?? await resolveConfig(cwd, options);
295
+ const suppressions = resolved ? resolved.suppressions : loadSuppressions(cwd);
269
296
  const [rendered, components, sourceFiles] = await Promise.all([
270
297
  collectRenderedHeads(prerenderPagesDir),
271
298
  collectComponentFacts$1(cwd),
@@ -296,7 +323,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
296
323
  kitModules,
297
324
  viteMinifyDisabled: project.viteMinifyDisabled
298
325
  });
299
- const { results, examined, failedRules, scoringConfig } = await runAnalysis(selected, {
326
+ const analysis = await runAnalysis(selected, {
300
327
  heads,
301
328
  headings,
302
329
  images,
@@ -307,6 +334,13 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
307
334
  kitModules,
308
335
  sourceFiles
309
336
  }, directives);
337
+ const { examined, failedRules, scoringConfig } = analysis;
338
+ let results = analysis.results;
339
+ if (suppressions !== void 0) {
340
+ const applied = applySourceSuppressions(results, suppressions, config);
341
+ results = applied.results;
342
+ warnings.push(...applied.notices);
343
+ }
310
344
  warnings.push(...skippedFileWarnings([...components, ...kitModules]));
311
345
  warnings.push(...unknownDirectiveIds(directives, allRules));
312
346
  for (const f of failedRules) warnings.push(formatFailedRuleWarning(f));
@@ -315,12 +349,14 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
315
349
  const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
316
350
  const coverageNote = `Analyzed ${heads.length} prerendered route(s)${shells.length > 0 ? ` (skipped ${shells.length} with ssr = false)` : ""}. SSR/dynamic routes are not covered — run \`npx svelte-vitals\` for those.
317
351
  Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Accessibility/Bundle findings.`;
352
+ const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
353
+ const jsonReport = formatJsonReport(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
318
354
  return {
319
355
  score,
320
356
  summary,
321
357
  results,
322
- consoleReport: formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n",
323
- jsonReport: formatJsonReport(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined),
358
+ consoleReport,
359
+ jsonReport,
324
360
  routeCount: rendered.heads.length,
325
361
  failed,
326
362
  failOn: scoringConfig.failOn,
@@ -635,6 +671,26 @@ function createStore() {
635
671
  //#endregion
636
672
  //#region src/ui/analysis.ts
637
673
  /**
674
+ * Apply `svelte-vitals-suppressions.json` to the whole-project layer, same entries as the build
675
+ * gate (see `applySourceSuppressions`). A malformed file is a warning here, not an error: dev
676
+ * must keep serving. Returns the results unchanged when the file is absent or `config` is
677
+ * unknown (an injected `analyze` that returns no config).
678
+ */
679
+ function suppress(root, results, config, warnings) {
680
+ if (!config) return results;
681
+ let entries;
682
+ try {
683
+ entries = loadSuppressions(root);
684
+ } catch (err) {
685
+ warnings.push(`${err instanceof Error ? err.message : String(err)} — ${SUPPRESSIONS_FILE} ignored.`);
686
+ return results;
687
+ }
688
+ if (entries === void 0) return results;
689
+ const applied = applySourceSuppressions(results, entries, config);
690
+ warnings.push(...applied.notices);
691
+ return applied.results;
692
+ }
693
+ /**
638
694
  * Owns the dev-dashboard's whole-project analysis lifecycle: an async run at startup,
639
695
  * a debounced re-run on source changes, and coalescing so a change that arrives mid-run
640
696
  * produces exactly one follow-up run rather than one per change (design doc §1).
@@ -659,14 +715,16 @@ function createAnalysisRunner(opts) {
659
715
  metaComponents: opts.metaComponents,
660
716
  rules: opts.rules,
661
717
  failOn: opts.failOn,
718
+ seo: opts.seo,
662
719
  parseCache
663
720
  });
664
- const { results, failedRuleIds } = result;
721
+ const { failedRuleIds } = result;
722
+ const warnings = [...result.warnings ?? []];
723
+ const results = suppress(opts.root, result.results, result.config, warnings);
665
724
  if (!stopped) {
666
725
  if (failedRuleIds !== void 0) opts.onResults(results, failedRuleIds);
667
726
  else opts.onResults(results);
668
727
  }
669
- const warnings = result.warnings ?? [];
670
728
  const key = warnings.join("\n");
671
729
  if (!stopped && warnings.length > 0 && key !== lastWarningsKey) opts.onWarnings?.(warnings);
672
730
  lastWarningsKey = key;
@@ -716,6 +774,7 @@ const CONFIG_BASENAMES = /* @__PURE__ */ new Set([
716
774
  "svelte.config.js",
717
775
  "svelte.config.ts",
718
776
  ...CONFIG_FILENAMES,
777
+ SUPPRESSIONS_FILE,
719
778
  "vite.config.js",
720
779
  "vite.config.mjs",
721
780
  "vite.config.ts",
@@ -819,10 +878,23 @@ function validateOverridesOption(overrides, rules) {
819
878
  });
820
879
  if (errors.length > 0) throw new Error(`svelte-vitals: invalid \`overrides\` option — ${errors.join(" ")}`);
821
880
  }
881
+ /**
882
+ * Validate the plugin's `seo` option the same way the config-file loader validates the
883
+ * `seo` key. A `vite.config.js` gets no type checking, and `{ indexable: 'no' }` would
884
+ * otherwise be silently inert — the failure mode `validateRulesOption` exists for.
885
+ */
886
+ function validateSeoOption(seo) {
887
+ if (seo === void 0) return;
888
+ if (!isPlainObject(seo)) throw new Error("svelte-vitals: invalid `seo` option — must be an object, e.g. { indexable: false }.");
889
+ const unknown = Object.keys(seo).filter((k) => k !== "indexable");
890
+ if (unknown.length > 0) throw new Error(`svelte-vitals: invalid \`seo\` option — unknown key(s): ${unknown.join(", ")}. Known keys: indexable.`);
891
+ if (seo.indexable !== void 0 && typeof seo.indexable !== "boolean") throw new Error("svelte-vitals: invalid `seo` option — seo.indexable must be a boolean.");
892
+ }
822
893
  /** svelte-vitals Vite/SvelteKit plugin. */
823
894
  function svelteVitals(options = {}) {
824
895
  validateRulesOption(options.rules);
825
896
  validateOverridesOption(options.overrides, options.rules);
897
+ validateSeoOption(options.seo);
826
898
  let root = options.cwd ?? process.cwd();
827
899
  let minify;
828
900
  let configFile;
@@ -841,7 +913,10 @@ function svelteVitals(options = {}) {
841
913
  const pagesDir = options.prerenderDir ? options.prerenderDir : join(root, DEFAULT_PRERENDER_DIR);
842
914
  const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
843
915
  if (!existsSync(resolved)) return;
844
- const resolvedConfig = await resolveConfig(root, options);
916
+ const resolvedConfig = {
917
+ ...await resolveConfig(root, options),
918
+ suppressions: loadSuppressions(root)
919
+ };
845
920
  let result;
846
921
  try {
847
922
  const viteMinifyDisabled = await resolveMinifyDisabled(minify, configFile, root);
@@ -867,7 +942,7 @@ function svelteVitals(options = {}) {
867
942
  if (options.ui === false) return buildPlugin;
868
943
  return [buildPlugin, {
869
944
  name: "svelte-vitals:ui",
870
- apply: "serve",
945
+ apply: (_config, env) => env.command === "serve" && !process.env.VITEST,
871
946
  async configureServer(server) {
872
947
  process.env.SVELTE_VITALS_UI = "1";
873
948
  const uiRoot = options.cwd ?? server.config.root;
@@ -897,6 +972,7 @@ function svelteVitals(options = {}) {
897
972
  metaComponents: options.metaComponents,
898
973
  rules: options.rules,
899
974
  failOn: options.failOn,
975
+ seo: options.seo,
900
976
  onResults: (results, failedRuleIds) => {
901
977
  store.setStatic(results);
902
978
  staticFailedRuleIds = failedRuleIds ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/vite",
3
- "version": "0.54.6",
3
+ "version": "0.55.0",
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",
@@ -43,9 +43,9 @@
43
43
  "node-html-parser": "^9.0.4"
44
44
  },
45
45
  "peerDependencies": {
46
- "@svelte-vitals/core": "^0.54.6",
46
+ "@svelte-vitals/core": "^0.55.0",
47
47
  "@sveltejs/kit": "^2.70.3",
48
- "svelte-vitals": "^0.54.6",
48
+ "svelte-vitals": "^0.55.0",
49
49
  "vite": "^8.3.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
@@ -54,12 +54,12 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
- "@svelte-vitals/core": "0.54.6",
57
+ "@svelte-vitals/core": "0.55.0",
58
58
  "@sveltejs/kit": "^2.70.3",
59
- "@types/node": "^24.13.4",
60
- "happy-dom": "^20.14.3",
59
+ "@types/node": "^24.13.5",
60
+ "happy-dom": "^20.14.5",
61
61
  "svelte": "^5.57.0",
62
- "svelte-vitals": "0.54.6",
62
+ "svelte-vitals": "0.55.0",
63
63
  "vite": "^8.3.0"
64
64
  },
65
65
  "scripts": {