svelte-vitals 0.29.0 → 0.31.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/README.md CHANGED
@@ -33,16 +33,16 @@ Architecture Score: 100/100 (route avg 100)
33
33
 
34
34
  Critical (2)
35
35
  ────────────────────────
36
- SEO001 Missing <title>
36
+ seo/title-presence Missing <title>
37
37
  /
38
38
  src/routes/+page.svelte
39
- SEO002 Missing <meta name="description">
39
+ seo/description-presence Missing <meta name="description">
40
40
  /
41
41
  src/routes/+page.svelte
42
42
 
43
43
  Warnings (10)
44
44
  ────────────────────────
45
- SEO003 Missing <link rel="canonical">
45
+ seo/canonical-url Missing <link rel="canonical">
46
46
  /
47
47
  src/routes/+page.svelte
48
48
  …and 1 more
package/dist/bin.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  readCoreVersion,
10
10
  readPackageVersion,
11
11
  run
12
- } from "./chunk-AAUZ37EW.js";
12
+ } from "./chunk-3YO2A3YF.js";
13
13
 
14
14
  // src/bin.ts
15
15
  import mri3 from "mri";
@@ -505,7 +505,7 @@ without a judgment call of its own.
505
505
  none today, by design), no code edits, no commits, no formatters, no
506
506
  dependency installs. Run svelte-vitals read-only, for evidence only.
507
507
  3. **Plans must be fully self-contained.** The executor has zero context
