@svelte-vitals/vite 0.36.1 → 0.36.3

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.
@@ -1,6 +1,6 @@
1
- import { i as parseHtmlHead, r as toOccurrenceMap, t as isLoopbackOrigin } from "../loopback-nE7_C-y_.js";
1
+ import { i as toOccurrenceMap, o as parseHtmlHead, t as isLoopbackOrigin } from "../loopback-CE47i4VD.js";
2
2
  import { defineConfig } from "@svelte-vitals/core";
3
- import { allRules, applyRuleSeverities, defaultProject, effectiveSeverity, formatFailedRuleWarning, isPenalized, runRules, selectRules, terminalSafe } from "@svelte-vitals/core/internal";
3
+ import { allRules, defaultProject, effectiveSeverity, formatFailedRuleWarning, isPenalized, runAnalysis, selectRules, terminalSafe } from "@svelte-vitals/core/internal";
4
4
  import { DEV } from "esm-env";
5
5
  //#region 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. */
@@ -66,16 +66,14 @@ async function analyzeAndIngest(html, route, origin, rules, config, lastSignatur
66
66
  idCandidates: [...new Set(ids)],
67
67
  fullyResolved: true
68
68
  }];
69
- const { results: ruleResults, failedRules } = await runRules(rules, {
69
+ const { results, failedRules, failedRuleIds } = await runAnalysis(rules, {
70
70
  heads: [head],
71
71
  headings,
72
72
  images,
73
73
  a11y,
74
74
  project: defaultProject,
75
75
  config
76
- });
77
- const results = applyRuleSeverities(ruleResults, config);
78
- const failedRuleIds = failedRules.map((f) => f.id);
76
+ }, /* @__PURE__ */ new Map());
79
77
  if (failedRules.length > 0 && globalThis.process?.env?.SVELTE_VITALS_DEBUG) for (const f of failedRules) warn(formatFailedRuleWarning(f));
80
78
  const signature = `${findingSignature(results, config)}|failed:${[...failedRuleIds].sort().join(",")}`;
81
79
  if (lastSignature.get(route) === signature) return;
package/dist/index.js CHANGED
@@ -1,11 +1,10 @@
1
- import { n as collectRenderedHeads, t as isLoopbackOrigin } from "./loopback-nE7_C-y_.js";
1
+ import { a as globFiles, n as isSameOrigin, r as collectRenderedHeads, t as isLoopbackOrigin } from "./loopback-CE47i4VD.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, allRules, applyInlineDirectives, applyOverrides, applyRuleSeverities, buildJsonReport, collectComponentFacts, collectKitModuleFacts, collectSourceFiles, collectSuppressions, computeScore, defaultConfig, findMinifyDisabled, formatConsoleReport, formatFailedRuleWarning, formatJsonReport, hasFailureAtOrAbove, renderAppShell, resolveKitAliases, resolveKitPathsBase, resolveRuleOptions, runRules, safeHref, selectRules, shouldSkipRangeCheck, skippedFileWarnings, summarize, terminalSafe, unknownDirectiveIds, validateRuleSetting, withFailedRulesOff, withReadLimit } from "@svelte-vitals/core/internal";
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, withReadLimit } from "@svelte-vitals/core/internal";
7
7
  import { CONFIG_FILENAMES, analyzeProject, findUnknownRuleIds, knownRuleIds, loadConfigFile, registryTag, ruleOptionsSpec } from "svelte-vitals";
8
- import { glob } from "tinyglobby";
9
8
  //#region src/providers/rendered/project.ts
10
9
  async function exists(path) {
11
10
  try {
@@ -78,10 +77,7 @@ const nodeRuntime = {
78
77
  return false;
79
78
  }
80
79
  },
81
- glob: (pattern, cwd) => glob(pattern, {
82
- cwd,
83
- dot: false
84
- }),
80
+ glob: (pattern, cwd) => globFiles(pattern, cwd),
85
81
  join: (...parts) => join(...parts)
86
82
  };
