svelte-vitals 0.5.2 → 0.7.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/dist/bin.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  readPackageVersion,
8
8
  run
9
- } from "./chunk-LQPB2WAL.js";
9
+ } from "./chunk-DVIKLRTP.js";
10
10
 
11
11
  // src/bin.ts
12
12
  import mri from "mri";
@@ -164,6 +164,13 @@ function attrValue(attributes, name) {
164
164
  if (v && v.type === "ExpressionTag") return "dynamic";
165
165
  return "absent";
166
166
  }
167
+ function lineOf(source, offset) {
168
+ if (typeof offset !== "number" || offset < 0) return 0;
169
+ let line = 1;
170
+ const end = Math.min(offset, source.length);
171
+ for (let i = 0; i < end; i++) if (source[i] === "\n") line++;
172
+ return line;
173
+ }
167
174
  function findAttr(attributes, name) {
168
175
  if (!Array.isArray(attributes)) return void 0;
169
176
  return attributes.find((a) => a?.type === "Attribute" && a.name === name);
@@ -231,16 +238,39 @@ function collectComponents(node, acc) {
231
238
  if (key in node) collectComponents(node[key], acc);
232
239
  }
233
240
  }
241
+ function collectImages(node, source, acc) {
242
+ if (Array.isArray(node)) {
243
+ for (const child of node) collectImages(child, source, acc);
244
+ return;
245
+ }
246
+ if (!node || typeof node !== "object") return;
247
+ if (node.type === "RegularElement" && node.name === "img") {
248
+ const attrs = node.attributes ?? [];
249
+ const hasSpread = attrs.some((a) => a?.type === "SpreadAttribute");
250
+ acc.push({
251
+ hasWidth: hasSpread || Boolean(findAttr(attrs, "width")),
252
+ hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
253
+ hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
254
+ line: lineOf(source, node.start)
255
+ });
256
+ }
257
+ for (const key of CHILD_NODE_KEYS) {
258
+ if (key in node) collectImages(node[key], source, acc);
259
+ }
260
+ }
234
261
  function parseFile(source, filename) {
235
262
  const ast = parse(source, { modern: true, filename });
236
263
  const heads = [];
237
264
  collectSvelteHeads(ast.fragment ?? ast, heads);
238
265
  const components = [];
239
266
  collectComponents(ast.fragment ?? ast, components);
267
+ const images = [];
268
+ collectImages(ast.fragment ?? ast, source, images);
240
269
  return {
241
270
  headTags: heads.flatMap(tagsFromHead),
242
271
  components,
243
- imports: collectImports(ast)
272
+ imports: collectImports(ast),
273
+ images
244
274
  };
245
275
  }
246
276
 
