@svelte-vitals/vite 0.55.1 → 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 +2 -2
- package/dist/hooks/index.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +115 -48
- package/dist/{loopback-D9pGJXl6.js → loopback-DxxU70UA.js} +20 -5
- package/package.json +10 -10
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.
|
|
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
|
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as toOccurrenceMap, r as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-
|
|
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((
|
|
53
|
-
|
|
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:
|
|
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,10 +1,11 @@
|
|
|
1
|
-
import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-
|
|
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
|
-
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
4
|
+
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
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,
|
|
7
|
-
import { CONFIG_FILENAMES, SUPPRESSIONS_FILE, analyzeProject, applySuppressions, createNodeRuntime, findUnknownRuleIds, knownRuleIds, loadConfigFile, loadSuppressions, registryTag, ruleOptionsSpec } from "svelte-vitals";
|
|
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";
|
|
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. */
|
|
10
11
|
function deriveRouteFromHtmlPath(relPath) {
|
|
@@ -13,14 +14,19 @@ function deriveRouteFromHtmlPath(relPath) {
|
|
|
13
14
|
if (p.endsWith("/index")) p = p.slice(0, -6);
|
|
14
15
|
return "/" + p;
|
|
15
16
|
}
|
|
17
|
+
/** The whole file SvelteKit's prerender writes for a route that redirected — no document of the page. */
|
|
18
|
+
const REDIRECT_STUB = /^<script>location\.href=.*;<\/script><meta http-equiv="refresh" content="0;url=[^"]*">\s*$/s;
|
|
16
19
|
/** Read every prerendered HTML page under `prerenderPagesDir` into ResolvedHead[]. */
|
|
17
20
|
async function collectRenderedHeads(prerenderPagesDir) {
|
|
18
21
|
const rt = createNodeRuntime();
|
|
19
22
|
const files = (await rt.glob("**/*.html", prerenderPagesDir)).sort();
|
|
20
|
-
const parsedFiles = await Promise.all(files.map(async (rel) =>
|
|
21
|
-
rel
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const parsedFiles = await Promise.all(files.map(async (rel) => {
|
|
24
|
+
const html = await rt.readFile(rt.join(prerenderPagesDir, rel));
|
|
25
|
+
return {
|
|
26
|
+
rel,
|
|
27
|
+
parsed: REDIRECT_STUB.test(html) ? void 0 : parseHtmlHead(html)
|
|
28
|
+
};
|
|
29
|
+
}));
|
|
24
30
|
const heads = [];
|
|
25
31
|
const headings = [];
|
|
26
32
|
const images = [];
|
|
@@ -30,6 +36,7 @@ async function collectRenderedHeads(prerenderPagesDir) {
|
|
|
30
36
|
value: "absent"
|
|
31
37
|
};
|
|
32
38
|
for (const { rel, parsed } of parsedFiles) {
|
|
39
|
+
if (!parsed) continue;
|
|
33
40
|
if (htmlLang.presence === "none" && parsed.htmlLang.presence === "own") htmlLang = parsed.htmlLang;
|
|
34
41
|
const route = deriveRouteFromHtmlPath(rel);
|
|
35
42
|
heads.push({
|
|
@@ -40,8 +47,8 @@ async function collectRenderedHeads(prerenderPagesDir) {
|
|
|
40
47
|
});
|
|
41
48
|
headings.push({
|
|
42
49
|
route,
|
|
43
|
-
headings: parsed.headings.map((
|
|
44
|
-
|
|
50
|
+
headings: parsed.headings.map((h) => ({
|
|
51
|
+
...h,
|
|
45
52
|
line: 0,
|
|
46
53
|
file: rel
|
|
47
54
|
}))
|
|
@@ -102,6 +109,13 @@ async function robotsRefsSitemap(cwd) {
|
|
|
102
109
|
return;
|
|
103
110
|
}
|
|
104
111
|
}
|
|
112
|
+
async function routeEndpoints(cwd) {
|
|
113
|
+
try {
|
|
114
|
+
return (await readdir(join(cwd, "src/routes"), { recursive: true })).map((e) => `src/routes/${e.split(sep).join("/")}`);
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
105
119
|
/** First existing config candidate with its source — same "only the first is loaded" rule as the CLI provider. */
|
|
106
120
|
async function readFirstConfig(cwd, files) {
|
|
107
121
|
for (const file of files) {
|
|
@@ -119,22 +133,27 @@ async function readFirstConfig(cwd, files) {
|
|
|
119
133
|
}
|
|
120
134
|
/** Project facts for plugin mode: robots/sitemap from source, htmlLang from rendered HTML. */
|
|
121
135
|
async function collectRenderedProject(cwd, htmlLang) {
|
|
122
|
-
const [
|
|
136
|
+
const [hasRobotsFile, hasSitemapFile, endpoints, viteConfig, svelteConfig, packageJson] = await Promise.all([
|
|
123
137
|
existsAny(cwd, ROBOTS_SOURCE_PATHS),
|
|
124
138
|
existsAny(cwd, SITEMAP_SOURCE_PATHS),
|
|
139
|
+
routeEndpoints(cwd),
|
|
125
140
|
readFirstConfig(cwd, VITE_CONFIG_FILES),
|
|
126
|
-
readFirstConfig(cwd, SVELTE_CONFIG_FILES)
|
|
141
|
+
readFirstConfig(cwd, SVELTE_CONFIG_FILES),
|
|
142
|
+
readFirstConfig(cwd, ["package.json"])
|
|
127
143
|
]);
|
|
128
144
|
const robotsReferencesSitemap = await robotsRefsSitemap(cwd);
|
|
129
145
|
const kitPathsBase = resolveKitPathsBase(viteConfig, svelteConfig);
|
|
130
|
-
const kitAliases = resolveKitAliases(viteConfig, svelteConfig);
|
|
146
|
+
const kitAliases = withPackageImports(resolveKitAliases(viteConfig, svelteConfig), packageJson?.source);
|
|
147
|
+
const componentAliases = withWorkspacePackages(kitAliases, await findWorkspacePackages(createNodeRuntime(), cwd, packageJson?.source));
|
|
131
148
|
return {
|
|
132
|
-
hasRobotsTxt,
|
|
133
|
-
hasSitemap,
|
|
149
|
+
hasRobotsTxt: hasRobotsFile || endpoints.some((p) => servesRootFile(p, "robots.txt")),
|
|
150
|
+
hasSitemap: hasSitemapFile || endpoints.some((p) => servesRootFile(p, "sitemap.xml")),
|
|
134
151
|
htmlLang,
|
|
135
152
|
...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
|
|
136
153
|
...kitPathsBase ? { kitPathsBase } : {},
|
|
137
|
-
...
|
|
154
|
+
...svelteConfig && findKitHashRouterInSvelteConfig(svelteConfig.source) ? { kitHashRouter: true } : {},
|
|
155
|
+
...kitAliases ? { kitAliases } : {},
|
|
156
|
+
...componentAliases !== kitAliases ? { componentAliases } : {}
|
|
138
157
|
};
|
|
139
158
|
}
|
|
140
159
|
//#endregion
|
|
@@ -148,6 +167,15 @@ const nodeRuntime = createNodeRuntime();
|
|
|
148
167
|
function collectComponentFacts$1(root) {
|
|
149
168
|
return collectComponentFacts(nodeRuntime, root);
|
|
150
169
|
}
|
|
170
|
+
/** `facts` without the `{#each}` blocks over an imported constant list (correctness/each-key, correctness/each-index-key). */
|
|
171
|
+
function dropConstantListEachBlocks$1(root, facts, aliases) {
|
|
172
|
+
return dropConstantListEachBlocks({
|
|
173
|
+
rt: nodeRuntime,
|
|
174
|
+
cwd: root,
|
|
175
|
+
cache: /* @__PURE__ */ new Map(),
|
|
176
|
+
aliases
|
|
177
|
+
}, facts);
|
|
178
|
+
}
|
|
151
179
|
/** 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
180
|
function collectKitModuleFacts$1(root, aliases) {
|
|
153
181
|
return collectKitModuleFacts(nodeRuntime, root, aliases);
|
|
@@ -246,11 +274,6 @@ async function resolveConfig(cwd, options) {
|
|
|
246
274
|
warnings: loaded?.warnings ?? []
|
|
247
275
|
};
|
|
248
276
|
}
|
|
249
|
-
/**
|
|
250
|
-
* `ssr` is a page option: only `+page`/`+layout` modules (universal or server) can set it. Same
|
|
251
|
-
* file set as `PAGE_OPTION_FILE_RE` in core's `rules/seo/ssr-disabled.ts`, which reports the fact.
|
|
252
|
-
*/
|
|
253
|
-
const PAGE_OPTION_FILE_RE = /^src\/routes\/(.*?)\/?\+(page|layout)(?:\.server)?\.[jt]s$/;
|
|
254
277
|
function escapeRegExp(s) {
|
|
255
278
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
256
279
|
}
|
|
@@ -260,27 +283,54 @@ function segmentPattern(segment) {
|
|
|
260
283
|
if (/^\[\[[^\]]+\]\]$/.test(segment)) return "(?:/[^/]+)?";
|
|
261
284
|
return "/" + segment.split(/\[[^\]]+\]/).map(escapeRegExp).join("[^/]+");
|
|
262
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
|
+
}
|
|
263
295
|
/**
|
|
264
296
|
* Routes whose prerendered HTML is the app shell rather than the page: SvelteKit still writes a
|
|
265
297
|
* file for an `ssr = false` route, but its `<head>` and body only exist once the client renders,
|
|
266
|
-
* so every rendered head/heading/landmark check would report the page as empty.
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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.
|
|
272
307
|
*/
|
|
273
|
-
function ssrDisabledRouteMatcher(kitModules) {
|
|
274
|
-
const
|
|
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 = [];
|
|
275
326
|
for (const m of kitModules) {
|
|
276
327
|
const match = m.ssrDisabled ? PAGE_OPTION_FILE_RE.exec(m.file) : null;
|
|
277
|
-
if (
|
|
278
|
-
const path = match[1].split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
|
|
279
|
-
patterns.push(new RegExp(match[2] === "layout" ? `^${path}(?:/.*)?$` : `^${path}$`));
|
|
328
|
+
if (match) disabled.push(routePattern(match[1], match[2] === "layout"));
|
|
280
329
|
}
|
|
281
330
|
return (route) => {
|
|
282
331
|
const r = route === "/" ? "" : route;
|
|
283
|
-
|
|
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));
|
|
284
334
|
};
|
|
285
335
|
}
|
|
286
336
|
/**
|
|
@@ -293,7 +343,7 @@ function ssrDisabledRouteMatcher(kitModules) {
|
|
|
293
343
|
async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resolved) {
|
|
294
344
|
const { config, warnings } = resolved ?? await resolveConfig(cwd, options);
|
|
295
345
|
const suppressions = resolved ? resolved.suppressions : loadSuppressions(cwd);
|
|
296
|
-
const [rendered,
|
|
346
|
+
const [rendered, componentFacts, sourceFiles] = await Promise.all([
|
|
297
347
|
collectRenderedHeads(prerenderPagesDir),
|
|
298
348
|
collectComponentFacts$1(cwd),
|
|
299
349
|
collectSourceFiles$1(cwd)
|
|
@@ -302,19 +352,20 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
302
352
|
...await collectRenderedProject(cwd, rendered.htmlLang),
|
|
303
353
|
...extraProjectFacts
|
|
304
354
|
};
|
|
305
|
-
const kitModules = await collectKitModuleFacts$1(cwd, project.kitAliases);
|
|
306
|
-
const isShell = ssrDisabledRouteMatcher(kitModules);
|
|
355
|
+
const [kitModules, components] = await Promise.all([collectKitModuleFacts$1(cwd, project.kitAliases), dropConstantListEachBlocks$1(cwd, componentFacts, project.componentAliases ?? project.kitAliases)]);
|
|
356
|
+
const isShell = project.kitHashRouter ? () => true : ssrDisabledRouteMatcher(kitModules, sourceFiles);
|
|
357
|
+
const shellCause = project.kitHashRouter ? "the hash router" : "ssr = false";
|
|
307
358
|
const keep = (items) => items.filter((i) => !isShell(i.route));
|
|
308
359
|
const heads = keep(rendered.heads);
|
|
309
360
|
const headings = keep(rendered.headings);
|
|
310
361
|
const images = keep(rendered.images);
|
|
311
362
|
const a11y = keep(rendered.a11y);
|
|
312
363
|
const shells = rendered.heads.filter((h) => isShell(h.route)).map((h) => h.route).sort();
|
|
313
|
-
const selected = selectRules(allRules, config);
|
|
364
|
+
const selected = selectRules(allRules, config).filter((r) => rendered.heads.length > 0 || r.id !== "seo/html-lang");
|
|
314
365
|
if (shells.length > 0) {
|
|
315
366
|
const shown = shells.slice(0, 10);
|
|
316
367
|
const list = shells.length > shown.length ? `${shown.join(", ")}, … and ${shells.length - shown.length} more` : shown.join(", ");
|
|
317
|
-
warnings.push(`skipped ${shells.length} prerendered route(s) with
|
|
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}`);
|
|
318
369
|
}
|
|
319
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.");
|
|
320
371
|
const directives = /* @__PURE__ */ new Map();
|
|
@@ -347,16 +398,19 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
347
398
|
const { score } = computeScore(results, scoringConfig);
|
|
348
399
|
const summary = summarize(results, scoringConfig);
|
|
349
400
|
const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
|
|
350
|
-
const coverageNote = `Analyzed ${heads.length} prerendered route(s)${shells.length > 0 ? ` (skipped ${shells.length} with
|
|
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.
|
|
351
402
|
Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Accessibility/Bundle findings.`;
|
|
352
403
|
const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
|
|
353
|
-
const
|
|
404
|
+
const jsonReportChunks = () => formatJsonReportChunks(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
|
|
354
405
|
return {
|
|
355
406
|
score,
|
|
356
407
|
summary,
|
|
357
408
|
results,
|
|
358
409
|
consoleReport,
|
|
359
|
-
jsonReport
|
|
410
|
+
get jsonReport() {
|
|
411
|
+
return [...jsonReportChunks()].join("");
|
|
412
|
+
},
|
|
413
|
+
jsonReportChunks,
|
|
360
414
|
routeCount: rendered.heads.length,
|
|
361
415
|
failed,
|
|
362
416
|
failOn: scoringConfig.failOn,
|
|
@@ -898,6 +952,8 @@ function svelteVitals(options = {}) {
|
|
|
898
952
|
let root = options.cwd ?? process.cwd();
|
|
899
953
|
let minify;
|
|
900
954
|
let configFile;
|
|
955
|
+
let ssrBuild = false;
|
|
956
|
+
let prerenderDir = DEFAULT_PRERENDER_DIR;
|
|
901
957
|
const buildPlugin = {
|
|
902
958
|
name: "svelte-vitals",
|
|
903
959
|
apply: "build",
|
|
@@ -908,11 +964,14 @@ function svelteVitals(options = {}) {
|
|
|
908
964
|
configResolved(config) {
|
|
909
965
|
if (!options.cwd) root = config.root;
|
|
910
966
|
configFile = config.configFile;
|
|
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");
|
|
911
970
|
},
|
|
912
971
|
async closeBundle() {
|
|
913
|
-
const pagesDir = options.prerenderDir ? options.prerenderDir :
|
|
972
|
+
const pagesDir = options.prerenderDir ? options.prerenderDir : prerenderDir;
|
|
914
973
|
const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
|
|
915
|
-
if (!existsSync(resolved)) return;
|
|
974
|
+
if (!ssrBuild && !existsSync(resolved)) return;
|
|
916
975
|
const resolvedConfig = {
|
|
917
976
|
...await resolveConfig(root, options),
|
|
918
977
|
suppressions: loadSuppressions(root)
|
|
@@ -926,15 +985,23 @@ function svelteVitals(options = {}) {
|
|
|
926
985
|
return;
|
|
927
986
|
}
|
|
928
987
|
for (const w of result.warnings) warn(`svelte-vitals: ${w}`);
|
|
929
|
-
if (result.routeCount === 0)
|
|
988
|
+
if (result.routeCount === 0) {
|
|
989
|
+
if (!ssrBuild) return;
|
|
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.`);
|
|
991
|
+
}
|
|
930
992
|
if (options.report !== false) {
|
|
931
|
-
|
|
932
|
-
|
|
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);
|
|
933
1000
|
}
|
|
934
1001
|
if (options.outFile) {
|
|
935
1002
|
const outPath = isAbsolute(options.outFile) ? options.outFile : join(root, options.outFile);
|
|
936
1003
|
await mkdir(dirname(outPath), { recursive: true });
|
|
937
|
-
await writeFile(outPath, result.
|
|
1004
|
+
await writeFile(outPath, result.jsonReportChunks());
|
|
938
1005
|
}
|
|
939
1006
|
if (result.failed) throw new Error(`svelte-vitals: build failed — findings at or above "${result.failOn}".`);
|
|
940
1007
|
}
|
|
@@ -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
|
-
|
|
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;
|
|
@@ -165,14 +171,23 @@ function parseHtmlHead(html) {
|
|
|
165
171
|
presence: "own",
|
|
166
172
|
value: attrValue(lang)
|
|
167
173
|
},
|
|
168
|
-
headings: (root.querySelector("body") ?? root).querySelectorAll("h1,h2,h3,h4,h5,h6").
|
|
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
|
+
}),
|
|
169
183
|
images: (root.querySelector("body") ?? root).querySelectorAll("img").map((img) => ({
|
|
170
184
|
hasWidth: img.hasAttribute("width"),
|
|
171
185
|
hasHeight: img.hasAttribute("height"),
|
|
172
186
|
hasLoading: img.hasAttribute("loading"),
|
|
173
187
|
hasAlt: img.hasAttribute("alt"),
|
|
174
188
|
lazy: img.getAttribute("loading") === "lazy",
|
|
175
|
-
hasSrcset: img.hasAttribute("srcset"),
|
|
189
|
+
hasSrcset: img.hasAttribute("srcset") || pictureOffersSrcset(img),
|
|
190
|
+
...isSvgSrc(img.getAttribute("src") ?? "") ? { svg: true } : {},
|
|
176
191
|
line: 0
|
|
177
192
|
})),
|
|
178
193
|
...collectA11y(root)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/vite",
|
|
3
|
-
"version": "0.55.
|
|
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,10 +43,10 @@
|
|
|
43
43
|
"node-html-parser": "^9.0.4"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@svelte-vitals/core": "^0.55.
|
|
47
|
-
"@sveltejs/kit": "^2.
|
|
48
|
-
"svelte-vitals": "^0.55.
|
|
49
|
-
"vite": "^
|
|
46
|
+
"@svelte-vitals/core": "^0.55.3",
|
|
47
|
+
"@sveltejs/kit": "^2.0.0",
|
|
48
|
+
"svelte-vitals": "^0.55.3",
|
|
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.
|
|
57
|
+
"@svelte-vitals/core": "0.55.3",
|
|
58
58
|
"@sveltejs/kit": "^2.70.3",
|
|
59
|
-
"@types/node": "^24.
|
|
59
|
+
"@types/node": "^24.19.1",
|
|
60
60
|
"happy-dom": "^20.14.5",
|
|
61
|
-
"svelte": "^5.57.
|
|
62
|
-
"svelte-vitals": "0.55.
|
|
63
|
-
"vite": "^8.3.
|
|
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",
|