87
83
  /**
@@ -185,7 +181,13 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
185
181
  const kitModules = await collectKitModuleFacts$1(cwd, project.kitAliases);
186
182
  const selected = selectRules(allRules, config);
187
183
  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.");
188
- const { results: rawResults, examined, failedRules } = await runRules(selected, {
184
+ const directives = /* @__PURE__ */ new Map();
185
+ addFactsDirectives(directives, {
186
+ components,
187
+ kitModules,
188
+ viteMinifyDisabled: project.viteMinifyDisabled
189
+ });
190
+ const { results, examined, failedRules, scoringConfig } = await runAnalysis(selected, {
189
191
  heads,
190
192
  headings,
191
193
  images,
@@ -195,16 +197,10 @@ async function analyze(prerenderPagesDir, cwd, options, extraProjectFacts, resol
195
197
  config,
196
198
  kitModules,
197
199
  sourceFiles
198
- });
199
- const directives = /* @__PURE__ */ new Map();
200
- for (const c of components) directives.set(c.file, c.suppressions ?? []);
201
- for (const m of kitModules) directives.set(m.file, m.suppressions ?? []);
202
- if (project.viteMinifyDisabled?.file) directives.set(project.viteMinifyDisabled.file, project.viteMinifyDisabled.suppressions ?? []);
203
- const results = applyInlineDirectives(applyOverrides(applyRuleSeverities(rawResults, config), config), directives, selected, config);
200
+ }, directives);
204
201
  warnings.push(...skippedFileWarnings([...components, ...kitModules]));
205
202
  warnings.push(...unknownDirectiveIds(directives, allRules));
206
203
  for (const f of failedRules) warnings.push(formatFailedRuleWarning(f));
207
- const scoringConfig = withFailedRulesOff(config, failedRules.map((f) => f.id));
208
204
  const { score } = computeScore(results, scoringConfig);
209
205
  const summary = summarize(results, scoringConfig);
210
206
  const failed = hasFailureAtOrAbove(summary, scoringConfig.failOn);
@@ -308,6 +304,7 @@ const SEVERITIES = /* @__PURE__ */ new Set([
308
304
  "info"
309
305
  ]);
310
306
  const CATEGORY_SET = new Set(CATEGORIES);
307
+ const INGEST_BODY_LIMIT = 4194304;
311
308
  function isOptionalString(v) {
312
309
  return v === void 0 || typeof v === "string";
313
310
  }
