@svelte-vitals/vite 0.55.1 → 0.55.2

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
@@ -36,7 +36,7 @@ export default {
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.
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.
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-D9pGJXl6.js";
1
+ import { i as toOccurrenceMap, r as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-CAFo1WZs.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";
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-D9pGJXl6.js";
1
+ import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-CAFo1WZs.js";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { access, mkdir, readFile, readdir, 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, 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";
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 { CONFIG_FILENAMES, SUPPRESSIONS_FILE, analyzeProject, applySuppressions, createNodeRuntime, dropConstantListEachBlocks, findUnknownRuleIds, findWorkspacePackages, 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) {
@@ -13,14 +13,19 @@ function deriveRouteFromHtmlPath(relPath) {
13
13
  if (p.endsWith("/index")) p = p.slice(0, -6);
14
14
  return "/" + p;
15
15
  }
16
+ /** The whole file SvelteKit's prerender writes for a route that redirected — no document of the page. */
17
+ const REDIRECT_STUB = /^<script>location\.href=.*;<\/script><meta http-equiv="refresh" content="0;url=[^"]*">\s*$/s;
16
18
  /** Read every prerendered HTML page under `prerenderPagesDir` into ResolvedHead[]. */
17
19
  async function collectRenderedHeads(prerenderPagesDir) {
18
20
  const rt = createNodeRuntime();
19
21
  const files = (await rt.glob("**/*.html", prerenderPagesDir)).sort();
20
- const parsedFiles = await Promise.all(files.map(async (rel) => ({
21
- rel,
22
- parsed: parseHtmlHead(await rt.readFile(rt.join(prerenderPagesDir, rel)))
23
- })));
22
+ const parsedFiles = await Promise.all(files.map(async (rel) => {
23
+ const html = await rt.readFile(rt.join(prerenderPagesDir, rel));
24
+ return {
25
+ rel,
26
+ parsed: REDIRECT_STUB.test(html) ? void 0 : parseHtmlHead(html)
27
+ };
28
+ }));
24
29
  const heads = [];
25
30
  const headings = [];
26
31
  const images = [];
@@ -30,6 +35,7 @@ async function collectRenderedHeads(prerenderPagesDir) {
30
35
  value: "absent"
31
36
  };
32
37
  for (const { rel, parsed } of parsedFiles) {
38
+ if (!parsed) continue;
33
39
  if (htmlLang.presence === "none" && parsed.htmlLang.presence === "own") htmlLang = parsed.htmlLang;
34
40
  const route = deriveRouteFromHtmlPath(rel);
35
41
  heads.push({
@@ -102,6 +108,13 @@ async function robotsRefsSitemap(cwd) {
102
108
  return;
103
109
  }
104
110
  }
111
+ async function routeEndpoints(cwd) {
112
+ try {
113
+ return (await readdir(join(cwd, "src/routes"), { recursive: true })).map((e) => `src/routes/${e.split(sep).join("/")}`);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
105
118
  /** First existing config candidate with its source — same "only the first is loaded" rule as the CLI provider. */
106
119
  async function readFirstConfig(cwd, files) {
107
120
  for (const file of files) {
@@ -119,22 +132,26 @@ async function readFirstConfig(cwd, files) {
119
132
  }
120
133
  /** Project facts for plugin mode: robots/sitemap from source, htmlLang from rendered HTML. */
121
134
  async function collectRenderedProject(cwd, htmlLang) {
122
- const [hasRobotsTxt, hasSitemap, viteConfig, svelteConfig] = await Promise.all([
135
+ const [hasRobotsFile, hasSitemapFile, endpoints, viteConfig, svelteConfig, packageJson] = await Promise.all([
123
136
  existsAny(cwd, ROBOTS_SOURCE_PATHS),
124
137
  existsAny(cwd, SITEMAP_SOURCE_PATHS),
138
+ routeEndpoints(cwd),
125
139
  readFirstConfig(cwd, VITE_CONFIG_FILES),
126
- readFirstConfig(cwd, SVELTE_CONFIG_FILES)
140
+ readFirstConfig(cwd, SVELTE_CONFIG_FILES),
141
+ readFirstConfig(cwd, ["package.json"])
127
142
  ]);
128
143
  const robotsReferencesSitemap = await robotsRefsSitemap(cwd);
129
144
  const kitPathsBase = resolveKitPathsBase(viteConfig, svelteConfig);
130
- const kitAliases = resolveKitAliases(viteConfig, svelteConfig);
145
+ const kitAliases = withPackageImports(resolveKitAliases(viteConfig, svelteConfig), packageJson?.source);
146
+ const componentAliases = withWorkspacePackages(kitAliases, await findWorkspacePackages(createNodeRuntime(), cwd, packageJson?.source));
131
147
  return {
132
- hasRobotsTxt,
133
- hasSitemap,
148
+ hasRobotsTxt: hasRobotsFile || endpoints.some((p) => servesRootFile(p, "robots.txt")),
149
+ hasSitemap: hasSitemapFile || endpoints.some((p) => servesRootFile(p, "sitemap.xml")),
134
150
  htmlLang,
135
151
  ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
136
152
  ...kitPathsBase ? { kitPathsBase } : {},
137
- ...kitAliases ? { kitAliases } : {}
153
+ ...kitAliases ? { kitAliases } : {},
154
+ ...componentAliases !== kitAliases ? { componentAliases } : {}
138
155
  };
139
156
  }
140
157
  //#endregion
@@ -148,6 +165,15 @@ const nodeRuntime = createNodeRuntime();
148
165
  function collectComponentFacts$1(root) {
149
166
  return collectComponentFacts(nodeRuntime, root);
150
167
  }
168
+ /** `facts` without the `{#each}` blocks over an imported constant list (correctness/each-key, correctness/each-index-key). */
169
+ function dropConstantListEachBlocks$1(root, facts, aliases) {
170
+ return dropConstantListEachBlocks({
171
+ rt: nodeRuntime,
172
+ cwd: root,
173
+ cache: /* @__PURE__ */ new Map(),
174
+ aliases
175
+ }, facts);
176
+ }
151
177
  /** Scan SvelteKit route/hooks files for SSR shared-state facts (security/handler-state-write, security/server-module-state, security/shared-state-import; build mode only). */
152
178
  function collectKitModuleFacts$1(root, aliases) {
153
179
  return collectKitModuleFacts(nodeRuntime, root, aliases);
@@ -293,7 +319,7 @@ function ssrDisabledRouteMatcher(kitModules) {
293
319
  async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resolved) {
294
320
  const { config, warnings } = resolved ?? await resolveConfig(cwd, options);
295
321
  const suppressions = resolved ? resolved.suppressions : loadSuppressions(cwd);
296
- const [rendered, components, sourceFiles] = await Promise.all([
322
+ const [rendered, componentFacts, sourceFiles] = await Promise.all([
297
323
  collectRenderedHeads(prerenderPagesDir),
298
324
  collectComponentFacts$1(cwd),
299
325
  collectSourceFiles$1(cwd)
@@ -302,7 +328,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
302
328
  ...await collectRenderedProject(cwd, rendered.htmlLang),
303
329
  ...extraProjectFacts
304
330
  };
305
- const kitModules = await collectKitModuleFacts$1(cwd, project.kitAliases);
331
+ const [kitModules, components] = await Promise.all([collectKitModuleFacts$1(cwd, project.kitAliases), dropConstantListEachBlocks$1(cwd, componentFacts, project.componentAliases ?? project.kitAliases)]);
306
332
  const isShell = ssrDisabledRouteMatcher(kitModules);
307
333
  const keep = (items) => items.filter((i) => !isShell(i.route));
308
334
  const heads = keep(rendered.heads);
@@ -310,7 +336,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
310
336
  const images = keep(rendered.images);
311
337
  const a11y = keep(rendered.a11y);
312
338
  const shells = rendered.heads.filter((h) => isShell(h.route)).map((h) => h.route).sort();
313
- const selected = selectRules(allRules, config);
339
+ const selected = selectRules(allRules, config).filter((r) => rendered.heads.length > 0 || r.id !== "seo/html-lang");
314
340
  if (shells.length > 0) {
315
341
  const shown = shells.slice(0, 10);
316
342
  const list = shells.length > shown.length ? `${shown.join(", ")}, … and ${shells.length - shown.length} more` : shown.join(", ");
@@ -898,6 +924,7 @@ function svelteVitals(options = {}) {
898
924
  let root = options.cwd ?? process.cwd();
899
925
  let minify;
900
926
  let configFile;
927
+ let ssrBuild = false;
901
928
  const buildPlugin = {
902
929
  name: "svelte-vitals",
903
930
  apply: "build",
@@ -908,11 +935,12 @@ function svelteVitals(options = {}) {
908
935
  configResolved(config) {
909
936
  if (!options.cwd) root = config.root;
910
937
  configFile = config.configFile;
938
+ ssrBuild = Boolean(config.build?.ssr);
911
939
  },
912
940
  async closeBundle() {
913
941
  const pagesDir = options.prerenderDir ? options.prerenderDir : join(root, DEFAULT_PRERENDER_DIR);
914
942
  const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
915
- if (!existsSync(resolved)) return;
943
+ if (!ssrBuild && !existsSync(resolved)) return;
916
944
  const resolvedConfig = {
917
945
  ...await resolveConfig(root, options),
918
946
  suppressions: loadSuppressions(root)
@@ -926,7 +954,10 @@ function svelteVitals(options = {}) {
926
954
  return;
927
955
  }
928
956
  for (const w of result.warnings) warn(`svelte-vitals: ${w}`);
929
- if (result.routeCount === 0) return;
957
+ if (result.routeCount === 0) {
958
+ 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.`);
960
+ }
930
961
  if (options.report !== false) {
931
962
  const out = options.report === "json" ? result.jsonReport : result.consoleReport;
932
963
  console.log(out);
@@ -1,6 +1,11 @@
1
- import { ASIDE_DEMOTING_TAGS, IDREF_ATTRS, NAMING_ATTRS, SECTIONING_TAGS, decodeFragmentId, isClassicScriptType, isTopFragment, resolveLandmark, splitTokens, stripTextDirective } from "@svelte-vitals/core/internal";
1
+ import { ASIDE_DEMOTING_TAGS, IDREF_ATTRS, NAMING_ATTRS, SECTIONING_TAGS, decodeFragmentId, isClassicScriptType, isSvgSrc, isTopFragment, resolveLandmark, splitTokens, stripTextDirective } from "@svelte-vitals/core/internal";
2
2
  import { parse } from "node-html-parser";
3
3
  //#region src/providers/rendered/parse-html.ts
4
+ /** The <img> is its `<picture>`'s fallback and a sibling `<source>` carries the srcset. */
5
+ function pictureOffersSrcset(img) {
6
+ const parent = img.parentNode;
7
+ return parent?.rawTagName?.toLowerCase() === "picture" && parent.children.some((c) => c.rawTagName.toLowerCase() === "source" && c.hasAttribute("srcset"));
8
+ }
4
9
  function attrValue(v) {
5
10
  return v !== void 0 && v.trim().length > 0 ? "static" : "absent";
6
11
  }
@@ -84,7 +89,7 @@ function parseHtmlHead(html) {
84
89
  });
85
90
  }
86
91
  for (const meta of head.querySelectorAll("meta")) {
87
- const name = meta.getAttribute("name");
92
+ const name = meta.getAttribute("name")?.toLowerCase();
88
93
  const property = meta.getAttribute("property");
89
94
  const charset = meta.getAttribute("charset");
90
95
  if (charset != null) {
@@ -131,7 +136,8 @@ function parseHtmlHead(html) {
131
136
  ...href ? { href } : {}
132
137
  });
133
138
  }
134
- for (const script of head.querySelectorAll("script")) {
139
+ const bodyJsonLd = ((head === root ? void 0 : root.querySelector("body"))?.querySelectorAll("script") ?? []).filter((s) => s.getAttribute("type") === "application/ld+json");
140
+ for (const script of [...head.querySelectorAll("script"), ...bodyJsonLd]) {
135
141
  const type = script.getAttribute("type");
136
142
  if (type === "application/ld+json") {
137
143
  const raw = script.rawText;
@@ -172,7 +178,8 @@ function parseHtmlHead(html) {
172
178
  hasLoading: img.hasAttribute("loading"),
173
179
  hasAlt: img.hasAttribute("alt"),
174
180
  lazy: img.getAttribute("loading") === "lazy",
175
- hasSrcset: img.hasAttribute("srcset"),
181
+ hasSrcset: img.hasAttribute("srcset") || pictureOffersSrcset(img),
182
+ ...isSvgSrc(img.getAttribute("src") ?? "") ? { svg: true } : {},
176
183
  line: 0
177
184
  })),
178
185
  ...collectA11y(root)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/vite",
3
- "version": "0.55.1",
3
+ "version": "0.55.2",
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,10 +43,10 @@
43
43
  "node-html-parser": "^9.0.4"
44
44
  },
45
45
  "peerDependencies": {
46
- "@svelte-vitals/core": "^0.55.1",
47
- "@sveltejs/kit": "^2.70.3",
48
- "svelte-vitals": "^0.55.1",
49
- "vite": "^8.3.0"
46
+ "@svelte-vitals/core": "^0.55.2",
47
+ "@sveltejs/kit": "^2.0.0",
48
+ "svelte-vitals": "^0.55.2",
49
+ "vite": "^5.0.3 || ^6.0.0 || ^7.0.0 || ^8.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "@sveltejs/kit": {
@@ -54,13 +54,13 @@
54
54
  }
55
55
  },
56
56
  "devDependencies": {
57
- "@svelte-vitals/core": "0.55.1",
57
+ "@svelte-vitals/core": "0.55.2",
58
58
  "@sveltejs/kit": "^2.70.3",
59
- "@types/node": "^24.13.5",
59
+ "@types/node": "^24.13.6",
60
60
  "happy-dom": "^20.14.5",
61
- "svelte": "^5.57.0",
62
- "svelte-vitals": "0.55.1",
63
- "vite": "^8.3.0"
61
+ "svelte": "^5.57.1",
62
+ "svelte-vitals": "0.55.2",
63
+ "vite": "^8.3.1"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsdown",