@svelte-vitals/vite 0.54.5 → 0.54.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.
- package/README.md +3 -1
- package/dist/hooks/index.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +125 -15
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -34,7 +34,9 @@ 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
|
-
|
|
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
|
+
|
|
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.
|
|
40
42
|
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -14,6 +14,6 @@ interface SvelteVitalsHookOptions {
|
|
|
14
14
|
* route's dashboard findings from static (source-only) to `measured` (real rendered
|
|
15
15
|
* HTML). Add it to `src/hooks.server.ts`, e.g. `sequence(svelteVitalsHandle())`.
|
|
16
16
|
*/
|
|
17
|
-
declare function svelteVitalsHandle(options?: SvelteVitalsHookOptions): Handle;
|
|
17
|
+
export declare function svelteVitalsHandle(options?: SvelteVitalsHookOptions): Handle;
|
|
18
18
|
//#endregion
|
|
19
|
-
export {
|
|
19
|
+
export type { SvelteVitalsHookOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,6 @@ interface SvelteVitalsOptions {
|
|
|
28
28
|
ui?: boolean;
|
|
29
29
|
}
|
|
30
30
|
/** svelte-vitals Vite/SvelteKit plugin. */
|
|
31
|
-
declare function svelteVitals(options?: SvelteVitalsOptions): Plugin | Plugin[];
|
|
31
|
+
export declare function svelteVitals(options?: SvelteVitalsOptions): Plugin | Plugin[];
|
|
32
32
|
//#endregion
|
|
33
|
-
export {
|
|
33
|
+
export type { SvelteVitalsOptions };
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ 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
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";
|
|
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() {
|
|
@@ -222,24 +245,75 @@ async function resolveConfig(cwd, options) {
|
|
|
222
245
|
};
|
|
223
246
|
}
|
|
224
247
|
/**
|
|
248
|
+
* `ssr` is a page option: only `+page`/`+layout` modules (universal or server) can set it. Same
|
|
249
|
+
* file set as `PAGE_OPTION_FILE_RE` in core's `rules/seo/ssr-disabled.ts`, which reports the fact.
|
|
250
|
+
*/
|
|
251
|
+
const PAGE_OPTION_FILE_RE = /^src\/routes\/(.*?)\/?\+(page|layout)(?:\.server)?\.[jt]s$/;
|
|
252
|
+
function escapeRegExp(s) {
|
|
253
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
254
|
+
}
|
|
255
|
+
/** One route directory segment as a pattern over the rendered (concrete) route path, leading `/` included. */
|
|
256
|
+
function segmentPattern(segment) {
|
|
257
|
+
if (/^\[\.\.\.[^\]]+\]$/.test(segment)) return "(?:/.*)?";
|
|
258
|
+
if (/^\[\[[^\]]+\]\]$/.test(segment)) return "(?:/[^/]+)?";
|
|
259
|
+
return "/" + segment.split(/\[[^\]]+\]/).map(escapeRegExp).join("[^/]+");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Routes whose prerendered HTML is the app shell rather than the page: SvelteKit still writes a
|
|
263
|
+
* file for an `ssr = false` route, but its `<head>` and body only exist once the client renders,
|
|
264
|
+
* so every rendered head/heading/landmark check would report the page as empty. A `+layout`
|
|
265
|
+
* covers its whole subtree, a `+page` only its own route; `(group)` directories are not URL
|
|
266
|
+
* segments. Two ceilings: a child route re-enabling `ssr = true` under a disabled layout is not
|
|
267
|
+
* tracked, and Kit's route precedence is not modelled, so a `[param]` route also claims a literal
|
|
268
|
+
* sibling (`/docs/[slug]` matches a prerendered `/docs/intro`) — the warning lists the skipped
|
|
269
|
+
* routes so a wrong skip is visible.
|
|
270
|
+
*/
|
|
271
|
+
function ssrDisabledRouteMatcher(kitModules) {
|
|
272
|
+
const patterns = [];
|
|
273
|
+
for (const m of kitModules) {
|
|
274
|
+
const match = m.ssrDisabled ? PAGE_OPTION_FILE_RE.exec(m.file) : null;
|
|
275
|
+
if (!match) continue;
|
|
276
|
+
const path = match[1].split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
|
|
277
|
+
patterns.push(new RegExp(match[2] === "layout" ? `^${path}(?:/.*)?$` : `^${path}$`));
|
|
278
|
+
}
|
|
279
|
+
return (route) => {
|
|
280
|
+
const r = route === "/" ? "" : route;
|
|
281
|
+
return patterns.some((p) => p.test(r));
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
225
285
|
* Collect prerendered heads + project facts + component facts, run the core pipeline, and
|
|
226
286
|
* format reports. Config precedence: see `resolveConfig`. Pass `resolved` when the caller
|
|
227
|
-
* already resolved config itself (build mode
|
|
228
|
-
*
|
|
287
|
+
* already resolved config and loaded `svelte-vitals-suppressions.json` itself (build mode does
|
|
288
|
+
* both outside `analyze`'s try/catch so a validation error fails the build instead of being
|
|
289
|
+
* caught as an analysis error); `suppressions: undefined` means the file does not exist.
|
|
229
290
|
*/
|
|
230
291
|
async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resolved) {
|
|
231
292
|
const { config, warnings } = resolved ?? await resolveConfig(cwd, options);
|
|
232
|
-
const
|
|
293
|
+
const suppressions = resolved ? resolved.suppressions : loadSuppressions(cwd);
|
|
294
|
+
const [rendered, components, sourceFiles] = await Promise.all([
|
|
233
295
|
collectRenderedHeads(prerenderPagesDir),
|
|
234
296
|
collectComponentFacts$1(cwd),
|
|
235
297
|
collectSourceFiles$1(cwd)
|
|
236
298
|
]);
|
|
237
299
|
const project = {
|
|
238
|
-
...await collectRenderedProject(cwd, htmlLang),
|
|
300
|
+
...await collectRenderedProject(cwd, rendered.htmlLang),
|
|
239
301
|
...extraProjectFacts
|
|
240
302
|
};
|
|
241
303
|
const kitModules = await collectKitModuleFacts$1(cwd, project.kitAliases);
|
|
304
|
+
const isShell = ssrDisabledRouteMatcher(kitModules);
|
|
305
|
+
const keep = (items) => items.filter((i) => !isShell(i.route));
|
|
306
|
+
const heads = keep(rendered.heads);
|
|
307
|
+
const headings = keep(rendered.headings);
|
|
308
|
+
const images = keep(rendered.images);
|
|
309
|
+
const a11y = keep(rendered.a11y);
|
|
310
|
+
const shells = rendered.heads.filter((h) => isShell(h.route)).map((h) => h.route).sort();
|
|
242
311
|
const selected = selectRules(allRules, config);
|
|
312
|
+
if (shells.length > 0) {
|
|
313
|
+
const shown = shells.slice(0, 10);
|
|
314
|
+
const list = shells.length > shown.length ? `${shown.join(", ")}, … and ${shells.length - shown.length} more` : shown.join(", ");
|
|
315
|
+
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}`);
|
|
316
|
+
}
|
|
243
317
|
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.");
|
|
244
318
|
const directives = /* @__PURE__ */ new Map();
|
|
245
319
|
addFactsDirectives(directives, {
|
|
@@ -247,7 +321,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
247
321
|
kitModules,
|
|
248
322
|
viteMinifyDisabled: project.viteMinifyDisabled
|
|
249
323
|
});
|
|
250
|
-
const
|
|
324
|
+
const analysis = await runAnalysis(selected, {
|
|
251
325
|
heads,
|
|
252
326
|
headings,
|
|
253
327
|
images,
|
|
@@ -258,21 +332,30 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
258
332
|
kitModules,
|
|
259
333
|
sourceFiles
|
|
260
334
|
}, directives);
|
|
335
|
+
const { examined, failedRules, scoringConfig } = analysis;
|
|
336
|
+
let results = analysis.results;
|
|
337
|
+
if (suppressions !== void 0) {
|
|
338
|
+
const applied = applySourceSuppressions(results, suppressions, config);
|
|
339
|
+
results = applied.results;
|
|
340
|
+
warnings.push(...applied.notices);
|
|
341
|
+
}
|
|
261
342
|
warnings.push(...skippedFileWarnings([...components, ...kitModules]));
|
|
262
343
|
warnings.push(...unknownDirectiveIds(directives, allRules));
|
|
263
344
|
for (const f of failedRules) warnings.push(formatFailedRuleWarning(f));
|
|
264
345
|
const { score } = computeScore(results, scoringConfig);
|
|
265
346
|
const summary = summarize(results, scoringConfig);
|
|
266
347
|
const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
|
|
267
|
-
const coverageNote = `Analyzed ${heads.length} prerendered route(s). SSR/dynamic routes are not covered — run \`npx svelte-vitals\` for those.
|
|
348
|
+
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.
|
|
268
349
|
Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Accessibility/Bundle findings.`;
|
|
350
|
+
const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
|
|
351
|
+
const jsonReport = formatJsonReport(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
|
|
269
352
|
return {
|
|
270
353
|
score,
|
|
271
354
|
summary,
|
|
272
355
|
results,
|
|
273
|
-
consoleReport
|
|
274
|
-
jsonReport
|
|
275
|
-
routeCount: heads.length,
|
|
356
|
+
consoleReport,
|
|
357
|
+
jsonReport,
|
|
358
|
+
routeCount: rendered.heads.length,
|
|
276
359
|
failed,
|
|
277
360
|
failOn: scoringConfig.failOn,
|
|
278
361
|
warnings
|
|
@@ -586,6 +669,26 @@ function createStore() {
|
|
|
586
669
|
//#endregion
|
|
587
670
|
//#region src/ui/analysis.ts
|
|
588
671
|
/**
|
|
672
|
+
* Apply `svelte-vitals-suppressions.json` to the whole-project layer, same entries as the build
|
|
673
|
+
* gate (see `applySourceSuppressions`). A malformed file is a warning here, not an error: dev
|
|
674
|
+
* must keep serving. Returns the results unchanged when the file is absent or `config` is
|
|
675
|
+
* unknown (an injected `analyze` that returns no config).
|
|
676
|
+
*/
|
|
677
|
+
function suppress(root, results, config, warnings) {
|
|
678
|
+
if (!config) return results;
|
|
679
|
+
let entries;
|
|
680
|
+
try {
|
|
681
|
+
entries = loadSuppressions(root);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
warnings.push(`${err instanceof Error ? err.message : String(err)} — ${SUPPRESSIONS_FILE} ignored.`);
|
|
684
|
+
return results;
|
|
685
|
+
}
|
|
686
|
+
if (entries === void 0) return results;
|
|
687
|
+
const applied = applySourceSuppressions(results, entries, config);
|
|
688
|
+
warnings.push(...applied.notices);
|
|
689
|
+
return applied.results;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
589
692
|
* Owns the dev-dashboard's whole-project analysis lifecycle: an async run at startup,
|
|
590
693
|
* a debounced re-run on source changes, and coalescing so a change that arrives mid-run
|
|
591
694
|
* produces exactly one follow-up run rather than one per change (design doc §1).
|
|
@@ -612,10 +715,13 @@ function createAnalysisRunner(opts) {
|
|
|
612
715
|
failOn: opts.failOn,
|
|
613
716
|
parseCache
|
|
614
717
|
});
|
|
615
|
-
const {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
718
|
+
const { failedRuleIds } = result;
|
|
719
|
+
const warnings = [...result.warnings ?? []];
|
|
720
|
+
const results = suppress(opts.root, result.results, result.config, warnings);
|
|
721
|
+
if (!stopped) {
|
|
722
|
+
if (failedRuleIds !== void 0) opts.onResults(results, failedRuleIds);
|
|
723
|
+
else opts.onResults(results);
|
|
724
|
+
}
|
|
619
725
|
const key = warnings.join("\n");
|
|
620
726
|
if (!stopped && warnings.length > 0 && key !== lastWarningsKey) opts.onWarnings?.(warnings);
|
|
621
727
|
lastWarningsKey = key;
|
|
@@ -665,6 +771,7 @@ const CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
|
665
771
|
"svelte.config.js",
|
|
666
772
|
"svelte.config.ts",
|
|
667
773
|
...CONFIG_FILENAMES,
|
|
774
|
+
SUPPRESSIONS_FILE,
|
|
668
775
|
"vite.config.js",
|
|
669
776
|
"vite.config.mjs",
|
|
670
777
|
"vite.config.ts",
|
|
@@ -790,7 +897,10 @@ function svelteVitals(options = {}) {
|
|
|
790
897
|
const pagesDir = options.prerenderDir ? options.prerenderDir : join(root, DEFAULT_PRERENDER_DIR);
|
|
791
898
|
const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
|
|
792
899
|
if (!existsSync(resolved)) return;
|
|
793
|
-
const resolvedConfig =
|
|
900
|
+
const resolvedConfig = {
|
|
901
|
+
...await resolveConfig(root, options),
|
|
902
|
+
suppressions: loadSuppressions(root)
|
|
903
|
+
};
|
|
794
904
|
let result;
|
|
795
905
|
try {
|
|
796
906
|
const viteMinifyDisabled = await resolveMinifyDisabled(minify, configFile, root);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/vite",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.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",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"esm-env": "^1.2.2",
|
|
43
|
-
"node-html-parser": "^9.0.
|
|
43
|
+
"node-html-parser": "^9.0.4"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@svelte-vitals/core": "^0.54.
|
|
46
|
+
"@svelte-vitals/core": "^0.54.7",
|
|
47
47
|
"@sveltejs/kit": "^2.70.3",
|
|
48
|
-
"svelte-vitals": "^0.54.
|
|
49
|
-
"vite": "^8.
|
|
48
|
+
"svelte-vitals": "^0.54.7",
|
|
49
|
+
"vite": "^8.3.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.54.
|
|
57
|
+
"@svelte-vitals/core": "0.54.7",
|
|
58
58
|
"@sveltejs/kit": "^2.70.3",
|
|
59
|
-
"@types/node": "^24.13.
|
|
60
|
-
"happy-dom": "^20.
|
|
59
|
+
"@types/node": "^24.13.4",
|
|
60
|
+
"happy-dom": "^20.14.3",
|
|
61
61
|
"svelte": "^5.57.0",
|
|
62
|
-
"svelte-vitals": "0.54.
|
|
63
|
-
"vite": "^8.
|
|
62
|
+
"svelte-vitals": "0.54.7",
|
|
63
|
+
"vite": "^8.3.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsdown",
|