@@ -348,7 +345,8 @@ function installUiMiddleware(server, config, version, store, coreVersion, getSta
348
345
  const url = req.url ?? "/";
349
346
  const origin = req.headers.origin;
350
347
  const host = req.headers.host;
351
- if (typeof origin === "string" && !isLoopbackOrigin(origin) || host === void 0 || !isLoopbackOrigin(`http://${host}`)) {
348
+ const isTls = Boolean(req.socket?.encrypted);
349
+ if (host === void 0 || !isLoopbackOrigin(`http://${host}`) || typeof origin === "string" && !isSameOrigin(origin, host, isTls)) {
352
350
  req.resume();
353
351
  res.statusCode = 403;
354
352
  res.end("svelte-vitals dev UI is only available from localhost");
@@ -356,8 +354,23 @@ function installUiMiddleware(server, config, version, store, coreVersion, getSta
356
354
  }
357
355
  if (req.method === "POST" && url.startsWith("/ingest")) {
358
356
  const chunks = [];
359
- req.on("data", (c) => chunks.push(c));
357
+ let received = 0;
358
+ let tooLarge = false;
359
+ req.on("data", (c) => {
360
+ if (tooLarge) return;
361
+ received += c.length;
362
+ if (received > INGEST_BODY_LIMIT) {
363
+ tooLarge = true;
364
+ chunks.length = 0;
365
+ res.statusCode = 413;
366
+ res.end();
367
+ req.destroy();
368
+ return;
369
+ }
370
+ chunks.push(c);
371
+ });
360
372
  req.on("end", () => {
373
+ if (tooLarge) return;
361
374
  try {
362
375
  const { route, results, failedRuleIds } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
363
376
  if (typeof route === "string" && Array.isArray(results)) {
@@ -1,48 +1,11 @@
1
- import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { IDREF_ATTRS, LANDMARK_ROLES, decodeFragmentId, isTopFragment, resolveRole, splitTokens, stripTextDirective } from "@svelte-vitals/core/internal";
4
- import { glob } from "tinyglobby";
1
+ import { glob, readFile, stat } from "node:fs/promises";
2
+ import { join, relative, sep } from "node:path";
3
+ import { ASIDE_DEMOTING_TAGS, IDREF_ATTRS, NAMING_ATTRS, SECTIONING_TAGS, decodeFragmentId, isClassicScriptType, isTopFragment, resolveLandmark, splitTokens, stripTextDirective } from "@svelte-vitals/core/internal";
5
4
  import { parse } from "node-html-parser";
6
5
  //#region src/providers/rendered/parse-html.ts
7
6
  function attrValue(v) {
8
7
  return v !== void 0 && v.trim().length > 0 ? "static" : "absent";
9
8
  }
10
- const JS_MIME_TYPES = /* @__PURE__ */ new Set([
11
- "application/ecmascript",
12
- "application/javascript",
13
- "application/x-ecmascript",
14
- "application/x-javascript",
15
- "text/ecmascript",
16
- "text/javascript",
17
- "text/javascript1.0",
18
- "text/javascript1.1",
19
- "text/javascript1.2",
20
- "text/javascript1.3",
21
- "text/javascript1.4",
22
- "text/javascript1.5",
23
- "text/jscript",
24
- "text/livescript",
25
- "text/x-ecmascript",
26
- "text/x-javascript"
27
- ]);
28
- function isClassicScriptType(type) {
29
- if (type === void 0) return true;
30
- const normalized = type.trim().toLowerCase();
31
- return normalized === "" || JS_MIME_TYPES.has(normalized);
32
- }
33
- const SECTIONING_TAGS = /* @__PURE__ */ new Set([
34
- "article",
35
- "aside",
36
- "main",
37
- "nav",
38
- "section"
39
- ]);
40
- const ASIDE_DEMOTING_TAGS = /* @__PURE__ */ new Set([
41
- "article",
42
- "aside",
43
- "nav",
44
- "section"
45
- ]);
46
9
  /** Whole-document scan (not body-scoped: app.html shell ids/refs are real occurrences too). */
47
10
  function collectA11y(root) {
48
11
  const landmarks = [];
@@ -54,16 +17,13 @@ function collectA11y(root) {
54
17
  const tag = el.rawTagName?.toLowerCase();
55
18
  if (tag && ctx.inBody) elementTags.add(tag);
56
19
  const roleAttr = el.getAttribute("role");
57
- let landmark;
58
- if (roleAttr !== void 0) {
59
- const role = resolveRole(splitTokens(roleAttr));
60
- landmark = role && LANDMARK_ROLES.has(role) ? role : void 0;
61
- } else if (tag === "main") landmark = "main";
62
- else if ((tag === "header" || tag === "footer") && ctx.sectioning === 0) landmark = tag === "header" ? "banner" : "contentinfo";
63
- else if (tag === "aside") {
64
- const named = ["aria-label", "aria-labelledby"].some((a) => (el.getAttribute(a) ?? "").trim().length > 0);
65
- landmark = ctx.asideDemoting === 0 || named ? "complementary" : void 0;
66
- }
20
+ const landmark = resolveLandmark({
21
+ tag,
22
+ roleTokens: roleAttr !== void 0 ? splitTokens(roleAttr) : void 0,
23
+ named: tag === "aside" && NAMING_ATTRS.some((a) => (el.getAttribute(a) ?? "").trim().length > 0),
24
+ insideSectioning: ctx.sectioning > 0,
25
+ insideAsideDemoting: ctx.asideDemoting > 0
26
+ });
67
27
  if (landmark) {
68
28
  landmarks.push(landmark);
69
29
  const within = ctx.landmarks.at(-1);
@@ -221,6 +181,32 @@ function parseHtmlHead(html) {
221
181
  };
222
182
  }
223
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
224
210
  //#region src/providers/rendered/collect.ts
225
211
  /** Group raw occurrence keys (one entry per hit, in document order) by key, `file` attached, `line: 0` (rendered mode does not track source lines). */
226
212
  function toOccurrenceMap(keys, file) {
@@ -240,7 +226,7 @@ function deriveRouteFromHtmlPath(relPath) {
240
226
  }
241
227
  /** Read every prerendered HTML page under `prerenderPagesDir` into ResolvedHead[]. */
242
228
  async function collectRenderedHeads(prerenderPagesDir) {
243
- const files = (await glob("**/*.html", { cwd: prerenderPagesDir })).sort();
229
+ const files = (await globFiles("**/*.html", prerenderPagesDir)).sort();
244
230
  const parsedFiles = await Promise.all(files.map(async (rel) => ({
245
231
  rel,
246
232
  parsed: parseHtmlHead(await readFile(join(prerenderPagesDir, rel), "utf8"))
@@ -317,5 +303,26 @@ function isLoopbackOrigin(origin) {
317
303
  return false;
318
304
  }
319
305
  }
306
+ /**
307
+ * Whether `origin` (an Origin header value) names exactly the server this request reached —
308
+ * `host` is the Host header, `hostname[:port]`, and `secure` is whether the connection is TLS.
309
+ * Compared as hostname + effective port: each side's omitted port resolves to its own scheme's
310
+ * default (the Origin's from its URL scheme, the Host's from `secure`), so `https://localhost`
311
+ * never matches a plain-http server on :80 and `https://localhost:443` does match a TLS server
312
+ * whose Host is `localhost:443`. The Origin's scheme is otherwise not compared: under
313
+ * `server.https` the Origin is https while the Host header is unchanged.
314
+ */
315
+ function isSameOrigin(origin, host, secure = false) {
316
+ try {
317
+ const o = new URL(origin);
318
+ const h = new URL(`${secure ? "https" : "http"}://${host}`);
319
+ return o.hostname === h.hostname && effectivePort(o) === effectivePort(h);
320
+ } catch {
321
+ return false;
322
+ }
323
+ }
324
+ function effectivePort(url) {
325
+ return url.port || (url.protocol === "https:" ? "443" : "80");
326
+ }
320
327
  //#endregion
321
- export { parseHtmlHead as i, collectRenderedHeads as n, toOccurrenceMap as r, isLoopbackOrigin as t };
328
+ export { globFiles as a, toOccurrenceMap as i, isSameOrigin as n, parseHtmlHead as o, collectRenderedHeads 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.1",
3
+ "version": "0.36.3",
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,12 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "esm-env": "^1.2.2",
43
- "node-html-parser": "^9.0.1",
44
- "tinyglobby": "^0.2.17"
43
+ "node-html-parser": "^9.0.2"
45
44
  },
46
45
  "peerDependencies": {
47
- "@svelte-vitals/core": "^0.50.1",
46
+ "@svelte-vitals/core": "^0.51.1",
48
47
  "@sveltejs/kit": "^2.70.3",
49
- "svelte-vitals": "^0.54.1",
48
+ "svelte-vitals": "^0.54.3",
50
49
  "vite": "^8.2.2"
51
50
  },
52
51
  "peerDependenciesMeta": {
@@ -55,12 +54,12 @@
55
54
  }
56
55
  },
57
56
  "devDependencies": {
58
- "@svelte-vitals/core": "0.50.1",
57
+ "@svelte-vitals/core": "0.51.1",
59
58
  "@sveltejs/kit": "^2.70.3",
60
59
  "@types/node": "^24.13.3",
61
- "happy-dom": "^20.11.6",
62
- "svelte": "^5.56.10",
63
- "svelte-vitals": "0.54.1",
60
+ "happy-dom": "^20.12.0",
61
+ "svelte": "^5.57.0",
62
+ "svelte-vitals": "0.54.3",
64
63
  "vite": "^8.2.2"
65
64
  },
66
65
  "scripts": {