@@ -403,9 +433,13 @@ async function resolveRoute(rt, cwd, pageRel, config) {
403
433
  const composed = /* @__PURE__ */ new Map();
404
434
  let broadOwn = false;
405
435
  let broadInherited = false;
436
+ const images = [];
406
437
  for (const { rel, isPage } of files) {
407
438
  const source = await rt.readFile(rt.join(cwd, rel));
408
439
  const parsed = parseFile(source, rel);
440
+ for (const img of parsed.images) {
441
+ images.push({ ...img, file: rel });
442
+ }
409
443
  const resolved = await resolveFileTags(rt, cwd, rel, parsed, config, MAX_DEPTH, /* @__PURE__ */ new Set([rel]));
410
444
  for (const tag of resolved.tags) {
411
445
  composed.set(tagKey(tag), { ...tag, presence: isPage ? "own" : "inherited", file: rel });
@@ -422,20 +456,119 @@ async function resolveRoute(rt, cwd, pageRel, config) {
422
456
  if (!composed.has(key)) composed.set(key, { ...tag, presence });
423
457
  }
424
458
  }
459
+ const route = deriveRoute(pageRel);
425
460
  return {
426
- route: deriveRoute(pageRel),
427
- source: "static",
428
- tags: [...composed.values()],
429
- file: pageRel
461
+ head: { route, source: "static", tags: [...composed.values()], file: pageRel },
462
+ images: { route, images }
430
463
  };
431
464
  }
432
- var sourceHeadProvider = {
433
- mode: "static",
434
- async collect(rt, cwd, config = defaultConfig) {
435
- const pages = await enumerateRoutePages(rt, cwd);
436
- return Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
465
+ async function collectRoutes(rt, cwd, config = defaultConfig) {
466
+ const pages = await enumerateRoutePages(rt, cwd);
467
+ const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
468
+ return {
469
+ heads: facts.map((f) => f.head),
470
+ images: facts.map((f) => f.images)
471
+ };
472
+ }
473
+
474
+ // src/providers/source/a11y.ts
475
+ import { compile } from "svelte/compiler";
476
+ import { defaultConfig as defaultConfig2 } from "@svelte-vitals/core";
477
+
478
+ // src/rules-config.ts
479
+ import { allRules } from "@svelte-vitals/core";
480
+ var KNOWN_IDS = new Set(allRules.map((r) => r.id));
481
+ var A11Y_CODE_PREFIX = "a11y_";
482
+ var A11Y_CATEGORY_KEY = "a11y_category";
483
+ function findUnknownRuleIds(ids) {
484
+ return [
485
+ ...new Set(ids.filter((id) => !KNOWN_IDS.has(id) && !(id.startsWith(A11Y_CODE_PREFIX) && id !== A11Y_CATEGORY_KEY)))
486
+ ];
487
+ }
488
+ function knownRuleIds() {
489
+ return [...KNOWN_IDS].sort();
490
+ }
491
+ function buildRulesConfig(allow, ignore) {
492
+ const rules = {};
493
+ if (allow.length > 0) {
494
+ for (const r of allRules) if (!allow.includes(r.id)) rules[r.id] = "off";
495
+ if (!allow.some((id) => id.startsWith(A11Y_CODE_PREFIX))) rules[A11Y_CATEGORY_KEY] = "off";
437
496
  }
438
- };
497
+ for (const id of ignore) rules[id] = "off";
498
+ return rules;
499
+ }
500
+
501
+ // src/providers/source/a11y.ts
502
+ function firstLine(message) {
503
+ return message.split("\n")[0] ?? message;
504
+ }
505
+ function fileA11y(source, rel) {
506
+ let warnings;
507
+ try {
508
+ ({ warnings } = compile(source, { generate: false, filename: rel }));
509
+ } catch {
510
+ return { ok: false, warnings: [] };
511
+ }
512
+ const out = [];
513
+ for (const w of warnings) {
514
+ if (w.code.startsWith(A11Y_CODE_PREFIX)) {
515
+ out.push({
516
+ code: w.code,
517
+ message: firstLine(w.message),
518
+ line: w.start?.line ?? 0
519
+ });
520
+ }
521
+ }
522
+ return { ok: true, warnings: out };
523
+ }
524
+ async function collectA11y(rt, cwd, config = defaultConfig2) {
525
+ if (config.rules[A11Y_CATEGORY_KEY] === "off") return [];
526
+ const pages = await enumerateRoutePages(rt, cwd);
527
+ const cache = /* @__PURE__ */ new Map();
528
+ const results = [];
529
+ for (const page of pages) {
530
+ const files = await chainFiles(rt, cwd, page);
531
+ const route = deriveRoute(page);
532
+ const fails = [];
533
+ let compiledAll = true;
534
+ for (const { rel } of files) {
535
+ let entry = cache.get(rel);
536
+ if (!entry) {
537
+ const source = await rt.readFile(rt.join(cwd, rel));
538
+ entry = fileA11y(source, rel);
539
+ cache.set(rel, entry);
540
+ }
541
+ if (!entry.ok) compiledAll = false;
542
+ for (const w of entry.warnings) {
543
+ if (config.rules[w.code] === "off") continue;
544
+ fails.push({
545
+ id: w.code,
546
+ category: "a11y",
547
+ severity: "warning",
548
+ detection: { presence: "none", value: "absent" },
549
+ route,
550
+ location: rel,
551
+ ...w.line > 0 ? { line: w.line } : {},
552
+ message: w.message,
553
+ docsUrl: `https://svelte.dev/e/${w.code}`
554
+ });
555
+ }
556
+ }
557
+ if (fails.length > 0) {
558
+ results.push(...fails);
559
+ } else if (compiledAll) {
560
+ results.push({
561
+ id: "a11y",
562
+ category: "a11y",
563
+ severity: "warning",
564
+ detection: { presence: "own", value: "static" },
565
+ route,
566
+ message: "Accessibility"
567
+ });
568
+ }
569
+ }
570
+ return results;
571
+ }
439
572
 
440
573
  // src/version.ts
441
574
  import { readFileSync } from "fs";
@@ -477,24 +610,6 @@ function isAutoDetectedGithub(explicit, env = process.env) {
477
610
  return !explicit && !isReporterName(env.SVELTE_VITALS_REPORTER) && !isAgentEnv(env) && isGithubActionsEnv(env);
478
611
  }
479
612
 
480
- // src/rules-config.ts
481
- import { allRules } from "@svelte-vitals/core";
482
- var KNOWN_IDS = new Set(allRules.map((r) => r.id));
483
- function findUnknownRuleIds(ids) {
484
- return [...new Set(ids.filter((id) => !KNOWN_IDS.has(id)))];
485
- }
486
- function knownRuleIds() {
487
- return [...KNOWN_IDS].sort();
488
- }
489
- function buildRulesConfig(allow, ignore) {
490
- const rules = {};
491
- if (allow.length > 0) {
492
- for (const r of allRules) if (!allow.includes(r.id)) rules[r.id] = "off";
493
- }
494
- for (const id of ignore) rules[id] = "off";
495
- return rules;
496
- }
497
-
498
613
  // src/index.ts
499
614
  function routeMatcher(glob) {
500
615
  if (!glob) return () => true;
@@ -513,10 +628,14 @@ async function analyzeProject(opts = {}) {
513
628
  });
514
629
  await detectProject(rt, cwd);
515
630
  const matches = routeMatcher(opts.route);
516
- const heads = (await sourceHeadProvider.collect(rt, cwd, config)).filter((h) => matches(h.route));
631
+ const collected = await collectRoutes(rt, cwd, config);
632
+ const heads = collected.heads.filter((h) => matches(h.route));
633
+ const images = collected.images.filter((i) => matches(i.route));
517
634
  const project = await collectProjectFacts(rt, cwd);
518
635
  const rules = selectRules(allRules2, config);
519
- const results = applyRuleSeverities(await runRules(rules, { heads, project, config }), config);
636
+ const a11y = (await collectA11y(rt, cwd, config)).filter((r) => r.route === void 0 || matches(r.route));
637
+ const ruleResults = await runRules(rules, { heads, images, project, config });
638
+ const results = applyRuleSeverities([...ruleResults, ...a11y], config);
520
639
  return { results, config, version: readPackageVersion() };
521
640
  }
522
641
  async function run(opts = {}) {
@@ -576,11 +695,11 @@ async function run(opts = {}) {
576
695
 
577
696
  export {
578
697
  ProjectError,
579
- readPackageVersion,
580
- isReporterName,
581
698
  findUnknownRuleIds,
582
699
  knownRuleIds,
583
700
  buildRulesConfig,
701
+ readPackageVersion,
702
+ isReporterName,
584
703
  routeMatcher,
585
704
  analyzeProject,
586
705
  run
package/dist/index.d.ts CHANGED
@@ -16,6 +16,10 @@ declare function knownRuleIds(): string[];
16
16
  * (--ignore). An allow-list disables every rule not listed; deny always wins.
17
17
  * Callers should reject unknown ids first (see findUnknownRuleIds) so a typo in
18
18
  * --rules can't silently disable every rule.
19
+ *
20
+ * When the allow-list is non-empty and contains no `a11y_*` entries, the
21
+ * `A11Y_CATEGORY_KEY` sentinel is set to `'off'`; `collectA11y` checks this key
22
+ * to suppress the entire Accessibility category.
19
23
  */
20
24
  declare function buildRulesConfig(allow: string[], ignore: string[]): Record<string, RuleSetting>;
21
25
 
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  knownRuleIds,
7
7
  routeMatcher,
8
8
  run
9
- } from "./chunk-LQPB2WAL.js";
9
+ } from "./chunk-DVIKLRTP.js";
10
10
  export {
11
11
  ProjectError,
12
12
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "A SvelteKit SEO checker — not a runtime Web Vitals reporter. Static analysis of your routes' head metadata.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -42,7 +42,7 @@
42
42
  "mri": "^1.2.0",
43
43
  "svelte": "^5.56.3",
44
44
  "tinyglobby": "^0.2.17",
45
- "@svelte-vitals/core": "0.7.0"
45
+ "@svelte-vitals/core": "0.8.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^24.7.0"