@svelte-vitals/vite 0.3.4 → 0.4.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.
@@ -35,7 +35,33 @@ function findingSignature(results, config) {
35
35
  }
36
36
 
37
37
  // src/hooks/handle.ts
38
- async function analyzeAndWarn(html, route, rules, config, lastSignature) {
38
+ function isLoopbackOrigin(origin) {
39
+ try {
40
+ const host = new URL(origin).hostname;
41
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1";
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+ async function postIngest(origin, route, results) {
47
+ if (!isLoopbackOrigin(origin)) {
48
+ if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) {
49
+ console.warn(
50
+ `[svelte-vitals] live UI ingest skipped for non-loopback origin ${origin} \u2014 open the dashboard via localhost`
51
+ );
52
+ }
53
+ return;
54
+ }
55
+ try {
56
+ await fetch(`${origin}/__svelte-vitals/ingest`, {
57
+ method: "POST",
58
+ headers: { "content-type": "application/json" },
59
+ body: JSON.stringify({ route, results })
60
+ });
61
+ } catch {
62
+ }
63
+ }
64
+ async function analyzeAndWarn(html, route, origin, rules, config, lastSignature) {
39
65
  try {
40
66
  const { tags, htmlLang } = parseHtmlHead(html);
41
67
  const head = { route, source: "rendered", tags, file: route };
@@ -46,6 +72,7 @@ async function analyzeAndWarn(html, route, rules, config, lastSignature) {
46
72
  lastSignature.set(route, signature);
47
73
  const report = formatDevReport(route, results, config);
48
74
  if (report) console.warn(report);
75
+ if (globalThis.process?.env?.SVELTE_VITALS_UI) void postIngest(origin, route, results);
49
76
  } catch (err) {
50
77
  if (globalThis.process?.env?.SVELTE_VITALS_DEBUG) {
51
78
  console.warn("[svelte-vitals] dev analysis failed:", err);
@@ -65,7 +92,15 @@ function svelteVitalsHandle(options = {}) {
65
92
  return resolve(event, {
66
93
  transformPageChunk: ({ html, done }) => {
67
94
  buffer += html;
68
- if (done) void analyzeAndWarn(buffer, event.route.id ?? event.url.pathname, rules, config, lastSignature);
95
+ if (done)
96
+ void analyzeAndWarn(
97
+ buffer,
98
+ event.route.id ?? event.url.pathname,
99
+ event.url.origin,
100
+ rules,
101
+ config,
102
+ lastSignature
103
+ );
69
104
  return html;
70
105
  }
71
106
  });
package/dist/index.d.ts CHANGED
@@ -15,8 +15,10 @@ interface SvelteVitalsOptions {
15
15
  outFile?: string;
16
16
  /** Override the prerendered-pages directory (default: .svelte-kit/output/prerendered/pages). */
17
17
  prerenderDir?: string;
18
+ /** Serve a live dashboard at /__svelte-vitals/ during `vite dev` (requires svelteVitalsHandle in hooks.server.ts). */
19
+ ui?: boolean;
18
20
  }
19
21
  /** svelte-vitals Vite/SvelteKit plugin. */
20
- declare function svelteVitals(options?: SvelteVitalsOptions): Plugin;
22
+ declare function svelteVitals(options?: SvelteVitalsOptions): Plugin | Plugin[];
21
23
 
22
24
  export { type SvelteVitalsOptions, svelteVitals };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  import { existsSync } from "fs";
7
7
  import { writeFile, mkdir } from "fs/promises";
8
8
  import { join as join3, isAbsolute, dirname } from "path";
9
+ import { defineConfig as defineConfig2 } from "@svelte-vitals/core";
9
10
 
10
11
  // src/analyze.ts
11
12
  import {
@@ -117,11 +118,113 @@ async function analyze(prerenderPagesDir, cwd, options) {
117
118
  };
118
119
  }
119
120
 
121
+ // src/ui/store.ts
122
+ function createStore() {
123
+ const byRoute = /* @__PURE__ */ new Map();
124
+ const subs = /* @__PURE__ */ new Set();
125
+ return {
126
+ set(route, results) {
127
+ byRoute.set(
128
+ route,
129
+ results.map((r) => r.route ? r : { ...r, route })
130
+ );
131
+ for (const fn of subs) fn();
132
+ },
133
+ snapshot() {
134
+ return [...byRoute.values()].flat();
135
+ },
136
+ subscribe(fn) {
137
+ subs.add(fn);
138
+ return () => subs.delete(fn);
139
+ }
140
+ };
141
+ }
142
+
143
+ // src/ui/serve.ts
144
+ import { buildHtmlDocument, buildJsonReport } from "@svelte-vitals/core";
145
+ var LIVE_SCRIPT = `<script data-live>
146
+ (function(){
147
+ var es=new EventSource('/__svelte-vitals/events');
148
+ es.addEventListener('update',function(){
149
+ fetch('/__svelte-vitals/').then(function(r){return r.text();}).then(function(html){
150
+ var doc=new DOMParser().parseFromString(html,'text/html');
151
+ var next=doc.querySelector('.wrap'),cur=document.querySelector('.wrap');
152
+ if(next&&cur)cur.replaceWith(next);
153
+ var scripts=doc.querySelectorAll('body > script:not([data-live])');
154
+ var core=scripts[scripts.length-1];
155
+ if(core){var s=document.createElement('script');s.textContent=core.textContent;document.body.appendChild(s);s.remove();}
156
+ }).catch(function(){});
157
+ });
158
+ })();
159
+ </script>`;
160
+ function renderDashboard(results, config, meta) {
161
+ const html = buildHtmlDocument(buildJsonReport(results, config, meta), meta);
162
+ return html.replace("</body>", LIVE_SCRIPT + "</body>");
163
+ }
164
+
165
+ // src/ui/middleware.ts
166
+ var SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning", "info"]);
167
+ function isResultLike(x) {
168
+ if (typeof x !== "object" || x === null) return false;
169
+ const r = x;
170
+ const d = r.detection;
171
+ return typeof r.id === "string" && typeof r.message === "string" && typeof r.severity === "string" && SEVERITIES.has(r.severity) && typeof d === "object" && d !== null && typeof d.presence === "string" && typeof d.value === "string";
172
+ }
173
+ function installUiMiddleware(server, config, version) {
174
+ const store = createStore();
175
+ const clients = /* @__PURE__ */ new Set();
176
+ store.subscribe(() => {
177
+ for (const res of clients) {
178
+ try {
179
+ res.write("event: update\ndata: {}\n\n");
180
+ } catch {
181
+ clients.delete(res);
182
+ }
183
+ }
184
+ });
185
+ server.httpServer?.once("close", () => {
186
+ for (const res of clients) res.end();
187
+ clients.clear();
188
+ });
189
+ server.middlewares.use("/__svelte-vitals", (req, res) => {
190
+ const url = req.url ?? "/";
191
+ if (req.method === "POST" && url.startsWith("/ingest")) {
192
+ const chunks = [];
193
+ req.on("data", (c) => chunks.push(c));
194
+ req.on("end", () => {
195
+ try {
196
+ const { route, results } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
197
+ if (typeof route === "string" && Array.isArray(results)) {
198
+ store.set(route, results.filter(isResultLike));
199
+ }
200
+ } catch {
201
+ }
202
+ res.statusCode = 204;
203
+ res.end();
204
+ });
205
+ return;
206
+ }
207
+ if (url.startsWith("/events")) {
208
+ res.writeHead(200, {
209
+ "Content-Type": "text/event-stream",
210
+ "Cache-Control": "no-cache",
211
+ Connection: "keep-alive"
212
+ });
213
+ res.write("\n");
214
+ clients.add(res);
215
+ req.on("close", () => clients.delete(res));
216
+ return;
217
+ }
218
+ res.setHeader("Content-Type", "text/html");
219
+ res.end(renderDashboard(store.snapshot(), config, { version }));
220
+ });
221
+ }
222
+
120
223
  // src/plugin.ts
121
224
  var DEFAULT_PRERENDER_DIR = ".svelte-kit/output/prerendered/pages";
122
225
  function svelteVitals(options = {}) {
123
226
  let root = options.cwd ?? process.cwd();
124
- return {
227
+ const buildPlugin = {
125
228
  name: "svelte-vitals",
126
229
  apply: "build",
127
230
  enforce: "post",
@@ -154,6 +257,25 @@ function svelteVitals(options = {}) {
154
257
  }
155
258
  }
156
259
  };
260
+ if (!options.ui) return buildPlugin;
261
+ const uiPlugin = {
262
+ name: "svelte-vitals:ui",
263
+ apply: "serve",
264
+ configureServer(server) {
265
+ process.env.SVELTE_VITALS_UI = "1";
266
+ server.httpServer?.once("close", () => {
267
+ delete process.env.SVELTE_VITALS_UI;
268
+ });
269
+ const config = defineConfig2({
270
+ treatDynamicAs: options.treatDynamicAs ?? "pass",
271
+ metaComponents: options.metaComponents ?? [],
272
+ rules: options.rules ?? {},
273
+ failOn: options.failOn ?? "critical"
274
+ });
275
+ installUiMiddleware(server, config, readPackageVersion());
276
+ }
277
+ };
278
+ return [buildPlugin, uiPlugin];
157
279
  }
158
280
  export {
159
281
  svelteVitals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/vite",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Vite/SvelteKit plugin for svelte-vitals — analyzes prerendered HTML during vite build.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "esm-env": "^1.2.2",
43
43
  "node-html-parser": "^6.1.13",
44
44
  "tinyglobby": "^0.2.17",
45
- "@svelte-vitals/core": "0.10.1"
45
+ "@svelte-vitals/core": "0.11.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@sveltejs/kit": "^2.0.0",