@svelte-vitals/vite 0.36.3 → 0.36.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/dist/hooks/index.js +35 -11
- package/dist/index.js +140 -36
- package/dist/{loopback-CE47i4VD.js → loopback-D9pGJXl6.js} +1 -106
- package/package.json +6 -6
package/dist/hooks/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as toOccurrenceMap,
|
|
1
|
+
import { i as toOccurrenceMap, r as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-D9pGJXl6.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";
|
|
@@ -8,24 +8,37 @@ 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("|");
|
|
9
9
|
}
|
|
10
10
|
const warn = (line) => console.warn(terminalSafe(line));
|
|
11
|
+
/** A dashboard that never answers must not block the route's later ingests forever. */
|
|
12
|
+
const INGEST_TIMEOUT_MS = 1e4;
|
|
13
|
+
/** True only when the dashboard acknowledged the POST. */
|
|
11
14
|
async function postIngest(origin, route, results, failedRuleIds) {
|
|
12
15
|
if (!isLoopbackOrigin(origin)) {
|
|
13
16
|
if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) warn(`svelte-vitals: live UI ingest skipped for non-loopback origin ${origin} — open the dashboard via localhost`);
|
|
14
|
-
return;
|
|
17
|
+
return false;
|
|
15
18
|
}
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timer = setTimeout(() => controller.abort(), INGEST_TIMEOUT_MS);
|
|
16
21
|
try {
|
|
17
|
-
await fetch(`${origin}/__svelte-vitals/ingest`, {
|
|
22
|
+
const res = await fetch(`${origin}/__svelte-vitals/ingest`, {
|
|
18
23
|
method: "POST",
|
|
19
24
|
headers: { "content-type": "application/json" },
|
|
20
25
|
body: JSON.stringify({
|
|
21
26
|
route,
|
|
22
27
|
results,
|
|
23
28
|
failedRuleIds
|
|
24
|
-
})
|
|
29
|
+
}),
|
|
30
|
+
signal: controller.signal
|
|
25
31
|
});
|
|
26
|
-
|
|
32
|
+
if (!res.ok && globalThis.process?.env?.SVELTE_VITALS_DEBUG) warn(`svelte-vitals: live UI ingest for ${route} rejected with HTTP ${res.status}`);
|
|
33
|
+
return res.ok;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) warn(`svelte-vitals: live UI ingest for ${route} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
36
|
+
return false;
|
|
37
|
+
} finally {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
|
-
async function analyzeAndIngest(html, route, origin, rules, config,
|
|
41
|
+
async function analyzeAndIngest(html, route, origin, rules, config, queued, inflight) {
|
|
29
42
|
try {
|
|
30
43
|
const { tags, headings: levels, images: imgs, landmarks, nestedLandmarks, ids, idRefs } = parseHtmlHead(html);
|
|
31
44
|
const head = {
|
|
@@ -76,9 +89,19 @@ async function analyzeAndIngest(html, route, origin, rules, config, lastSignatur
|
|
|
76
89
|
}, /* @__PURE__ */ new Map());
|
|
77
90
|
if (failedRules.length > 0 && globalThis.process?.env?.SVELTE_VITALS_DEBUG) for (const f of failedRules) warn(formatFailedRuleWarning(f));
|
|
78
91
|
const signature = `${findingSignature(results, config)}|failed:${[...failedRuleIds].sort().join(",")}`;
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
92
|
+
if (queued.get(route)?.signature === signature) return;
|
|
93
|
+
if (!globalThis.process?.env?.SVELTE_VITALS_UI) return;
|
|
94
|
+
const entry = { signature };
|
|
95
|
+
queued.set(route, entry);
|
|
96
|
+
const next = (inflight.get(route) ?? Promise.resolve()).then(async () => {
|
|
97
|
+
if (!await postIngest(origin, route, results, failedRuleIds) && queued.get(route) === entry) queued.delete(route);
|
|
98
|
+
});
|
|
99
|
+
inflight.set(route, next);
|
|
100
|
+
try {
|
|
101
|
+
await next;
|
|
102
|
+
} finally {
|
|
103
|
+
if (inflight.get(route) === next) inflight.delete(route);
|
|
104
|
+
}
|
|
82
105
|
} catch (err) {
|
|
83
106
|
if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) warn(`svelte-vitals: dev analysis failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
84
107
|
}
|
|
@@ -96,12 +119,13 @@ function svelteVitalsHandle(options = {}) {
|
|
|
96
119
|
rules: options.rules ?? {}
|
|
97
120
|
});
|
|
98
121
|
const rules = selectRules(allRules, config).filter((r) => r.scope === "route" && !r.crossRoute);
|
|
99
|
-
const
|
|
122
|
+
const queued = /* @__PURE__ */ new Map();
|
|
123
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
100
124
|
return ({ event, resolve }) => {
|
|
101
125
|
let buffer = "";
|
|
102
126
|
return resolve(event, { transformPageChunk: ({ html, done }) => {
|
|
103
127
|
buffer += html;
|
|
104
|
-
if (done && event.route.id != null) analyzeAndIngest(buffer, event.route.id, event.url.origin, rules, config,
|
|
128
|
+
if (done && event.route.id != null) analyzeAndIngest(buffer, event.route.id, event.url.origin, rules, config, queued, inflight);
|
|
105
129
|
return html;
|
|
106
130
|
} });
|
|
107
131
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,88 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as toOccurrenceMap, n as isSameOrigin, r as parseHtmlHead, t as isLoopbackOrigin } from "./loopback-D9pGJXl6.js";
|
|
2
2
|
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
|
|
7
|
-
import { CONFIG_FILENAMES, analyzeProject, 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, 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";
|
|
8
|
+
//#region src/providers/rendered/collect.ts
|
|
9
|
+
/** Map a prerendered HTML path (relative to pages/, POSIX) to its route. */
|
|
10
|
+
function deriveRouteFromHtmlPath(relPath) {
|
|
11
|
+
let p = relPath.replace(/\\/g, "/").replace(/\.html$/, "");
|
|
12
|
+
if (p === "index") return "/";
|
|
13
|
+
if (p.endsWith("/index")) p = p.slice(0, -6);
|
|
14
|
+
return "/" + p;
|
|
15
|
+
}
|
|
16
|
+
/** Read every prerendered HTML page under `prerenderPagesDir` into ResolvedHead[]. */
|
|
17
|
+
async function collectRenderedHeads(prerenderPagesDir) {
|
|
18
|
+
const rt = createNodeRuntime();
|
|
19
|
+
const files = (await rt.glob("**/*.html", prerenderPagesDir)).sort();
|
|
20
|
+
const parsedFiles = await Promise.all(files.map(async (rel) => ({
|
|
21
|
+
rel,
|
|
22
|
+
parsed: parseHtmlHead(await rt.readFile(rt.join(prerenderPagesDir, rel)))
|
|
23
|
+
})));
|
|
24
|
+
const heads = [];
|
|
25
|
+
const headings = [];
|
|
26
|
+
const images = [];
|
|
27
|
+
const a11y = [];
|
|
28
|
+
let htmlLang = {
|
|
29
|
+
presence: "none",
|
|
30
|
+
value: "absent"
|
|
31
|
+
};
|
|
32
|
+
for (const { rel, parsed } of parsedFiles) {
|
|
33
|
+
if (htmlLang.presence === "none" && parsed.htmlLang.presence === "own") htmlLang = parsed.htmlLang;
|
|
34
|
+
const route = deriveRouteFromHtmlPath(rel);
|
|
35
|
+
heads.push({
|
|
36
|
+
route,
|
|
37
|
+
source: "rendered",
|
|
38
|
+
tags: parsed.tags,
|
|
39
|
+
file: rel
|
|
40
|
+
});
|
|
41
|
+
headings.push({
|
|
42
|
+
route,
|
|
43
|
+
headings: parsed.headings.map((level) => ({
|
|
44
|
+
level,
|
|
45
|
+
line: 0,
|
|
46
|
+
file: rel
|
|
47
|
+
}))
|
|
48
|
+
});
|
|
49
|
+
images.push({
|
|
50
|
+
route,
|
|
51
|
+
images: parsed.images.map((img) => ({
|
|
52
|
+
...img,
|
|
53
|
+
file: rel
|
|
54
|
+
}))
|
|
55
|
+
});
|
|
56
|
+
a11y.push({
|
|
57
|
+
route,
|
|
58
|
+
landmarks: toOccurrenceMap(parsed.landmarks, rel),
|
|
59
|
+
nestedLandmarks: parsed.nestedLandmarks.map((n) => ({
|
|
60
|
+
...n,
|
|
61
|
+
file: rel,
|
|
62
|
+
line: 0
|
|
63
|
+
})),
|
|
64
|
+
ids: toOccurrenceMap(parsed.ids, rel),
|
|
65
|
+
idRefs: parsed.idRefs.map((r) => ({
|
|
66
|
+
...r,
|
|
67
|
+
file: rel,
|
|
68
|
+
line: 0
|
|
69
|
+
})),
|
|
70
|
+
idCandidates: [...new Set(parsed.ids)],
|
|
71
|
+
fullyResolved: true,
|
|
72
|
+
elementTags: parsed.elementTags,
|
|
73
|
+
elementsClosed: true,
|
|
74
|
+
file: rel
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
heads,
|
|
79
|
+
headings,
|
|
80
|
+
images,
|
|
81
|
+
a11y,
|
|
82
|
+
htmlLang
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
8
86
|
//#region src/providers/rendered/project.ts
|
|
9
87
|
async function exists(path) {
|
|
10
88
|
try {
|
|
@@ -61,25 +139,7 @@ async function collectRenderedProject(cwd, htmlLang) {
|
|
|
61
139
|
}
|
|
62
140
|
//#endregion
|
|
63
141
|
//#region src/providers/source/components.ts
|
|
64
|
-
|
|
65
|
-
* Node-backed Runtime adapter (design §8). vite always runs in Node, so no
|
|
66
|
-
* swappable runtime is needed here — this just satisfies the interface the
|
|
67
|
-
* shared core implementation expects.
|
|
68
|
-
*/
|
|
69
|
-
const boundedRead = withReadLimit((path) => readFile(path, "utf8"));
|
|
70
|
-
const nodeRuntime = {
|
|
71
|
-
readFile: (path) => boundedRead(path),
|
|
72
|
-
async exists(path) {
|
|
73
|
-
try {
|
|
74
|
-
await access(path);
|
|
75
|
-
return true;
|
|
76
|
-
} catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
glob: (pattern, cwd) => globFiles(pattern, cwd),
|
|
81
|
-
join: (...parts) => join(...parts)
|
|
82
|
-
};
|
|
142
|
+
const nodeRuntime = createNodeRuntime();
|
|
83
143
|
/**
|
|
84
144
|
* Scan every `.svelte` component under `src/` for Correctness/Security/Architecture/
|
|
85
145
|
* Bundle-Performance/Accessibility facts (build mode only). Implementation lives in
|
|
@@ -330,6 +390,7 @@ function isResultLike(x) {
|
|
|
330
390
|
*/
|
|
331
391
|
function installUiMiddleware(server, config, version, store, coreVersion, getStaticFailedRuleIds) {
|
|
332
392
|
const clients = /* @__PURE__ */ new Set();
|
|
393
|
+
const currentConfig = () => typeof config === "function" ? config() : config;
|
|
333
394
|
store.subscribe(() => {
|
|
334
395
|
for (const res of clients) try {
|
|
335
396
|
res.write("event: update\ndata: {}\n\n");
|
|
@@ -396,7 +457,7 @@ function installUiMiddleware(server, config, version, store, coreVersion, getSta
|
|
|
396
457
|
}
|
|
397
458
|
if (url.startsWith("/data.json")) {
|
|
398
459
|
try {
|
|
399
|
-
const snapshot = buildSnapshot(store,
|
|
460
|
+
const snapshot = buildSnapshot(store, currentConfig(), {
|
|
400
461
|
version,
|
|
401
462
|
coreVersion
|
|
402
463
|
}, getStaticFailedRuleIds?.());
|
|
@@ -409,7 +470,7 @@ function installUiMiddleware(server, config, version, store, coreVersion, getSta
|
|
|
409
470
|
return;
|
|
410
471
|
}
|
|
411
472
|
try {
|
|
412
|
-
const html = renderAppShell(buildSnapshot(store,
|
|
473
|
+
const html = renderAppShell(buildSnapshot(store, currentConfig(), {
|
|
413
474
|
version,
|
|
414
475
|
coreVersion
|
|
415
476
|
}, getStaticFailedRuleIds?.()));
|
|
@@ -456,6 +517,12 @@ function composeBadges(staticResults, liveByRoute) {
|
|
|
456
517
|
for (const route of liveByRoute.keys()) badges[route] = "measured";
|
|
457
518
|
return badges;
|
|
458
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* In-memory findings store for the dev UI. Owned by the ui plugin, shared between the
|
|
522
|
+
* analysis runner (writes the static, whole-project layer) and the ingest middleware
|
|
523
|
+
* (writes the live, per-route layer from rendered pages). `snapshot()`/`badges()` expose
|
|
524
|
+
* the merged view per the design doc (2026-07-08-dev-dashboard-whole-project-design.md §2).
|
|
525
|
+
*/
|
|
459
526
|
function createStore() {
|
|
460
527
|
let staticResults = [];
|
|
461
528
|
const liveByRoute = /* @__PURE__ */ new Map();
|
|
@@ -468,6 +535,11 @@ function createStore() {
|
|
|
468
535
|
for (const fn of subs) fn();
|
|
469
536
|
}
|
|
470
537
|
return {
|
|
538
|
+
/**
|
|
539
|
+
* Replace a route's live findings (route stamped onto results missing one) and notify
|
|
540
|
+
* subscribers. `failedRuleIds` replaces that route's live-layer failed-rule set — an
|
|
541
|
+
* omitted or empty array clears it, so a route re-analyzed with no failures recovers.
|
|
542
|
+
*/
|
|
471
543
|
set(route, results, failedRuleIds) {
|
|
472
544
|
liveByRoute.set(route, results.map((r) => r.route ? r : {
|
|
473
545
|
...r,
|
|
@@ -481,6 +553,7 @@ function createStore() {
|
|
|
481
553
|
staticResults = results;
|
|
482
554
|
notify();
|
|
483
555
|
},
|
|
556
|
+
/** Whether a whole-project analysis run is in flight; participates in subscribe/notify like a findings change. */
|
|
484
557
|
setAnalyzing(next) {
|
|
485
558
|
analyzing = next;
|
|
486
559
|
notify();
|
|
@@ -488,15 +561,19 @@ function createStore() {
|
|
|
488
561
|
isAnalyzing() {
|
|
489
562
|
return analyzing;
|
|
490
563
|
},
|
|
564
|
+
/** Composed findings across both layers — feed straight into buildJsonReport. */
|
|
491
565
|
snapshot() {
|
|
492
566
|
return composeSnapshot(staticResults, liveByRoute);
|
|
493
567
|
},
|
|
568
|
+
/** Per-route provenance for the dashboard's badges: 'measured' (live) or 'static'. */
|
|
494
569
|
badges() {
|
|
495
570
|
return composeBadges(staticResults, liveByRoute);
|
|
496
571
|
},
|
|
572
|
+
/** Union of failed rule ids across every live (ingested) route, for `withFailedRulesOff`. */
|
|
497
573
|
failedRuleIds() {
|
|
498
574
|
return [...new Set([...liveFailedByRoute.values()].flat())].sort();
|
|
499
575
|
},
|
|
576
|
+
/** Bumped once per notify() — lets consumers discard stale fetches. */
|
|
500
577
|
sequence() {
|
|
501
578
|
return seq;
|
|
502
579
|
},
|
|
@@ -521,12 +598,13 @@ function createAnalysisRunner(opts) {
|
|
|
521
598
|
let running = false;
|
|
522
599
|
let pending = false;
|
|
523
600
|
let timer;
|
|
601
|
+
let lastWarningsKey;
|
|
524
602
|
async function runOnce() {
|
|
525
603
|
if (stopped) return;
|
|
526
604
|
running = true;
|
|
527
605
|
opts.onStatusChange?.(true);
|
|
528
606
|
try {
|
|
529
|
-
const
|
|
607
|
+
const result = await analyze({
|
|
530
608
|
cwd: opts.root,
|
|
531
609
|
treatDynamicAs: opts.treatDynamicAs,
|
|
532
610
|
metaComponents: opts.metaComponents,
|
|
@@ -534,8 +612,13 @@ function createAnalysisRunner(opts) {
|
|
|
534
612
|
failOn: opts.failOn,
|
|
535
613
|
parseCache
|
|
536
614
|
});
|
|
615
|
+
const { results, failedRuleIds } = result;
|
|
537
616
|
if (!stopped) if (failedRuleIds !== void 0) opts.onResults(results, failedRuleIds);
|
|
538
617
|
else opts.onResults(results);
|
|
618
|
+
const warnings = result.warnings ?? [];
|
|
619
|
+
const key = warnings.join("\n");
|
|
620
|
+
if (!stopped && warnings.length > 0 && key !== lastWarningsKey) opts.onWarnings?.(warnings);
|
|
621
|
+
lastWarningsKey = key;
|
|
539
622
|
} catch (err) {
|
|
540
623
|
if (!stopped) opts.onError(err);
|
|
541
624
|
} finally {
|
|
@@ -548,10 +631,12 @@ function createAnalysisRunner(opts) {
|
|
|
548
631
|
}
|
|
549
632
|
}
|
|
550
633
|
return {
|
|
634
|
+
/** Kick off the first whole-project analysis. Fire-and-forget — never blocks the caller. */
|
|
551
635
|
start() {
|
|
552
636
|
if (stopped) return;
|
|
553
637
|
runOnce();
|
|
554
638
|
},
|
|
639
|
+
/** A relevant source file changed; schedule a debounced re-analysis. */
|
|
555
640
|
notifyChange(file) {
|
|
556
641
|
if (stopped) return;
|
|
557
642
|
const rel = relative(opts.root, file).split(sep).join("/");
|
|
@@ -736,15 +821,23 @@ function svelteVitals(options = {}) {
|
|
|
736
821
|
process.env.SVELTE_VITALS_UI = "1";
|
|
737
822
|
const uiRoot = options.cwd ?? server.config.root;
|
|
738
823
|
let config;
|
|
739
|
-
let
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
824
|
+
let printedConfigWarnings = /* @__PURE__ */ new Set();
|
|
825
|
+
let configGeneration = 0;
|
|
826
|
+
const applyConfig = async () => {
|
|
827
|
+
const generation = ++configGeneration;
|
|
828
|
+
try {
|
|
829
|
+
const resolved = await resolveConfig(uiRoot, options);
|
|
830
|
+
if (generation !== configGeneration) return;
|
|
831
|
+
config = resolved.config;
|
|
832
|
+
printedConfigWarnings = new Set(resolved.warnings);
|
|
833
|
+
for (const w of resolved.warnings) warn(`svelte-vitals: ${w}`);
|
|
834
|
+
} catch (err) {
|
|
835
|
+
if (generation !== configGeneration) return;
|
|
836
|
+
warn(`svelte-vitals: config file invalid — dashboard using ${config ? "the previous config" : "plugin options/defaults"}: ${err instanceof Error ? err.message : String(err)}`);
|
|
837
|
+
config ??= mergeConfig(options, void 0);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
await applyConfig();
|
|
748
841
|
const store = createStore();
|
|
749
842
|
let staticFailedRuleIds = [];
|
|
750
843
|
const runner = createAnalysisRunner({
|
|
@@ -758,17 +851,28 @@ function svelteVitals(options = {}) {
|
|
|
758
851
|
staticFailedRuleIds = failedRuleIds ?? [];
|
|
759
852
|
},
|
|
760
853
|
onError: (err) => warn(`svelte-vitals: dev analysis failed: ${err instanceof Error ? err.message : String(err)}`),
|
|
854
|
+
onWarnings: (warnings) => {
|
|
855
|
+
for (const w of warnings) if (!printedConfigWarnings.has(w)) warn(`svelte-vitals: ${w}`);
|
|
856
|
+
},
|
|
761
857
|
onStatusChange: (analyzing) => store.setAnalyzing(analyzing)
|
|
762
858
|
});
|
|
763
859
|
runner.start();
|
|
860
|
+
let configTimer;
|
|
764
861
|
server.watcher?.on("all", (_event, file) => {
|
|
765
|
-
if (isRelevant(file, uiRoot))
|
|
862
|
+
if (!isRelevant(file, uiRoot)) return;
|
|
863
|
+
if (CONFIG_FILENAMES.includes(basename(file))) {
|
|
864
|
+
clearTimeout(configTimer);
|
|
865
|
+
configTimer = setTimeout(() => void applyConfig().then(() => runner.notifyChange(file)), 500);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
runner.notifyChange(file);
|
|
766
869
|
});
|
|
767
870
|
server.httpServer?.once("close", () => {
|
|
768
871
|
delete process.env.SVELTE_VITALS_UI;
|
|
872
|
+
clearTimeout(configTimer);
|
|
769
873
|
runner.stop();
|
|
770
874
|
});
|
|
771
|
-
installUiMiddleware(server, config, readPackageVersion(), store, readCoreVersion(), () => staticFailedRuleIds);
|
|
875
|
+
installUiMiddleware(server, () => config, readPackageVersion(), store, readCoreVersion(), () => staticFailedRuleIds);
|
|
772
876
|
if (typeof server.printUrls === "function") {
|
|
773
877
|
const printUrls = server.printUrls.bind(server);
|
|
774
878
|
server.printUrls = () => {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { glob, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { join, relative, sep } from "node:path";
|
|
3
1
|
import { ASIDE_DEMOTING_TAGS, IDREF_ATTRS, NAMING_ATTRS, SECTIONING_TAGS, decodeFragmentId, isClassicScriptType, isTopFragment, resolveLandmark, splitTokens, stripTextDirective } from "@svelte-vitals/core/internal";
|
|
4
2
|
import { parse } from "node-html-parser";
|
|
5
3
|
//#region src/providers/rendered/parse-html.ts
|
|
@@ -180,34 +178,6 @@ function parseHtmlHead(html) {
|
|
|
180
178
|
...collectA11y(root)
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
|
-
//#endregion
|
|
184
|
-
//#region src/glob.ts
|
|
185
|
-
/**
|
|
186
|
-
* `node:fs` glob constrained to the contract callers rely on: files only (fs.glob also matches
|
|
187
|
-
* directories), cwd-relative POSIX-separated paths on every platform (fs.glob returns platform
|
|
188
|
-
* separators), dotfiles excluded (fs.glob's default). A symlink's Dirent never reports isFile(),
|
|
189
|
-
* so symlinks are stat'd and kept when they point at a file; symlinked *directories* are not
|
|
190
|
-
* traversed — fs.glob never descends into them. Duplicated in the CLI package — core purity
|
|
191
|
-
* (no `node:` imports in core) rules out the shared home.
|
|
192
|
-
*/
|
|
193
|
-
async function globFiles(pattern, cwd) {
|
|
194
|
-
const out = [];
|
|
195
|
-
for await (const entry of glob(pattern, {
|
|
196
|
-
cwd,
|
|
197
|
-
withFileTypes: true
|
|
198
|
-
})) {
|
|
199
|
-
const path = join(entry.parentPath, entry.name);
|
|
200
|
-
if (!entry.isFile()) {
|
|
201
|
-
if (!entry.isSymbolicLink()) continue;
|
|
202
|
-
if (!(await stat(path).catch(() => null))?.isFile()) continue;
|
|
203
|
-
}
|
|
204
|
-
const rel = relative(cwd, path);
|
|
205
|
-
out.push(sep === "/" ? rel : rel.split(sep).join("/"));
|
|
206
|
-
}
|
|
207
|
-
return out;
|
|
208
|
-
}
|
|
209
|
-
//#endregion
|
|
210
|
-
//#region src/providers/rendered/collect.ts
|
|
211
181
|
/** Group raw occurrence keys (one entry per hit, in document order) by key, `file` attached, `line: 0` (rendered mode does not track source lines). */
|
|
212
182
|
function toOccurrenceMap(keys, file) {
|
|
213
183
|
const out = Object.create(null);
|
|
@@ -217,81 +187,6 @@ function toOccurrenceMap(keys, file) {
|
|
|
217
187
|
});
|
|
218
188
|
return out;
|
|
219
189
|
}
|
|
220
|
-
/** Map a prerendered HTML path (relative to pages/, POSIX) to its route. */
|
|
221
|
-
function deriveRouteFromHtmlPath(relPath) {
|
|
222
|
-
let p = relPath.replace(/\\/g, "/").replace(/\.html$/, "");
|
|
223
|
-
if (p === "index") return "/";
|
|
224
|
-
if (p.endsWith("/index")) p = p.slice(0, -6);
|
|
225
|
-
return "/" + p;
|
|
226
|
-
}
|
|
227
|
-
/** Read every prerendered HTML page under `prerenderPagesDir` into ResolvedHead[]. */
|
|
228
|
-
async function collectRenderedHeads(prerenderPagesDir) {
|
|
229
|
-
const files = (await globFiles("**/*.html", prerenderPagesDir)).sort();
|
|
230
|
-
const parsedFiles = await Promise.all(files.map(async (rel) => ({
|
|
231
|
-
rel,
|
|
232
|
-
parsed: parseHtmlHead(await readFile(join(prerenderPagesDir, rel), "utf8"))
|
|
233
|
-
})));
|
|
234
|
-
const heads = [];
|
|
235
|
-
const headings = [];
|
|
236
|
-
const images = [];
|
|
237
|
-
const a11y = [];
|
|
238
|
-
let htmlLang = {
|
|
239
|
-
presence: "none",
|
|
240
|
-
value: "absent"
|
|
241
|
-
};
|
|
242
|
-
for (const { rel, parsed } of parsedFiles) {
|
|
243
|
-
if (htmlLang.presence === "none" && parsed.htmlLang.presence === "own") htmlLang = parsed.htmlLang;
|
|
244
|
-
const route = deriveRouteFromHtmlPath(rel);
|
|
245
|
-
heads.push({
|
|
246
|
-
route,
|
|
247
|
-
source: "rendered",
|
|
248
|
-
tags: parsed.tags,
|
|
249
|
-
file: rel
|
|
250
|
-
});
|
|
251
|
-
headings.push({
|
|
252
|
-
route,
|
|
253
|
-
headings: parsed.headings.map((level) => ({
|
|
254
|
-
level,
|
|
255
|
-
line: 0,
|
|
256
|
-
file: rel
|
|
257
|
-
}))
|
|
258
|
-
});
|
|
259
|
-
images.push({
|
|
260
|
-
route,
|
|
261
|
-
images: parsed.images.map((img) => ({
|
|
262
|
-
...img,
|
|
263
|
-
file: rel
|
|
264
|
-
}))
|
|
265
|
-
});
|
|
266
|
-
a11y.push({
|
|
267
|
-
route,
|
|
268
|
-
landmarks: toOccurrenceMap(parsed.landmarks, rel),
|
|
269
|
-
nestedLandmarks: parsed.nestedLandmarks.map((n) => ({
|
|
270
|
-
...n,
|
|
271
|
-
file: rel,
|
|
272
|
-
line: 0
|
|
273
|
-
})),
|
|
274
|
-
ids: toOccurrenceMap(parsed.ids, rel),
|
|
275
|
-
idRefs: parsed.idRefs.map((r) => ({
|
|
276
|
-
...r,
|
|
277
|
-
file: rel,
|
|
278
|
-
line: 0
|
|
279
|
-
})),
|
|
280
|
-
idCandidates: [...new Set(parsed.ids)],
|
|
281
|
-
fullyResolved: true,
|
|
282
|
-
elementTags: parsed.elementTags,
|
|
283
|
-
elementsClosed: true,
|
|
284
|
-
file: rel
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
heads,
|
|
289
|
-
headings,
|
|
290
|
-
images,
|
|
291
|
-
a11y,
|
|
292
|
-
htmlLang
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
190
|
//#endregion
|
|
296
191
|
//#region src/loopback.ts
|
|
297
192
|
/** Only the local dev server hosts the ingest endpoint, so never POST off-box. */
|
|
@@ -325,4 +220,4 @@ function effectivePort(url) {
|
|
|
325
220
|
return url.port || (url.protocol === "https:" ? "443" : "80");
|
|
326
221
|
}
|
|
327
222
|
//#endregion
|
|
328
|
-
export {
|
|
223
|
+
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.36.
|
|
3
|
+
"version": "0.36.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",
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"node-html-parser": "^9.0.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"@svelte-vitals/core": "^0.51.
|
|
46
|
+
"@svelte-vitals/core": "^0.51.2",
|
|
47
47
|
"@sveltejs/kit": "^2.70.3",
|
|
48
|
-
"svelte-vitals": "^0.54.
|
|
48
|
+
"svelte-vitals": "^0.54.4",
|
|
49
49
|
"vite": "^8.2.2"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@svelte-vitals/core": "0.51.
|
|
57
|
+
"@svelte-vitals/core": "0.51.2",
|
|
58
58
|
"@sveltejs/kit": "^2.70.3",
|
|
59
59
|
"@types/node": "^24.13.3",
|
|
60
|
-
"happy-dom": "^20.
|
|
60
|
+
"happy-dom": "^20.13.2",
|
|
61
61
|
"svelte": "^5.57.0",
|
|
62
|
-
"svelte-vitals": "0.54.
|
|
62
|
+
"svelte-vitals": "0.54.4",
|
|
63
63
|
"vite": "^8.2.2"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|