svelte-vitals 0.30.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-QBXO46PU.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 = "81d49a11b71a16ea1294212d43520a0137b4bd5e";
1002
- var ACTION_VERSION = "0.3.5";
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) {
@@ -53,7 +53,8 @@ import { defaultConfig } from "@svelte-vitals/core";
53
53
  // src/providers/source/project.ts
54
54
  import {
55
55
  ROBOTS_SOURCE_PATHS,
56
- SITEMAP_SOURCE_PATHS
56
+ SITEMAP_SOURCE_PATHS,
57
+ findMinifyDisabled
57
58
  } from "@svelte-vitals/core";
58
59
  var ProjectError = class extends Error {
59
60
  constructor(message) {
@@ -110,18 +111,39 @@ async function robotsRefsSitemap(rt, cwd) {
110
111
  return void 0;
111
112
  }
112
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
+ }
113
133
  async function collectProjectFacts(rt, cwd) {
114
- const [hasRobotsTxt, hasSitemap, htmlLang] = await Promise.all([
134
+ const [hasRobotsTxt, hasSitemap, htmlLang, viteMinifyDisabled] = await Promise.all([
115
135
  existsAny(rt, cwd, ROBOTS_SOURCE_PATHS),
116
136
  existsAny(rt, cwd, SITEMAP_SOURCE_PATHS),
117
- detectAppHtmlLang(rt, cwd)
137
+ detectAppHtmlLang(rt, cwd),
138
+ detectViteMinifyDisabled(rt, cwd)
118
139
  ]);
119
140
  const robotsReferencesSitemap = await robotsRefsSitemap(rt, cwd);
120
141
  return {
121
142
  hasRobotsTxt,
122
143
  hasSitemap,
123
144
  htmlLang,
124
- ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {}
145
+ ...robotsReferencesSitemap !== void 0 ? { robotsReferencesSitemap } : {},
146
+ ...viteMinifyDisabled ? { viteMinifyDisabled } : {}
125
147
  };
126
148
  }
127
149
 
@@ -139,16 +161,16 @@ function exprValue(node) {
139
161
  }
140
162
  function resolveMetaObject(attr, keyMap) {
141
163
  if (!attr) return { tags: [], opaque: false };
142
- 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;
143
165
  if (!expr || expr.type !== "ObjectExpression") return { tags: [], opaque: true };
144
166
  const tags = [];
145
167
  let opaque = false;
146
- for (const prop of expr.properties ?? []) {
147
- if (prop?.type !== "Property" || prop.computed) {
168
+ for (const prop of expr.properties) {
169
+ if (prop.type !== "Property" || prop.computed) {
148
170
  opaque = true;
149
171
  continue;
150
172
  }
151
- 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;
152
174
  const make = typeof key === "string" ? keyMap[key] : void 0;
153
175
  if (make) tags.push(make(exprValue(prop.value)));
154
176
  }
@@ -168,7 +190,7 @@ var TWITTER_KEYS = {
168
190
 
169
191
  // src/providers/source/adapters/svelte-meta-tags.ts
170
192
  function findAttr(attributes, name) {
171
- return attributes.find((a) => a?.type === "Attribute" && a.name === name);
193
+ return attributes.find((a) => a.type === "Attribute" && a.name === name);
172
194
  }
173
195
  var svelteMetaTagsAdapter = {
174
196
  match(info) {
@@ -221,7 +243,7 @@ var svelteMetaTagsJsonLdAdapter = {
221
243
  // src/providers/source/adapters/svelte-seo.ts
222
244
  import { attrValueOf as attrValueOf2, attrTextOf as attrTextOf2 } from "@svelte-vitals/core";
223
245
  function findAttr2(attributes, name) {
224
- return attributes.find((a) => a?.type === "Attribute" && a.name === name);
246
+ return attributes.find((a) => a.type === "Attribute" && a.name === name);
225
247
  }
226
248
  var svelteSeoAdapter = {
227
249
  match(info) {
@@ -273,28 +295,31 @@ import {
273
295
  // src/providers/source/imports.ts
274
296
  function addImportsFromProgram(program, map) {
275
297
  for (const node of program?.body ?? []) {
276
- if (node?.type !== "ImportDeclaration") continue;
277
- const source = String(node.source?.value ?? "");
278
- for (const spec of node.specifiers ?? []) {
279
- 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;
280
302
  if (!local) continue;
281
303
  if (spec.type === "ImportDefaultSpecifier") {
282
304
  map.set(local, { source, imported: "default" });
283
305
  } else if (spec.type === "ImportSpecifier") {
284
- 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 });
285
308
  }
286
309
  }
287
310
  }
288
311
  }
289
312
  function collectImports(ast) {
290
- const root = ast;
291
313
  const map = /* @__PURE__ */ new Map();
292
- addImportsFromProgram(root?.instance?.content, map);
293
- addImportsFromProgram(root?.module?.content, map);
314
+ addImportsFromProgram(ast.instance?.content, map);
315
+ addImportsFromProgram(ast.module?.content, map);
294
316
  return map;
295
317
  }
296
318
 
297
319
  // src/providers/source/parse.ts
320
+ function childOf(node, key) {
321
+ return node[key];
322
+ }
298
323
  function collectSvelteHeads(node, acc) {
299
324
  if (Array.isArray(node)) {
300
325
  for (const child of node) collectSvelteHeads(child, acc);
@@ -303,32 +328,33 @@ function collectSvelteHeads(node, acc) {
303
328
  if (!node || typeof node !== "object") return;
304
329
  if (node.type === "SvelteHead") acc.push(node);
305
330
  for (const key of CHILD_NODE_KEYS) {
306
- if (key in node) collectSvelteHeads(node[key], acc);
331
+ if (key in node) collectSvelteHeads(childOf(node, key), acc);
307
332
  }
308
333
  }
309
334
  function tagsFromHead(head) {
310
335
  const tags = [];
311
- const children = head?.fragment?.nodes ?? [];
336
+ const children = head.fragment.nodes;
312
337
  for (const node of children) {
313
- if (node?.type === "TitleElement") {
314
- const titleNodes = node.fragment?.nodes ?? [];
338
+ if (node.type === "TitleElement") {
339
+ const titleNodes = node.fragment.nodes;
315
340
  const text = textFromNodes(titleNodes);
316
341
  tags.push({ kind: "title", value: valueFromNodes(titleNodes), ...text !== void 0 ? { text } : {} });
317
342
  continue;
318
343
  }
319
- if (node?.type !== "RegularElement") continue;
344
+ if (node.type !== "RegularElement") continue;
345
+ const attributes = node.attributes;
320
346
  if (node.name === "meta") {
321
- const charset = attrValue(node.attributes, "charset");
347
+ const charset = attrValue(attributes, "charset");
322
348
  if (charset !== "absent") {
323
349
  tags.push({ kind: "meta", name: "charset", value: charset });
324
350
  continue;
325
351
  }
326
- const name = attrText(node.attributes, "name");
327
- const property = attrText(node.attributes, "property");
328
- 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;
329
355
  const noindex = content !== void 0 && /(^|[\s,])(noindex|none)([\s,]|$)/i.test(content);
330
- const contentValue = attrValue(node.attributes, "content");
331
- 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;
332
358
  tags.push({
333
359
  kind: "meta",
334
360
  ...name ? { name } : {},
@@ -338,33 +364,33 @@ function tagsFromHead(head) {
338
364
  ...descText !== void 0 ? { text: descText } : {}
339
365
  });
340
366
  } else if (node.name === "link") {
341
- const rel = attrText(node.attributes, "rel");
342
- const hasAs = findAttr3(node.attributes, "as") !== void 0;
343
- const asLiteral = attrText(node.attributes, "as");
344
- const hasCrossorigin = findAttr3(node.attributes, "crossorigin") !== void 0;
345
- const hreflang = attrText(node.attributes, "hreflang");
346
- 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");
347
373
  tags.push({
348
374
  kind: "link",
349
375
  ...rel ? { rel } : {},
350
- value: attrValue(node.attributes, "href"),
376
+ value: attrValue(attributes, "href"),
351
377
  ...hasAs ? { hasAs: true } : {},
352
378
  ...asLiteral ? { as: asLiteral } : {},
353
379
  ...hasCrossorigin ? { hasCrossorigin: true } : {},
354
- // 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.
355
381
  ...hreflang !== void 0 ? { hreflang } : {},
356
382
  ...href ? { href } : {}
357
383
  });
358
384
  } else if (node.name === "script") {
359
- const type = attrText(node.attributes, "type");
385
+ const type = attrText(attributes, "type");
360
386
  if (type === "application/ld+json") {
361
- const nodes = node.fragment?.nodes ?? [];
387
+ const nodes = node.fragment.nodes;
362
388
  const raw = textFromNodes(nodes);
363
389
  tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
364
390
  } else {
365
- const src = attrText(node.attributes, "src");
391
+ const src = attrText(attributes, "src");
366
392
  if (src) {
367
- 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";
368
394
  tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
369
395
  }
370
396
  }
@@ -378,16 +404,16 @@ function collectComponents(node, acc) {
378
404
  return;
379
405
  }
380
406
  if (!node || typeof node !== "object") return;
381
- if (node.type === "Component" && typeof node.name === "string") {
382
- const attributes = node.attributes ?? [];
407
+ if (node.type === "Component") {
408
+ const attributes = node.attributes;
383
409
  acc.push({
384
410
  name: node.name,
385
411
  attributes,
386
- hasSpread: attributes.some((a) => a?.type === "SpreadAttribute")
412
+ hasSpread: attributes.some((a) => a.type === "SpreadAttribute")
387
413
  });
388
414
  }
389
415
  for (const key of CHILD_NODE_KEYS) {
390
- if (key in node) collectComponents(node[key], acc);
416
+ if (key in node) collectComponents(childOf(node, key), acc);
391
417
  }
392
418
  }
393
419
  function collectImages(node, source, acc) {
@@ -397,8 +423,8 @@ function collectImages(node, source, acc) {
397
423
  }
398
424
  if (!node || typeof node !== "object") return;
399
425
  if (node.type === "RegularElement" && node.name === "img") {
400
- const attrs = node.attributes ?? [];
401
- const hasSpread = attrs.some((a) => a?.type === "SpreadAttribute");
426
+ const attrs = node.attributes;
427
+ const hasSpread = node.attributes.some((a) => a.type === "SpreadAttribute");
402
428
  acc.push({
403
429
  hasWidth: hasSpread || Boolean(findAttr3(attrs, "width")),
404
430
  hasHeight: hasSpread || Boolean(findAttr3(attrs, "height")),
@@ -411,7 +437,7 @@ function collectImages(node, source, acc) {
411
437
  });
412
438
  }
413
439
  for (const key of CHILD_NODE_KEYS) {
414
- if (key in node) collectImages(node[key], source, acc);
440
+ if (key in node) collectImages(childOf(node, key), source, acc);
415
441
  }
416
442
  }
417
443
  function collectHeadings(node, source, acc) {
@@ -421,23 +447,23 @@ function collectHeadings(node, source, acc) {
421
447
  }
422
448
  if (!node || typeof node !== "object") return;
423
449
  if (node.type === "SvelteHead") return;
424
- 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)) {
425
451
  acc.push({ level: Number(node.name[1]), line: lineOf(source, node.start) });
426
452
  }
427
453
  for (const key of CHILD_NODE_KEYS) {
428
- if (key in node) collectHeadings(node[key], source, acc);
454
+ if (key in node) collectHeadings(childOf(node, key), source, acc);
429
455
  }
430
456
  }
431
457
  function parseFile(source, filename) {
432
458
  const ast = parse(source, { modern: true, filename });
433
459
  const heads = [];
434
- collectSvelteHeads(ast.fragment ?? ast, heads);
460
+ collectSvelteHeads(ast.fragment, heads);
435
461
  const components = [];
436
- collectComponents(ast.fragment ?? ast, components);
462
+ collectComponents(ast.fragment, components);
437
463
  const images = [];
438
- collectImages(ast.fragment ?? ast, source, images);
464
+ collectImages(ast.fragment, source, images);
439
465
  const headings = [];
440
- collectHeadings(ast.fragment ?? ast, source, headings);
466
+ collectHeadings(ast.fragment, source, headings);
441
467
  return {
442
468
  headTags: heads.flatMap(tagsFromHead),
443
469
  components,
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-QBXO46PU.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.30.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.27.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": {