@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 +2 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +89 -13
- package/package.json +7 -7
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.
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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
|
|
265
|
-
*
|
|
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
|
|
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
|
|
323
|
-
jsonReport
|
|
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 {
|
|
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 =
|
|
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.
|
|
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.
|
|
46
|
+
"@svelte-vitals/core": "^0.55.0",
|
|
47
47
|
"@sveltejs/kit": "^2.70.3",
|
|
48
|
-
"svelte-vitals": "^0.
|
|
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.
|
|
57
|
+
"@svelte-vitals/core": "0.55.0",
|
|
58
58
|
"@sveltejs/kit": "^2.70.3",
|
|
59
|
-
"@types/node": "^24.13.
|
|
60
|
-
"happy-dom": "^20.14.
|
|
59
|
+
"@types/node": "^24.13.5",
|
|
60
|
+
"happy-dom": "^20.14.5",
|
|
61
61
|
"svelte": "^5.57.0",
|
|
62
|
-
"svelte-vitals": "0.
|
|
62
|
+
"svelte-vitals": "0.55.0",
|
|
63
63
|
"vite": "^8.3.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|