508
- from this conversation. Never write "fix it like SEO001 above" \u2014 inline
508
+ from this conversation. Never write "fix it like seo/title-presence above" \u2014 inline
509
509
  the exact file, line, current code, and the exact fix (svelte-vitals'
510
510
  \`fix.snippet\`/\`fix.description\` for the rule, quoted verbatim \u2014 see
511
511
  below).
@@ -789,7 +789,7 @@ function buildConfigFileTemplate(opts = {}) {
789
789
  const header = "// svelte-vitals config file \u2014 https://oekazuma.github.io/svelte-vitals/guides/configuration/\n";
790
790
  const options = ` // treatDynamicAs: 'pass', // 'pass' | 'warn' | 'fail' \u2014 how {data.title}-style dynamic values are scored
791
791
  // metaComponents: ['Seo'], // component names that resolve SEO tags into <head>
792
- // rules: {}, // e.g. { SEO001: 'off' } to disable a rule
792
+ // rules: {}, // e.g. { 'seo/title-presence': 'off' } to disable a rule
793
793
  // failOn: 'critical', // 'critical' | 'warning' | 'info'
794
794
  // weights: {} // e.g. { seo: 2 } \u2014 per-category weight for the combined Health score`;
795
795
  if (opts.useDefineConfig) {
@@ -998,8 +998,8 @@ function readInstalledViteVersion(io) {
998
998
  }
999
999
 
1000
1000
  // src/ci/action-pin.generated.ts
1001
- var ACTION_SHA = "a9fe2c88981fcf02498935df079e733e1317398c";
1002
- var ACTION_VERSION = "0.3.4";
1001
+ var ACTION_SHA = "4625e1a6266d99282463c8882bbd600b14123d92";
1002
+ var ACTION_VERSION = "0.3.6";
1003
1003
 
1004
1004
  // src/install/index.ts
1005
1005
  function detectPackageManagerNear(io, appDir) {
@@ -17,6 +17,7 @@ import {
17
17
  defineConfig,
18
18
  selectRules,
19
19
  applyRuleSeverities,
20
+ applyOverrides,
20
21
  collectKitModuleFacts
21
22
  } from "@svelte-vitals/core";
22
23
 
@@ -52,7 +53,8 @@ import { defaultConfig } from "@svelte-vitals/core";
52
53
  // src/providers/source/project.ts
53
54
  import {
54
55
  ROBOTS_SOURCE_PATHS,
55
- SITEMAP_SOURCE_PATHS
56
+ SITEMAP_SOURCE_PATHS,
57
+ findMinifyDisabled
56
58
  } from "@svelte-vitals/core";
57
59
  var ProjectError = class extends Error {
58
60
  constructor(message) {
@@ -109,18 +111,39 @@ async function robotsRefsSitemap(rt, cwd) {
109
111
  return void 0;
110
112
  }
111
113
  }
114
+ var VITE_CONFIG_FILES = [
115
+ "vite.config.js",
116
+ "vite.config.mjs",
117
+ "vite.config.ts",
118
+ "vite.config.cjs",
119
+ "vite.config.mts",
120
+ "vite.config.cts"
121
+ ];
122
+ async function detectViteMinifyDisabled(rt, cwd) {
123
+ const exists = await Promise.all(VITE_CONFIG_FILES.map((f) => rt.exists(rt.join(cwd, f))));
124
+ const file = VITE_CONFIG_FILES[exists.indexOf(true)];
125
+ if (!file) return void 0;
126
+ try {
127
+ const hit = findMinifyDisabled(await rt.readFile(rt.join(cwd, file)));
128
+ return hit ? { file, line: hit.line } : void 0;
129
+ } catch {
130
+ return void 0;
131
+ }
132
+ }
112
133
  async function collectProjectFacts(rt, cwd) {
113
- const [hasRobotsTxt, hasSitemap, htmlLang] = await Promise.all([
134
+ const [hasRobotsTxt, hasSitemap, htmlLang, viteMinifyDisabled] = await Promise.all([
114
135
  existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
115
136
  existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
116
- detectAppHtmlLang(rt, cwd)
137
+ detectAppHtmlLang(rt, cwd),
138
+ detectViteMinifyDisabled(rt, cwd)
117
139
  ]);
118
140
  const robotsReferencesSitemap = await robotsRefsSitemap(rt, cwd);
119
141
  return {
120
142
  hasRobotsTxt,
121
143
  hasSitemap,
122
144
  htmlLang,
123
- ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {}
145
+ ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
146
+ ...viteMinifyDisabled ? { viteMinifyDisabled } : {}
124
147
  };
125
148
  }
126
149
 
@@ -138,16 +161,16 @@ function exprValue(node) {
138
161
  }
139
162
  function resolveMetaObject(attr, keyMap) {
140
163
  if (!attr) return { tags: [], opaque: false };
141
- const expr = attr.value?.type === "ExpressionTag" ? attr.value.expression : void 0;
164
+ const expr = attr.value !== true && !Array.isArray(attr.value) ? attr.value.expression : void 0;
142
165
  if (!expr || expr.type !== "ObjectExpression") return { tags: [], opaque: true };
143
166
  const tags = [];
144
167
  let opaque = false;
145
- for (const prop of expr.properties ?? []) {
146
- if (prop?.type !== "Property" || prop.computed) {
168
+ for (const prop of expr.properties) {
169
+ if (prop.type !== "Property" || prop.computed) {
147
170
  opaque = true;
148
171
  continue;
149
172
  }
150
- const key = prop.key?.name ?? prop.key?.value;
173
+ const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? prop.key.value : void 0;
151
174
  const make = typeof key === "string" ? keyMap[key] : void 0;
152
175
  if (make) tags.push(make(exprValue(prop.value)));
153
176
  }
@@ -167,7 +190,7 @@ var TWITTER_KEYS = {
167
190
 
168
191
  // src/providers/source/adapters/svelte-meta-tags.ts
169
192
  function findAttr(attributes, name) {
170
- return attributes.find((a) => a?.type === "Attribute" && a.name === name);
193
+ return attributes.find((a) => a.type === "Attribute" && a.name === name);
171
194
  }
172
195
  var svelteMetaTagsAdapter = {
173
196
  match(info) {
@@ -220,7 +243,7 @@ var svelteMetaTagsJsonLdAdapter = {
220
243
  // src/providers/source/adapters/svelte-seo.ts
221
244
  import { attrValueOf as attrValueOf2, attrTextOf as attrTextOf2 } from "@svelte-vitals/core";
222
245
  function findAttr2(attributes, name) {
223
- return attributes.find((a) => a?.type === "Attribute" && a.name === name);
246
+ return attributes.find((a) => a.type === "Attribute" && a.name === name);
224
247
  }
225
248
  var svelteSeoAdapter = {
226
249
  match(info) {
@@ -272,28 +295,31 @@ import {
272
295
  // src/providers/source/imports.ts
273
296
  function addImportsFromProgram(program, map) {
274
297
  for (const node of program?.body ?? []) {
275
- if (node?.type !== "ImportDeclaration") continue;
276
- const source = String(node.source?.value ?? "");
277
- for (const spec of node.specifiers ?? []) {
278
- const local = spec?.local?.name;
298
+ if (node.type !== "ImportDeclaration") continue;
299
+ const source = String(node.source.value ?? "");
300
+ for (const spec of node.specifiers) {
301
+ const local = spec.local?.name;
279
302
  if (!local) continue;
280
303
  if (spec.type === "ImportDefaultSpecifier") {
281
304
  map.set(local, { source, imported: "default" });
282
305
  } else if (spec.type === "ImportSpecifier") {
283
- map.set(local, { source, imported: spec.imported?.name ?? spec.imported?.value ?? local });
306
+ const imported = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
307
+ map.set(local, { source, imported: typeof imported === "string" ? imported : local });
284
308
  }
285
309
  }
286
310
  }
287
311
  }
288
312
  function collectImports(ast) {
289
- const root = ast;
290
313
  const map = /* @__PURE__ */ new Map();
291
- addImportsFromProgram(root?.instance?.content, map);
292
- addImportsFromProgram(root?.module?.content, map);
314
+ addImportsFromProgram(ast.instance?.content, map);
315
+ addImportsFromProgram(ast.module?.content, map);
293
316
  return map;
294
317
  }
295
318
 
296
319
  // src/providers/source/parse.ts
320
+ function childOf(node, key) {
321
+ return node[key];
322
+ }
297
323
  function collectSvelteHeads(node, acc) {
298
324
  if (Array.isArray(node)) {
299
325
  for (const child of node) collectSvelteHeads(child, acc);
@@ -302,32 +328,33 @@ function collectSvelteHeads(node, acc) {
302
328
  if (!node || typeof node !== "object") return;
303
329
  if (node.type === "SvelteHead") acc.push(node);
304
330
  for (const key of CHILD_NODE_KEYS) {
305
- if (key in node) collectSvelteHeads(node[key], acc);
331
+ if (key in node) collectSvelteHeads(childOf(node, key), acc);
306
332
  }
307
333
  }
308
334
  function tagsFromHead(head) {
309
335
  const tags = [];
310
- const children = head?.fragment?.nodes ?? [];
336
+ const children = head.fragment.nodes;
311
337
  for (const node of children) {
312
- if (node?.type === "TitleElement") {
313
- const titleNodes = node.fragment?.nodes ?? [];
338
+ if (node.type === "TitleElement") {
339
+ const titleNodes = node.fragment.nodes;
314
340
  const text = textFromNodes(titleNodes);
315
341
  tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
316
342
  continue;
317
343
  }
318
- if (node?.type !== "RegularElement") continue;
344
+ if (node.type !== "RegularElement") continue;
345
+ const attributes = node.attributes;
319
346
  if (node.name === "meta") {
320
- const charset = attrValue(node.attributes, "charset");
347
+ const charset = attrValue(attributes, "charset");
321
348
  if (charset !== "absent") {
322
349
  tags.push({ kind: "meta", name: "charset", value: charset });
323
350
  continue;
324
351
  }
325
- const name = attrText(node.attributes, "name");
326
- const property = attrText(node.attributes, "property");
327
- const content = name === "robots" ? attrText(node.attributes, "content") : void 0;
352
+ const name = attrText(attributes, "name");
353
+ const property = attrText(attributes, "property");
354
+ const content = name === "robots" ? attrText(attributes, "content") : void 0;
328
355
  const noindex = content !== void 0 && /(^|[\s,])(noindex|none)([\s,]|$)/i.test(content);
329
- const contentValue = attrValue(node.attributes, "content");
330
- const descText = name === "description" && contentValue === "static" ? attrText(node.attributes, "content") : void 0;
356
+ const contentValue = attrValue(attributes, "content");
357
+ const descText = name === "description" && contentValue === "static" ? attrText(attributes, "content") : void 0;
331
358
  tags.push({
332
359
  kind: "meta",
333
360
  ...name ? { name } : {},
@@ -337,33 +364,33 @@ function tagsFromHead(head) {
337
364
  ...descText !== void 0 ? { text: descText } : {}
338
365
  });
339
366
  } else if (node.name === "link") {
340
- const rel = attrText(node.attributes, "rel");
341
- const hasAs = findAttr3(node.attributes, "as") !== void 0;
342
- const asLiteral = attrText(node.attributes, "as");
343
- const hasCrossorigin = findAttr3(node.attributes, "crossorigin") !== void 0;
344
- const hreflang = attrText(node.attributes, "hreflang");
345
- const href = attrText(node.attributes, "href");
367
+ const rel = attrText(attributes, "rel");
368
+ const hasAs = findAttr3(attributes, "as") !== void 0;
369
+ const asLiteral = attrText(attributes, "as");
370
+ const hasCrossorigin = findAttr3(attributes, "crossorigin") !== void 0;
371
+ const hreflang = attrText(attributes, "hreflang");
372
+ const href = attrText(attributes, "href");
346
373
  tags.push({
347
374
  kind: "link",
348
375
  ...rel ? { rel } : {},
349
- value: attrValue(node.attributes, "href"),
376
+ value: attrValue(attributes, "href"),
350
377
  ...hasAs ? { hasAs: true } : {},
351
378
  ...asLiteral ? { as: asLiteral } : {},
352
379
  ...hasCrossorigin ? { hasCrossorigin: true } : {},
353
- // Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
380
+ // Keep a literal empty hreflang="" (present-but-invalid) so seo/hreflang can flag it.
354
381
  ...hreflang !== void 0 ? { hreflang } : {},
355
382
  ...href ? { href } : {}
356
383
  });
357
384
  } else if (node.name === "script") {
358
- const type = attrText(node.attributes, "type");
385
+ const type = attrText(attributes, "type");
359
386
  if (type === "application/ld+json") {
360
- const nodes = node.fragment?.nodes ?? [];
387
+ const nodes = node.fragment.nodes;
361
388
  const raw = textFromNodes(nodes);
362
389
  tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
363
390
  } else {
364
- const src = attrText(node.attributes, "src");
391
+ const src = attrText(attributes, "src");
365
392
  if (src) {
366
- const blocking = findAttr3(node.attributes, "defer") === void 0 && findAttr3(node.attributes, "async") === void 0 && type !== "module";
393
+ const blocking = findAttr3(attributes, "defer") === void 0 && findAttr3(attributes, "async") === void 0 && type !== "module";
367
394
  tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
368
395
  }
369
396
  }
@@ -377,16 +404,16 @@ function collectComponents(node, acc) {
377
404
  return;
378
405
  }
379
406
  if (!node || typeof node !== "object") return;
380
- if (node.type === "Component" && typeof node.name === "string") {
381
- const attributes = node.attributes ?? [];
407
+ if (node.type === "Component") {
408
+ const attributes = node.attributes;
382
409
  acc.push({
383
410
  name: node.name,
384
411
  attributes,
385
- hasSpread: attributes.some((a) => a?.type === "SpreadAttribute")
412
+ hasSpread: attributes.some((a) => a.type === "SpreadAttribute")
386
413
  });
387
414
  }
388
415
  for (const key of CHILD_NODE_KEYS) {
389
- if (key in node) collectComponents(node[key], acc);
416
+ if (key in node) collectComponents(childOf(node, key), acc);
390
417
  }
391
418
  }
392
419
  function collectImages(node, source, acc) {
@@ -396,8 +423,8 @@ function collectImages(node, source, acc) {
396
423
  }
397
424
  if (!node || typeof node !== "object") return;
398
425
  if (node.type === "RegularElement" && node.name === "img") {
399
- const attrs = node.attributes ?? [];
400
- const hasSpread = attrs.some((a) => a?.type === "SpreadAttribute");
426
+ const attrs = node.attributes;
427
+ const hasSpread = node.attributes.some((a) => a.type === "SpreadAttribute");
401
428
  acc.push({
402
429
  hasWidth: hasSpread || Boolean(findAttr3(attrs, "width")),
403
430
  hasHeight: hasSpread || Boolean(findAttr3(attrs, "height")),
@@ -410,7 +437,7 @@ function collectImages(node, source, acc) {
410
437
  });
411
438
  }
412
439
  for (const key of CHILD_NODE_KEYS) {
413
- if (key in node) collectImages(node[key], source, acc);
440
+ if (key in node) collectImages(childOf(node, key), source, acc);
414
441
  }
415
442
  }
416
443
  function collectHeadings(node, source, acc) {
@@ -420,23 +447,23 @@ function collectHeadings(node, source, acc) {
420
447
  }
421
448
  if (!node || typeof node !== "object") return;
422
449
  if (node.type === "SvelteHead") return;
423
- if (node.type === "RegularElement" && typeof node.name === "string" && /^h[1-6]$/.test(node.name)) {
450
+ if (node.type === "RegularElement" && /^h[1-6]$/.test(node.name)) {
424
451
  acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
425
452
  }
426
453
  for (const key of CHILD_NODE_KEYS) {
427
- if (key in node) collectHeadings(node[key], source, acc);
454
+ if (key in node) collectHeadings(childOf(node, key), source, acc);
428
455
  }
429
456
  }
430
457
  function parseFile(source, filename) {
431
458
  const ast = parse(source, { modern: true, filename });
432
459
  const heads = [];
433
- collectSvelteHeads(ast.fragment ?? ast, heads);
460
+ collectSvelteHeads(ast.fragment, heads);
434
461
  const components = [];
435
- collectComponents(ast.fragment ?? ast, components);
462
+ collectComponents(ast.fragment, components);
436
463
  const images = [];
437
- collectImages(ast.fragment ?? ast, source, images);
464
+ collectImages(ast.fragment, source, images);
438
465
  const headings = [];
439
- collectHeadings(ast.fragment ?? ast, source, headings);
466
+ collectHeadings(ast.fragment, source, headings);
440
467
  return {
441
468
  headTags: heads.flatMap(tagsFromHead),
442
469
  components,
@@ -1132,7 +1159,8 @@ var CONFIG_FILENAMES = ["svelte-vitals.config.mjs", "svelte-vitals.config.js", "
1132
1159
  var CATEGORIES = ["seo", "performance", "correctness", "security", "architecture"];
1133
1160
  var TREAT_DYNAMIC_AS_VALUES = ["pass", "warn", "fail"];
1134
1161
  var FAIL_ON_VALUES = ["critical", "warning", "info"];
1135
- var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["treatDynamicAs", "metaComponents", "rules", "failOn", "weights"]);
1162
+ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["treatDynamicAs", "metaComponents", "rules", "failOn", "weights", "overrides"]);
1163
+ var RULE_SETTING_VALUES = ["off", "critical", "warning", "info"];
1136
1164
  function isPlainObject2(value) {
1137
1165
  return typeof value === "object" && value !== null && !Array.isArray(value);
1138
1166
  }
@@ -1183,6 +1211,58 @@ function validateConfigFile(raw, path) {
1183
1211
  }
1184
1212
  config.rules = rules;
1185
1213
  }
1214
+ if (raw.overrides !== void 0) {
1215
+ if (!Array.isArray(raw.overrides)) {
1216
+ throw new Error(`${path}: overrides must be an array of { route/files, rules } entries.`);
1217
+ }
1218
+ const isGlob = (v) => typeof v === "string" && v.length > 0;
1219
+ const isGlobs = (v) => isGlob(v) || Array.isArray(v) && v.length > 0 && v.every(isGlob);
1220
+ const overrides = [];
1221
+ raw.overrides.forEach((entry, i) => {
1222
+ if (!isPlainObject2(entry)) {
1223
+ throw new Error(`${path}: overrides[${i}] must be an object with 'route' and/or 'files', and 'rules'.`);
1224
+ }
1225
+ if (entry.route !== void 0 && !isGlobs(entry.route)) {
1226
+ throw new Error(
1227
+ `${path}: overrides[${i}].route must be a non-empty string or a non-empty array of non-empty strings.`
1228
+ );
1229
+ }
1230
+ if (entry.files !== void 0 && !isGlobs(entry.files)) {
1231
+ throw new Error(
1232
+ `${path}: overrides[${i}].files must be a non-empty string or a non-empty array of non-empty strings.`
1233
+ );
1234
+ }
1235
+ if (entry.route === void 0 && entry.files === void 0) {
1236
+ throw new Error(`${path}: overrides[${i}] must set 'route' and/or 'files' to scope the override.`);
1237
+ }
1238
+ if (!isPlainObject2(entry.rules)) {
1239
+ throw new Error(`${path}: overrides[${i}].rules must be an object of rule-id/category \u2192 setting.`);
1240
+ }
1241
+ if (Object.keys(entry.rules).length === 0) {
1242
+ throw new Error(`${path}: overrides[${i}].rules must contain at least one rule id or category.`);
1243
+ }
1244
+ const nonCategoryKeys = Object.keys(entry.rules).filter((k) => !CATEGORIES.includes(k));
1245
+ const unknown = findUnknownRuleIds(nonCategoryKeys);
1246
+ if (unknown.length > 0) {
1247
+ throw new Error(
1248
+ `${path}: unknown rule id(s) or categories in overrides[${i}].rules: ${unknown.join(", ")}. Known categories: ${CATEGORIES.join(", ")}. Known rule ids: ${knownRuleIds().join(", ")}`
1249
+ );
1250
+ }
1251
+ for (const [key, setting] of Object.entries(entry.rules)) {
1252
+ if (!RULE_SETTING_VALUES.includes(setting)) {
1253
+ throw new Error(
1254
+ `${path}: overrides[${i}].rules.${key}: invalid setting '${String(setting)}'; expected ${RULE_SETTING_VALUES.join("|")}.`
1255
+ );
1256
+ }
1257
+ }
1258
+ overrides.push({
1259
+ ...entry.route !== void 0 ? { route: entry.route } : {},
1260
+ ...entry.files !== void 0 ? { files: entry.files } : {},
1261
+ rules: entry.rules
1262
+ });
1263
+ });
1264
+ config.overrides = overrides;
1265
+ }
1186
1266
  if (raw.weights !== void 0) {
1187
1267
  if (!isPlainObject2(raw.weights)) {
1188
1268
  throw new Error(`${path}: weights must be an object of category \u2192 number.`);
@@ -1303,7 +1383,8 @@ async function analyzeProject(opts = {}) {
1303
1383
  metaComponents: opts.metaComponents ?? file?.metaComponents ?? [],
1304
1384
  rules: opts.rules ?? file?.rules ?? {},
1305
1385
  failOn: opts.failOn ?? file?.failOn ?? "critical",
1306
- ...weights !== void 0 ? { weights } : {}
1386
+ ...weights !== void 0 ? { weights } : {},
1387
+ ...file?.overrides !== void 0 ? { overrides: file.overrides } : {}
1307
1388
  });
1308
1389
  await detectProject(rt, cwd);
1309
1390
  const matches = routeMatcher(opts.route);
@@ -1316,8 +1397,11 @@ async function analyzeProject(opts = {}) {
1316
1397
  const kitModules = opts.route ? [] : await collectKitModuleFacts(rt, cwd);
1317
1398
  const selected = selectRules(allRules2, config);
1318
1399
  const rules = opts.categories ? selected.filter((r) => opts.categories.includes(r.category)) : selected;
1319
- const results = applyRuleSeverities(
1320
- await runRules(rules, { heads, images, headings, components, project, config, kitModules }),
1400
+ const results = applyOverrides(
1401
+ applyRuleSeverities(
1402
+ await runRules(rules, { heads, images, headings, components, project, config, kitModules }),
1403
+ config
1404
+ ),
1321
1405
  config
1322
1406
  );
1323
1407
  return { results, config, version: readPackageVersion(), warnings };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { HeadTag, RuleSetting, Config, Severity, Category, Result } from '@svelte-vitals/core';
2
2
  export { defineConfig } from '@svelte-vitals/core';
3
+ import { AST } from 'svelte/compiler';
3
4
 
4
5
  /** A resolved import binding: which module, and which export ('default' for default imports). */
5
6
  interface ImportInfo {
@@ -11,10 +12,9 @@ type ImportMap = Map<string, ImportInfo>;
11
12
 
12
13
  /** A head tag parsed from one file, before layout-chain presence is assigned. */
13
14
  type ParsedTag = Omit<HeadTag, 'presence' | 'file'>;
14
- type Node = any;
15
15
  interface ComponentUse {
16
16
  name: string;
17
- attributes: Node[];
17
+ attributes: AST.Component['attributes'];
18
18
  hasSpread: boolean;
19
19
  }
20
20
  interface ParsedImage {
@@ -27,7 +27,7 @@ interface ParsedImage {
27
27
  /** 1-based source line, or 0 if unknown. */
28
28
  line: number;
29
29
  }
30
- /** A page-body heading (<h1>–<h6>) parsed from one file (SEO027). */
30
+ /** A page-body heading (<h1>–<h6>) parsed from one file (seo/single-h1). */
31
31
  interface ParsedHeading {
32
32
  /** Heading level 1–6. */
33
33
  level: number;
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  routeMatcher,
11
11
  run,
12
12
  spinnerEnabled
13
- } from "./chunk-AAUZ37EW.js";
13
+ } from "./chunk-3YO2A3YF.js";
14
14
  export {
15
15
  ProjectError,
16
16
  analyzeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-vitals",
3
- "version": "0.29.0",
3
+ "version": "0.31.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",
@@ -44,11 +44,12 @@
44
44
  "magicast": "^0.5.3",
45
45
  "mri": "^1.2.0",
46
46
  "smol-toml": "^1.7.0",
47
- "svelte": "^5.56.4",
47
+ "svelte": "^5.56.6",
48
48
  "tinyglobby": "^0.2.17",
49
- "@svelte-vitals/core": "0.26.0"
49
+ "@svelte-vitals/core": "0.28.0"
50
50
  },
51
51
  "devDependencies": {
52
+ "@types/estree": "^1.0.9",
52
53
  "@types/node": "^24.13.3"
53
54
  },
54
55
  "scripts": {