svelte-vitals 0.14.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-YMRN4VPN.js → chunk-2NZBWTKF.js} +189 -23
- 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))));
|
|
@@ -363,6 +366,109 @@ function parseFile(source, filename) {
|
|
|
363
366
|
headings
|
|
364
367
|
};
|
|
365
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
|
+
}
|
|
366
472
|
|
|
367
473
|
// src/providers/source/adapters/svelte-meta-tags.ts
|
|
368
474
|
function findAttr2(attributes, name) {
|
|
@@ -519,27 +625,68 @@ var MAX_DEPTH = 5;
|
|
|
519
625
|
function isGroupSegment(segment) {
|
|
520
626
|
return /^\(.+\)$/.test(segment);
|
|
521
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
|
+
}
|
|
522
652
|
function deriveRoute(pageRel) {
|
|
523
|
-
const
|
|
524
|
-
const segments = inner.length === 0 ? [] : inner.split("/").filter((s) => !isGroupSegment(s));
|
|
653
|
+
const segments = dirSegments(dirOf(pageRel)).filter((s) => !isGroupSegment(s));
|
|
525
654
|
return "/" + segments.join("/");
|
|
526
655
|
}
|
|
527
|
-
async function
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
for (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
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);
|
|
543
690
|
const composed = /* @__PURE__ */ new Map();
|
|
544
691
|
let broadOwn = false;
|
|
545
692
|
let broadInherited = false;
|
|
@@ -578,8 +725,8 @@ async function resolveRoute(rt, cwd, pageRel, config) {
|
|
|
578
725
|
};
|
|
579
726
|
}
|
|
580
727
|
async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
581
|
-
const pages = await enumerateRoutePages(rt, cwd);
|
|
582
|
-
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)));
|
|
583
730
|
return {
|
|
584
731
|
heads: facts.map((f) => f.head),
|
|
585
732
|
images: facts.map((f) => f.images),
|
|
@@ -587,6 +734,21 @@ async function collectRoutes(rt, cwd, config = defaultConfig) {
|
|
|
587
734
|
};
|
|
588
735
|
}
|
|
589
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
|
+
|
|
590
752
|
// src/version.ts
|
|
591
753
|
import { readFileSync } from "fs";
|
|
592
754
|
function readPackageVersion() {
|
|
@@ -668,8 +830,12 @@ async function analyzeProject(opts = {}) {
|
|
|
668
830
|
const images = collected.images.filter((i) => matches(i.route));
|
|
669
831
|
const headings = collected.headings.filter((h) => matches(h.route));
|
|
670
832
|
const project = await collectProjectFacts(rt, cwd);
|
|
833
|
+
const components = opts.route ? [] : await collectComponentFacts(rt, cwd);
|
|
671
834
|
const rules = selectRules(allRules2, config);
|
|
672
|
-
const results = applyRuleSeverities(
|
|
835
|
+
const results = applyRuleSeverities(
|
|
836
|
+
await runRules(rules, { heads, images, headings, components, project, config }),
|
|
837
|
+
config
|
|
838
|
+
);
|
|
673
839
|
return { results, config, version: readPackageVersion() };
|
|
674
840
|
}
|
|
675
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"
|