@svelte-vitals/vite 0.55.2 → 0.55.4
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 +4 -2
- package/dist/hooks/index.d.ts +0 -2
- package/dist/hooks/index.js +4 -5
- package/dist/index.d.ts +1 -3
- package/dist/index.js +80 -56
- package/dist/{loopback-CAFo1WZs.js → loopback-D_07LGfE.js} +11 -5
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -30,13 +30,15 @@ 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
|
-
- `
|
|
33
|
+
- `overrides`: route- and file-scoped rule overrides, same as the config file's `overrides`.
|
|
34
|
+
- `seo`: `{ indexable?: boolean }`; `indexable: false` turns off the search-result rules.
|
|
35
|
+
- `prerenderDir`: override the prerendered-pages directory (default: `output/prerendered/pages` under `kit.outDir`).
|
|
34
36
|
- `cwd`: project root (defaults to the Vite config root).
|
|
35
37
|
- `ui`: serve the live dev dashboard (default `true`); pass `false` to keep only the build-time gate.
|
|
36
38
|
|
|
37
39
|
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
40
|
|
|
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.
|
|
41
|
+
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
42
|
|
|
41
43
|
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
44
|
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Config, RuleSetting } from "@svelte-vitals/core";
|
|
2
2
|
import { Handle } from "@sveltejs/kit";
|
|
3
|
-
//#region src/hooks/handle.d.ts
|
|
4
3
|
/** Options for the dev-time SvelteKit handle. A focused subset of the plugin options. */
|
|
5
4
|
interface SvelteVitalsHookOptions {
|
|
6
5
|
/** Component names treated as meta sources (design §11 layer 4). Mirrors the plugin option. */
|
|
@@ -15,5 +14,4 @@ interface SvelteVitalsHookOptions {
|
|
|
15
14
|
* HTML). Add it to `src/hooks.server.ts`, e.g. `sequence(svelteVitalsHandle())`.
|
|
16
15
|
*/
|
|
17
16
|
export declare function svelteVitalsHandle(options?: SvelteVitalsHookOptions): Handle;
|
|
18
|
-
//#endregion
|
|
19
17
|
export type { SvelteVitalsHookOptions };
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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-D_07LGfE.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";
|
|
5
|
-
|
|
5
|
+
// src/hooks/handle.ts
|
|
6
6
|
/** Stable signature of a route's penalized findings, so ingest is skipped when a repeat visit finds nothing new. */
|
|
7
7
|
function findingSignature(results, config) {
|
|
8
8
|
return results.filter((r) => isPenalized(r.detection, config.treatDynamicAs)).map((r) => `${r.id}:${effectiveSeverity(r, config)}:${r.detection.presence}:${r.detection.value}`).sort().join("|");
|
|
@@ -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
|
}))
|
|
@@ -130,5 +130,4 @@ function svelteVitalsHandle(options = {}) {
|
|
|
130
130
|
} });
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
-
//#endregion
|
|
134
133
|
export { svelteVitalsHandle };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Category, Config, RuleOverride, RuleSetting, Severity, TreatDynamicAs } from "@svelte-vitals/core";
|
|
2
2
|
import { Plugin } from "vite";
|
|
3
|
-
//#region src/plugin.d.ts
|
|
4
3
|
interface SvelteVitalsOptions {
|
|
5
4
|
/** Project root (defaults to the Vite config root / cwd). */
|
|
6
5
|
cwd?: string;
|
|
@@ -19,7 +18,7 @@ interface SvelteVitalsOptions {
|
|
|
19
18
|
report?: 'console' | 'json' | false;
|
|
20
19
|
/** Write the JSON report to this path. */
|
|
21
20
|
outFile?: string;
|
|
22
|
-
/** Override the prerendered-pages directory (default:
|
|
21
|
+
/** Override the prerendered-pages directory (default: `output/prerendered/pages` under SvelteKit's `kit.outDir`). */
|
|
23
22
|
prerenderDir?: string;
|
|
24
23
|
/**
|
|
25
24
|
* Serve a live dashboard at /__svelte-vitals/ during `vite dev` (add
|
|
@@ -31,5 +30,4 @@ interface SvelteVitalsOptions {
|
|
|
31
30
|
}
|
|
32
31
|
/** svelte-vitals Vite/SvelteKit plugin. */
|
|
33
32
|
export declare function svelteVitals(options?: SvelteVitalsOptions): Plugin | Plugin[];
|
|
34
|
-
//#endregion
|
|
35
33
|
export type { SvelteVitalsOptions };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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-D_07LGfE.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,
|
|
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
|
+
// src/providers/rendered/collect.ts
|
|
9
10
|
/** Map a prerendered HTML path (relative to pages/, POSIX) to its route. */
|
|
10
11
|
function deriveRouteFromHtmlPath(relPath) {
|
|
11
12
|
let p = relPath.replace(/\\/g, "/").replace(/\.html$/, "");
|
|
@@ -46,8 +47,8 @@ async function collectRenderedHeads(prerenderPagesDir) {
|
|
|
46
47
|
});
|
|
47
48
|
headings.push({
|
|
48
49
|
route,
|
|
49
|
-
headings: parsed.headings.map((
|
|
50
|
-
|
|
50
|
+
headings: parsed.headings.map((h) => ({
|
|
51
|
+
...h,
|
|
51
52
|
line: 0,
|
|
52
53
|
file: rel
|
|
53
54
|
}))
|
|
@@ -88,8 +89,7 @@ async function collectRenderedHeads(prerenderPagesDir) {
|
|
|
88
89
|
htmlLang
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
|
-
|
|
92
|
-
//#region src/providers/rendered/project.ts
|
|
92
|
+
// src/providers/rendered/project.ts
|
|
93
93
|
async function exists(path) {
|
|
94
94
|
try {
|
|
95
95
|
await access(path);
|
|
@@ -150,12 +150,12 @@ async function collectRenderedProject(cwd, htmlLang) {
|
|
|
150
150
|
htmlLang,
|
|
151
151
|
...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
|
|
152
152
|
...kitPathsBase ? { kitPathsBase } : {},
|
|
153
|
+
...svelteConfig && findKitHashRouterInSvelteConfig(svelteConfig.source) ? { kitHashRouter: true } : {},
|
|
153
154
|
...kitAliases ? { kitAliases } : {},
|
|
154
155
|
...componentAliases !== kitAliases ? { componentAliases } : {}
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
|
-
|
|
158
|
-
//#region src/providers/source/components.ts
|
|
158
|
+
// src/providers/source/components.ts
|
|
159
159
|
const nodeRuntime = createNodeRuntime();
|
|
160
160
|
/**
|
|
161
161
|
* Scan every `.svelte` component under `src/` for Correctness/Security/Architecture/
|
|
@@ -182,8 +182,7 @@ function collectKitModuleFacts$1(root, aliases) {
|
|
|
182
182
|
function collectSourceFiles$1(root) {
|
|
183
183
|
return collectSourceFiles(nodeRuntime, root);
|
|
184
184
|
}
|
|
185
|
-
|
|
186
|
-
//#region src/suppressions.ts
|
|
185
|
+
// src/suppressions.ts
|
|
187
186
|
/**
|
|
188
187
|
* Apply `svelte-vitals-suppressions.json` the way every plugin surface must: source-scan entries
|
|
189
188
|
* only. A route-level entry (route starting with `/`) is written by the CLI against
|
|
@@ -205,8 +204,7 @@ function applySourceSuppressions(results, entries, config) {
|
|
|
205
204
|
notices
|
|
206
205
|
};
|
|
207
206
|
}
|
|
208
|
-
|
|
209
|
-
//#region src/version.ts
|
|
207
|
+
// src/version.ts
|
|
210
208
|
/** Read this package's version from its own package.json so the report never drifts. */
|
|
211
209
|
function readPackageVersion() {
|
|
212
210
|
try {
|
|
@@ -235,8 +233,7 @@ function readCoreVersion() {
|
|
|
235
233
|
return "0.0.0";
|
|
236
234
|
}
|
|
237
235
|
}
|
|
238
|
-
|
|
239
|
-
//#region src/analyze.ts
|
|
236
|
+
// src/analyze.ts
|
|
240
237
|
/**
|
|
241
238
|
* Merge plugin options over an optional loaded config file, per-field precedence (an
|
|
242
239
|
* explicit `options` value wins, otherwise the file, otherwise the built-in default).
|
|
@@ -272,11 +269,6 @@ async function resolveConfig(cwd, options) {
|
|
|
272
269
|
warnings: loaded?.warnings ?? []
|
|
273
270
|
};
|
|
274
271
|
}
|
|
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
272
|
function escapeRegExp(s) {
|
|
281
273
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
282
274
|
}
|
|
@@ -286,27 +278,54 @@ function segmentPattern(segment) {
|
|
|
286
278
|
if (/^\[\[[^\]]+\]\]$/.test(segment)) return "(?:/[^/]+)?";
|
|
287
279
|
return "/" + segment.split(/\[[^\]]+\]/).map(escapeRegExp).join("[^/]+");
|
|
288
280
|
}
|
|
281
|
+
/** A route's page component, with an optional `@segment` layout reset. */
|
|
282
|
+
const PAGE_FILE_RE = /^src\/routes\/(.*?)\/?\+page(?:@[^/]*)?\.svelte$/;
|
|
283
|
+
/** A module that can set the `ssr` page option (same file set as core's `seo/ssr-disabled`). */
|
|
284
|
+
const PAGE_OPTION_FILE_RE = /^src\/routes\/(.*?)\/?\+(page|layout)(?:\.server)?\.[jt]s$/;
|
|
285
|
+
/** A route directory as a pattern over rendered paths: `(group)` directories are not URL segments. */
|
|
286
|
+
function routePattern(dir, subtree) {
|
|
287
|
+
const path = dir.split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
|
|
288
|
+
return new RegExp(subtree ? `^${path}(?:/.*)?$` : `^${path}$`);
|
|
289
|
+
}
|
|
289
290
|
/**
|
|
290
291
|
* Routes whose prerendered HTML is the app shell rather than the page: SvelteKit still writes a
|
|
291
292
|
* 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.
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
293
|
+
* so every rendered head/heading/landmark check would report the page as empty. Each page takes the
|
|
294
|
+
* nearest `ssr` export along its own files and its layout chain, resets followed, as the CLI reads it
|
|
295
|
+
* (`routeNeverSsr`), so a page that sets `ssr = true` under a disabled layout is read. Only a literal
|
|
296
|
+
* `true` turns SSR back on here: an option file that did not parse, or an `ssr` export the analyzer
|
|
297
|
+
* cannot read (an imported value), inherits instead, since analyzing a shell would report it empty,
|
|
298
|
+
* and a route no page in `sourceFiles` can match is a shell when any `ssr = false` module's
|
|
299
|
+
* route covers it. Kit's route precedence is not modelled: a route is a shell when any page whose
|
|
300
|
+
* path can match it is, so `/docs/[slug]` with `ssr = false` also claims a prerendered `/docs/intro`
|
|
301
|
+
* — the warning lists the skipped routes so a wrong skip is visible.
|
|
298
302
|
*/
|
|
299
|
-
function ssrDisabledRouteMatcher(kitModules) {
|
|
300
|
-
const
|
|
303
|
+
function ssrDisabledRouteMatcher(kitModules, sourceFiles) {
|
|
304
|
+
const scope = {
|
|
305
|
+
kitModules: kitModules.map((m) => m.parseFailed || m.ssrEnabled && !m.ssrTrue ? {
|
|
306
|
+
...m,
|
|
307
|
+
parseFailed: void 0,
|
|
308
|
+
ssrEnabled: void 0
|
|
309
|
+
} : m),
|
|
310
|
+
sourceFiles: [...sourceFiles]
|
|
311
|
+
};
|
|
312
|
+
const pages = [];
|
|
313
|
+
for (const file of sourceFiles) {
|
|
314
|
+
const match = PAGE_FILE_RE.exec(file);
|
|
315
|
+
if (match) pages.push({
|
|
316
|
+
pattern: routePattern(match[1], false),
|
|
317
|
+
shell: routeNeverSsr(file, scope)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const disabled = [];
|
|
301
321
|
for (const m of kitModules) {
|
|
302
322
|
const match = m.ssrDisabled ? PAGE_OPTION_FILE_RE.exec(m.file) : null;
|
|
303
|
-
if (
|
|
304
|
-
const path = match[1].split("/").filter((s) => s !== "" && !/^\(.+\)$/.test(s)).map(segmentPattern).join("");
|
|
305
|
-
patterns.push(new RegExp(match[2] === "layout" ? `^${path}(?:/.*)?$` : `^${path}$`));
|
|
323
|
+
if (match) disabled.push(routePattern(match[1], match[2] === "layout"));
|
|
306
324
|
}
|
|
307
325
|
return (route) => {
|
|
308
326
|
const r = route === "/" ? "" : route;
|
|
309
|
-
|
|
327
|
+
const matched = pages.filter((p) => p.pattern.test(r));
|
|
328
|
+
return matched.length > 0 ? matched.some((p) => p.shell) : disabled.some((p) => p.test(r));
|
|
310
329
|
};
|
|
311
330
|
}
|
|
312
331
|
/**
|
|
@@ -329,7 +348,8 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
329
348
|
...extraProjectFacts
|
|
330
349
|
};
|
|
331
350
|
const [kitModules, components] = await Promise.all([collectKitModuleFacts$1(cwd, project.kitAliases), dropConstantListEachBlocks$1(cwd, componentFacts, project.componentAliases ?? project.kitAliases)]);
|
|
332
|
-
const isShell = ssrDisabledRouteMatcher(kitModules);
|
|
351
|
+
const isShell = project.kitHashRouter ? () => true : ssrDisabledRouteMatcher(kitModules, sourceFiles);
|
|
352
|
+
const shellCause = project.kitHashRouter ? "the hash router" : "ssr = false";
|
|
333
353
|
const keep = (items) => items.filter((i) => !isShell(i.route));
|
|
334
354
|
const heads = keep(rendered.heads);
|
|
335
355
|
const headings = keep(rendered.headings);
|
|
@@ -340,7 +360,7 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
340
360
|
if (shells.length > 0) {
|
|
341
361
|
const shown = shells.slice(0, 10);
|
|
342
362
|
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
|
|
363
|
+
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
364
|
}
|
|
345
365
|
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
366
|
const directives = /* @__PURE__ */ new Map();
|
|
@@ -373,24 +393,26 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
|
|
|
373
393
|
const { score } = computeScore(results, scoringConfig);
|
|
374
394
|
const summary = summarize(results, scoringConfig);
|
|
375
395
|
const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
|
|
376
|
-
const coverageNote = `Analyzed ${heads.length} prerendered route(s)${shells.length > 0 ? ` (skipped ${shells.length} with
|
|
396
|
+
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
397
|
Scanned ${components.length} component(s) under src/ for Correctness/Security/Architecture/Accessibility/Bundle findings.`;
|
|
378
398
|
const consoleReport = formatConsoleReport(results, scoringConfig, { mode: "rendered / plugin" }) + "\n" + coverageNote + "\n";
|
|
379
|
-
const
|
|
399
|
+
const jsonReportChunks = () => formatJsonReportChunks(results, scoringConfig, { version: readPackageVersion() }, selected.map((r) => r.id), examined);
|
|
380
400
|
return {
|
|
381
401
|
score,
|
|
382
402
|
summary,
|
|
383
403
|
results,
|
|
384
404
|
consoleReport,
|
|
385
|
-
jsonReport
|
|
405
|
+
get jsonReport() {
|
|
406
|
+
return [...jsonReportChunks()].join("");
|
|
407
|
+
},
|
|
408
|
+
jsonReportChunks,
|
|
386
409
|
routeCount: rendered.heads.length,
|
|
387
410
|
failed,
|
|
388
411
|
failOn: scoringConfig.failOn,
|
|
389
412
|
warnings
|
|
390
413
|
};
|
|
391
414
|
}
|
|
392
|
-
|
|
393
|
-
//#region src/minify-flag.ts
|
|
415
|
+
// src/minify-flag.ts
|
|
394
416
|
/**
|
|
395
417
|
* performance/minify-disabled fact from the evaluated Vite user config (the value the plugin's
|
|
396
418
|
* `config` hook sees) — exact, so it also catches function-form/conditional configs the CLI's
|
|
@@ -421,8 +443,7 @@ async function resolveMinifyDisabled(minify, configFile, root) {
|
|
|
421
443
|
...suppressions !== void 0 ? { suppressions } : {}
|
|
422
444
|
};
|
|
423
445
|
}
|
|
424
|
-
|
|
425
|
-
//#region src/ui/snapshot.ts
|
|
446
|
+
// src/ui/snapshot.ts
|
|
426
447
|
/**
|
|
427
448
|
* `docsUrl` on an ingested (live) result flows into `/data.json` responses, which the
|
|
428
449
|
* client script renders into an <a href> without going through `renderAppShell`'s own
|
|
@@ -467,8 +488,7 @@ function buildSnapshot(store, config, meta, staticFailedRuleIds) {
|
|
|
467
488
|
meta
|
|
468
489
|
};
|
|
469
490
|
}
|
|
470
|
-
|
|
471
|
-
//#region src/ui/middleware.ts
|
|
491
|
+
// src/ui/middleware.ts
|
|
472
492
|
const SEVERITIES = /* @__PURE__ */ new Set([
|
|
473
493
|
"critical",
|
|
474
494
|
"warning",
|
|
@@ -593,8 +613,7 @@ function installUiMiddleware(server, config, version, store, coreVersion, getSta
|
|
|
593
613
|
}
|
|
594
614
|
});
|
|
595
615
|
}
|
|
596
|
-
|
|
597
|
-
//#region src/ui/store.ts
|
|
616
|
+
// src/ui/store.ts
|
|
598
617
|
/**
|
|
599
618
|
* Merge rule (design doc §2): for a route with a live result set, static results whose
|
|
600
619
|
* rule id appears in the live payload are replaced by the live ones — the handle reports
|
|
@@ -694,8 +713,7 @@ function createStore() {
|
|
|
694
713
|
}
|
|
695
714
|
};
|
|
696
715
|
}
|
|
697
|
-
|
|
698
|
-
//#region src/ui/analysis.ts
|
|
716
|
+
// src/ui/analysis.ts
|
|
699
717
|
/**
|
|
700
718
|
* Apply `svelte-vitals-suppressions.json` to the whole-project layer, same entries as the build
|
|
701
719
|
* gate (see `applySourceSuppressions`). A malformed file is a warning here, not an error: dev
|
|
@@ -794,8 +812,7 @@ function createAnalysisRunner(opts) {
|
|
|
794
812
|
}
|
|
795
813
|
};
|
|
796
814
|
}
|
|
797
|
-
|
|
798
|
-
//#region src/plugin.ts
|
|
815
|
+
// src/plugin.ts
|
|
799
816
|
const CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
800
817
|
"svelte.config.js",
|
|
801
818
|
"svelte.config.ts",
|
|
@@ -925,6 +942,7 @@ function svelteVitals(options = {}) {
|
|
|
925
942
|
let minify;
|
|
926
943
|
let configFile;
|
|
927
944
|
let ssrBuild = false;
|
|
945
|
+
let prerenderDir = DEFAULT_PRERENDER_DIR;
|
|
928
946
|
const buildPlugin = {
|
|
929
947
|
name: "svelte-vitals",
|
|
930
948
|
apply: "build",
|
|
@@ -936,9 +954,11 @@ function svelteVitals(options = {}) {
|
|
|
936
954
|
if (!options.cwd) root = config.root;
|
|
937
955
|
configFile = config.configFile;
|
|
938
956
|
ssrBuild = Boolean(config.build?.ssr);
|
|
957
|
+
const out = config.build?.outDir ? resolve(config.root ?? root, config.build.outDir) : "";
|
|
958
|
+
if (/[\\/]output[\\/](?:server|client)$/.test(out)) prerenderDir = join(dirname(out), "prerendered/pages");
|
|
939
959
|
},
|
|
940
960
|
async closeBundle() {
|
|
941
|
-
const pagesDir = options.prerenderDir ? options.prerenderDir :
|
|
961
|
+
const pagesDir = options.prerenderDir ? options.prerenderDir : prerenderDir;
|
|
942
962
|
const resolved = isAbsolute(pagesDir) ? pagesDir : join(root, pagesDir);
|
|
943
963
|
if (!ssrBuild && !existsSync(resolved)) return;
|
|
944
964
|
const resolvedConfig = {
|
|
@@ -956,16 +976,21 @@ function svelteVitals(options = {}) {
|
|
|
956
976
|
for (const w of result.warnings) warn(`svelte-vitals: ${w}`);
|
|
957
977
|
if (result.routeCount === 0) {
|
|
958
978
|
if (!ssrBuild) return;
|
|
959
|
-
warn(`svelte-vitals: no prerendered pages found in ${
|
|
979
|
+
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
980
|
}
|
|
961
981
|
if (options.report !== false) {
|
|
962
|
-
|
|
963
|
-
|
|
982
|
+
if (options.report === "json") {
|
|
983
|
+
for (const chunk of result.jsonReportChunks()) {
|
|
984
|
+
if (process.stdout.destroyed) break;
|
|
985
|
+
if (!process.stdout.write(chunk)) await once(process.stdout, "drain");
|
|
986
|
+
}
|
|
987
|
+
process.stdout.write("\n");
|
|
988
|
+
} else console.log(result.consoleReport);
|
|
964
989
|
}
|
|
965
990
|
if (options.outFile) {
|
|
966
991
|
const outPath = isAbsolute(options.outFile) ? options.outFile : join(root, options.outFile);
|
|
967
992
|
await mkdir(dirname(outPath), { recursive: true });
|
|
968
|
-
await writeFile(outPath, result.
|
|
993
|
+
await writeFile(outPath, result.jsonReportChunks());
|
|
969
994
|
}
|
|
970
995
|
if (result.failed) throw new Error(`svelte-vitals: build failed — findings at or above "${result.failOn}".`);
|
|
971
996
|
}
|
|
@@ -1042,5 +1067,4 @@ function svelteVitals(options = {}) {
|
|
|
1042
1067
|
}
|
|
1043
1068
|
}];
|
|
1044
1069
|
}
|
|
1045
|
-
//#endregion
|
|
1046
1070
|
export { svelteVitals };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
+
// src/providers/rendered/parse-html.ts
|
|
4
4
|
/** The <img> is its `<picture>`'s fallback and a sibling `<source>` carries the srcset. */
|
|
5
5
|
function pictureOffersSrcset(img) {
|
|
6
6
|
const parent = img.parentNode;
|
|
@@ -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").
|
|
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"),
|
|
@@ -194,8 +202,7 @@ function toOccurrenceMap(keys, file) {
|
|
|
194
202
|
});
|
|
195
203
|
return out;
|
|
196
204
|
}
|
|
197
|
-
|
|
198
|
-
//#region src/loopback.ts
|
|
205
|
+
// src/loopback.ts
|
|
199
206
|
/** Only the local dev server hosts the ingest endpoint, so never POST off-box. */
|
|
200
207
|
function isLoopbackOrigin(origin) {
|
|
201
208
|
try {
|
|
@@ -226,5 +233,4 @@ function isSameOrigin(origin, host, secure = false) {
|
|
|
226
233
|
function effectivePort(url) {
|
|
227
234
|
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
228
235
|
}
|
|
229
|
-
//#endregion
|
|
230
236
|
export { toOccurrenceMap as i, isSameOrigin as n, parseHtmlHead as r, isLoopbackOrigin as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svelte-vitals/vite",
|
|
3
|
-
"version": "0.55.
|
|
3
|
+
"version": "0.55.4",
|
|
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,12 +40,12 @@
|
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"esm-env": "^1.2.2",
|
|
43
|
-
"node-html-parser": "^9.0.
|
|
43
|
+
"node-html-parser": "^9.0.7"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@svelte-vitals/core": "^0.55.
|
|
46
|
+
"@svelte-vitals/core": "^0.55.4",
|
|
47
47
|
"@sveltejs/kit": "^2.0.0",
|
|
48
|
-
"svelte-vitals": "^0.55.
|
|
48
|
+
"svelte-vitals": "^0.55.4",
|
|
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.
|
|
58
|
-
"@sveltejs/kit": "^2.70.
|
|
59
|
-
"@types/node": "^24.
|
|
60
|
-
"happy-dom": "^
|
|
61
|
-
"svelte": "^5.57.
|
|
62
|
-
"svelte-vitals": "0.55.
|
|
63
|
-
"vite": "^8.3.
|
|
57
|
+
"@svelte-vitals/core": "0.55.4",
|
|
58
|
+
"@sveltejs/kit": "^2.70.4",
|
|
59
|
+
"@types/node": "^24.19.2",
|
|
60
|
+
"happy-dom": "^21.0.4",
|
|
61
|
+
"svelte": "^5.57.3",
|
|
62
|
+
"svelte-vitals": "0.55.4",
|
|
63
|
+
"vite": "^8.3.6"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsdown",
|