@svelte-vitals/vite 0.55.2 → 0.55.3

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
@@ -30,13 +30,13 @@ export default {
30
30
  - `outFile`: write the JSON report to a path.
31
31
  - `rules` / `metaComponents` / `treatDynamicAs`: same as the CLI/core config.
32
32
  - `weights`: per-category weight overrides for the combined Health score.
33
- - `prerenderDir`: override the prerendered-pages directory.
33
+ - `prerenderDir`: override the prerendered-pages directory (default: `output/prerendered/pages` under `kit.outDir`).
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
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
38
 
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. When the build prerenders nothing at all (an SPA served from a fallback page, say), the plugin prints one warning that route analysis was skipped and still runs and gates the source scan; `seo/html-lang`, which reads the rendered `<html>`, is skipped with it.
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 `kit.router.type: 'hash'` every route) 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. When the build prerenders nothing at all (an SPA served from a fallback page, say), the plugin prints one warning that route analysis was skipped and still runs and gates the source scan; `seo/html-lang`, which reads the rendered `<html>`, is skipped with it.
40
40
 
41
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.
42
42
 
@@ -1,4 +1,4 @@
1
- import { i as toOccurrenceMap, r as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-CAFo1WZs.js";
1
+ import { i as toOccurrenceMap, r as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-DxxU70UA.js";
2
2
  import { defineConfig } from "@svelte-vitals/core";
3
3
  import { allRules, defaultProject, effectiveSeverity, formatFailedRuleWarning, isPenalized, runAnalysis, selectRules, terminalSafe } from "@svelte-vitals/core/internal";
4
4
  import { DEV } from "esm-env";
@@ -49,8 +49,8 @@ async function analyzeAndIngest(html, route, origin, rules, config, queued, infl
49
49
  };
50
50
  const headings = [{
51
51
  route,
52
- headings: levels.map((level) => ({
53
- level,
52
+ headings: levels.map((h) => ({
53
+ ...h,
54
54
  line: 0,
55
55
  file: route
56
56
  }))
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ interface SvelteVitalsOptions {
19
19
  report?: 'console' | 'json' | false;
20
20
  /** Write the JSON report to this path. */
21
21
  outFile?: string;
22
- /** Override the prerendered-pages directory (default: .svelte-kit/output/prerendered/pages). */
22
+ /** Override the prerendered-pages directory (default: `output/prerendered/pages` under SvelteKit's `kit.outDir`). */
23
23
  prerenderDir?: string;
24
24
  /**
25
25
  * Serve a live dashboard at /__svelte-vitals/ during `vite dev` (add
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
- import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-CAFo1WZs.js";
1
+ import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-DxxU70UA.js";
2
+ import { once } from "node:events";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
- import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
6
  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, isPlainObject, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, runAnalysis, safeHref, selectRules, servesRootFile, shouldSkipRangeCheck, skippedFileWarnings, summarize, terminalSafe, unknownDirectiveIds, validateRuleSetting, withFailedRulesOff, withIndexableOff, withPackageImports, withWorkspacePackages } from "@svelte-vitals/core/internal";
7
+ import { ROBOTS_SOURCE_PATHS, SITEMAP_SOURCE_PATHS, SVELTE_CONFIG_FILES, VITE_CONFIG_FILES, addFactsDirectives, allRules, buildJsonReport, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, computeScore, defaultConfig, findKitHashRouterInSvelteConfig, findMinifyDisabled, formatConsoleReport, formatFailedRuleWarning, formatJsonReportChunks, hasFailureAtOrAbove, isPlainObject, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, routeNeverSsr, runAnalysis, safeHref, selectRules, servesRootFile, shouldSkipRangeCheck, skippedFileWarnings, summarize, terminalSafe, unknownDirectiveIds, validateRuleSetting, withFailedRulesOff, withIndexableOff, withPackageImports, withWorkspacePackages } from "@svelte-vitals/core/internal";
7
8
  import { CONFIG_FILENAMES, SUPPRESSIONS_FILE, analyzeProject, applySuppressions, createNodeRuntime, dropConstantListEachBlocks, findUnknownRuleIds, findWorkspacePackages, knownRuleIds, loadConfigFile, loadSuppressions, registryTag, ruleOptionsSpec } from "svelte-vitals";
8
9
  //#region src/providers/rendered/collect.ts
9
10
  /** Map a prerendered HTML path (relative to pages/, POSIX) to its route. */
@@ -46,8 +47,8 @@ async function collectRenderedHeads(prerenderPagesDir) {
46
47
  });
47
48
  headings.push({
48
49
  route,
49
- headings: parsed.headings.map((level) => ({
50
- level,
50
+ headings: parsed.headings.map((h) => ({
51
+ ...h,
51
52
  line: 0,
52
53
  file: rel
53
54
  }))
@@ -150,6 +151,7 @@ async function collectRenderedProject(cwd, htmlLang) {
150
151
  htmlLang,
151
152
  ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
152
153
  ...kitPathsBase ? { kitPathsBase } : {},
154
+ ...svelteConfig && findKitHashRouterInSvelteConfig(svelteConfig.source) ? { kitHashRouter: true } : {},
153
155
  ...kitAliases ? { kitAliases } : {},
154
156
  ...componentAliases !== kitAliases ? { componentAliases } : {}
155
157
  };
@@ -272,11 +274,6 @@ async function resolveConfig(cwd, options) {
272
274
  warnings: loaded?.warnings ?? []
273
275
  };
274
276
  }
275
- /**
276
- * `ssr` is a page option: only `+page`/`+layout` modules (universal or server) can set it. Same
277
- * file set as `PAGE_OPTION_FILE_RE` in core's `rules/seo/ssr-disabled.ts`, which reports the fact.
278
- */
279
- const PAGE_OPTION_FILE_RE = /^src\/routes\/(.*?)\/?\+(page|layout)(?:\.server)?\.[jt]s$/;
280
277
  function escapeRegExp(s) {
281
278
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
282
279
  }
@@ -286,27 +283,54 @@ function segmentPattern(segment) {
286
283
  if (/^\[\[[^\]]+\]\]$/.test(segment)) return "(?:/[^/]+)?";
287
284
  return "/" + segment.split(/\[[^\]]+\]/).map(escapeRegExp).join("[^/]+");
288
285
  }
286
+ /** A route's page component, with an optional `@segment` layout reset. */
287
+ const PAGE_FILE_RE = /^src\/routes\/(.*?)\/?\+page(?:@[^/]*)?\.svelte$/;
288
+ /** A module that can set the `ssr` page option (same file set as core's `seo/ssr-disabled`). */
289
+ const PAGE_OPTION_FILE_RE = /^src\/routes\/(.*?)\/?\+(page|layout)(?:\.server)?\.[jt]s$/;
290
+ /** A route directory as a pattern over rendered paths: `(group)` directories are not URL segments. */
291
+ function routePattern(dir, subtree) {
292
+ const path = dir.split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
293
+ return new RegExp(subtree ? `^${path}(?:/.*)?$` : `^${path}$`);
294
+ }
289
295
  /**
290
296
  * Routes whose prerendered HTML is the app shell rather than the page: SvelteKit still writes a
291
297
  * file for an `ssr = false` route, but its `<head>` and body only exist once the client renders,
292
- * so every rendered head/heading/landmark check would report the page as empty. A `+layout`
293
- * covers its whole subtree, a `+page` only its own route; `(group)` directories are not URL
294
- * segments. Two ceilings: a child route re-enabling `ssr = true` under a disabled layout is not
295
- * tracked, and Kit's route precedence is not modelled, so a `[param]` route also claims a literal
296
- * sibling (`/docs/[slug]` matches a prerendered `/docs/intro`) — the warning lists the skipped
297
- * routes so a wrong skip is visible.
298
+ * so every rendered head/heading/landmark check would report the page as empty. Each page takes the
299
+ * nearest `ssr` export along its own files and its layout chain, resets followed, as the CLI reads it
300
+ * (`routeNeverSsr`), so a page that sets `ssr = true` under a disabled layout is read. Only a literal
301
+ * `true` turns SSR back on here: an option file that did not parse, or an `ssr` export the analyzer
302
+ * cannot read (an imported value), inherits instead, since analyzing a shell would report it empty,
303
+ * and a route no page in `sourceFiles` can match is a shell when any `ssr = false` module's
304
+ * route covers it. Kit's route precedence is not modelled: a route is a shell when any page whose
305
+ * path can match it is, so `/docs/[slug]` with `ssr = false` also claims a prerendered `/docs/intro`
306
+ * — the warning lists the skipped routes so a wrong skip is visible.
298
307
  */
299
- function ssrDisabledRouteMatcher(kitModules) {
300
- const patterns = [];
308
+ function ssrDisabledRouteMatcher(kitModules, sourceFiles) {
309
+ const scope = {
310
+ kitModules: kitModules.map((m) => m.parseFailed || m.ssrEnabled && !m.ssrTrue ? {
311
+ ...m,
312
+ parseFailed: void 0,
313
+ ssrEnabled: void 0
314
+ } : m),
315
+ sourceFiles: [...sourceFiles]
316
+ };
317
+ const pages = [];
318
+ for (const file of sourceFiles) {
319
+ const match = PAGE_FILE_RE.exec(file);
320
+ if (match) pages.push({
321
+ pattern: routePattern(match[1], false),
322
+ shell: routeNeverSsr(file, scope)
323
+ });
324
+ }
325
+ const disabled = [];
301
326
  for (const m of kitModules) {
302
327
  const match = m.ssrDisabled ? PAGE_OPTION_FILE_RE.exec(m.file) : null;
303
- if (!match) continue;
304
- const path = match[1].split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
305
- patterns.push(new RegExp(match[2] === "layout" ? `^${path}(?:/.*)?$` : `^${path}$`));
328
+ if (match) disabled.push(routePattern(match[1], match[2] === "layout"));
306
329
  }
307
330
  return (route) => {
308
331
  const r = route === "/" ? "" : route;
309
- return patterns.some((p) => p.test(r));
332
+ const matched = pages.filter((p) => p.pattern.test(r));
333
+ return matched.length > 0 ? matched.some((p) => p.shell) : disabled.some((p) => p.test(r));
310
334
  };
311
335
  }
312
336
  /**
@@ -329,7 +353,8 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
329
353
  ...extraProjectFacts
330
354
  };
331
355
  const [kitModules, components] = await Promise.all([collectKitModuleFacts$1(cwd, project.kitAliases), dropConstantListEachBlocks$1(cwd, componentFacts, project.componentAliases ?? project.kitAliases)]);
332
- const isShell = ssrDisabledRouteMatcher(kitModules);
356
+ const isShell = project.kitHashRouter ? () => true : ssrDisabledRouteMatcher(kitModules, sourceFiles);
357
+ const shellCause = project.kitHashRouter ? "the hash router" : "ssr = false";
333
358
  const keep = (items) => items.filter((i) => !isShell(i.route));
334
359
  const heads = keep(rendered.heads);
335
360
  const headings = keep(rendered.headings);
@@ -340,7 +365,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
340
365
  if (shells.length > 0) {
341
366
  const shown = shells.slice(0, 10);
342
367
  const list = shells.length > shown.length ? `${shown.join(", ")}, … and ${shells.length - shown.length} more` : shown.join(", ");
343
- warnings.push(`skipped ${shells.length} prerendered route(s) with ssr = false — their HTML is the app shell, not the page; run \`npx svelte-vitals\` to check them from source: ${list}`);
368
+ warnings.push(`skipped ${shells.length} prerendered route(s) with ${shellCause} — their HTML is the app shell, not the page; run \`npx svelte-vitals\` to check them from source: ${list}`);
344
369
  }
345
370
  if (selected.some((r) => r.id === "a11y/unverified-id-ref")) warnings.push("a11y/unverified-id-ref has no effect in rendered mode — the prerendered document is always fully resolved.");
346
371
  const directives = /* @__PURE__ */ new Map();
@@ -373,16 +398,19 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
373
398
  const { score } = computeScore(results, scoringConfig);
374
399
  const summary = summarize(results, scoringConfig);
375
400
  const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
376
- 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.
401
+ const coverageNote = `Analyzed ${heads.length} prerendered route(s)${shells.length > 0 ? ` (skipped ${shells.length} with ${shellCause})` : ""}. SSR/dynamic routes are not covered — run \`npx svelte-vitals\` for those.
377
402
  Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Accessibility/Bundle findings.`;
378
403
  const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
379
- const jsonReport = formatJsonReport(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
404
+ const jsonReportChunks = () => formatJsonReportChunks(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
380
405
  return {
381
406
  score,
382
407
  summary,
383
408
  results,
384
409
  consoleReport,
385
- jsonReport,
410
+ get jsonReport() {
411
+ return [...jsonReportChunks()].join("");
412
+ },
413
+ jsonReportChunks,
386
414
  routeCount: rendered.heads.length,
387
415
  failed,
388
416
  failOn: scoringConfig.failOn,
@@ -925,6 +953,7 @@ function svelteVitals(options = {}) {
925
953
  let minify;
926
954
  let configFile;
927
955
  let ssrBuild = false;
956
+ let prerenderDir = DEFAULT_PRERENDER_DIR;
928
957
  const buildPlugin = {
929
958
  name: "svelte-vitals",
930
959
  apply: "build",
@@ -936,9 +965,11 @@ function svelteVitals(options = {}) {
936
965
  if (!options.cwd) root = config.root;
937
966
  configFile = config.configFile;
938
967
  ssrBuild = Boolean(config.build?.ssr);
968
+ const out = config.build?.outDir ? resolve(config.root ?? root, config.build.outDir) : "";
969
+ if (/[\\/]output[\\/](?:server|client)$/.test(out)) prerenderDir = join(dirname(out), "prerendered/pages");
939
970
  },
940
971
  async closeBundle() {
941
- const pagesDir = options.prerenderDir ? options.prerenderDir : join(root, DEFAULT_PRERENDER_DIR);
972
+ const pagesDir = options.prerenderDir ? options.prerenderDir : prerenderDir;
942
973
  const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
943
974
  if (!ssrBuild && !existsSync(resolved)) return;
944
975
  const resolvedConfig = {
@@ -956,16 +987,21 @@ function svelteVitals(options = {}) {
956
987
  for (const w of result.warnings) warn(`svelte-vitals: ${w}`);
957
988
  if (result.routeCount === 0) {
958
989
  if (!ssrBuild) return;
959
- warn(`svelte-vitals: no prerendered pages found in ${options.prerenderDir ?? DEFAULT_PRERENDER_DIR} — route analysis skipped; the source scan still ran. Prerender routes to check their HTML, or run \`npx svelte-vitals\` to check every route from source.`);
990
+ warn(`svelte-vitals: no prerendered pages found in ${relative(root, resolved) || "."} — route analysis skipped; the source scan still ran. Prerender routes to check their HTML, or run \`npx svelte-vitals\` to check every route from source.`);
960
991
  }
961
992
  if (options.report !== false) {
962
- const out = options.report === "json" ? result.jsonReport : result.consoleReport;
963
- console.log(out);
993
+ if (options.report === "json") {
994
+ for (const chunk of result.jsonReportChunks()) {
995
+ if (process.stdout.destroyed) break;
996
+ if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
997
+ }
998
+ process.stdout.write("\n");
999
+ } else console.log(result.consoleReport);
964
1000
  }
965
1001
  if (options.outFile) {
966
1002
  const outPath = isAbsolute(options.outFile) ? options.outFile : join(root, options.outFile);
967
1003
  await mkdir(dirname(outPath), { recursive: true });
968
- await writeFile(outPath, result.jsonReport);
1004
+ await writeFile(outPath, result.jsonReportChunks());
969
1005
  }
970
1006
  if (result.failed) throw new Error(`svelte-vitals: build failed — findings at or above "${result.failOn}".`);
971
1007
  }
@@ -171,7 +171,15 @@ function parseHtmlHead(html) {
171
171
  presence: "own",
172
172
  value: attrValue(lang)
173
173
  },
174
- headings: (root.querySelector("body") ?? root).querySelectorAll("h1,h2,h3,h4,h5,h6").map((el) => Number(el.rawTagName[1])),
174
+ headings: (root.querySelector("body") ?? root).querySelectorAll("h1,h2,h3,h4,h5,h6,[role]").flatMap((el) => {
175
+ if (/^h[1-6]$/i.test(el.rawTagName)) return [{ level: Number(el.rawTagName[1]) }];
176
+ if (el.getAttribute("role")?.trim().toLowerCase() !== "heading") return [];
177
+ const level = Number(el.getAttribute("aria-level") ?? 2);
178
+ return [{
179
+ level: Number.isInteger(level) && level >= 1 ? level : 0,
180
+ aria: true
181
+ }];
182
+ }),
175
183
  images: (root.querySelector("body") ?? root).querySelectorAll("img").map((img) => ({
176
184
  hasWidth: img.hasAttribute("width"),
177
185
  hasHeight: img.hasAttribute("height"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/vite",
3
- "version": "0.55.2",
3
+ "version": "0.55.3",
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.55.2",
46
+ "@svelte-vitals/core": "^0.55.3",
47
47
  "@sveltejs/kit": "^2.0.0",
48
- "svelte-vitals": "^0.55.2",
48
+ "svelte-vitals": "^0.55.3",
49
49
  "vite": "^5.0.3 || ^6.0.0 || ^7.0.0 || ^8.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
@@ -54,13 +54,13 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
- "@svelte-vitals/core": "0.55.2",
57
+ "@svelte-vitals/core": "0.55.3",
58
58
  "@sveltejs/kit": "^2.70.3",
59
- "@types/node": "^24.13.6",
59
+ "@types/node": "^24.19.1",
60
60
  "happy-dom": "^20.14.5",
61
- "svelte": "^5.57.1",
62
- "svelte-vitals": "0.55.2",
63
- "vite": "^8.3.1"
61
+ "svelte": "^5.57.2",
62
+ "svelte-vitals": "0.55.3",
63
+ "vite": "^8.3.4"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsdown",