svelte-vitals 0.13.0 → 0.15.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 +1 -1
- package/dist/{chunk-Y4JY3ODE.js → chunk-2NZBWTKF.js} +210 -28
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/dist/bin.js
CHANGED
|
@@ -77,8 +77,11 @@ async function detectProject(rt, cwd) {
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
async function enumerateRoutePages(rt, cwd) {
|
|
80
|
-
const
|
|
81
|
-
|
|
80
|
+
const [plain, breakout] = await Promise.all([
|
|
81
|
+
rt.glob(`${ROUTES_DIR}/**/+page.svelte`, cwd),
|
|
82
|
+
rt.glob(`${ROUTES_DIR}/**/+page@*.svelte`, cwd)
|
|
83
|
+
]);
|
|
84
|
+
return [.../* @__PURE__ */ new Set([...plain, ...breakout])].sort();
|
|
82
85
|
}
|
|
83
86
|
async function existsAny(rt, cwd, paths) {
|
|
84
87
|
const found = await Promise.all(paths.map((p) => rt.exists(rt.join(cwd, p))));
|
|
@@ -247,6 +250,7 @@ function tagsFromHead(head) {
|
|
|
247
250
|
const asLiteral = attrText(node.attributes, "as");
|
|
248
251
|
const hasCrossorigin = findAttr(node.attributes, "crossorigin") !== void 0;
|
|
249
252
|
const hreflang = attrText(node.attributes, "hreflang");
|
|
253
|
+
const href = attrText(node.attributes, "href");
|
|
250
254
|
tags.push({
|
|
251
255
|
kind: "link",
|
|
252
256
|
...rel ? { rel } : {},
|
|
@@ -255,12 +259,22 @@ function tagsFromHead(head) {
|
|
|
255
259
|
...asLiteral ? { as: asLiteral } : {},
|
|
256
260
|
...hasCrossorigin ? { hasCrossorigin: true } : {},
|
|
257
261
|
// Keep a literal empty hreflang="" (present-but-invalid) so SEO026 can flag it.
|
|
258
|
-
...hreflang !== void 0 ? { hreflang } : {}
|
|
262
|
+
...hreflang !== void 0 ? { hreflang } : {},
|
|
263
|
+
...href ? { href } : {}
|
|
259
264
|
});
|
|
260
|
-
} else if (node.name === "script"
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
265
|
+
} else if (node.name === "script") {
|
|
266
|
+
const type = attrText(node.attributes, "type");
|
|
267
|
+
if (type === "application/ld+json") {
|
|
268
|
+
const nodes = node.fragment?.nodes ?? [];
|
|
269
|
+
const raw = textFromNodes(nodes);
|
|
270
|
+
tags.push({ kind: "jsonld", value: valueFromNodes(nodes), ...raw !== void 0 ? { jsonld: raw } : {} });
|
|
271
|
+
} else {
|
|
272
|
+
const src = attrText(node.attributes, "src");
|
|
273
|
+
if (src) {
|
|
274
|
+
const blocking = findAttr(node.attributes, "defer") === void 0 && findAttr(node.attributes, "async") === void 0 && type !== "module";
|
|
275
|
+
tags.push({ kind: "script", value: "static", href: src, ...blocking ? { blocking: true } : {} });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
264
278
|
}
|
|
265
279
|
}
|
|
266
280
|
return tags;
|
|
@@ -310,6 +324,9 @@ function collectImages(node, source, acc) {
|
|
|
310
324
|
hasHeight: hasSpread || Boolean(findAttr(attrs, "height")),
|
|
311
325
|
hasLoading: hasSpread || Boolean(findAttr(attrs, "loading")),
|
|
312
326
|
hasAlt: hasSpread || Boolean(findAttr(attrs, "alt")),
|
|
327
|
+
// A literal loading="lazy" only — a spread or dynamic loading={…} must not be flagged.
|
|
328
|
+
lazy: attrText(attrs, "loading") === "lazy",
|
|
329
|
+
hasSrcset: hasSpread || Boolean(findAttr(attrs, "srcset")),
|
|
313
330
|
line: lineOf(source, node.start)
|
|
314
331
|
});
|
|
315
332
|
}
|
|
@@ -349,6 +366,109 @@ function parseFile(source, filename) {
|
|
|
349
366
|
headings
|
|
350
367
|
};
|
|
351
368
|
}
|
|
369
|
+
function isConstantListEach(node) {
|
|
370
|
+
const expr = node?.expression;
|
|
371
|
+
return expr?.type === "ArrayExpression" && Array.isArray(expr.elements) && !expr.elements.some((el) => el?.type === "SpreadElement");
|
|
372
|
+
}
|
|
373
|
+
function collectEachBlocks(node, source, acc) {
|
|
374
|
+
if (Array.isArray(node)) {
|
|
375
|
+
for (const child of node) collectEachBlocks(child, source, acc);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!node || typeof node !== "object") return;
|
|
379
|
+
if (node.type === "EachBlock" && !isConstantListEach(node)) {
|
|
380
|
+
acc.push({ hasKey: node.key != null, line: lineOf(source, node.start) });
|
|
381
|
+
}
|
|
382
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
383
|
+
if (key in node) collectEachBlocks(node[key], source, acc);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function walkEstree(node, visit) {
|
|
387
|
+
if (Array.isArray(node)) {
|
|
388
|
+
for (const child of node) walkEstree(child, visit);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
392
|
+
visit(node);
|
|
393
|
+
for (const key of Object.keys(node)) {
|
|
394
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
|
|
395
|
+
walkEstree(node[key], visit);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function isEffectCall(node) {
|
|
399
|
+
const c = node?.callee;
|
|
400
|
+
if (c?.type === "Identifier") return c.name === "$effect";
|
|
401
|
+
if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$effect") {
|
|
402
|
+
return c.property?.type === "Identifier" && c.property.name === "pre";
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
function isStateDeclaration(node) {
|
|
407
|
+
const c = node?.callee;
|
|
408
|
+
if (c?.type === "Identifier") return c.name === "$state";
|
|
409
|
+
if (c?.type === "MemberExpression" && c.object?.type === "Identifier" && c.object.name === "$state") {
|
|
410
|
+
return c.property?.type === "Identifier" && (c.property.name === "raw" || c.property.name === "frozen");
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
function bodyOnlyAssignsState(fn, stateNames) {
|
|
415
|
+
const isStateAssign = (expr) => expr?.type === "AssignmentExpression" && expr.operator === "=" && expr.left?.type === "Identifier" && stateNames.has(expr.left.name);
|
|
416
|
+
const body = fn?.body;
|
|
417
|
+
if (!body) return false;
|
|
418
|
+
if (body.type !== "BlockStatement") return isStateAssign(body);
|
|
419
|
+
if (body.body.length === 0) return false;
|
|
420
|
+
return body.body.every((s) => s?.type === "ExpressionStatement" && isStateAssign(s.expression));
|
|
421
|
+
}
|
|
422
|
+
var URL_ATTRS = ["href", "src", "action", "formaction"];
|
|
423
|
+
function collectSecurityFacts(node, source, htmlTags, jsUrls) {
|
|
424
|
+
if (Array.isArray(node)) {
|
|
425
|
+
for (const child of node) collectSecurityFacts(child, source, htmlTags, jsUrls);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (!node || typeof node !== "object") return;
|
|
429
|
+
if (node.type === "HtmlTag") htmlTags.push({ line: lineOf(source, node.start) });
|
|
430
|
+
if ((node.type === "RegularElement" || node.type === "SvelteElement") && Array.isArray(node.attributes)) {
|
|
431
|
+
for (const name of URL_ATTRS) {
|
|
432
|
+
const attr = findAttr(node.attributes, name);
|
|
433
|
+
if (!attr) continue;
|
|
434
|
+
const value = attrTextOf(attr);
|
|
435
|
+
if (value !== void 0 && /^\s*javascript:/i.test(value)) {
|
|
436
|
+
jsUrls.push({ line: lineOf(source, attr.start ?? node.start) });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
441
|
+
if (key in node) collectSecurityFacts(node[key], source, htmlTags, jsUrls);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function parseComponentFacts(source, filename) {
|
|
445
|
+
const ast = parse(source, { modern: true, filename });
|
|
446
|
+
const eachBlocks = [];
|
|
447
|
+
collectEachBlocks(ast.fragment ?? ast, source, eachBlocks);
|
|
448
|
+
const htmlTags = [];
|
|
449
|
+
const javascriptUrls = [];
|
|
450
|
+
collectSecurityFacts(ast.fragment ?? ast, source, htmlTags, javascriptUrls);
|
|
451
|
+
const effects = [];
|
|
452
|
+
const program = ast.instance?.content;
|
|
453
|
+
if (program) {
|
|
454
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
455
|
+
walkEstree(program, (n) => {
|
|
456
|
+
if (n.type === "VariableDeclarator" && n.init && isStateDeclaration(n.init) && n.id?.type === "Identifier") {
|
|
457
|
+
stateNames.add(n.id.name);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
walkEstree(program, (n) => {
|
|
461
|
+
if (n.type !== "CallExpression" || !isEffectCall(n)) return;
|
|
462
|
+
const fn = n.arguments?.[0];
|
|
463
|
+
const isFn = fn?.type === "ArrowFunctionExpression" || fn?.type === "FunctionExpression";
|
|
464
|
+
effects.push({
|
|
465
|
+
line: lineOf(source, n.start),
|
|
466
|
+
assignsOnlyState: isFn ? bodyOnlyAssignsState(fn, stateNames) : false
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return { eachBlocks, effects, htmlTags, javascriptUrls };
|
|
471
|
+
}
|
|
352
472
|
|
|
353
473
|
// src/providers/source/adapters/svelte-meta-tags.ts
|
|
354
474
|
function findAttr2(attributes, name) {
|
|
@@ -443,6 +563,8 @@ function tagKey(tag) {
|
|
|
443
563
|
return `link:${tag.rel ?? "?"}`;
|
|
444
564
|
case "jsonld":
|
|
445
565
|
return "jsonld";
|
|
566
|
+
case "script":
|
|
567
|
+
return `script:${tag.href ?? "?"}`;
|
|
446
568
|
}
|
|
447
569
|
}
|
|
448
570
|
function resolveComponentPath(source, fromFileRel) {
|
|
@@ -503,27 +625,68 @@ var MAX_DEPTH = 5;
|
|
|
503
625
|
function isGroupSegment(segment) {
|
|
504
626
|
return /^\(.+\)$/.test(segment);
|
|
505
627
|
}
|
|
628
|
+
function dirOf(rel) {
|
|
629
|
+
const i = rel.lastIndexOf("/");
|
|
630
|
+
return i >= 0 ? rel.slice(0, i) : "";
|
|
631
|
+
}
|
|
632
|
+
function dirSegments(dir) {
|
|
633
|
+
const extra = dir.slice(ROUTES_DIR2.length);
|
|
634
|
+
return extra.length === 0 ? [] : extra.split("/").filter(Boolean);
|
|
635
|
+
}
|
|
636
|
+
function dirKey(segs) {
|
|
637
|
+
return segs.length === 0 ? ROUTES_DIR2 : `${ROUTES_DIR2}/${segs.join("/")}`;
|
|
638
|
+
}
|
|
639
|
+
function parseAt(rel) {
|
|
640
|
+
const file = rel.slice(rel.lastIndexOf("/") + 1);
|
|
641
|
+
const m = /^\+(?:page|layout)@(.*)\.svelte$/.exec(file);
|
|
642
|
+
return m ? m[1] : null;
|
|
643
|
+
}
|
|
644
|
+
function atTarget(rel, dirSegs, strictAncestor = false) {
|
|
645
|
+
const at = parseAt(rel);
|
|
646
|
+
if (at === null) return null;
|
|
647
|
+
if (at === "") return [];
|
|
648
|
+
const haystack = strictAncestor ? dirSegs.slice(0, -1) : dirSegs;
|
|
649
|
+
const i = haystack.lastIndexOf(at);
|
|
650
|
+
return i >= 0 ? dirSegs.slice(0, i + 1) : null;
|
|
651
|
+
}
|
|
506
652
|
function deriveRoute(pageRel) {
|
|
507
|
-
const
|
|
508
|
-
const segments = inner.length === 0 ? [] : inner.split("/").filter((s) => !isGroupSegment(s));
|
|
653
|
+
const segments = dirSegments(dirOf(pageRel)).filter((s) => !isGroupSegment(s));
|
|
509
654
|
return "/" + segments.join("/");
|
|
510
655
|
}
|
|
511
|
-
async function
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
for (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
656
|
+
async function collectLayouts(rt, cwd) {
|
|
657
|
+
const [plain, breakout] = await Promise.all([
|
|
658
|
+
rt.glob(`${ROUTES_DIR2}/**/+layout.svelte`, cwd),
|
|
659
|
+
rt.glob(`${ROUTES_DIR2}/**/+layout@*.svelte`, cwd)
|
|
660
|
+
]);
|
|
661
|
+
const map = /* @__PURE__ */ new Map();
|
|
662
|
+
for (const rel of [...plain, ...breakout]) map.set(dirOf(rel), rel);
|
|
663
|
+
return map;
|
|
664
|
+
}
|
|
665
|
+
function layoutAtOrAbove(segs, layouts) {
|
|
666
|
+
for (let j = segs.length; j >= 0; j--) {
|
|
667
|
+
const rel = layouts.get(dirKey(segs.slice(0, j)));
|
|
668
|
+
if (rel) return { segs: segs.slice(0, j), rel };
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
function chainFiles(pageRel, layouts) {
|
|
673
|
+
const pageSegs = dirSegments(dirOf(pageRel));
|
|
674
|
+
let dir = atTarget(pageRel, pageSegs) ?? pageSegs;
|
|
675
|
+
const chain = [];
|
|
676
|
+
const seen = /* @__PURE__ */ new Set();
|
|
677
|
+
while (dir !== null) {
|
|
678
|
+
const found = layoutAtOrAbove(dir, layouts);
|
|
679
|
+
if (!found || seen.has(found.rel)) break;
|
|
680
|
+
seen.add(found.rel);
|
|
681
|
+
chain.unshift(found.rel);
|
|
682
|
+
const reset = atTarget(found.rel, found.segs, true);
|
|
683
|
+
dir = reset !== null && reset.length < found.segs.length ? reset : found.segs.slice(0, -1);
|
|
684
|
+
if (dir.length === 0 && found.segs.length === 0) dir = null;
|
|
685
|
+
}
|
|
686
|
+
return [...chain.map((rel) => ({ rel, isPage: false })), { rel: pageRel, isPage: true }];
|
|
687
|
+
}
|
|
688
|
+
async function resolveRoute(rt, cwd, pageRel, config, layouts) {
|
|
689
|
+
const files = chainFiles(pageRel, layouts);
|
|
527
690
|
const composed = /* @__PURE__ */ new Map();
|
|
528
691
|
let broadOwn = false;
|
|
529
692
|
let broadInherited = false;
|
|
@@ -562,8 +725,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
562
725
|
};
|
|
563
726
|
}
|
|
564
727
|
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
565
|
-
const pages = await enumerateRoutePages(rt, cwd);
|
|
566
|
-
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config)));
|
|
728
|
+
const [pages, layouts] = await Promise.all([enumerateRoutePages(rt, cwd), collectLayouts(rt, cwd)]);
|
|
729
|
+
const facts = await Promise.all(pages.map((page) => resolveRoute(rt, cwd, page, config, layouts)));
|
|
567
730
|
return {
|
|
568
731
|
heads: facts.map((f) => f.head),
|
|
569
732
|
images: facts.map((f) => f.images),
|
|
@@ -571,6 +734,21 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
571
734
|
};
|
|
572
735
|
}
|
|
573
736
|
|
|
737
|
+
// src/providers/source/components.ts
|
|
738
|
+
async function collectComponentFacts(rt, cwd) {
|
|
739
|
+
const files = await rt.glob("src/**/*.svelte", cwd);
|
|
740
|
+
return Promise.all(
|
|
741
|
+
files.sort().map(async (rel) => {
|
|
742
|
+
try {
|
|
743
|
+
const source = await rt.readFile(rt.join(cwd, rel));
|
|
744
|
+
return { file: rel, ...parseComponentFacts(source, rel) };
|
|
745
|
+
} catch {
|
|
746
|
+
return { file: rel, eachBlocks: [], effects: [], htmlTags: [], javascriptUrls: [] };
|
|
747
|
+
}
|
|
748
|
+
})
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
574
752
|
// src/version.ts
|
|
575
753
|
import { readFileSync } from "fs";
|
|
576
754
|
function readPackageVersion() {
|
|
@@ -652,8 +830,12 @@ async function analyzeProject(opts = {}) {
|
|
|
652
830
|
const images = collected.images.filter((i) => matches(i.route));
|
|
653
831
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
654
832
|
const project = await collectProjectFacts(rt, cwd);
|
|
833
|
+
const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
|
|
655
834
|
const rules = selectRules(allRules2, config);
|
|
656
|
-
const results = applyRuleSeverities(
|
|
835
|
+
const results = applyRuleSeverities(
|
|
836
|
+
await runRules(rules, { heads, images, headings, components, project, config }),
|
|
837
|
+
config
|
|
838
|
+
);
|
|
657
839
|
return { results, config, version: readPackageVersion() };
|
|
658
840
|
}
|
|
659
841
|
async function run(opts = {}) {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-vitals",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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.
|
|
45
|
+
"@svelte-vitals/core": "0.16.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^24.7.0"
|