@svelte-vitals/core 0.26.0 → 0.28.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/index.d.ts +271 -123
- package/dist/index.js +1326 -546
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -86,9 +86,62 @@ function attrTextOf(attr) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// src/component-parse.ts
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
function unwrapTs(expr) {
|
|
90
|
+
let cur = expr;
|
|
91
|
+
while (cur.type === "TSSatisfiesExpression" || cur.type === "TSAsExpression" || cur.type === "TSNonNullExpression")
|
|
92
|
+
cur = cur.expression;
|
|
93
|
+
return cur;
|
|
94
|
+
}
|
|
95
|
+
function isLengthOnlyArrayCall(expr) {
|
|
96
|
+
const e = unwrapTs(expr);
|
|
97
|
+
if (!e) return false;
|
|
98
|
+
if ((e.type === "CallExpression" || e.type === "NewExpression") && e.callee?.type === "Identifier" && e.callee.name === "Array") {
|
|
99
|
+
return (e.arguments?.length ?? 0) === 1;
|
|
100
|
+
}
|
|
101
|
+
if (e.type === "CallExpression" && e.callee?.type === "MemberExpression" && !e.callee.computed && e.callee.object?.type === "Identifier" && e.callee.object.name === "Array" && e.callee.property.type === "Identifier" && e.callee.property.name === "from" && e.arguments?.[0]?.type === "ObjectExpression") {
|
|
102
|
+
return (e.arguments[0].properties ?? []).some(
|
|
103
|
+
(p) => p?.type === "Property" && !p.computed && (p.key?.name === "length" || p.key?.value === "length")
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function isIdentityFreeEach(node) {
|
|
109
|
+
const expr = unwrapTs(node.expression);
|
|
110
|
+
if (expr.type === "ArrayExpression" && Array.isArray(expr.elements)) {
|
|
111
|
+
return expr.elements.every((el) => el?.type !== "SpreadElement" || isLengthOnlyArrayCall(el.argument));
|
|
112
|
+
}
|
|
113
|
+
return isLengthOnlyArrayCall(expr);
|
|
114
|
+
}
|
|
115
|
+
function isIndexExpression(expr, index) {
|
|
116
|
+
const e = unwrapTs(expr);
|
|
117
|
+
if (e.type === "Identifier") return e.name === index;
|
|
118
|
+
if (e.type === "CallExpression") {
|
|
119
|
+
const callee = e.callee;
|
|
120
|
+
if (callee.type === "Identifier" && (callee.name === "String" || callee.name === "Number") && e.arguments.length === 1) {
|
|
121
|
+
return isIndexExpression(e.arguments[0], index);
|
|
122
|
+
}
|
|
123
|
+
if (callee.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "toString" && e.arguments.length === 0) {
|
|
124
|
+
return isIndexExpression(callee.object, index);
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (e.type === "TemplateLiteral") {
|
|
129
|
+
const exprs = e.expressions;
|
|
130
|
+
if (exprs.length !== 1) return false;
|
|
131
|
+
const hasText = e.quasis.some((q) => (q.value.cooked ?? q.value.raw) !== "");
|
|
132
|
+
if (hasText) return false;
|
|
133
|
+
return isIndexExpression(exprs[0], index);
|
|
134
|
+
}
|
|
135
|
+
if (e.type === "BinaryExpression" && e.operator === "+") {
|
|
136
|
+
const emptyString = (n) => n.type === "Literal" && n.value === "";
|
|
137
|
+
if (emptyString(e.left)) return isIndexExpression(e.right, index);
|
|
138
|
+
if (emptyString(e.right)) return isIndexExpression(e.left, index);
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function isIndexKey(each) {
|
|
143
|
+
if (typeof each.index !== "string" || each.key == null) return false;
|
|
144
|
+
return isIndexExpression(each.key, each.index);
|
|
92
145
|
}
|
|
93
146
|
function collectEachBlocks(node, source, acc) {
|
|
94
147
|
if (Array.isArray(node)) {
|
|
@@ -96,8 +149,12 @@ function collectEachBlocks(node, source, acc) {
|
|
|
96
149
|
return;
|
|
97
150
|
}
|
|
98
151
|
if (!node || typeof node !== "object") return;
|
|
99
|
-
if (node.type === "EachBlock" && node.context != null && !
|
|
100
|
-
acc.push({
|
|
152
|
+
if (node.type === "EachBlock" && node.context != null && !isIdentityFreeEach(node)) {
|
|
153
|
+
acc.push({
|
|
154
|
+
hasKey: node.key != null,
|
|
155
|
+
line: lineOf(source, node.start),
|
|
156
|
+
...isIndexKey(node) ? { indexKey: true } : {}
|
|
157
|
+
});
|
|
101
158
|
}
|
|
102
159
|
for (const key of CHILD_NODE_KEYS) {
|
|
103
160
|
if (key in node) collectEachBlocks(node[key], source, acc);
|
|
@@ -199,6 +256,12 @@ function scopeIntroducedNames(node) {
|
|
|
199
256
|
}
|
|
200
257
|
} else if (node.type === "EachBlock" && node.context) {
|
|
201
258
|
addBoundNames(node.context, introduced);
|
|
259
|
+
if (typeof node.index === "string") introduced.add(node.index);
|
|
260
|
+
} else if (node.type === "SnippetBlock") {
|
|
261
|
+
for (const p of node.parameters ?? []) addBoundNames(p, introduced);
|
|
262
|
+
} else if (node.type === "AwaitBlock") {
|
|
263
|
+
if (node.value) addBoundNames(node.value, introduced);
|
|
264
|
+
if (node.error) addBoundNames(node.error, introduced);
|
|
202
265
|
}
|
|
203
266
|
return introduced;
|
|
204
267
|
}
|
|
@@ -216,43 +279,240 @@ function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
|
|
|
216
279
|
walkScoped(node[key], visit, scope);
|
|
217
280
|
}
|
|
218
281
|
}
|
|
219
|
-
function collectStateWrites(root, stateNames, acc) {
|
|
282
|
+
function collectStateWrites(root, stateNames, acc, kinds) {
|
|
283
|
+
const record = (name, kind) => {
|
|
284
|
+
acc.add(name);
|
|
285
|
+
if (kinds) {
|
|
286
|
+
let set = kinds.get(name);
|
|
287
|
+
if (!set) kinds.set(name, set = /* @__PURE__ */ new Set());
|
|
288
|
+
set.add(kind);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
220
291
|
walkScoped(root, (n, scope) => {
|
|
221
292
|
const shadowed = (name) => name === void 0 || scope.has(name);
|
|
222
293
|
if (n?.type === "AssignmentExpression") {
|
|
223
294
|
if (n.left?.type === "Identifier" && stateNames.has(n.left.name) && !shadowed(n.left.name)) {
|
|
224
|
-
|
|
295
|
+
record(n.left.name, "reassign");
|
|
225
296
|
} else if (n.left?.type === "MemberExpression") {
|
|
226
297
|
const r = rootObjectName(n.left);
|
|
227
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
298
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
228
299
|
} else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
|
|
229
300
|
const bound = /* @__PURE__ */ new Set();
|
|
230
301
|
addBoundNames(n.left, bound);
|
|
231
|
-
for (const name of bound) if (stateNames.has(name) && !shadowed(name))
|
|
302
|
+
for (const name of bound) if (stateNames.has(name) && !shadowed(name)) record(name, "reassign");
|
|
232
303
|
}
|
|
233
304
|
} else if (n?.type === "UpdateExpression") {
|
|
234
|
-
|
|
235
|
-
|
|
305
|
+
if (n.argument?.type === "Identifier") {
|
|
306
|
+
if (stateNames.has(n.argument.name) && !shadowed(n.argument.name)) record(n.argument.name, "reassign");
|
|
307
|
+
} else {
|
|
308
|
+
const r = rootObjectName(n.argument);
|
|
309
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
310
|
+
}
|
|
236
311
|
} else if (n?.type === "UnaryExpression" && n.operator === "delete") {
|
|
237
312
|
const r = rootObjectName(n.argument);
|
|
238
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
313
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
239
314
|
} else if (n?.type === "CallExpression") {
|
|
240
315
|
if (n.callee?.type === "MemberExpression") {
|
|
241
316
|
const r = rootObjectName(n.callee);
|
|
242
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
317
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
243
318
|
}
|
|
244
319
|
for (const a of n.arguments ?? []) {
|
|
245
320
|
const arg = a?.type === "SpreadElement" ? a.argument : a;
|
|
246
321
|
const r = rootObjectName(arg);
|
|
247
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
322
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "escape");
|
|
248
323
|
}
|
|
249
324
|
}
|
|
250
325
|
});
|
|
251
326
|
}
|
|
327
|
+
function isDeferredBody(n) {
|
|
328
|
+
return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
|
|
329
|
+
}
|
|
330
|
+
function isPlainStateCall(node) {
|
|
331
|
+
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$state";
|
|
332
|
+
}
|
|
333
|
+
function collectPatternAliasRefs(node, names, acc, scope, ownRhs) {
|
|
334
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
335
|
+
if (node.type === "Identifier") return;
|
|
336
|
+
if (node.type === "ObjectPattern") {
|
|
337
|
+
for (const prop of node.properties ?? []) {
|
|
338
|
+
if (prop?.type === "RestElement") {
|
|
339
|
+
collectPatternAliasRefs(prop.argument, names, acc, scope, ownRhs);
|
|
340
|
+
} else if (prop?.type === "Property") {
|
|
341
|
+
if (prop.computed) collectAliasRefs(prop.key, names, acc, scope, ownRhs);
|
|
342
|
+
collectPatternAliasRefs(prop.value, names, acc, scope, ownRhs);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (node.type === "ArrayPattern") {
|
|
348
|
+
for (const el of node.elements ?? []) collectPatternAliasRefs(el, names, acc, scope, ownRhs);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (node.type === "AssignmentPattern") {
|
|
352
|
+
collectPatternAliasRefs(node.left, names, acc, scope, ownRhs);
|
|
353
|
+
collectAliasRefs(node.right, names, acc, scope, ownRhs);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (node.type === "RestElement") {
|
|
357
|
+
collectPatternAliasRefs(node.argument, names, acc, scope, ownRhs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function collectAliasRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set(), ownRhs = null) {
|
|
361
|
+
if (Array.isArray(node)) {
|
|
362
|
+
for (const child of node) collectAliasRefs(child, names, acc, shadowed, ownRhs);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
366
|
+
const introduced = scopeIntroducedNames(node);
|
|
367
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
368
|
+
if (node.type === "AssignmentExpression") {
|
|
369
|
+
const lhsIsCandidate = node.left?.type === "Identifier" && names.has(node.left.name) && !scope.has(node.left.name);
|
|
370
|
+
if (!lhsIsCandidate) collectAliasRefs(node.left, names, acc, scope, null);
|
|
371
|
+
collectAliasRefs(node.right, names, acc, scope, lhsIsCandidate ? node.left.name : null);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (node.type === "VariableDeclarator") {
|
|
375
|
+
collectPatternAliasRefs(node.id, names, acc, scope, ownRhs);
|
|
376
|
+
if (node.init) collectAliasRefs(node.init, names, acc, scope, ownRhs);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name) && node.name !== ownRhs) {
|
|
380
|
+
acc.add(node.name);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
for (const key of Object.keys(node)) {
|
|
384
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
385
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
386
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
387
|
+
collectAliasRefs(node[key], names, acc, scope, ownRhs);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function collectFragmentAliasRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
391
|
+
if (Array.isArray(node)) {
|
|
392
|
+
for (const child of node) collectFragmentAliasRefs(child, names, acc, shadowed);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
396
|
+
if (isDeferredBody(node)) {
|
|
397
|
+
const introduced2 = scopeIntroducedNames(node);
|
|
398
|
+
const scope2 = introduced2.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced2]) : shadowed;
|
|
399
|
+
collectAliasRefs(node.body, names, acc, scope2, null);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const introduced = scopeIntroducedNames(node);
|
|
403
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
404
|
+
if (Array.isArray(node.attributes)) collectFragmentAliasRefs(node.attributes, names, acc, scope);
|
|
405
|
+
for (const key of Object.keys(node)) {
|
|
406
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "attributes") continue;
|
|
407
|
+
collectFragmentAliasRefs(node[key], names, acc, scope);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function collectEachContextTaint(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
411
|
+
if (Array.isArray(node)) {
|
|
412
|
+
for (const child of node) collectEachContextTaint(child, names, acc, shadowed);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
416
|
+
const introduced = scopeIntroducedNames(node);
|
|
417
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
418
|
+
if (node.type === "EachBlock") {
|
|
419
|
+
const expr = unwrapTs(node.expression);
|
|
420
|
+
const target = expr?.type === "Identifier" ? expr.name : expr?.type === "MemberExpression" ? rootObjectName(expr) : void 0;
|
|
421
|
+
if (target !== void 0 && names.has(target) && !shadowed.has(target)) {
|
|
422
|
+
const ctxNames = /* @__PURE__ */ new Set();
|
|
423
|
+
addBoundNames(node.context, ctxNames);
|
|
424
|
+
if (typeof node.index === "string") ctxNames.add(node.index);
|
|
425
|
+
if (ctxNames.size > 0) {
|
|
426
|
+
const union = /* @__PURE__ */ new Set();
|
|
427
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
428
|
+
collectStateWrites(node.body, ctxNames, union, kinds);
|
|
429
|
+
collectTemplateEscapes(node.body, ctxNames, union, kinds);
|
|
430
|
+
const dirty = [...union].some((n) => {
|
|
431
|
+
const k = kinds.get(n);
|
|
432
|
+
return !k || [...k].some((kind) => kind !== "reassign");
|
|
433
|
+
});
|
|
434
|
+
if (dirty) acc.add(target);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const key of Object.keys(node)) {
|
|
439
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
440
|
+
collectEachContextTaint(node[key], names, acc, scope);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function refsNamesEagerly(node, names, shadowed = /* @__PURE__ */ new Set()) {
|
|
444
|
+
if (Array.isArray(node)) return node.some((c) => refsNamesEagerly(c, names, shadowed));
|
|
445
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return false;
|
|
446
|
+
if (isDeferredBody(node)) return false;
|
|
447
|
+
const introduced = scopeIntroducedNames(node);
|
|
448
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
449
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name)) return true;
|
|
450
|
+
for (const key of Object.keys(node)) {
|
|
451
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
452
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
453
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
454
|
+
if (refsNamesEagerly(node[key], names, scope)) return true;
|
|
455
|
+
}
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
function containsCallLike(node) {
|
|
459
|
+
let found = false;
|
|
460
|
+
walkEstree(node, (n) => {
|
|
461
|
+
if (n?.type === "CallExpression" || n?.type === "NewExpression" || n?.type === "AwaitExpression") found = true;
|
|
462
|
+
});
|
|
463
|
+
return found;
|
|
464
|
+
}
|
|
465
|
+
function collectFragmentRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
466
|
+
if (Array.isArray(node)) {
|
|
467
|
+
for (const c of node) collectFragmentRefs(c, names, acc, shadowed);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
471
|
+
if (isDeferredBody(node)) return;
|
|
472
|
+
const introduced = scopeIntroducedNames(node);
|
|
473
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
474
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name)) acc.add(node.name);
|
|
475
|
+
if (node.type === "EachBlock" || node.type === "AwaitBlock") {
|
|
476
|
+
collectFragmentRefs(node.expression, names, acc, shadowed);
|
|
477
|
+
for (const key of Object.keys(node)) {
|
|
478
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "expression") continue;
|
|
479
|
+
collectFragmentRefs(node[key], names, acc, scope);
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (Array.isArray(node.attributes)) collectFragmentRefs(node.attributes, names, acc, scope);
|
|
484
|
+
for (const key of Object.keys(node)) {
|
|
485
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "attributes") continue;
|
|
486
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
487
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
488
|
+
collectFragmentRefs(node[key], names, acc, scope);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function collectStalePropCandidates(program, propNames, source) {
|
|
492
|
+
const out = [];
|
|
493
|
+
for (const stmt of program.body ?? []) {
|
|
494
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
495
|
+
for (const d of stmt.declarations ?? []) {
|
|
496
|
+
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
497
|
+
if (containsCallLike(d.init)) continue;
|
|
498
|
+
if (!refsNamesEagerly(d.init, propNames)) continue;
|
|
499
|
+
out.push({ name: d.id.name, line: lineOf(source, d.start) });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return out;
|
|
503
|
+
}
|
|
252
504
|
var COMPONENT_LIKE_TYPES = /* @__PURE__ */ new Set(["Component", "SvelteComponent", "SvelteSelf"]);
|
|
253
|
-
function collectTemplateEscapes(node, stateNames, acc) {
|
|
505
|
+
function collectTemplateEscapes(node, stateNames, acc, kinds) {
|
|
506
|
+
const record = (name) => {
|
|
507
|
+
acc.add(name);
|
|
508
|
+
if (kinds) {
|
|
509
|
+
let set = kinds.get(name);
|
|
510
|
+
if (!set) kinds.set(name, set = /* @__PURE__ */ new Set());
|
|
511
|
+
set.add("escape");
|
|
512
|
+
}
|
|
513
|
+
};
|
|
254
514
|
if (Array.isArray(node)) {
|
|
255
|
-
for (const c of node) collectTemplateEscapes(c, stateNames, acc);
|
|
515
|
+
for (const c of node) collectTemplateEscapes(c, stateNames, acc, kinds);
|
|
256
516
|
return;
|
|
257
517
|
}
|
|
258
518
|
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
@@ -260,16 +520,36 @@ function collectTemplateEscapes(node, stateNames, acc) {
|
|
|
260
520
|
for (const attr of node.attributes) {
|
|
261
521
|
if (attr?.type === "BindDirective") {
|
|
262
522
|
const r = rootObjectName(attr.expression);
|
|
263
|
-
if (r && stateNames.has(r))
|
|
523
|
+
if (r && stateNames.has(r)) record(r);
|
|
264
524
|
} else if (COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
265
525
|
walkEstree(attr, (m) => {
|
|
266
|
-
if (m?.type === "Identifier" && stateNames.has(m.name))
|
|
526
|
+
if (m?.type === "Identifier" && stateNames.has(m.name)) record(m.name);
|
|
267
527
|
});
|
|
268
528
|
}
|
|
269
529
|
}
|
|
270
530
|
}
|
|
271
531
|
for (const key of CHILD_NODE_KEYS) {
|
|
272
|
-
if (key in node) collectTemplateEscapes(node[key], stateNames, acc);
|
|
532
|
+
if (key in node) collectTemplateEscapes(node[key], stateNames, acc, kinds);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
var DIRECTIVE_ESCAPE_TYPES = /* @__PURE__ */ new Set(["UseDirective", "TransitionDirective", "AnimateDirective"]);
|
|
536
|
+
function collectDirectiveEscapes(node, names, acc) {
|
|
537
|
+
if (Array.isArray(node)) {
|
|
538
|
+
for (const c of node) collectDirectiveEscapes(c, names, acc);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
542
|
+
if (Array.isArray(node.attributes)) {
|
|
543
|
+
for (const attr of node.attributes) {
|
|
544
|
+
if (DIRECTIVE_ESCAPE_TYPES.has(attr?.type) && attr.expression) {
|
|
545
|
+
walkEstree(attr.expression, (m) => {
|
|
546
|
+
if (m?.type === "Identifier" && names.has(m.name)) acc.add(m.name);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
552
|
+
if (key in node) collectDirectiveEscapes(node[key], names, acc);
|
|
273
553
|
}
|
|
274
554
|
}
|
|
275
555
|
var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
|
|
@@ -341,7 +621,7 @@ function isPropsCall(node) {
|
|
|
341
621
|
function isBindableCall(node) {
|
|
342
622
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$bindable";
|
|
343
623
|
}
|
|
344
|
-
function
|
|
624
|
+
function collectPropNames(program, includeBindable) {
|
|
345
625
|
const names = /* @__PURE__ */ new Set();
|
|
346
626
|
let seen = 0;
|
|
347
627
|
let ambiguous = false;
|
|
@@ -361,7 +641,8 @@ function collectNonBindableProps(program) {
|
|
|
361
641
|
addBoundNames(p.argument, names);
|
|
362
642
|
} else if (p?.type === "Property") {
|
|
363
643
|
if (p.value?.type === "AssignmentPattern") {
|
|
364
|
-
if (!isBindableCall(p.value.right) && p.value.left?.type === "Identifier")
|
|
644
|
+
if ((includeBindable || !isBindableCall(p.value.right)) && p.value.left?.type === "Identifier")
|
|
645
|
+
names.add(p.value.left.name);
|
|
365
646
|
} else if (p.value?.type === "Identifier") {
|
|
366
647
|
names.add(p.value.name);
|
|
367
648
|
}
|
|
@@ -443,15 +724,20 @@ function collectNamespaceImports(program, source, acc) {
|
|
|
443
724
|
}
|
|
444
725
|
});
|
|
445
726
|
}
|
|
446
|
-
var
|
|
447
|
-
var
|
|
727
|
+
var RULE_ID_RE = "[a-z]+\\/[a-z][a-z0-9-]*";
|
|
728
|
+
var JS_DIRECTIVE = new RegExp(
|
|
729
|
+
`^\\s*//\\s*svelte-vitals-disable-next-line(?:\\s+(${RULE_ID_RE}(?:\\s*,\\s*${RULE_ID_RE})*))?\\s*$`
|
|
730
|
+
);
|
|
731
|
+
var HTML_DIRECTIVE = new RegExp(
|
|
732
|
+
`^\\s*<!--\\s*svelte-vitals-disable-next-line(?:\\s+(${RULE_ID_RE}(?:\\s*,\\s*${RULE_ID_RE})*))?\\s*-->\\s*$`
|
|
733
|
+
);
|
|
448
734
|
function collectSuppressions(source) {
|
|
449
735
|
const out = [];
|
|
450
736
|
const lines = source.split("\n");
|
|
451
737
|
lines.forEach((line, i) => {
|
|
452
738
|
const m = JS_DIRECTIVE.exec(line) ?? HTML_DIRECTIVE.exec(line);
|
|
453
739
|
if (!m) return;
|
|
454
|
-
const ruleIds = m[1]?.split(",").map((s) => s.trim()
|
|
740
|
+
const ruleIds = m[1]?.split(",").map((s) => s.trim());
|
|
455
741
|
out.push({ line: i + 2, ruleIds });
|
|
456
742
|
});
|
|
457
743
|
return out;
|
|
@@ -807,6 +1093,8 @@ function parseModuleFacts(source, filename) {
|
|
|
807
1093
|
namespaceImports: [],
|
|
808
1094
|
constableStates: [],
|
|
809
1095
|
mutatedProps: [],
|
|
1096
|
+
stalePropDerivations: [],
|
|
1097
|
+
rawableStates: [],
|
|
810
1098
|
suppressions: collectSuppressions(source),
|
|
811
1099
|
orphanEffects,
|
|
812
1100
|
orphanLifecycleCalls,
|
|
@@ -842,15 +1130,35 @@ function parseComponentFacts(source, filename) {
|
|
|
842
1130
|
const effects = [];
|
|
843
1131
|
const constableStates = [];
|
|
844
1132
|
const mutatedProps = [];
|
|
1133
|
+
const stalePropDerivations = [];
|
|
1134
|
+
const rawableStates = [];
|
|
845
1135
|
let propCount = 0;
|
|
846
1136
|
const program = ast.instance?.content;
|
|
847
1137
|
if (program) {
|
|
848
1138
|
collectImportSources(program, source, importSpans);
|
|
849
1139
|
collectNamespaceImports(program, source, namespaceImports);
|
|
850
1140
|
propCount = countProps(program);
|
|
851
|
-
const nonBindableProps =
|
|
1141
|
+
const nonBindableProps = collectPropNames(program, false);
|
|
852
1142
|
collectPropMutations(program, nonBindableProps, source, mutatedProps);
|
|
853
1143
|
if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, mutatedProps);
|
|
1144
|
+
const allPropNames = collectPropNames(program, true);
|
|
1145
|
+
if (allPropNames.size > 0) {
|
|
1146
|
+
const candidates = collectStalePropCandidates(program, allPropNames, source);
|
|
1147
|
+
if (candidates.length > 0) {
|
|
1148
|
+
const candidateNames = new Set(candidates.map((c) => c.name));
|
|
1149
|
+
const disqualified = /* @__PURE__ */ new Set();
|
|
1150
|
+
collectStateWrites(program, candidateNames, disqualified);
|
|
1151
|
+
if (ast.fragment) {
|
|
1152
|
+
collectStateWrites(ast.fragment, candidateNames, disqualified);
|
|
1153
|
+
collectTemplateEscapes(ast.fragment, candidateNames, disqualified);
|
|
1154
|
+
}
|
|
1155
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1156
|
+
if (ast.fragment) collectFragmentRefs(ast.fragment, candidateNames, referenced);
|
|
1157
|
+
for (const c of candidates) {
|
|
1158
|
+
if (!disqualified.has(c.name) && referenced.has(c.name)) stalePropDerivations.push(c);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
854
1162
|
const stateNames = /* @__PURE__ */ new Set();
|
|
855
1163
|
const reactiveNames = /* @__PURE__ */ new Set();
|
|
856
1164
|
const stateDecls = [];
|
|
@@ -882,6 +1190,41 @@ function parseComponentFacts(source, filename) {
|
|
|
882
1190
|
for (const d of stateDecls) {
|
|
883
1191
|
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
884
1192
|
}
|
|
1193
|
+
const rawableCandidates = [];
|
|
1194
|
+
for (const stmt of program.body ?? []) {
|
|
1195
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1196
|
+
for (const d of stmt.declarations ?? []) {
|
|
1197
|
+
if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
|
|
1198
|
+
const arg = unwrapTs(d.init.arguments?.[0]);
|
|
1199
|
+
if (arg?.type === "ObjectExpression" || arg?.type === "ArrayExpression") {
|
|
1200
|
+
rawableCandidates.push({ name: d.id.name, line: lineOf(source, d.start) });
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
if (rawableCandidates.length > 0) {
|
|
1205
|
+
const candNames = new Set(rawableCandidates.map((c) => c.name));
|
|
1206
|
+
const union = /* @__PURE__ */ new Set();
|
|
1207
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
1208
|
+
collectStateWrites(program, candNames, union, kinds);
|
|
1209
|
+
if (ast.fragment) {
|
|
1210
|
+
collectStateWrites(ast.fragment, candNames, union, kinds);
|
|
1211
|
+
collectTemplateEscapes(ast.fragment, candNames, union, kinds);
|
|
1212
|
+
}
|
|
1213
|
+
const aliasEscapes = /* @__PURE__ */ new Set();
|
|
1214
|
+
collectAliasRefs(program, candNames, aliasEscapes);
|
|
1215
|
+
const eachTaint = /* @__PURE__ */ new Set();
|
|
1216
|
+
if (ast.fragment) {
|
|
1217
|
+
collectFragmentAliasRefs(ast.fragment, candNames, aliasEscapes);
|
|
1218
|
+
collectDirectiveEscapes(ast.fragment, candNames, aliasEscapes);
|
|
1219
|
+
collectEachContextTaint(ast.fragment, candNames, eachTaint);
|
|
1220
|
+
}
|
|
1221
|
+
for (const c of rawableCandidates) {
|
|
1222
|
+
const k = kinds.get(c.name);
|
|
1223
|
+
const reassigned = k?.has("reassign") ?? false;
|
|
1224
|
+
const dirty = k !== void 0 && [...k].some((kind) => kind !== "reassign") || aliasEscapes.has(c.name) || eachTaint.has(c.name);
|
|
1225
|
+
if (reassigned && !dirty) rawableStates.push(c);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
885
1228
|
let moduleExtra;
|
|
886
1229
|
if (moduleProgram) {
|
|
887
1230
|
const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
|
|
@@ -907,6 +1250,8 @@ function parseComponentFacts(source, filename) {
|
|
|
907
1250
|
namespaceImports,
|
|
908
1251
|
constableStates,
|
|
909
1252
|
mutatedProps,
|
|
1253
|
+
stalePropDerivations,
|
|
1254
|
+
rawableStates,
|
|
910
1255
|
orphanEffects,
|
|
911
1256
|
orphanLifecycleCalls,
|
|
912
1257
|
browserGlobalRefs,
|
|
@@ -930,6 +1275,8 @@ function emptyComponentFacts(file) {
|
|
|
930
1275
|
namespaceImports: [],
|
|
931
1276
|
constableStates: [],
|
|
932
1277
|
mutatedProps: [],
|
|
1278
|
+
stalePropDerivations: [],
|
|
1279
|
+
rawableStates: [],
|
|
933
1280
|
orphanEffects: [],
|
|
934
1281
|
orphanLifecycleCalls: [],
|
|
935
1282
|
browserGlobalRefs: [],
|
|
@@ -966,11 +1313,6 @@ var HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
|
966
1313
|
"OPTIONS",
|
|
967
1314
|
"fallback"
|
|
968
1315
|
]);
|
|
969
|
-
function unwrapTs(expr) {
|
|
970
|
-
let cur = expr;
|
|
971
|
-
while (cur?.type === "TSSatisfiesExpression" || cur?.type === "TSAsExpression") cur = cur.expression;
|
|
972
|
-
return cur;
|
|
973
|
-
}
|
|
974
1316
|
function isFunctionNode(n) {
|
|
975
1317
|
return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
|
|
976
1318
|
}
|
|
@@ -995,102 +1337,201 @@ function addActionsMembers(obj, handlers) {
|
|
|
995
1337
|
if (isFunctionNode(v)) handlers.add(v);
|
|
996
1338
|
}
|
|
997
1339
|
}
|
|
998
|
-
function
|
|
1340
|
+
function forEachNamedExport(program, visit) {
|
|
999
1341
|
for (const stmt of program.body ?? []) {
|
|
1000
|
-
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.
|
|
1342
|
+
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
|
|
1343
|
+
const decl = stmt.declaration;
|
|
1344
|
+
if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier") {
|
|
1345
|
+
if (visit(decl.id.name, decl, decl)) return;
|
|
1001
1346
|
continue;
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
handlers.add(resolved);
|
|
1008
|
-
} else if (exportedName === "actions" && resolved?.type === "ObjectExpression") {
|
|
1009
|
-
addActionsMembers(resolved, handlers);
|
|
1010
|
-
}
|
|
1347
|
+
}
|
|
1348
|
+
if (decl.type !== "VariableDeclaration") continue;
|
|
1349
|
+
for (const d of decl.declarations ?? []) {
|
|
1350
|
+
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
1351
|
+
if (visit(d.id.name, unwrapTs(d.init), d)) return;
|
|
1011
1352
|
}
|
|
1012
1353
|
}
|
|
1013
|
-
|
|
1014
|
-
function resolveAliasStartupExports(program, bindings, startup) {
|
|
1354
|
+
let bindings;
|
|
1015
1355
|
for (const stmt of program.body ?? []) {
|
|
1016
1356
|
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
|
|
1017
1357
|
continue;
|
|
1018
1358
|
for (const s of stmt.specifiers) {
|
|
1019
1359
|
if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
|
|
1020
|
-
|
|
1360
|
+
bindings ??= collectTopLevelBindings(program);
|
|
1021
1361
|
const resolved = bindings.get(s.local.name);
|
|
1022
|
-
if (
|
|
1362
|
+
if (resolved === void 0) continue;
|
|
1363
|
+
if (visit(s.exported.name, resolved, resolved)) return;
|
|
1023
1364
|
}
|
|
1024
1365
|
}
|
|
1025
1366
|
}
|
|
1026
1367
|
function collectHandlerFunctions(program) {
|
|
1027
1368
|
const handlers = /* @__PURE__ */ new Set();
|
|
1028
|
-
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
continue;
|
|
1034
|
-
}
|
|
1035
|
-
if (decl.type !== "VariableDeclaration") continue;
|
|
1036
|
-
for (const d of decl.declarations ?? []) {
|
|
1037
|
-
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
1038
|
-
const init = unwrapTs(d.init);
|
|
1039
|
-
if (HANDLER_NAMES.has(d.id.name) && isFunctionNode(init)) {
|
|
1040
|
-
handlers.add(init);
|
|
1041
|
-
} else if (d.id.name === "actions" && init?.type === "ObjectExpression") {
|
|
1042
|
-
addActionsMembers(init, handlers);
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
resolveAliasHandlerExports(program, collectTopLevelBindings(program), handlers);
|
|
1369
|
+
forEachNamedExport(program, (name, value) => {
|
|
1370
|
+
if (HANDLER_NAMES.has(name) && isFunctionNode(value)) handlers.add(value);
|
|
1371
|
+
else if (name === "actions" && value?.type === "ObjectExpression") addActionsMembers(value, handlers);
|
|
1372
|
+
return void 0;
|
|
1373
|
+
});
|
|
1047
1374
|
return handlers;
|
|
1048
1375
|
}
|
|
1049
1376
|
function collectStartupFunctions(program) {
|
|
1050
1377
|
const startup = /* @__PURE__ */ new Set();
|
|
1051
|
-
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
startup.add(decl);
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
if (decl.type !== "VariableDeclaration") continue;
|
|
1059
|
-
for (const d of decl.declarations ?? []) {
|
|
1060
|
-
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
1061
|
-
const init = unwrapTs(d.init);
|
|
1062
|
-
if (d.id.name === "init" && isFunctionNode(init)) startup.add(init);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
resolveAliasStartupExports(program, collectTopLevelBindings(program), startup);
|
|
1378
|
+
forEachNamedExport(program, (name, value) => {
|
|
1379
|
+
if (name === "init" && isFunctionNode(value)) startup.add(value);
|
|
1380
|
+
return void 0;
|
|
1381
|
+
});
|
|
1066
1382
|
return startup;
|
|
1067
1383
|
}
|
|
1068
|
-
function
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1384
|
+
function findFalseOptOut(program, source, name) {
|
|
1385
|
+
let hit;
|
|
1386
|
+
forEachNamedExport(program, (exported, value, anchor) => {
|
|
1387
|
+
if (exported !== name || value?.type !== "Literal" || value.value !== false) return void 0;
|
|
1388
|
+
hit = { line: lineOf(source, anchor.start) };
|
|
1389
|
+
return true;
|
|
1390
|
+
});
|
|
1391
|
+
return hit;
|
|
1392
|
+
}
|
|
1393
|
+
function findLoadFunction(program) {
|
|
1394
|
+
let load;
|
|
1395
|
+
forEachNamedExport(program, (name, value) => {
|
|
1396
|
+
if (name !== "load" || !isFunctionNode(value)) return void 0;
|
|
1397
|
+
load = value;
|
|
1398
|
+
return true;
|
|
1399
|
+
});
|
|
1400
|
+
return load;
|
|
1401
|
+
}
|
|
1402
|
+
function collectAwaits(node, out = []) {
|
|
1403
|
+
if (Array.isArray(node)) {
|
|
1404
|
+
for (const child of node) collectAwaits(child, out);
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
|
|
1408
|
+
if (isFunctionNode(node)) return out;
|
|
1409
|
+
if (node.type === "AwaitExpression") out.push(node);
|
|
1410
|
+
for (const key of Object.keys(node)) {
|
|
1411
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1412
|
+
collectAwaits(node[key], out);
|
|
1413
|
+
}
|
|
1414
|
+
return out;
|
|
1415
|
+
}
|
|
1416
|
+
function isParentCall(arg) {
|
|
1417
|
+
const e = unwrapTs(arg);
|
|
1418
|
+
if (e?.type !== "CallExpression") return false;
|
|
1419
|
+
const callee = e.callee;
|
|
1420
|
+
if (callee?.type === "Identifier" && callee.name === "parent") return true;
|
|
1421
|
+
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "parent";
|
|
1422
|
+
}
|
|
1423
|
+
var BODY_METHODS = /* @__PURE__ */ new Set(["json", "text", "blob", "arrayBuffer", "formData", "bytes"]);
|
|
1424
|
+
function isBodyParseCall(arg) {
|
|
1425
|
+
const e = unwrapTs(arg);
|
|
1426
|
+
if (e?.type !== "CallExpression" || e.arguments?.length) return false;
|
|
1427
|
+
const callee = e.callee;
|
|
1428
|
+
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && BODY_METHODS.has(callee.property.name);
|
|
1429
|
+
}
|
|
1430
|
+
function refsTainted(node, tainted) {
|
|
1431
|
+
let hit = false;
|
|
1432
|
+
const walk = (n, shadowed) => {
|
|
1433
|
+
if (hit) return;
|
|
1434
|
+
if (Array.isArray(n)) {
|
|
1435
|
+
for (const child of n) walk(child, shadowed);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (!n || typeof n !== "object" || typeof n.type !== "string") return;
|
|
1439
|
+
const introduced = scopeIntroducedNames(n);
|
|
1440
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
1441
|
+
if (n.type === "Identifier" && tainted.has(n.name) && !scope.has(n.name)) {
|
|
1442
|
+
hit = true;
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
for (const key of Object.keys(n)) {
|
|
1446
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1447
|
+
if (n.type === "MemberExpression" && key === "property" && !n.computed) continue;
|
|
1448
|
+
if (n.type === "Property" && key === "key" && !n.computed) continue;
|
|
1449
|
+
walk(n[key], scope);
|
|
1450
|
+
}
|
|
1072
1451
|
};
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1452
|
+
walk(node, /* @__PURE__ */ new Set());
|
|
1453
|
+
return hit;
|
|
1454
|
+
}
|
|
1455
|
+
function collectLoadWaterfalls(program, wrapped) {
|
|
1456
|
+
const dependentLines = [];
|
|
1457
|
+
const independentLines = [];
|
|
1458
|
+
const load = findLoadFunction(program);
|
|
1459
|
+
if (!load?.body || load.body.type !== "BlockStatement") return { dependentLines, independentLines };
|
|
1460
|
+
const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
|
|
1461
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
1462
|
+
let sawAwaitSite = false;
|
|
1463
|
+
const taintAssignTarget = (left) => {
|
|
1464
|
+
if (left?.type === "MemberExpression") {
|
|
1465
|
+
const root = rootObjectName(left);
|
|
1466
|
+
if (root) tainted.add(root);
|
|
1467
|
+
} else {
|
|
1468
|
+
addBoundNames(left, tainted);
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const taintOnly = (node) => {
|
|
1472
|
+
if (Array.isArray(node)) {
|
|
1473
|
+
for (const child of node) taintOnly(child);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
1477
|
+
if (isFunctionNode(node)) return;
|
|
1478
|
+
if (node.type === "AssignmentExpression") {
|
|
1479
|
+
if (collectAwaits(node.right).length > 0 || refsTainted(node.right, tainted)) taintAssignTarget(node.left);
|
|
1480
|
+
} else if (node.type === "VariableDeclaration") {
|
|
1481
|
+
for (const d of node.declarations ?? []) {
|
|
1482
|
+
if (d?.id && d.init && (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted))) {
|
|
1483
|
+
addBoundNames(d.id, tainted);
|
|
1484
|
+
}
|
|
1079
1485
|
}
|
|
1080
1486
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
|
|
1085
|
-
continue;
|
|
1086
|
-
for (const s of stmt.specifiers) {
|
|
1087
|
-
if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
|
|
1088
|
-
if (s.exported.name !== "ssr") continue;
|
|
1089
|
-
const resolved = bindings.get(s.local.name);
|
|
1090
|
-
if (resolved?.type === "Literal" && resolved.value === false) return true;
|
|
1487
|
+
for (const key of Object.keys(node)) {
|
|
1488
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1489
|
+
taintOnly(node[key]);
|
|
1091
1490
|
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1491
|
+
};
|
|
1492
|
+
const processStatements = (body) => {
|
|
1493
|
+
for (const stmt of body ?? []) {
|
|
1494
|
+
if (!stmt) continue;
|
|
1495
|
+
if (stmt.type === "TryStatement") {
|
|
1496
|
+
if (stmt.block?.type === "BlockStatement") processStatements(stmt.block.body);
|
|
1497
|
+
if (stmt.handler) taintOnly(stmt.handler);
|
|
1498
|
+
if (stmt.finalizer) taintOnly(stmt.finalizer);
|
|
1499
|
+
continue;
|
|
1500
|
+
}
|
|
1501
|
+
if (stmt.type === "VariableDeclaration" || stmt.type === "ExpressionStatement" || stmt.type === "ReturnStatement") {
|
|
1502
|
+
const sites = collectAwaits(stmt).filter((a) => !isParentCall(a.argument) && !isBodyParseCall(a.argument));
|
|
1503
|
+
if (sites.length > 0) {
|
|
1504
|
+
const dependent = sites.filter((a) => refsTainted(a.argument, tainted));
|
|
1505
|
+
if (dependent.length > 0) {
|
|
1506
|
+
const anchor = dependent.reduce((m, a) => a.start < m.start ? a : m);
|
|
1507
|
+
dependentLines.push(line(anchor.start));
|
|
1508
|
+
} else if (sawAwaitSite) {
|
|
1509
|
+
const workSites = sites.filter((a) => unwrapTs(a.argument)?.type !== "Identifier");
|
|
1510
|
+
if (workSites.length > 0) {
|
|
1511
|
+
const anchor = workSites.reduce((m, a) => a.start < m.start ? a : m);
|
|
1512
|
+
independentLines.push(line(anchor.start));
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
sawAwaitSite = true;
|
|
1516
|
+
}
|
|
1517
|
+
if (stmt.type === "VariableDeclaration") {
|
|
1518
|
+
for (const d of stmt.declarations ?? []) {
|
|
1519
|
+
if (!d?.id || !d.init) continue;
|
|
1520
|
+
if (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted)) addBoundNames(d.id, tainted);
|
|
1521
|
+
}
|
|
1522
|
+
} else if (stmt.type === "ExpressionStatement") {
|
|
1523
|
+
const expr = unwrapTs(stmt.expression);
|
|
1524
|
+
if (expr?.type === "AssignmentExpression") {
|
|
1525
|
+
if (collectAwaits(expr.right).length > 0 || refsTainted(expr.right, tainted)) taintAssignTarget(expr.left);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
} else {
|
|
1529
|
+
taintOnly(stmt);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
processStatements(load.body.body);
|
|
1534
|
+
return { dependentLines, independentLines };
|
|
1094
1535
|
}
|
|
1095
1536
|
function walkKit(node, handlerFns, startupFns, visit, shadowed = /* @__PURE__ */ new Set(), inFunction = false, inHandler = false, inStartup = false) {
|
|
1096
1537
|
if (Array.isArray(node)) {
|
|
@@ -1187,7 +1628,10 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1187
1628
|
const handlerFns = collectHandlerFunctions(program);
|
|
1188
1629
|
const startupFns = collectStartupFunctions(program);
|
|
1189
1630
|
const svelteImports = collectSvelteLifecycleImports(program);
|
|
1190
|
-
|
|
1631
|
+
const ssrOptOut = findFalseOptOut(program, wrapped, "ssr");
|
|
1632
|
+
const csrOptOut = findFalseOptOut(program, wrapped, "csr");
|
|
1633
|
+
const waterfalls = collectLoadWaterfalls(program, wrapped);
|
|
1634
|
+
if (!ssrOptOut) {
|
|
1191
1635
|
const shiftLine = (l) => Math.max(0, l - 1);
|
|
1192
1636
|
const browserImports = collectBrowserGuardImports(program);
|
|
1193
1637
|
const guards = /* @__PURE__ */ new Set([...browserImports, ...collectDerivedGuardBindings(program, browserImports)]);
|
|
@@ -1287,6 +1731,9 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1287
1731
|
runesModuleImports: byLine(runesModuleImports),
|
|
1288
1732
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
1289
1733
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
1734
|
+
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
1735
|
+
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
1736
|
+
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
1290
1737
|
suppressions
|
|
1291
1738
|
};
|
|
1292
1739
|
}
|
|
@@ -1331,6 +1778,81 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1331
1778
|
);
|
|
1332
1779
|
}
|
|
1333
1780
|
|
|
1781
|
+
// src/vite-config-parse.ts
|
|
1782
|
+
function propOf(obj, name) {
|
|
1783
|
+
let found;
|
|
1784
|
+
for (const p of obj.properties) {
|
|
1785
|
+
if (p.type === "SpreadElement") {
|
|
1786
|
+
if (found) found = void 0;
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (p.type !== "Property" || p.computed) continue;
|
|
1790
|
+
if (p.key.type === "Identifier" && p.key.name === name) found = p;
|
|
1791
|
+
else if (p.key.type === "Literal" && p.key.value === name) found = p;
|
|
1792
|
+
}
|
|
1793
|
+
return found;
|
|
1794
|
+
}
|
|
1795
|
+
function unwrapToObjectExpression(expr, bindings) {
|
|
1796
|
+
let current = expr;
|
|
1797
|
+
for (let i = 0; i < 4 && current; i++) {
|
|
1798
|
+
const e = unwrapTs(current);
|
|
1799
|
+
if (e.type === "ObjectExpression") return e;
|
|
1800
|
+
if (e.type === "Identifier") {
|
|
1801
|
+
current = bindings.get(e.name);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
if (e.type === "CallExpression") {
|
|
1805
|
+
current = e.arguments[0];
|
|
1806
|
+
continue;
|
|
1807
|
+
}
|
|
1808
|
+
return void 0;
|
|
1809
|
+
}
|
|
1810
|
+
const final = current ? unwrapTs(current) : void 0;
|
|
1811
|
+
return final?.type === "ObjectExpression" ? final : void 0;
|
|
1812
|
+
}
|
|
1813
|
+
function findExportedExpression(program) {
|
|
1814
|
+
let exported;
|
|
1815
|
+
for (const stmt of program.body) {
|
|
1816
|
+
if (stmt.type === "ExportDefaultDeclaration") exported = stmt.declaration;
|
|
1817
|
+
}
|
|
1818
|
+
if (exported) return exported;
|
|
1819
|
+
let cjsExported;
|
|
1820
|
+
for (const stmt of program.body) {
|
|
1821
|
+
if (stmt.type !== "ExpressionStatement") continue;
|
|
1822
|
+
const expr = stmt.expression;
|
|
1823
|
+
if (expr.type !== "AssignmentExpression" || expr.operator !== "=") continue;
|
|
1824
|
+
const left = expr.left;
|
|
1825
|
+
if (left.type === "MemberExpression" && !left.computed && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports") {
|
|
1826
|
+
cjsExported = expr.right;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
return cjsExported;
|
|
1830
|
+
}
|
|
1831
|
+
function resolveConfigObject(program) {
|
|
1832
|
+
const exported = findExportedExpression(program);
|
|
1833
|
+
if (!exported) return void 0;
|
|
1834
|
+
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
1835
|
+
}
|
|
1836
|
+
function findMinifyDisabled(source) {
|
|
1837
|
+
let program;
|
|
1838
|
+
let wrapped;
|
|
1839
|
+
try {
|
|
1840
|
+
({ program, wrapped } = parseModuleProgram(source, "vite.config.ts"));
|
|
1841
|
+
} catch {
|
|
1842
|
+
return void 0;
|
|
1843
|
+
}
|
|
1844
|
+
if (!program) return void 0;
|
|
1845
|
+
const config = resolveConfigObject(program);
|
|
1846
|
+
if (!config) return void 0;
|
|
1847
|
+
const build = propOf(config, "build");
|
|
1848
|
+
const buildValue = build ? unwrapTs(build.value) : void 0;
|
|
1849
|
+
if (buildValue?.type !== "ObjectExpression") return void 0;
|
|
1850
|
+
const minify = propOf(buildValue, "minify");
|
|
1851
|
+
const minifyValue = minify ? unwrapTs(minify.value) : void 0;
|
|
1852
|
+
if (!minify || minifyValue?.type !== "Literal" || minifyValue.value !== false) return void 0;
|
|
1853
|
+
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1334
1856
|
// src/project-paths.ts
|
|
1335
1857
|
var ROBOTS_SOURCE_PATHS = [
|
|
1336
1858
|
"static/robots.txt",
|
|
@@ -1360,7 +1882,7 @@ async function runRules(rules, ctx) {
|
|
|
1360
1882
|
return perRule.flat();
|
|
1361
1883
|
}
|
|
1362
1884
|
|
|
1363
|
-
// src/rules/seo/
|
|
1885
|
+
// src/rules/seo/title-presence.ts
|
|
1364
1886
|
var FIX = {
|
|
1365
1887
|
description: "Add a <title> inside <svelte:head> (a dynamic title is fine).",
|
|
1366
1888
|
snippet: "<svelte:head>\n <title>{data.title}</title>\n</svelte:head>",
|
|
@@ -1378,8 +1900,8 @@ function messageFor(detection) {
|
|
|
1378
1900
|
if (detection.value === "absent") return "Empty <title>";
|
|
1379
1901
|
return "<title>";
|
|
1380
1902
|
}
|
|
1381
|
-
var
|
|
1382
|
-
id: "
|
|
1903
|
+
var seoTitlePresence = {
|
|
1904
|
+
id: "seo/title-presence",
|
|
1383
1905
|
title: "Title presence",
|
|
1384
1906
|
category: "seo",
|
|
1385
1907
|
severity: "critical",
|
|
@@ -1390,7 +1912,7 @@ var seo001Title = {
|
|
|
1390
1912
|
return ctx.heads.map((head) => {
|
|
1391
1913
|
const detection = detectTitle(head);
|
|
1392
1914
|
return {
|
|
1393
|
-
id: "
|
|
1915
|
+
id: "seo/title-presence",
|
|
1394
1916
|
category: "seo",
|
|
1395
1917
|
severity: "critical",
|
|
1396
1918
|
detection,
|
|
@@ -1398,7 +1920,7 @@ var seo001Title = {
|
|
|
1398
1920
|
location: head.file,
|
|
1399
1921
|
message: messageFor(detection),
|
|
1400
1922
|
recommendation: "Add a <title> inside <svelte:head>, e.g. <title>{data.title}</title>, or set it via your meta component.",
|
|
1401
|
-
docsUrl: docsUrlFor("
|
|
1923
|
+
docsUrl: docsUrlFor("seo/title-presence"),
|
|
1402
1924
|
fix: { ...FIX }
|
|
1403
1925
|
};
|
|
1404
1926
|
});
|
|
@@ -1444,9 +1966,9 @@ function headTagRule(opts) {
|
|
|
1444
1966
|
};
|
|
1445
1967
|
}
|
|
1446
1968
|
|
|
1447
|
-
// src/rules/seo/
|
|
1448
|
-
var
|
|
1449
|
-
id: "
|
|
1969
|
+
// src/rules/seo/description-presence.ts
|
|
1970
|
+
var seoDescriptionPresence = headTagRule({
|
|
1971
|
+
id: "seo/description-presence",
|
|
1450
1972
|
title: "Description presence",
|
|
1451
1973
|
severity: "critical",
|
|
1452
1974
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
@@ -1459,8 +1981,10 @@ var seo002Description = headTagRule({
|
|
|
1459
1981
|
lang: "svelte"
|
|
1460
1982
|
}
|
|
1461
1983
|
});
|
|
1462
|
-
|
|
1463
|
-
|
|
1984
|
+
|
|
1985
|
+
// src/rules/seo/canonical-url.ts
|
|
1986
|
+
var seoCanonicalUrl = headTagRule({
|
|
1987
|
+
id: "seo/canonical-url",
|
|
1464
1988
|
title: "Canonical URL",
|
|
1465
1989
|
severity: "warning",
|
|
1466
1990
|
match: (t) => t.kind === "link" && t.rel === "canonical",
|
|
@@ -1473,8 +1997,10 @@ var seo003Canonical = headTagRule({
|
|
|
1473
1997
|
lang: "svelte"
|
|
1474
1998
|
}
|
|
1475
1999
|
});
|
|
1476
|
-
|
|
1477
|
-
|
|
2000
|
+
|
|
2001
|
+
// src/rules/seo/og-image.ts
|
|
2002
|
+
var seoOgImage = headTagRule({
|
|
2003
|
+
id: "seo/og-image",
|
|
1478
2004
|
title: "Open Graph image",
|
|
1479
2005
|
severity: "warning",
|
|
1480
2006
|
match: (t) => t.kind === "meta" && t.property === "og:image",
|
|
@@ -1487,8 +2013,10 @@ var seo004OgImage = headTagRule({
|
|
|
1487
2013
|
lang: "svelte"
|
|
1488
2014
|
}
|
|
1489
2015
|
});
|
|
1490
|
-
|
|
1491
|
-
|
|
2016
|
+
|
|
2017
|
+
// src/rules/seo/og-title.ts
|
|
2018
|
+
var seoOgTitle = headTagRule({
|
|
2019
|
+
id: "seo/og-title",
|
|
1492
2020
|
title: "Open Graph title",
|
|
1493
2021
|
severity: "warning",
|
|
1494
2022
|
match: (t) => t.kind === "meta" && t.property === "og:title",
|
|
@@ -1501,8 +2029,10 @@ var seo005OgTitle = headTagRule({
|
|
|
1501
2029
|
lang: "svelte"
|
|
1502
2030
|
}
|
|
1503
2031
|
});
|
|
1504
|
-
|
|
1505
|
-
|
|
2032
|
+
|
|
2033
|
+
// src/rules/seo/json-ld.ts
|
|
2034
|
+
var seoJsonLd = headTagRule({
|
|
2035
|
+
id: "seo/json-ld",
|
|
1506
2036
|
title: "JSON-LD structured data",
|
|
1507
2037
|
severity: "info",
|
|
1508
2038
|
match: (t) => t.kind === "jsonld",
|
|
@@ -1519,93 +2049,99 @@ var seo008JsonLd = headTagRule({
|
|
|
1519
2049
|
}
|
|
1520
2050
|
});
|
|
1521
2051
|
|
|
1522
|
-
// src/rules/seo/
|
|
2052
|
+
// src/rules/seo/robots-txt.ts
|
|
1523
2053
|
var present = { presence: "own", value: "static" };
|
|
1524
2054
|
var absent = { presence: "none", value: "absent" };
|
|
1525
|
-
var
|
|
2055
|
+
var FIX2 = {
|
|
1526
2056
|
description: "Create static/robots.txt (or a src/routes/robots.txt/+server endpoint).",
|
|
1527
2057
|
snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
|
|
1528
2058
|
lang: "text"
|
|
1529
2059
|
};
|
|
1530
|
-
var
|
|
1531
|
-
id: "
|
|
2060
|
+
var seoRobotsTxt = {
|
|
2061
|
+
id: "seo/robots-txt",
|
|
1532
2062
|
title: "robots.txt",
|
|
1533
2063
|
category: "seo",
|
|
1534
2064
|
severity: "warning",
|
|
1535
2065
|
scope: "project",
|
|
1536
2066
|
rationale: "robots.txt tells crawlers which paths they may fetch and points them to your sitemap; missing it leaves crawl behaviour to defaults.",
|
|
1537
|
-
fix:
|
|
2067
|
+
fix: FIX2,
|
|
1538
2068
|
async check(ctx) {
|
|
1539
2069
|
const detection = ctx.project.hasRobotsTxt ? present : absent;
|
|
1540
2070
|
return [
|
|
1541
2071
|
{
|
|
1542
|
-
id: "
|
|
2072
|
+
id: "seo/robots-txt",
|
|
1543
2073
|
category: "seo",
|
|
1544
2074
|
severity: "warning",
|
|
1545
2075
|
detection,
|
|
1546
2076
|
message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
|
|
1547
2077
|
recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
|
|
1548
|
-
docsUrl: docsUrlFor("
|
|
1549
|
-
fix: { ...
|
|
2078
|
+
docsUrl: docsUrlFor("seo/robots-txt"),
|
|
2079
|
+
fix: { ...FIX2 }
|
|
1550
2080
|
}
|
|
1551
2081
|
];
|
|
1552
2082
|
}
|
|
1553
2083
|
};
|
|
1554
|
-
|
|
2084
|
+
|
|
2085
|
+
// src/rules/seo/sitemap-xml.ts
|
|
2086
|
+
var present2 = { presence: "own", value: "static" };
|
|
2087
|
+
var absent2 = { presence: "none", value: "absent" };
|
|
2088
|
+
var FIX3 = {
|
|
1555
2089
|
description: "Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint).",
|
|
1556
2090
|
snippet: '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://example.com/</loc></url>\n</urlset>',
|
|
1557
2091
|
lang: "xml"
|
|
1558
2092
|
};
|
|
1559
|
-
var
|
|
1560
|
-
id: "
|
|
2093
|
+
var seoSitemapXml = {
|
|
2094
|
+
id: "seo/sitemap-xml",
|
|
1561
2095
|
title: "sitemap.xml",
|
|
1562
2096
|
category: "seo",
|
|
1563
2097
|
severity: "warning",
|
|
1564
2098
|
scope: "project",
|
|
1565
2099
|
rationale: "A sitemap.xml lists your URLs so search engines can discover and prioritise them, especially pages not well linked internally.",
|
|
1566
|
-
fix:
|
|
2100
|
+
fix: FIX3,
|
|
1567
2101
|
async check(ctx) {
|
|
1568
|
-
const detection = ctx.project.hasSitemap ?
|
|
2102
|
+
const detection = ctx.project.hasSitemap ? present2 : absent2;
|
|
1569
2103
|
return [
|
|
1570
2104
|
{
|
|
1571
|
-
id: "
|
|
2105
|
+
id: "seo/sitemap-xml",
|
|
1572
2106
|
category: "seo",
|
|
1573
2107
|
severity: "warning",
|
|
1574
2108
|
detection,
|
|
1575
2109
|
message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
|
|
1576
2110
|
recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
|
|
1577
|
-
docsUrl: docsUrlFor("
|
|
1578
|
-
fix: { ...
|
|
2111
|
+
docsUrl: docsUrlFor("seo/sitemap-xml"),
|
|
2112
|
+
fix: { ...FIX3 }
|
|
1579
2113
|
}
|
|
1580
2114
|
];
|
|
1581
2115
|
}
|
|
1582
2116
|
};
|
|
1583
|
-
|
|
2117
|
+
|
|
2118
|
+
// src/rules/seo/html-lang.ts
|
|
2119
|
+
var FIX4 = {
|
|
1584
2120
|
description: "Set the lang attribute on <html> in src/app.html.",
|
|
1585
2121
|
snippet: '<html lang="en">',
|
|
1586
2122
|
lang: "html"
|
|
1587
2123
|
};
|
|
1588
|
-
var
|
|
1589
|
-
id: "
|
|
2124
|
+
var seoHtmlLang = {
|
|
2125
|
+
id: "seo/html-lang",
|
|
1590
2126
|
title: "<html lang>",
|
|
1591
2127
|
category: "seo",
|
|
1592
2128
|
severity: "warning",
|
|
1593
2129
|
scope: "project",
|
|
1594
2130
|
rationale: "The <html lang> attribute declares the page language for search engines, screen readers, and translation tools.",
|
|
1595
|
-
fix:
|
|
2131
|
+
fix: FIX4,
|
|
1596
2132
|
async check(ctx) {
|
|
1597
2133
|
const detection = ctx.project.htmlLang;
|
|
1598
2134
|
const message = detection.presence === "none" ? "Missing <html lang>" : detection.value === "absent" ? "Empty <html lang>" : "<html lang>";
|
|
1599
2135
|
return [
|
|
1600
2136
|
{
|
|
1601
|
-
id: "
|
|
2137
|
+
id: "seo/html-lang",
|
|
1602
2138
|
category: "seo",
|
|
1603
2139
|
severity: "warning",
|
|
1604
2140
|
detection,
|
|
1605
2141
|
message,
|
|
1606
2142
|
recommendation: 'Set <html lang="..."> in src/app.html.',
|
|
1607
|
-
docsUrl: docsUrlFor("
|
|
1608
|
-
fix: { ...
|
|
2143
|
+
docsUrl: docsUrlFor("seo/html-lang"),
|
|
2144
|
+
fix: { ...FIX4 }
|
|
1609
2145
|
}
|
|
1610
2146
|
];
|
|
1611
2147
|
}
|
|
@@ -1662,9 +2198,9 @@ function imageRule(opts) {
|
|
|
1662
2198
|
};
|
|
1663
2199
|
}
|
|
1664
2200
|
|
|
1665
|
-
// src/rules/perf/
|
|
1666
|
-
var
|
|
1667
|
-
id: "
|
|
2201
|
+
// src/rules/perf/image-dimensions.ts
|
|
2202
|
+
var performanceImageDimensions = imageRule({
|
|
2203
|
+
id: "performance/image-dimensions",
|
|
1668
2204
|
title: "Image dimensions",
|
|
1669
2205
|
severity: "warning",
|
|
1670
2206
|
label: "<img> width/height",
|
|
@@ -1677,8 +2213,10 @@ var perf001ImageDimensions = imageRule({
|
|
|
1677
2213
|
},
|
|
1678
2214
|
ok: (img) => img.hasWidth && img.hasHeight
|
|
1679
2215
|
});
|
|
1680
|
-
|
|
1681
|
-
|
|
2216
|
+
|
|
2217
|
+
// src/rules/perf/image-loading-hint.ts
|
|
2218
|
+
var performanceImageLoadingHint = imageRule({
|
|
2219
|
+
id: "performance/image-loading-hint",
|
|
1682
2220
|
title: "Image loading hint",
|
|
1683
2221
|
severity: "info",
|
|
1684
2222
|
label: "<img> loading attribute",
|
|
@@ -1691,8 +2229,10 @@ var perf002ImageLoading = imageRule({
|
|
|
1691
2229
|
},
|
|
1692
2230
|
ok: (img) => img.hasLoading
|
|
1693
2231
|
});
|
|
1694
|
-
|
|
1695
|
-
|
|
2232
|
+
|
|
2233
|
+
// src/rules/perf/responsive-image.ts
|
|
2234
|
+
var performanceResponsiveImage = imageRule({
|
|
2235
|
+
id: "performance/responsive-image",
|
|
1696
2236
|
title: "Responsive image",
|
|
1697
2237
|
severity: "info",
|
|
1698
2238
|
label: "<img> srcset",
|
|
@@ -1759,9 +2299,9 @@ function linkRule(opts) {
|
|
|
1759
2299
|
};
|
|
1760
2300
|
}
|
|
1761
2301
|
|
|
1762
|
-
// src/rules/perf/
|
|
1763
|
-
var
|
|
1764
|
-
id: "
|
|
2302
|
+
// src/rules/perf/preload-missing-as.ts
|
|
2303
|
+
var performancePreloadMissingAs = linkRule({
|
|
2304
|
+
id: "performance/preload-missing-as",
|
|
1765
2305
|
title: "Preload missing as",
|
|
1766
2306
|
severity: "warning",
|
|
1767
2307
|
label: "`as` on a preloaded `<link>`",
|
|
@@ -1775,8 +2315,10 @@ var perf003PreloadAs = linkRule({
|
|
|
1775
2315
|
relevant: (t) => t.rel === "preload",
|
|
1776
2316
|
ok: (t) => t.hasAs === true
|
|
1777
2317
|
});
|
|
1778
|
-
|
|
1779
|
-
|
|
2318
|
+
|
|
2319
|
+
// src/rules/perf/font-preload-crossorigin.ts
|
|
2320
|
+
var performanceFontPreloadCrossorigin = linkRule({
|
|
2321
|
+
id: "performance/font-preload-crossorigin",
|
|
1780
2322
|
title: "Font preload missing crossorigin",
|
|
1781
2323
|
severity: "warning",
|
|
1782
2324
|
label: "`crossorigin` on a font preload",
|
|
@@ -1791,11 +2333,11 @@ var perf004FontPreloadCrossorigin = linkRule({
|
|
|
1791
2333
|
ok: (t) => t.hasCrossorigin === true
|
|
1792
2334
|
});
|
|
1793
2335
|
|
|
1794
|
-
// src/rules/perf/
|
|
1795
|
-
var docsUrl = docsUrlFor("
|
|
2336
|
+
// src/rules/perf/lcp-image.ts
|
|
2337
|
+
var docsUrl = docsUrlFor("performance/lcp-image");
|
|
1796
2338
|
var recommendation = 'Remove loading="lazy" from the LCP/first image and consider fetchpriority="high" so it loads as early as possible.';
|
|
1797
|
-
var
|
|
1798
|
-
id: "
|
|
2339
|
+
var performanceLcpImage = {
|
|
2340
|
+
id: "performance/lcp-image",
|
|
1799
2341
|
title: "LCP image eager loading",
|
|
1800
2342
|
category: "performance",
|
|
1801
2343
|
severity: "warning",
|
|
@@ -1813,7 +2355,7 @@ var perf005LcpImage = {
|
|
|
1813
2355
|
if (!first) continue;
|
|
1814
2356
|
out.push(
|
|
1815
2357
|
first.lazy ? {
|
|
1816
|
-
id: "
|
|
2358
|
+
id: "performance/lcp-image",
|
|
1817
2359
|
category: "performance",
|
|
1818
2360
|
severity: "warning",
|
|
1819
2361
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1823,9 +2365,9 @@ var perf005LcpImage = {
|
|
|
1823
2365
|
message: 'First image (likely LCP) is loading="lazy"',
|
|
1824
2366
|
recommendation,
|
|
1825
2367
|
docsUrl,
|
|
1826
|
-
fix: { ...
|
|
2368
|
+
fix: { ...performanceLcpImage.fix }
|
|
1827
2369
|
} : {
|
|
1828
|
-
id: "
|
|
2370
|
+
id: "performance/lcp-image",
|
|
1829
2371
|
category: "performance",
|
|
1830
2372
|
severity: "warning",
|
|
1831
2373
|
detection: { presence: "own", value: "static" },
|
|
@@ -1840,11 +2382,11 @@ var perf005LcpImage = {
|
|
|
1840
2382
|
}
|
|
1841
2383
|
};
|
|
1842
2384
|
|
|
1843
|
-
// src/rules/perf/
|
|
1844
|
-
var docsUrl2 = docsUrlFor("
|
|
2385
|
+
// src/rules/perf/render-blocking-script.ts
|
|
2386
|
+
var docsUrl2 = docsUrlFor("performance/render-blocking-script");
|
|
1845
2387
|
var recommendation2 = 'Add defer (or type="module"), or async, to the <script> so it does not block HTML parsing.';
|
|
1846
|
-
var
|
|
1847
|
-
id: "
|
|
2388
|
+
var performanceRenderBlockingScript = {
|
|
2389
|
+
id: "performance/render-blocking-script",
|
|
1848
2390
|
title: "Render-blocking script",
|
|
1849
2391
|
category: "performance",
|
|
1850
2392
|
severity: "warning",
|
|
@@ -1864,7 +2406,7 @@ var perf007RenderBlockingScript = {
|
|
|
1864
2406
|
if (blocking.length > 0) {
|
|
1865
2407
|
for (const tag of blocking) {
|
|
1866
2408
|
out.push({
|
|
1867
|
-
id: "
|
|
2409
|
+
id: "performance/render-blocking-script",
|
|
1868
2410
|
category: "performance",
|
|
1869
2411
|
severity: "warning",
|
|
1870
2412
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1874,12 +2416,12 @@ var perf007RenderBlockingScript = {
|
|
|
1874
2416
|
message: `Render-blocking <script>${tag.href ? ` (${tag.href})` : ""} in <head>`,
|
|
1875
2417
|
recommendation: recommendation2,
|
|
1876
2418
|
docsUrl: docsUrl2,
|
|
1877
|
-
fix: { ...
|
|
2419
|
+
fix: { ...performanceRenderBlockingScript.fix }
|
|
1878
2420
|
});
|
|
1879
2421
|
}
|
|
1880
2422
|
} else {
|
|
1881
2423
|
out.push({
|
|
1882
|
-
id: "
|
|
2424
|
+
id: "performance/render-blocking-script",
|
|
1883
2425
|
category: "performance",
|
|
1884
2426
|
severity: "warning",
|
|
1885
2427
|
detection: { presence: "own", value: "static" },
|
|
@@ -1894,16 +2436,16 @@ var perf007RenderBlockingScript = {
|
|
|
1894
2436
|
}
|
|
1895
2437
|
};
|
|
1896
2438
|
|
|
1897
|
-
// src/rules/perf/
|
|
1898
|
-
var docsUrl3 = docsUrlFor("
|
|
2439
|
+
// src/rules/perf/preconnect.ts
|
|
2440
|
+
var docsUrl3 = docsUrlFor("performance/preconnect");
|
|
1899
2441
|
var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
|
|
1900
2442
|
var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
|
|
1901
2443
|
function hostOf(href) {
|
|
1902
2444
|
const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
|
|
1903
2445
|
return m ? m[1].toLowerCase() : void 0;
|
|
1904
2446
|
}
|
|
1905
|
-
var
|
|
1906
|
-
id: "
|
|
2447
|
+
var performancePreconnect = {
|
|
2448
|
+
id: "performance/preconnect",
|
|
1907
2449
|
title: "Preconnect third-party origin",
|
|
1908
2450
|
category: "performance",
|
|
1909
2451
|
severity: "info",
|
|
@@ -1930,7 +2472,7 @@ var perf008Preconnect = {
|
|
|
1930
2472
|
const missing = [...referenced].filter(([host]) => !covered.has(host));
|
|
1931
2473
|
if (missing.length === 0) {
|
|
1932
2474
|
out.push({
|
|
1933
|
-
id: "
|
|
2475
|
+
id: "performance/preconnect",
|
|
1934
2476
|
category: "performance",
|
|
1935
2477
|
severity: "info",
|
|
1936
2478
|
detection: { presence: "own", value: "static" },
|
|
@@ -1943,7 +2485,7 @@ var perf008Preconnect = {
|
|
|
1943
2485
|
}
|
|
1944
2486
|
for (const [host, file] of missing) {
|
|
1945
2487
|
out.push({
|
|
1946
|
-
id: "
|
|
2488
|
+
id: "performance/preconnect",
|
|
1947
2489
|
category: "performance",
|
|
1948
2490
|
severity: "info",
|
|
1949
2491
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1952,7 +2494,7 @@ var perf008Preconnect = {
|
|
|
1952
2494
|
message: `Third-party origin ${host} used without a preconnect`,
|
|
1953
2495
|
recommendation: recommendation3,
|
|
1954
2496
|
docsUrl: docsUrl3,
|
|
1955
|
-
fix: { ...
|
|
2497
|
+
fix: { ...performancePreconnect.fix }
|
|
1956
2498
|
});
|
|
1957
2499
|
}
|
|
1958
2500
|
}
|
|
@@ -1960,28 +2502,28 @@ var perf008Preconnect = {
|
|
|
1960
2502
|
}
|
|
1961
2503
|
};
|
|
1962
2504
|
|
|
1963
|
-
// src/rules/seo/
|
|
1964
|
-
var
|
|
2505
|
+
// src/rules/seo/indexability.ts
|
|
2506
|
+
var FIX5 = {
|
|
1965
2507
|
description: 'If this route should be indexed, drop noindex from its <meta name="robots">.',
|
|
1966
2508
|
snippet: '<svelte:head>\n <meta name="robots" content="index, follow" />\n</svelte:head>',
|
|
1967
2509
|
lang: "svelte"
|
|
1968
2510
|
};
|
|
1969
|
-
var
|
|
1970
|
-
id: "
|
|
2511
|
+
var seoIndexability = {
|
|
2512
|
+
id: "seo/indexability",
|
|
1971
2513
|
title: "Indexability",
|
|
1972
2514
|
category: "seo",
|
|
1973
2515
|
severity: "info",
|
|
1974
2516
|
scope: "route",
|
|
1975
2517
|
rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
|
|
1976
|
-
fix:
|
|
2518
|
+
fix: FIX5,
|
|
1977
2519
|
async check(ctx) {
|
|
1978
|
-
const docsUrl7 = docsUrlFor("
|
|
2520
|
+
const docsUrl7 = docsUrlFor("seo/indexability");
|
|
1979
2521
|
const out = [];
|
|
1980
2522
|
for (const head of ctx.heads) {
|
|
1981
2523
|
const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
|
|
1982
2524
|
if (!noindexed) continue;
|
|
1983
2525
|
out.push({
|
|
1984
|
-
id: "
|
|
2526
|
+
id: "seo/indexability",
|
|
1985
2527
|
category: "seo",
|
|
1986
2528
|
severity: "info",
|
|
1987
2529
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1991,14 +2533,16 @@ var seo010Indexability = {
|
|
|
1991
2533
|
message: "Route is noindex \u2014 verify this is intentional",
|
|
1992
2534
|
recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
|
|
1993
2535
|
docsUrl: docsUrl7,
|
|
1994
|
-
fix: { ...
|
|
2536
|
+
fix: { ...FIX5 }
|
|
1995
2537
|
});
|
|
1996
2538
|
}
|
|
1997
2539
|
return out;
|
|
1998
2540
|
}
|
|
1999
2541
|
};
|
|
2000
|
-
|
|
2001
|
-
|
|
2542
|
+
|
|
2543
|
+
// src/rules/seo/twitter-card.ts
|
|
2544
|
+
var seoTwitterCard = headTagRule({
|
|
2545
|
+
id: "seo/twitter-card",
|
|
2002
2546
|
title: "Twitter Card",
|
|
2003
2547
|
severity: "info",
|
|
2004
2548
|
match: (t) => t.kind === "meta" && t.name === "twitter:card",
|
|
@@ -2011,8 +2555,10 @@ var seo011TwitterCard = headTagRule({
|
|
|
2011
2555
|
lang: "svelte"
|
|
2012
2556
|
}
|
|
2013
2557
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2558
|
+
|
|
2559
|
+
// src/rules/seo/og-description.ts
|
|
2560
|
+
var seoOgDescription = headTagRule({
|
|
2561
|
+
id: "seo/og-description",
|
|
2016
2562
|
title: "Open Graph description",
|
|
2017
2563
|
severity: "warning",
|
|
2018
2564
|
match: (t) => t.kind === "meta" && t.property === "og:description",
|
|
@@ -2025,8 +2571,10 @@ var seo012OgDescription = headTagRule({
|
|
|
2025
2571
|
lang: "svelte"
|
|
2026
2572
|
}
|
|
2027
2573
|
});
|
|
2028
|
-
|
|
2029
|
-
|
|
2574
|
+
|
|
2575
|
+
// src/rules/seo/og-url.ts
|
|
2576
|
+
var seoOgUrl = headTagRule({
|
|
2577
|
+
id: "seo/og-url",
|
|
2030
2578
|
title: "Open Graph URL",
|
|
2031
2579
|
severity: "info",
|
|
2032
2580
|
match: (t) => t.kind === "meta" && t.property === "og:url",
|
|
@@ -2039,8 +2587,10 @@ var seo013OgUrl = headTagRule({
|
|
|
2039
2587
|
lang: "svelte"
|
|
2040
2588
|
}
|
|
2041
2589
|
});
|
|
2042
|
-
|
|
2043
|
-
|
|
2590
|
+
|
|
2591
|
+
// src/rules/seo/viewport.ts
|
|
2592
|
+
var seoViewport = headTagRule({
|
|
2593
|
+
id: "seo/viewport",
|
|
2044
2594
|
title: "Viewport",
|
|
2045
2595
|
severity: "warning",
|
|
2046
2596
|
match: (t) => t.kind === "meta" && t.name === "viewport",
|
|
@@ -2057,37 +2607,43 @@ var seo014Viewport = headTagRule({
|
|
|
2057
2607
|
lang: "html"
|
|
2058
2608
|
}
|
|
2059
2609
|
});
|
|
2060
|
-
|
|
2610
|
+
|
|
2611
|
+
// src/rules/seo/sitemap-in-robots.ts
|
|
2612
|
+
var FIX6 = {
|
|
2061
2613
|
description: "Add a Sitemap: line to static/robots.txt.",
|
|
2062
2614
|
snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
|
|
2063
2615
|
lang: "text"
|
|
2064
2616
|
};
|
|
2065
|
-
var
|
|
2066
|
-
id: "
|
|
2617
|
+
var seoSitemapInRobots = {
|
|
2618
|
+
id: "seo/sitemap-in-robots",
|
|
2067
2619
|
title: "Sitemap referenced in robots.txt",
|
|
2068
2620
|
category: "seo",
|
|
2069
2621
|
severity: "info",
|
|
2070
2622
|
scope: "project",
|
|
2071
2623
|
rationale: "A Sitemap: line in robots.txt helps crawlers discover your sitemap; without it discovery relies on manual submission.",
|
|
2072
|
-
fix:
|
|
2624
|
+
fix: FIX6,
|
|
2073
2625
|
async check(ctx) {
|
|
2074
2626
|
const { hasRobotsTxt, hasSitemap, robotsReferencesSitemap } = ctx.project;
|
|
2075
2627
|
if (!(hasRobotsTxt && hasSitemap && robotsReferencesSitemap === false)) return [];
|
|
2076
2628
|
return [
|
|
2077
2629
|
{
|
|
2078
|
-
id: "
|
|
2630
|
+
id: "seo/sitemap-in-robots",
|
|
2079
2631
|
category: "seo",
|
|
2080
2632
|
severity: "info",
|
|
2081
2633
|
detection: { presence: "none", value: "absent" },
|
|
2082
2634
|
message: "robots.txt does not reference your sitemap",
|
|
2083
2635
|
recommendation: "Add a Sitemap: line to static/robots.txt pointing at your sitemap.xml.",
|
|
2084
|
-
docsUrl: docsUrlFor("
|
|
2085
|
-
fix: { ...
|
|
2636
|
+
docsUrl: docsUrlFor("seo/sitemap-in-robots"),
|
|
2637
|
+
fix: { ...FIX6 }
|
|
2086
2638
|
}
|
|
2087
2639
|
];
|
|
2088
2640
|
}
|
|
2089
2641
|
};
|
|
2090
2642
|
|
|
2643
|
+
// src/rules/seo/detection.ts
|
|
2644
|
+
var PENALIZED = { presence: "none", value: "absent" };
|
|
2645
|
+
var PASS = { presence: "own", value: "static" };
|
|
2646
|
+
|
|
2091
2647
|
// src/rules/seo/jsonld-engine.ts
|
|
2092
2648
|
function parseJsonLd(raw) {
|
|
2093
2649
|
let data;
|
|
@@ -2216,72 +2772,16 @@ var REQUIRED_PROPS = {
|
|
|
2216
2772
|
Product: ["name", "offers"],
|
|
2217
2773
|
BreadcrumbList: ["itemListElement"],
|
|
2218
2774
|
Organization: ["name", "url"],
|
|
2219
|
-
WebSite: ["name", "url"],
|
|
2220
|
-
Event: ["name", "startDate", "location"],
|
|
2221
|
-
Recipe: ["name", "image", "recipeIngredient", "recipeInstructions"],
|
|
2222
|
-
Person: ["name"],
|
|
2223
|
-
VideoObject: ["name", "description", "thumbnailUrl", "uploadDate"],
|
|
2224
|
-
LocalBusiness: ["name", "address"]
|
|
2225
|
-
};
|
|
2226
|
-
|
|
2227
|
-
// src/rules/seo/detection.ts
|
|
2228
|
-
var PENALIZED = { presence: "none", value: "absent" };
|
|
2229
|
-
var PASS = { presence: "own", value: "static" };
|
|
2230
|
-
|
|
2231
|
-
// src/rules/seo/seo016-021.ts
|
|
2232
|
-
function jsonldTags(head) {
|
|
2233
|
-
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2234
|
-
}
|
|
2235
|
-
var seo016JsonLdValidity = {
|
|
2236
|
-
id: "SEO016",
|
|
2237
|
-
title: "JSON-LD validity",
|
|
2238
|
-
category: "seo",
|
|
2239
|
-
severity: "warning",
|
|
2240
|
-
scope: "route",
|
|
2241
|
-
rationale: "Invalid JSON-LD \u2014 unparseable, or missing @context/@type \u2014 is silently ignored by search engines, so the structured data does nothing.",
|
|
2242
|
-
fix: {
|
|
2243
|
-
description: "Make the JSON-LD valid: parseable JSON with both @context (schema.org) and @type.",
|
|
2244
|
-
snippet: '<svelte:head>\n <script type="application/ld+json">\n {"@context":"https://schema.org","@type":"WebPage","name":"\u2026"}\n </script>\n</svelte:head>',
|
|
2245
|
-
lang: "svelte"
|
|
2246
|
-
},
|
|
2247
|
-
async check(ctx) {
|
|
2248
|
-
const docsUrl7 = docsUrlFor("SEO016");
|
|
2249
|
-
const out = [];
|
|
2250
|
-
for (const head of ctx.heads) {
|
|
2251
|
-
for (const tag of jsonldTags(head)) {
|
|
2252
|
-
const parsed = parseJsonLd(tag.jsonld);
|
|
2253
|
-
let problem;
|
|
2254
|
-
if (!parsed.ok) problem = "JSON-LD is not valid JSON";
|
|
2255
|
-
else if (!parsed.nodes.some((n) => "@context" in n)) problem = "JSON-LD is missing @context";
|
|
2256
|
-
else if (!parsed.nodes.some((n) => typeOf(n).length > 0)) problem = "JSON-LD is missing @type";
|
|
2257
|
-
out.push(
|
|
2258
|
-
problem ? {
|
|
2259
|
-
id: "SEO016",
|
|
2260
|
-
category: "seo",
|
|
2261
|
-
severity: "warning",
|
|
2262
|
-
detection: PENALIZED,
|
|
2263
|
-
route: head.route,
|
|
2264
|
-
location: head.file,
|
|
2265
|
-
message: problem,
|
|
2266
|
-
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2267
|
-
docsUrl: docsUrl7,
|
|
2268
|
-
fix: { ...seo016JsonLdValidity.fix }
|
|
2269
|
-
} : {
|
|
2270
|
-
id: "SEO016",
|
|
2271
|
-
category: "seo",
|
|
2272
|
-
severity: "warning",
|
|
2273
|
-
detection: PASS,
|
|
2274
|
-
route: head.route,
|
|
2275
|
-
message: "JSON-LD validity",
|
|
2276
|
-
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2277
|
-
docsUrl: docsUrl7
|
|
2278
|
-
}
|
|
2279
|
-
);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
return out;
|
|
2283
|
-
}
|
|
2775
|
+
WebSite: ["name", "url"],
|
|
2776
|
+
Event: ["name", "startDate", "location"],
|
|
2777
|
+
Recipe: ["name", "image", "recipeIngredient", "recipeInstructions"],
|
|
2778
|
+
Person: ["name"],
|
|
2779
|
+
VideoObject: ["name", "description", "thumbnailUrl", "uploadDate"],
|
|
2780
|
+
LocalBusiness: ["name", "address"]
|
|
2284
2781
|
};
|
|
2782
|
+
function jsonldTags(head) {
|
|
2783
|
+
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2784
|
+
}
|
|
2285
2785
|
function jsonldRule(opts) {
|
|
2286
2786
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2287
2787
|
return {
|
|
@@ -2330,8 +2830,62 @@ function jsonldRule(opts) {
|
|
|
2330
2830
|
}
|
|
2331
2831
|
};
|
|
2332
2832
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2833
|
+
|
|
2834
|
+
// src/rules/seo/json-ld-validity.ts
|
|
2835
|
+
var seoJsonLdValidity = {
|
|
2836
|
+
id: "seo/json-ld-validity",
|
|
2837
|
+
title: "JSON-LD validity",
|
|
2838
|
+
category: "seo",
|
|
2839
|
+
severity: "warning",
|
|
2840
|
+
scope: "route",
|
|
2841
|
+
rationale: "Invalid JSON-LD \u2014 unparseable, or missing @context/@type \u2014 is silently ignored by search engines, so the structured data does nothing.",
|
|
2842
|
+
fix: {
|
|
2843
|
+
description: "Make the JSON-LD valid: parseable JSON with both @context (schema.org) and @type.",
|
|
2844
|
+
snippet: '<svelte:head>\n <script type="application/ld+json">\n {"@context":"https://schema.org","@type":"WebPage","name":"\u2026"}\n </script>\n</svelte:head>',
|
|
2845
|
+
lang: "svelte"
|
|
2846
|
+
},
|
|
2847
|
+
async check(ctx) {
|
|
2848
|
+
const docsUrl7 = docsUrlFor("seo/json-ld-validity");
|
|
2849
|
+
const out = [];
|
|
2850
|
+
for (const head of ctx.heads) {
|
|
2851
|
+
for (const tag of jsonldTags(head)) {
|
|
2852
|
+
const parsed = parseJsonLd(tag.jsonld);
|
|
2853
|
+
let problem;
|
|
2854
|
+
if (!parsed.ok) problem = "JSON-LD is not valid JSON";
|
|
2855
|
+
else if (!parsed.nodes.some((n) => "@context" in n)) problem = "JSON-LD is missing @context";
|
|
2856
|
+
else if (!parsed.nodes.some((n) => typeOf(n).length > 0)) problem = "JSON-LD is missing @type";
|
|
2857
|
+
out.push(
|
|
2858
|
+
problem ? {
|
|
2859
|
+
id: "seo/json-ld-validity",
|
|
2860
|
+
category: "seo",
|
|
2861
|
+
severity: "warning",
|
|
2862
|
+
detection: PENALIZED,
|
|
2863
|
+
route: head.route,
|
|
2864
|
+
location: head.file,
|
|
2865
|
+
message: problem,
|
|
2866
|
+
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2867
|
+
docsUrl: docsUrl7,
|
|
2868
|
+
fix: { ...seoJsonLdValidity.fix }
|
|
2869
|
+
} : {
|
|
2870
|
+
id: "seo/json-ld-validity",
|
|
2871
|
+
category: "seo",
|
|
2872
|
+
severity: "warning",
|
|
2873
|
+
detection: PASS,
|
|
2874
|
+
route: head.route,
|
|
2875
|
+
message: "JSON-LD validity",
|
|
2876
|
+
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2877
|
+
docsUrl: docsUrl7
|
|
2878
|
+
}
|
|
2879
|
+
);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return out;
|
|
2883
|
+
}
|
|
2884
|
+
};
|
|
2885
|
+
|
|
2886
|
+
// src/rules/seo/json-ld-deprecated-type.ts
|
|
2887
|
+
var seoJsonLdDeprecatedType = jsonldRule({
|
|
2888
|
+
id: "seo/json-ld-deprecated-type",
|
|
2335
2889
|
title: "Deprecated structured-data type",
|
|
2336
2890
|
severity: "info",
|
|
2337
2891
|
label: "Structured-data type",
|
|
@@ -2342,8 +2896,10 @@ var seo017DeprecatedType = jsonldRule({
|
|
|
2342
2896
|
return dep ? `@type "${dep}" no longer reliably produces a Google rich result` : void 0;
|
|
2343
2897
|
}
|
|
2344
2898
|
});
|
|
2345
|
-
|
|
2346
|
-
|
|
2899
|
+
|
|
2900
|
+
// src/rules/seo/json-ld-relative-url.ts
|
|
2901
|
+
var seoJsonLdRelativeUrl = jsonldRule({
|
|
2902
|
+
id: "seo/json-ld-relative-url",
|
|
2347
2903
|
title: "JSON-LD relative URL",
|
|
2348
2904
|
severity: "warning",
|
|
2349
2905
|
label: "JSON-LD URLs",
|
|
@@ -2359,8 +2915,10 @@ var seo018RelativeUrl = jsonldRule({
|
|
|
2359
2915
|
return bad ? `Relative URL in JSON-LD: "${bad}" \u2014 use an absolute URL` : void 0;
|
|
2360
2916
|
}
|
|
2361
2917
|
});
|
|
2362
|
-
|
|
2363
|
-
|
|
2918
|
+
|
|
2919
|
+
// src/rules/seo/json-ld-date-format.ts
|
|
2920
|
+
var seoJsonLdDateFormat = jsonldRule({
|
|
2921
|
+
id: "seo/json-ld-date-format",
|
|
2364
2922
|
title: "JSON-LD date format",
|
|
2365
2923
|
severity: "info",
|
|
2366
2924
|
label: "JSON-LD dates",
|
|
@@ -2376,8 +2934,10 @@ var seo019DateFormat = jsonldRule({
|
|
|
2376
2934
|
return bad ? `Non-ISO-8601 date in JSON-LD: "${bad}"` : void 0;
|
|
2377
2935
|
}
|
|
2378
2936
|
});
|
|
2379
|
-
|
|
2380
|
-
|
|
2937
|
+
|
|
2938
|
+
// src/rules/seo/json-ld-placeholder.ts
|
|
2939
|
+
var seoJsonLdPlaceholder = jsonldRule({
|
|
2940
|
+
id: "seo/json-ld-placeholder",
|
|
2381
2941
|
title: "JSON-LD placeholder text",
|
|
2382
2942
|
severity: "info",
|
|
2383
2943
|
label: "JSON-LD content",
|
|
@@ -2388,8 +2948,10 @@ var seo020Placeholder = jsonldRule({
|
|
|
2388
2948
|
return bad ? `Placeholder text in JSON-LD: "${bad}"` : void 0;
|
|
2389
2949
|
}
|
|
2390
2950
|
});
|
|
2391
|
-
|
|
2392
|
-
|
|
2951
|
+
|
|
2952
|
+
// src/rules/seo/json-ld-required-props.ts
|
|
2953
|
+
var seoJsonLdRequiredProps = jsonldRule({
|
|
2954
|
+
id: "seo/json-ld-required-props",
|
|
2393
2955
|
title: "JSON-LD required properties",
|
|
2394
2956
|
severity: "warning",
|
|
2395
2957
|
label: "JSON-LD required properties",
|
|
@@ -2421,7 +2983,7 @@ function visibleLength(s) {
|
|
|
2421
2983
|
return [...segmenter.segment(collapsed)].length;
|
|
2422
2984
|
}
|
|
2423
2985
|
|
|
2424
|
-
// src/rules/seo/
|
|
2986
|
+
// src/rules/seo/length-rule.ts
|
|
2425
2987
|
function lengthRule(opts) {
|
|
2426
2988
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2427
2989
|
return {
|
|
@@ -2467,8 +3029,10 @@ function lengthRule(opts) {
|
|
|
2467
3029
|
}
|
|
2468
3030
|
};
|
|
2469
3031
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
3032
|
+
|
|
3033
|
+
// src/rules/seo/title-length.ts
|
|
3034
|
+
var seoTitleLength = lengthRule({
|
|
3035
|
+
id: "seo/title-length",
|
|
2472
3036
|
title: "Title length",
|
|
2473
3037
|
label: "Title length",
|
|
2474
3038
|
noun: "Title",
|
|
@@ -2478,8 +3042,10 @@ var seo022TitleLength = lengthRule({
|
|
|
2478
3042
|
recommendation: "Aim for a title of 30\u201360 characters so it is not truncated in search results.",
|
|
2479
3043
|
rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
|
|
2480
3044
|
});
|
|
2481
|
-
|
|
2482
|
-
|
|
3045
|
+
|
|
3046
|
+
// src/rules/seo/description-length.ts
|
|
3047
|
+
var seoDescriptionLength = lengthRule({
|
|
3048
|
+
id: "seo/description-length",
|
|
2483
3049
|
title: "Description length",
|
|
2484
3050
|
label: "Description length",
|
|
2485
3051
|
noun: "Description",
|
|
@@ -2490,9 +3056,9 @@ var seo023DescriptionLength = lengthRule({
|
|
|
2490
3056
|
rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
|
|
2491
3057
|
});
|
|
2492
3058
|
|
|
2493
|
-
// src/rules/seo/
|
|
2494
|
-
var
|
|
2495
|
-
id: "
|
|
3059
|
+
// src/rules/seo/charset.ts
|
|
3060
|
+
var seoCharset = headTagRule({
|
|
3061
|
+
id: "seo/charset",
|
|
2496
3062
|
title: "Character encoding",
|
|
2497
3063
|
severity: "warning",
|
|
2498
3064
|
match: (t) => t.kind === "meta" && t.name === "charset",
|
|
@@ -2507,9 +3073,9 @@ var seo024Charset = headTagRule({
|
|
|
2507
3073
|
}
|
|
2508
3074
|
});
|
|
2509
3075
|
|
|
2510
|
-
// src/rules/seo/
|
|
2511
|
-
var
|
|
2512
|
-
id: "
|
|
3076
|
+
// src/rules/seo/image-alt.ts
|
|
3077
|
+
var seoImageAlt = imageRule({
|
|
3078
|
+
id: "seo/image-alt",
|
|
2513
3079
|
title: "Image alt text",
|
|
2514
3080
|
category: "seo",
|
|
2515
3081
|
severity: "warning",
|
|
@@ -2524,15 +3090,15 @@ var seo025ImageAlt = imageRule({
|
|
|
2524
3090
|
ok: (img) => img.hasAlt
|
|
2525
3091
|
});
|
|
2526
3092
|
|
|
2527
|
-
// src/rules/seo/
|
|
2528
|
-
var docsUrl4 = docsUrlFor("
|
|
3093
|
+
// src/rules/seo/hreflang.ts
|
|
3094
|
+
var docsUrl4 = docsUrlFor("seo/hreflang");
|
|
2529
3095
|
var recommendation4 = 'Use valid hreflang codes (e.g. "en", "en-US", "x-default") and include an x-default when you have multiple language alternates.';
|
|
2530
3096
|
var HREFLANG_RE = /^[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?$/i;
|
|
2531
3097
|
function isValidHreflang(v) {
|
|
2532
3098
|
return v.toLowerCase() === "x-default" || HREFLANG_RE.test(v);
|
|
2533
3099
|
}
|
|
2534
|
-
var
|
|
2535
|
-
id: "
|
|
3100
|
+
var seoHreflang = {
|
|
3101
|
+
id: "seo/hreflang",
|
|
2536
3102
|
title: "hreflang validity",
|
|
2537
3103
|
category: "seo",
|
|
2538
3104
|
severity: "warning",
|
|
@@ -2557,7 +3123,7 @@ var seo026Hreflang = {
|
|
|
2557
3123
|
}
|
|
2558
3124
|
out.push(
|
|
2559
3125
|
problem ? {
|
|
2560
|
-
id: "
|
|
3126
|
+
id: "seo/hreflang",
|
|
2561
3127
|
category: "seo",
|
|
2562
3128
|
severity: "warning",
|
|
2563
3129
|
detection: PENALIZED,
|
|
@@ -2567,7 +3133,7 @@ var seo026Hreflang = {
|
|
|
2567
3133
|
recommendation: recommendation4,
|
|
2568
3134
|
docsUrl: docsUrl4
|
|
2569
3135
|
} : {
|
|
2570
|
-
id: "
|
|
3136
|
+
id: "seo/hreflang",
|
|
2571
3137
|
category: "seo",
|
|
2572
3138
|
severity: "warning",
|
|
2573
3139
|
detection: PASS,
|
|
@@ -2582,11 +3148,11 @@ var seo026Hreflang = {
|
|
|
2582
3148
|
}
|
|
2583
3149
|
};
|
|
2584
3150
|
|
|
2585
|
-
// src/rules/seo/
|
|
2586
|
-
var docsUrl5 = docsUrlFor("
|
|
3151
|
+
// src/rules/seo/single-h1.ts
|
|
3152
|
+
var docsUrl5 = docsUrlFor("seo/single-h1");
|
|
2587
3153
|
var recommendation5 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
|
|
2588
|
-
var
|
|
2589
|
-
id: "
|
|
3154
|
+
var seoSingleH1 = {
|
|
3155
|
+
id: "seo/single-h1",
|
|
2590
3156
|
title: "Heading hierarchy",
|
|
2591
3157
|
category: "seo",
|
|
2592
3158
|
severity: "warning",
|
|
@@ -2609,7 +3175,7 @@ var seo027Heading = {
|
|
|
2609
3175
|
}
|
|
2610
3176
|
out.push(
|
|
2611
3177
|
problem ? {
|
|
2612
|
-
id: "
|
|
3178
|
+
id: "seo/single-h1",
|
|
2613
3179
|
category: "seo",
|
|
2614
3180
|
severity: "warning",
|
|
2615
3181
|
detection: PENALIZED,
|
|
@@ -2619,7 +3185,7 @@ var seo027Heading = {
|
|
|
2619
3185
|
recommendation: recommendation5,
|
|
2620
3186
|
docsUrl: docsUrl5
|
|
2621
3187
|
} : {
|
|
2622
|
-
id: "
|
|
3188
|
+
id: "seo/single-h1",
|
|
2623
3189
|
category: "seo",
|
|
2624
3190
|
severity: "warning",
|
|
2625
3191
|
detection: PASS,
|
|
@@ -2634,7 +3200,7 @@ var seo027Heading = {
|
|
|
2634
3200
|
}
|
|
2635
3201
|
};
|
|
2636
3202
|
|
|
2637
|
-
// src/rules/seo/
|
|
3203
|
+
// src/rules/seo/uniqueness-rule.ts
|
|
2638
3204
|
function uniquenessRule(opts) {
|
|
2639
3205
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2640
3206
|
return {
|
|
@@ -2681,8 +3247,10 @@ function uniquenessRule(opts) {
|
|
|
2681
3247
|
}
|
|
2682
3248
|
};
|
|
2683
3249
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
3250
|
+
|
|
3251
|
+
// src/rules/seo/duplicate-title.ts
|
|
3252
|
+
var seoDuplicateTitle = uniquenessRule({
|
|
3253
|
+
id: "seo/duplicate-title",
|
|
2686
3254
|
title: "Duplicate title",
|
|
2687
3255
|
label: "Unique title",
|
|
2688
3256
|
noun: "Title",
|
|
@@ -2690,8 +3258,10 @@ var seo028TitleUnique = uniquenessRule({
|
|
|
2690
3258
|
recommendation: "Give each route a unique <title> that describes that page specifically.",
|
|
2691
3259
|
rationale: "Duplicate titles across pages make them compete in search results and weaken each page\u2019s relevance signal."
|
|
2692
3260
|
});
|
|
2693
|
-
|
|
2694
|
-
|
|
3261
|
+
|
|
3262
|
+
// src/rules/seo/duplicate-description.ts
|
|
3263
|
+
var seoDuplicateDescription = uniquenessRule({
|
|
3264
|
+
id: "seo/duplicate-description",
|
|
2695
3265
|
title: "Duplicate description",
|
|
2696
3266
|
label: "Unique description",
|
|
2697
3267
|
noun: "Description",
|
|
@@ -2700,11 +3270,11 @@ var seo029DescriptionUnique = uniquenessRule({
|
|
|
2700
3270
|
rationale: "Duplicate meta descriptions give search engines no per-page summary, so they are often ignored or rewritten."
|
|
2701
3271
|
});
|
|
2702
3272
|
|
|
2703
|
-
// src/rules/seo/
|
|
2704
|
-
var docsUrl6 = docsUrlFor("
|
|
3273
|
+
// src/rules/seo/heading-level-skip.ts
|
|
3274
|
+
var docsUrl6 = docsUrlFor("seo/heading-level-skip");
|
|
2705
3275
|
var recommendation6 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
|
|
2706
|
-
var
|
|
2707
|
-
id: "
|
|
3276
|
+
var seoHeadingLevelSkip = {
|
|
3277
|
+
id: "seo/heading-level-skip",
|
|
2708
3278
|
title: "Heading order",
|
|
2709
3279
|
category: "seo",
|
|
2710
3280
|
severity: "info",
|
|
@@ -2726,7 +3296,7 @@ var seo030HeadingOrder = {
|
|
|
2726
3296
|
}
|
|
2727
3297
|
out.push(
|
|
2728
3298
|
skip ? {
|
|
2729
|
-
id: "
|
|
3299
|
+
id: "seo/heading-level-skip",
|
|
2730
3300
|
category: "seo",
|
|
2731
3301
|
severity: "info",
|
|
2732
3302
|
detection: PENALIZED,
|
|
@@ -2737,7 +3307,7 @@ var seo030HeadingOrder = {
|
|
|
2737
3307
|
recommendation: recommendation6,
|
|
2738
3308
|
docsUrl: docsUrl6
|
|
2739
3309
|
} : {
|
|
2740
|
-
id: "
|
|
3310
|
+
id: "seo/heading-level-skip",
|
|
2741
3311
|
category: "seo",
|
|
2742
3312
|
severity: "info",
|
|
2743
3313
|
detection: PASS,
|
|
@@ -2752,10 +3322,85 @@ var seo030HeadingOrder = {
|
|
|
2752
3322
|
}
|
|
2753
3323
|
};
|
|
2754
3324
|
|
|
2755
|
-
// src/rules/
|
|
3325
|
+
// src/rules/kit-module-rule.ts
|
|
2756
3326
|
var PENALIZED2 = { presence: "none", value: "absent" };
|
|
2757
3327
|
var PASS2 = { presence: "own", value: "static" };
|
|
2758
|
-
function isSuppressed(
|
|
3328
|
+
function isSuppressed(m, ruleId, line) {
|
|
3329
|
+
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3330
|
+
}
|
|
3331
|
+
function kitModuleRule(opts) {
|
|
3332
|
+
const docsUrl7 = docsUrlFor(opts.id);
|
|
3333
|
+
const severity = opts.severity ?? "warning";
|
|
3334
|
+
return {
|
|
3335
|
+
id: opts.id,
|
|
3336
|
+
title: opts.title,
|
|
3337
|
+
category: opts.category,
|
|
3338
|
+
severity,
|
|
3339
|
+
scope: "component",
|
|
3340
|
+
rationale: opts.rationale,
|
|
3341
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
3342
|
+
async check(ctx) {
|
|
3343
|
+
const out = [];
|
|
3344
|
+
for (const m of ctx.kitModules ?? []) {
|
|
3345
|
+
if (!opts.applies(m, ctx)) continue;
|
|
3346
|
+
const bad = opts.bad(m, ctx).filter((b) => !(b.line > 0 && isSuppressed(m, opts.id, b.line)));
|
|
3347
|
+
if (bad.length === 0) {
|
|
3348
|
+
out.push({
|
|
3349
|
+
id: opts.id,
|
|
3350
|
+
category: opts.category,
|
|
3351
|
+
severity,
|
|
3352
|
+
detection: PASS2,
|
|
3353
|
+
route: m.file,
|
|
3354
|
+
message: opts.label,
|
|
3355
|
+
recommendation: opts.recommendation,
|
|
3356
|
+
docsUrl: docsUrl7
|
|
3357
|
+
});
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
for (const b of bad) {
|
|
3361
|
+
out.push({
|
|
3362
|
+
id: opts.id,
|
|
3363
|
+
category: opts.category,
|
|
3364
|
+
severity,
|
|
3365
|
+
detection: PENALIZED2,
|
|
3366
|
+
route: m.file,
|
|
3367
|
+
location: m.file,
|
|
3368
|
+
...b.line > 0 ? { line: b.line } : {},
|
|
3369
|
+
message: b.message,
|
|
3370
|
+
recommendation: opts.recommendation,
|
|
3371
|
+
docsUrl: docsUrl7,
|
|
3372
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
return out;
|
|
3377
|
+
}
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/rules/seo/ssr-disabled.ts
|
|
3382
|
+
var ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
|
|
3383
|
+
var PAGE_OPTION_FILE_RE = /\+(page|layout)(\.server)?\.(ts|js)$/;
|
|
3384
|
+
var seoSsrDisabled = kitModuleRule({
|
|
3385
|
+
id: "seo/ssr-disabled",
|
|
3386
|
+
title: "SSR disabled",
|
|
3387
|
+
category: "seo",
|
|
3388
|
+
label: "SSR enabled",
|
|
3389
|
+
recommendation: "Keep SSR on for indexable pages; restrict ssr = false to routes that don't need SEO (authenticated dashboards, app-only views). For a deliberate SPA, turn this rule off in the config or add an inline suppression.",
|
|
3390
|
+
rationale: "SvelteKit's SEO guidance is to leave SSR on unless there is a good reason not to: server-rendered content is indexed more frequently and reliably, and SPA mode costs an extra network round trip before anything renders.",
|
|
3391
|
+
applies: (m) => m.ssrDisabled !== void 0 && PAGE_OPTION_FILE_RE.test(m.file),
|
|
3392
|
+
bad: (m) => [
|
|
3393
|
+
{
|
|
3394
|
+
line: m.ssrDisabled.line,
|
|
3395
|
+
message: ROOT_LAYOUT_RE.test(m.file) ? "SSR is disabled for the whole app \u2014 search engines index server-rendered content more reliably, and SPA mode adds a network round trip before first paint" : "SSR is disabled for this route \u2014 its content is invisible to crawlers that don't execute JavaScript and indexes less reliably"
|
|
3396
|
+
}
|
|
3397
|
+
]
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
// src/rules/component-rule.ts
|
|
3401
|
+
var PENALIZED3 = { presence: "none", value: "absent" };
|
|
3402
|
+
var PASS3 = { presence: "own", value: "static" };
|
|
3403
|
+
function isSuppressed2(c, ruleId, line) {
|
|
2759
3404
|
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
2760
3405
|
}
|
|
2761
3406
|
function componentRule(opts) {
|
|
@@ -2768,17 +3413,18 @@ function componentRule(opts) {
|
|
|
2768
3413
|
severity,
|
|
2769
3414
|
scope: "component",
|
|
2770
3415
|
rationale: opts.rationale,
|
|
3416
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
2771
3417
|
async check(ctx) {
|
|
2772
3418
|
const out = [];
|
|
2773
3419
|
for (const c of ctx.components ?? []) {
|
|
2774
3420
|
if (!opts.applies(c)) continue;
|
|
2775
|
-
const bad = opts.bad(c).filter((b) => !(b.line > 0 &&
|
|
3421
|
+
const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
|
|
2776
3422
|
if (bad.length === 0) {
|
|
2777
3423
|
out.push({
|
|
2778
3424
|
id: opts.id,
|
|
2779
3425
|
category: opts.category,
|
|
2780
3426
|
severity,
|
|
2781
|
-
detection:
|
|
3427
|
+
detection: PASS3,
|
|
2782
3428
|
route: c.file,
|
|
2783
3429
|
message: opts.label,
|
|
2784
3430
|
recommendation: opts.recommendation,
|
|
@@ -2791,13 +3437,14 @@ function componentRule(opts) {
|
|
|
2791
3437
|
id: opts.id,
|
|
2792
3438
|
category: opts.category,
|
|
2793
3439
|
severity,
|
|
2794
|
-
detection:
|
|
3440
|
+
detection: PENALIZED3,
|
|
2795
3441
|
route: c.file,
|
|
2796
3442
|
location: c.file,
|
|
2797
3443
|
...b.line > 0 ? { line: b.line } : {},
|
|
2798
3444
|
message: b.message,
|
|
2799
3445
|
recommendation: opts.recommendation,
|
|
2800
|
-
docsUrl: docsUrl7
|
|
3446
|
+
docsUrl: docsUrl7,
|
|
3447
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2801
3448
|
});
|
|
2802
3449
|
}
|
|
2803
3450
|
}
|
|
@@ -2806,9 +3453,9 @@ function componentRule(opts) {
|
|
|
2806
3453
|
};
|
|
2807
3454
|
}
|
|
2808
3455
|
|
|
2809
|
-
// src/rules/correctness/
|
|
2810
|
-
var
|
|
2811
|
-
id: "
|
|
3456
|
+
// src/rules/correctness/each-key.ts
|
|
3457
|
+
var correctnessEachKey = componentRule({
|
|
3458
|
+
id: "correctness/each-key",
|
|
2812
3459
|
title: "Keyed each block",
|
|
2813
3460
|
category: "correctness",
|
|
2814
3461
|
label: "Keyed {#each}",
|
|
@@ -2817,8 +3464,25 @@ var correct001EachKey = componentRule({
|
|
|
2817
3464
|
applies: (c) => c.eachBlocks.length > 0,
|
|
2818
3465
|
bad: (c) => c.eachBlocks.filter((e) => !e.hasKey).map((e) => ({ line: e.line, message: "{#each} block has no key" }))
|
|
2819
3466
|
});
|
|
2820
|
-
|
|
2821
|
-
|
|
3467
|
+
|
|
3468
|
+
// src/rules/correctness/each-index-key.ts
|
|
3469
|
+
var correctnessEachIndexKey = componentRule({
|
|
3470
|
+
id: "correctness/each-index-key",
|
|
3471
|
+
title: "Index used as each key",
|
|
3472
|
+
category: "correctness",
|
|
3473
|
+
label: "Item-keyed {#each}",
|
|
3474
|
+
recommendation: "Key by a value that uniquely identifies the item, e.g. (item.id).",
|
|
3475
|
+
rationale: "Svelte's guidance is explicit: the key must uniquely identify the object \u2014 do not use the index. An index key gives items position-based identity, so element state (focus, inputs, transitions) sticks to positions when the list reorders or items are inserted or removed, exactly like an unkeyed block \u2014 but the visible key masks the problem.",
|
|
3476
|
+
applies: (c) => c.eachBlocks.some((e) => e.indexKey),
|
|
3477
|
+
bad: (c) => c.eachBlocks.filter((e) => e.indexKey).map((e) => ({
|
|
3478
|
+
line: e.line,
|
|
3479
|
+
message: "{#each} is keyed by its index \u2014 identity follows position, exactly like an unkeyed block, but the key makes it look safe."
|
|
3480
|
+
}))
|
|
3481
|
+
});
|
|
3482
|
+
|
|
3483
|
+
// src/rules/correctness/effect-as-derived.ts
|
|
3484
|
+
var correctnessEffectAsDerived = componentRule({
|
|
3485
|
+
id: "correctness/effect-as-derived",
|
|
2822
3486
|
title: "Effect used to derive state",
|
|
2823
3487
|
category: "correctness",
|
|
2824
3488
|
label: "$effect usage",
|
|
@@ -2827,8 +3491,10 @@ var correct002EffectDerived = componentRule({
|
|
|
2827
3491
|
applies: (c) => c.effects.length > 0,
|
|
2828
3492
|
bad: (c) => c.effects.filter((e) => e.assignsOnlyState).map((e) => ({ line: e.line, message: "$effect only assigns state \u2014 use $derived instead" }))
|
|
2829
3493
|
});
|
|
2830
|
-
|
|
2831
|
-
|
|
3494
|
+
|
|
3495
|
+
// src/rules/correctness/effect-as-onmount.ts
|
|
3496
|
+
var correctnessEffectAsOnMount = componentRule({
|
|
3497
|
+
id: "correctness/effect-as-onmount",
|
|
2832
3498
|
title: "Effect used as onMount",
|
|
2833
3499
|
category: "correctness",
|
|
2834
3500
|
label: "$effect usage",
|
|
@@ -2838,14 +3504,14 @@ var correct003EffectAsOnMount = componentRule({
|
|
|
2838
3504
|
bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({ line: e.line, message: "$effect reads no reactive value \u2014 use onMount instead" }))
|
|
2839
3505
|
});
|
|
2840
3506
|
|
|
2841
|
-
// src/rules/correctness/
|
|
2842
|
-
var
|
|
2843
|
-
id: "
|
|
3507
|
+
// src/rules/correctness/unmutated-state.ts
|
|
3508
|
+
var correctnessUnmutatedState = componentRule({
|
|
3509
|
+
id: "correctness/unmutated-state",
|
|
2844
3510
|
title: "Unmutated $state",
|
|
2845
3511
|
category: "correctness",
|
|
2846
3512
|
severity: "info",
|
|
2847
3513
|
label: "$state usage",
|
|
2848
|
-
recommendation: "If a value never changes, use const; if you only ever reassign it wholesale (never mutate its properties), use $state.raw to skip deep proxying.",
|
|
3514
|
+
recommendation: "If a value never changes, use const \u2014 or $derived if it is computed from props or state; if you only ever reassign it wholesale (never mutate its properties), use $state.raw to skip deep proxying.",
|
|
2849
3515
|
rationale: "A $state that is never mutated pays for reactivity (deep proxying, tracking) it never uses; const (or $state.raw) is clearer and cheaper.",
|
|
2850
3516
|
applies: (c) => c.constableStates.length > 0,
|
|
2851
3517
|
bad: (c) => c.constableStates.map((s) => ({
|
|
@@ -2854,9 +3520,9 @@ var correct004UnmutatedState = componentRule({
|
|
|
2854
3520
|
}))
|
|
2855
3521
|
});
|
|
2856
3522
|
|
|
2857
|
-
// src/rules/correctness/
|
|
2858
|
-
var
|
|
2859
|
-
id: "
|
|
3523
|
+
// src/rules/correctness/prop-mutation.ts
|
|
3524
|
+
var correctnessPropMutation = componentRule({
|
|
3525
|
+
id: "correctness/prop-mutation",
|
|
2860
3526
|
title: "Mutated non-bindable prop",
|
|
2861
3527
|
category: "correctness",
|
|
2862
3528
|
label: "Prop mutation",
|
|
@@ -2869,9 +3535,28 @@ var correct005PropMutation = componentRule({
|
|
|
2869
3535
|
}))
|
|
2870
3536
|
});
|
|
2871
3537
|
|
|
2872
|
-
// src/rules/correctness/
|
|
2873
|
-
var
|
|
2874
|
-
id: "
|
|
3538
|
+
// src/rules/correctness/stale-prop-derivation.ts
|
|
3539
|
+
var correctnessStalePropDerivation = componentRule({
|
|
3540
|
+
id: "correctness/stale-prop-derivation",
|
|
3541
|
+
title: "Stale prop derivation",
|
|
3542
|
+
category: "correctness",
|
|
3543
|
+
severity: "warning",
|
|
3544
|
+
label: "Props derived reactively",
|
|
3545
|
+
recommendation: "Wrap the computation in $derived(...), or $derived.by(() => ...) when it needs a function body.",
|
|
3546
|
+
rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. $derived keeps the computation live at no cost.",
|
|
3547
|
+
fix: {
|
|
3548
|
+
description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body), keeping the same expression."
|
|
3549
|
+
},
|
|
3550
|
+
applies: (c) => c.stalePropDerivations.length > 0,
|
|
3551
|
+
bad: (c) => c.stalePropDerivations.map((s) => ({
|
|
3552
|
+
line: s.line,
|
|
3553
|
+
message: `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
|
|
3554
|
+
}))
|
|
3555
|
+
});
|
|
3556
|
+
|
|
3557
|
+
// src/rules/correctness/orphan-effect.ts
|
|
3558
|
+
var correctnessOrphanEffect = componentRule({
|
|
3559
|
+
id: "correctness/orphan-effect",
|
|
2875
3560
|
title: "Orphan $effect",
|
|
2876
3561
|
category: "correctness",
|
|
2877
3562
|
severity: "critical",
|
|
@@ -2888,25 +3573,25 @@ var correct006OrphanEffect = componentRule({
|
|
|
2888
3573
|
}))
|
|
2889
3574
|
});
|
|
2890
3575
|
|
|
2891
|
-
// src/rules/correctness/
|
|
2892
|
-
var
|
|
2893
|
-
var
|
|
2894
|
-
var ID = "
|
|
3576
|
+
// src/rules/correctness/orphan-lifecycle.ts
|
|
3577
|
+
var PENALIZED4 = { presence: "none", value: "absent" };
|
|
3578
|
+
var PASS4 = { presence: "own", value: "static" };
|
|
3579
|
+
var ID = "correctness/orphan-lifecycle";
|
|
2895
3580
|
var DOCS_URL = docsUrlFor(ID);
|
|
2896
3581
|
var LABEL = "Lifecycle-call context";
|
|
2897
3582
|
var RECOMMENDATION = "Call lifecycle/context functions during component initialisation (the top level of a component's <script>). In load, return the data and call setContext in a layout/page component; in shared modules, expose a setup function that components call during init.";
|
|
2898
3583
|
var topLevelMessage = (name) => `${name}() runs at module evaluation, outside component initialisation \u2014 it throws lifecycle_outside_component at runtime`;
|
|
2899
|
-
function
|
|
3584
|
+
function isSuppressed3(suppressions, line) {
|
|
2900
3585
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID)));
|
|
2901
3586
|
}
|
|
2902
3587
|
function emitFile(out, file, issues, suppressions) {
|
|
2903
|
-
const bad = issues.filter((b) => !(b.line > 0 &&
|
|
3588
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed3(suppressions, b.line)));
|
|
2904
3589
|
if (bad.length === 0) {
|
|
2905
3590
|
out.push({
|
|
2906
3591
|
id: ID,
|
|
2907
3592
|
category: "correctness",
|
|
2908
3593
|
severity: "critical",
|
|
2909
|
-
detection:
|
|
3594
|
+
detection: PASS4,
|
|
2910
3595
|
route: file,
|
|
2911
3596
|
message: LABEL,
|
|
2912
3597
|
recommendation: RECOMMENDATION,
|
|
@@ -2919,7 +3604,7 @@ function emitFile(out, file, issues, suppressions) {
|
|
|
2919
3604
|
id: ID,
|
|
2920
3605
|
category: "correctness",
|
|
2921
3606
|
severity: "critical",
|
|
2922
|
-
detection:
|
|
3607
|
+
detection: PENALIZED4,
|
|
2923
3608
|
route: file,
|
|
2924
3609
|
location: file,
|
|
2925
3610
|
...b.line > 0 ? { line: b.line } : {},
|
|
@@ -2929,7 +3614,7 @@ function emitFile(out, file, issues, suppressions) {
|
|
|
2929
3614
|
});
|
|
2930
3615
|
}
|
|
2931
3616
|
}
|
|
2932
|
-
var
|
|
3617
|
+
var correctnessOrphanLifecycle = {
|
|
2933
3618
|
id: ID,
|
|
2934
3619
|
title: "Lifecycle call outside component initialisation",
|
|
2935
3620
|
category: "correctness",
|
|
@@ -2968,25 +3653,25 @@ var correct007OrphanLifecycle = {
|
|
|
2968
3653
|
}
|
|
2969
3654
|
};
|
|
2970
3655
|
|
|
2971
|
-
// src/rules/correctness/
|
|
2972
|
-
var
|
|
2973
|
-
var
|
|
2974
|
-
var ID2 = "
|
|
3656
|
+
// src/rules/correctness/server-browser-global.ts
|
|
3657
|
+
var PENALIZED5 = { presence: "none", value: "absent" };
|
|
3658
|
+
var PASS5 = { presence: "own", value: "static" };
|
|
3659
|
+
var ID2 = "correctness/server-browser-global";
|
|
2975
3660
|
var DOCS_URL2 = docsUrlFor(ID2);
|
|
2976
3661
|
var LABEL2 = "Server-safe module code";
|
|
2977
3662
|
var RECOMMENDATION2 = "Move browser-only code into onMount or $effect (they never run on the server), or guard it with browser from $app/environment (or a typeof check).";
|
|
2978
3663
|
var moduleMessage = (name) => `${name} is accessed at module scope \u2014 it does not exist on the server, so importing this file crashes SSR with "${name} is not defined"`;
|
|
2979
|
-
function
|
|
3664
|
+
function isSuppressed4(suppressions, line) {
|
|
2980
3665
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
|
|
2981
3666
|
}
|
|
2982
3667
|
function emitFile2(out, file, issues, suppressions) {
|
|
2983
|
-
const bad = issues.filter((b) => !(b.line > 0 &&
|
|
3668
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed4(suppressions, b.line)));
|
|
2984
3669
|
if (bad.length === 0) {
|
|
2985
3670
|
out.push({
|
|
2986
3671
|
id: ID2,
|
|
2987
3672
|
category: "correctness",
|
|
2988
3673
|
severity: "critical",
|
|
2989
|
-
detection:
|
|
3674
|
+
detection: PASS5,
|
|
2990
3675
|
route: file,
|
|
2991
3676
|
message: LABEL2,
|
|
2992
3677
|
recommendation: RECOMMENDATION2,
|
|
@@ -2999,7 +3684,7 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
2999
3684
|
id: ID2,
|
|
3000
3685
|
category: "correctness",
|
|
3001
3686
|
severity: "critical",
|
|
3002
|
-
detection:
|
|
3687
|
+
detection: PENALIZED5,
|
|
3003
3688
|
route: file,
|
|
3004
3689
|
location: file,
|
|
3005
3690
|
...b.line > 0 ? { line: b.line } : {},
|
|
@@ -3009,7 +3694,7 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
3009
3694
|
});
|
|
3010
3695
|
}
|
|
3011
3696
|
}
|
|
3012
|
-
var
|
|
3697
|
+
var correctnessServerBrowserGlobal = {
|
|
3013
3698
|
id: ID2,
|
|
3014
3699
|
title: "Browser global in server module code",
|
|
3015
3700
|
category: "correctness",
|
|
@@ -3045,9 +3730,9 @@ var correct008BrowserGlobals = {
|
|
|
3045
3730
|
}
|
|
3046
3731
|
};
|
|
3047
3732
|
|
|
3048
|
-
// src/rules/correctness/
|
|
3049
|
-
var
|
|
3050
|
-
id: "
|
|
3733
|
+
// src/rules/correctness/instance-browser-global.ts
|
|
3734
|
+
var correctnessInstanceBrowserGlobal = componentRule({
|
|
3735
|
+
id: "correctness/instance-browser-global",
|
|
3051
3736
|
title: "Browser global during component initialisation",
|
|
3052
3737
|
category: "correctness",
|
|
3053
3738
|
label: "Server-safe component init",
|
|
@@ -3060,9 +3745,9 @@ var correct009InstanceBrowserGlobals = componentRule({
|
|
|
3060
3745
|
}))
|
|
3061
3746
|
});
|
|
3062
3747
|
|
|
3063
|
-
// src/rules/security/
|
|
3064
|
-
var
|
|
3065
|
-
id: "
|
|
3748
|
+
// src/rules/security/raw-html.ts
|
|
3749
|
+
var securityRawHtml = componentRule({
|
|
3750
|
+
id: "security/raw-html",
|
|
3066
3751
|
title: "Raw HTML render",
|
|
3067
3752
|
category: "security",
|
|
3068
3753
|
label: "{@html} usage",
|
|
@@ -3071,8 +3756,10 @@ var sec001Html = componentRule({
|
|
|
3071
3756
|
applies: (c) => c.htmlTags.length > 0,
|
|
3072
3757
|
bad: (c) => c.htmlTags.map((h) => ({ line: h.line, message: "{@html} renders unescaped HTML \u2014 ensure it is sanitized" }))
|
|
3073
3758
|
});
|
|
3074
|
-
|
|
3075
|
-
|
|
3759
|
+
|
|
3760
|
+
// src/rules/security/javascript-url.ts
|
|
3761
|
+
var securityJavascriptUrl = componentRule({
|
|
3762
|
+
id: "security/javascript-url",
|
|
3076
3763
|
title: "javascript: URL",
|
|
3077
3764
|
category: "security",
|
|
3078
3765
|
label: "No javascript: URLs",
|
|
@@ -3082,63 +3769,9 @@ var sec002JavascriptUrl = componentRule({
|
|
|
3082
3769
|
bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
|
|
3083
3770
|
});
|
|
3084
3771
|
|
|
3085
|
-
// src/rules/
|
|
3086
|
-
var
|
|
3087
|
-
|
|
3088
|
-
function isSuppressed4(m, ruleId, line) {
|
|
3089
|
-
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3090
|
-
}
|
|
3091
|
-
function kitModuleRule(opts) {
|
|
3092
|
-
const docsUrl7 = docsUrlFor(opts.id);
|
|
3093
|
-
const severity = opts.severity ?? "warning";
|
|
3094
|
-
return {
|
|
3095
|
-
id: opts.id,
|
|
3096
|
-
title: opts.title,
|
|
3097
|
-
category: opts.category,
|
|
3098
|
-
severity,
|
|
3099
|
-
scope: "component",
|
|
3100
|
-
rationale: opts.rationale,
|
|
3101
|
-
async check(ctx) {
|
|
3102
|
-
const out = [];
|
|
3103
|
-
for (const m of ctx.kitModules ?? []) {
|
|
3104
|
-
if (!opts.applies(m, ctx)) continue;
|
|
3105
|
-
const bad = opts.bad(m, ctx).filter((b) => !(b.line > 0 && isSuppressed4(m, opts.id, b.line)));
|
|
3106
|
-
if (bad.length === 0) {
|
|
3107
|
-
out.push({
|
|
3108
|
-
id: opts.id,
|
|
3109
|
-
category: opts.category,
|
|
3110
|
-
severity,
|
|
3111
|
-
detection: PASS5,
|
|
3112
|
-
route: m.file,
|
|
3113
|
-
message: opts.label,
|
|
3114
|
-
recommendation: opts.recommendation,
|
|
3115
|
-
docsUrl: docsUrl7
|
|
3116
|
-
});
|
|
3117
|
-
continue;
|
|
3118
|
-
}
|
|
3119
|
-
for (const b of bad) {
|
|
3120
|
-
out.push({
|
|
3121
|
-
id: opts.id,
|
|
3122
|
-
category: opts.category,
|
|
3123
|
-
severity,
|
|
3124
|
-
detection: PENALIZED5,
|
|
3125
|
-
route: m.file,
|
|
3126
|
-
location: m.file,
|
|
3127
|
-
...b.line > 0 ? { line: b.line } : {},
|
|
3128
|
-
message: b.message,
|
|
3129
|
-
recommendation: opts.recommendation,
|
|
3130
|
-
docsUrl: docsUrl7
|
|
3131
|
-
});
|
|
3132
|
-
}
|
|
3133
|
-
}
|
|
3134
|
-
return out;
|
|
3135
|
-
}
|
|
3136
|
-
};
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
// src/rules/security/sec003-load-state-write.ts
|
|
3140
|
-
var sec003LoadStateWrite = kitModuleRule({
|
|
3141
|
-
id: "SEC003",
|
|
3772
|
+
// src/rules/security/handler-state-write.ts
|
|
3773
|
+
var securityHandlerStateWrite = kitModuleRule({
|
|
3774
|
+
id: "security/handler-state-write",
|
|
3142
3775
|
title: "Handler writes imported state",
|
|
3143
3776
|
category: "security",
|
|
3144
3777
|
severity: "critical",
|
|
@@ -3152,9 +3785,9 @@ var sec003LoadStateWrite = kitModuleRule({
|
|
|
3152
3785
|
}))
|
|
3153
3786
|
});
|
|
3154
3787
|
|
|
3155
|
-
// src/rules/security/
|
|
3156
|
-
var
|
|
3157
|
-
id: "
|
|
3788
|
+
// src/rules/security/server-module-state.ts
|
|
3789
|
+
var securityServerModuleState = kitModuleRule({
|
|
3790
|
+
id: "security/server-module-state",
|
|
3158
3791
|
title: "Server module-scope state",
|
|
3159
3792
|
category: "security",
|
|
3160
3793
|
label: "Server module state",
|
|
@@ -3167,12 +3800,12 @@ var sec004ServerModuleState = kitModuleRule({
|
|
|
3167
3800
|
}))
|
|
3168
3801
|
});
|
|
3169
3802
|
|
|
3170
|
-
// src/rules/security/
|
|
3803
|
+
// src/rules/security/shared-state-import.ts
|
|
3171
3804
|
function extSibling(path) {
|
|
3172
3805
|
return path.endsWith(".svelte.ts") ? path.replace(/\.svelte\.ts$/, ".svelte.js") : path.replace(/\.svelte\.js$/, ".svelte.ts");
|
|
3173
3806
|
}
|
|
3174
|
-
var
|
|
3175
|
-
id: "
|
|
3807
|
+
var securitySharedStateImport = kitModuleRule({
|
|
3808
|
+
id: "security/shared-state-import",
|
|
3176
3809
|
title: "Shared runes-state import on the server",
|
|
3177
3810
|
category: "security",
|
|
3178
3811
|
label: "Server state imports",
|
|
@@ -3198,11 +3831,10 @@ var sec005SharedStateImport = kitModuleRule({
|
|
|
3198
3831
|
}
|
|
3199
3832
|
});
|
|
3200
3833
|
|
|
3201
|
-
// src/rules/architecture/
|
|
3834
|
+
// src/rules/architecture/component-size.ts
|
|
3202
3835
|
var MAX_LOC = 400;
|
|
3203
|
-
var
|
|
3204
|
-
|
|
3205
|
-
id: "ARCH001",
|
|
3836
|
+
var architectureComponentSize = componentRule({
|
|
3837
|
+
id: "architecture/component-size",
|
|
3206
3838
|
title: "Component size",
|
|
3207
3839
|
category: "architecture",
|
|
3208
3840
|
severity: "info",
|
|
@@ -3213,8 +3845,11 @@ var arch001ComponentSize = componentRule({
|
|
|
3213
3845
|
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
3214
3846
|
bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
|
|
3215
3847
|
});
|
|
3216
|
-
|
|
3217
|
-
|
|
3848
|
+
|
|
3849
|
+
// src/rules/architecture/prop-count.ts
|
|
3850
|
+
var MAX_PROPS = 10;
|
|
3851
|
+
var architecturePropCount = componentRule({
|
|
3852
|
+
id: "architecture/prop-count",
|
|
3218
3853
|
title: "Prop count",
|
|
3219
3854
|
category: "architecture",
|
|
3220
3855
|
severity: "info",
|
|
@@ -3226,13 +3861,13 @@ var arch002PropCount = componentRule({
|
|
|
3226
3861
|
bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
|
|
3227
3862
|
});
|
|
3228
3863
|
|
|
3229
|
-
// src/rules/perf/
|
|
3864
|
+
// src/rules/perf/heavy-import.ts
|
|
3230
3865
|
var HEAVY_PACKAGES = {
|
|
3231
3866
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
3232
3867
|
moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
|
|
3233
3868
|
};
|
|
3234
|
-
var
|
|
3235
|
-
id: "
|
|
3869
|
+
var performanceHeavyImport = componentRule({
|
|
3870
|
+
id: "performance/heavy-import",
|
|
3236
3871
|
title: "Heavy dependency import",
|
|
3237
3872
|
category: "performance",
|
|
3238
3873
|
severity: "info",
|
|
@@ -3256,9 +3891,9 @@ var perf009HeavyImport = componentRule({
|
|
|
3256
3891
|
}
|
|
3257
3892
|
});
|
|
3258
3893
|
|
|
3259
|
-
// src/rules/perf/
|
|
3260
|
-
var
|
|
3261
|
-
id: "
|
|
3894
|
+
// src/rules/perf/namespace-import.ts
|
|
3895
|
+
var performanceNamespaceImport = componentRule({
|
|
3896
|
+
id: "performance/namespace-import",
|
|
3262
3897
|
title: "Namespace import",
|
|
3263
3898
|
category: "performance",
|
|
3264
3899
|
severity: "info",
|
|
@@ -3279,68 +3914,168 @@ var perf010NamespaceImport = componentRule({
|
|
|
3279
3914
|
}
|
|
3280
3915
|
});
|
|
3281
3916
|
|
|
3917
|
+
// src/rules/perf/minify-disabled.ts
|
|
3918
|
+
var PENALIZED6 = { presence: "none", value: "absent" };
|
|
3919
|
+
var MINIFY_DISABLED_FIX = {
|
|
3920
|
+
description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
|
|
3921
|
+
snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
|
|
3922
|
+
lang: "ts"
|
|
3923
|
+
};
|
|
3924
|
+
var RECOMMENDATION3 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
3925
|
+
var performanceMinifyDisabled = {
|
|
3926
|
+
id: "performance/minify-disabled",
|
|
3927
|
+
title: "Minification disabled",
|
|
3928
|
+
category: "performance",
|
|
3929
|
+
severity: "warning",
|
|
3930
|
+
scope: "project",
|
|
3931
|
+
rationale: "Disabling minification ships unminified JS/CSS to production, inflating bundle size several-fold and slowing every page load; the override is usually a leftover from debugging.",
|
|
3932
|
+
fix: MINIFY_DISABLED_FIX,
|
|
3933
|
+
async check(ctx) {
|
|
3934
|
+
const hit = ctx.project.viteMinifyDisabled;
|
|
3935
|
+
if (!hit) return [];
|
|
3936
|
+
const provenance = hit.file === void 0 ? " The override comes from an inline (programmatic) Vite config." : hit.line === void 0 ? " The override was resolved from the actual build \u2014 it may come from a plugin or a conditional config, not a literal in the file." : "";
|
|
3937
|
+
return [
|
|
3938
|
+
{
|
|
3939
|
+
id: "performance/minify-disabled",
|
|
3940
|
+
category: "performance",
|
|
3941
|
+
severity: "warning",
|
|
3942
|
+
detection: PENALIZED6,
|
|
3943
|
+
...hit.file !== void 0 ? { location: hit.file } : {},
|
|
3944
|
+
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
3945
|
+
message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
|
|
3946
|
+
recommendation: RECOMMENDATION3,
|
|
3947
|
+
docsUrl: docsUrlFor("performance/minify-disabled"),
|
|
3948
|
+
fix: { ...MINIFY_DISABLED_FIX }
|
|
3949
|
+
}
|
|
3950
|
+
];
|
|
3951
|
+
}
|
|
3952
|
+
};
|
|
3953
|
+
|
|
3954
|
+
// src/rules/perf/load-waterfall.ts
|
|
3955
|
+
var MESSAGE = "Sequential dependent awaits in a universal load create a client-side request waterfall \u2014 each hop is a network round trip from the browser. Move this chain to a server load (+page.server.ts / +layout.server.ts), where the hops run server-side.";
|
|
3956
|
+
var performanceLoadWaterfall = kitModuleRule({
|
|
3957
|
+
id: "performance/load-waterfall",
|
|
3958
|
+
title: "Load waterfall",
|
|
3959
|
+
category: "performance",
|
|
3960
|
+
severity: "warning",
|
|
3961
|
+
label: "No load waterfalls",
|
|
3962
|
+
recommendation: "Move the dependent await chain into a server load (+page.server.ts / +layout.server.ts), where the hops run server-to-server.",
|
|
3963
|
+
rationale: "In a universal load, every await that depends on a previous result costs a full network round trip from the browser on client-side navigation; chains multiply latency on every page visit. A server load runs the same hops server-side.",
|
|
3964
|
+
fix: {
|
|
3965
|
+
description: "Move the dependent await chain into a server load (+page.server.ts), where hops run server-to-server.",
|
|
3966
|
+
snippet: "// +page.server.ts \u2014 same chain, server-side hops\nexport async function load({ fetch }) {\n const user = await fetch(`/api/user`).then((r) => r.json());\n const posts = await fetch(`/api/posts/${user.id}`).then((r) => r.json());\n return { user, posts };\n}",
|
|
3967
|
+
lang: "ts"
|
|
3968
|
+
},
|
|
3969
|
+
applies: (m) => m.kind === "universal" && m.csrDisabled === void 0 && (m.loadWaterfalls?.dependentLines.length ?? 0) > 0,
|
|
3970
|
+
bad: (m) => m.loadWaterfalls.dependentLines.map((line) => ({ line, message: MESSAGE }))
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
// src/rules/perf/sequential-awaits.ts
|
|
3974
|
+
var MESSAGE2 = "This await does not use the results of the awaits before it \u2014 the requests run sequentially for no reason. Start them together and await them with Promise.all.";
|
|
3975
|
+
var performanceSequentialAwaits = kitModuleRule({
|
|
3976
|
+
id: "performance/sequential-awaits",
|
|
3977
|
+
title: "Sequential independent awaits",
|
|
3978
|
+
category: "performance",
|
|
3979
|
+
severity: "info",
|
|
3980
|
+
label: "No needlessly sequential awaits",
|
|
3981
|
+
recommendation: "Start the independent requests together and await them with Promise.all.",
|
|
3982
|
+
rationale: "Awaits that do not use each other's results still run one after another, adding their latencies; starting them together costs nothing and bounds the wait to the slowest request.",
|
|
3983
|
+
fix: {
|
|
3984
|
+
description: "Start the independent requests together and await them with Promise.all.",
|
|
3985
|
+
snippet: "const [a, b] = await Promise.all([fetchA(), fetchB()]);",
|
|
3986
|
+
lang: "ts"
|
|
3987
|
+
},
|
|
3988
|
+
applies: (m) => (m.loadWaterfalls?.independentLines.length ?? 0) > 0,
|
|
3989
|
+
bad: (m) => m.loadWaterfalls.independentLines.map((line) => ({ line, message: MESSAGE2 }))
|
|
3990
|
+
});
|
|
3991
|
+
|
|
3992
|
+
// src/rules/perf/state-raw.ts
|
|
3993
|
+
var performanceStateRaw = componentRule({
|
|
3994
|
+
id: "performance/state-raw",
|
|
3995
|
+
title: "Raw state opportunity",
|
|
3996
|
+
category: "performance",
|
|
3997
|
+
severity: "info",
|
|
3998
|
+
label: "Deep reactivity only where mutated",
|
|
3999
|
+
recommendation: "Declare it with $state.raw(...) \u2014 reassignment stays reactive; only property-level mutation needs the deep proxy.",
|
|
4000
|
+
rationale: "Objects and arrays in $state are made deeply reactive through proxying, which taxes every property access. A binding that is only ever reassigned \u2014 API responses are the canonical case \u2014 never uses that machinery; Svelte's own guidance is to use $state.raw for it.",
|
|
4001
|
+
fix: {
|
|
4002
|
+
description: "Replace $state(...) with $state.raw(...); keep the same initializer."
|
|
4003
|
+
},
|
|
4004
|
+
applies: (c) => c.rawableStates.length > 0,
|
|
4005
|
+
bad: (c) => c.rawableStates.map((s) => ({
|
|
4006
|
+
line: s.line,
|
|
4007
|
+
message: `"${s.name}" is an object/array $state that is only ever reassigned, never mutated \u2014 $state.raw skips the deep-proxy overhead (reassignment stays reactive).`
|
|
4008
|
+
}))
|
|
4009
|
+
});
|
|
4010
|
+
|
|
3282
4011
|
// src/rules/index.ts
|
|
3283
4012
|
var allRules = [
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
4013
|
+
seoTitlePresence,
|
|
4014
|
+
seoDescriptionPresence,
|
|
4015
|
+
seoCanonicalUrl,
|
|
4016
|
+
seoOgImage,
|
|
4017
|
+
seoOgTitle,
|
|
4018
|
+
seoRobotsTxt,
|
|
4019
|
+
seoSitemapXml,
|
|
4020
|
+
seoJsonLd,
|
|
4021
|
+
seoHtmlLang,
|
|
4022
|
+
performanceImageDimensions,
|
|
4023
|
+
performanceImageLoadingHint,
|
|
4024
|
+
performancePreloadMissingAs,
|
|
4025
|
+
performanceFontPreloadCrossorigin,
|
|
4026
|
+
seoIndexability,
|
|
4027
|
+
seoTwitterCard,
|
|
4028
|
+
seoOgDescription,
|
|
4029
|
+
seoOgUrl,
|
|
4030
|
+
seoViewport,
|
|
4031
|
+
seoSitemapInRobots,
|
|
4032
|
+
seoJsonLdValidity,
|
|
4033
|
+
seoJsonLdDeprecatedType,
|
|
4034
|
+
seoJsonLdRelativeUrl,
|
|
4035
|
+
seoJsonLdDateFormat,
|
|
4036
|
+
seoJsonLdPlaceholder,
|
|
4037
|
+
seoJsonLdRequiredProps,
|
|
4038
|
+
seoTitleLength,
|
|
4039
|
+
seoDescriptionLength,
|
|
4040
|
+
seoCharset,
|
|
4041
|
+
seoImageAlt,
|
|
4042
|
+
seoHreflang,
|
|
4043
|
+
seoSingleH1,
|
|
4044
|
+
performanceLcpImage,
|
|
4045
|
+
performanceResponsiveImage,
|
|
4046
|
+
performanceRenderBlockingScript,
|
|
4047
|
+
performancePreconnect,
|
|
4048
|
+
seoDuplicateTitle,
|
|
4049
|
+
seoDuplicateDescription,
|
|
4050
|
+
seoHeadingLevelSkip,
|
|
4051
|
+
seoSsrDisabled,
|
|
4052
|
+
correctnessEachKey,
|
|
4053
|
+
correctnessEachIndexKey,
|
|
4054
|
+
correctnessEffectAsDerived,
|
|
4055
|
+
correctnessEffectAsOnMount,
|
|
4056
|
+
correctnessUnmutatedState,
|
|
4057
|
+
correctnessPropMutation,
|
|
4058
|
+
correctnessStalePropDerivation,
|
|
4059
|
+
correctnessOrphanEffect,
|
|
4060
|
+
correctnessOrphanLifecycle,
|
|
4061
|
+
correctnessServerBrowserGlobal,
|
|
4062
|
+
correctnessInstanceBrowserGlobal,
|
|
4063
|
+
securityRawHtml,
|
|
4064
|
+
securityJavascriptUrl,
|
|
4065
|
+
securityHandlerStateWrite,
|
|
4066
|
+
securityServerModuleState,
|
|
4067
|
+
securitySharedStateImport,
|
|
4068
|
+
architectureComponentSize,
|
|
4069
|
+
architecturePropCount,
|
|
4070
|
+
performanceHeavyImport,
|
|
4071
|
+
performanceNamespaceImport,
|
|
4072
|
+
performanceMinifyDisabled,
|
|
4073
|
+
performanceLoadWaterfall,
|
|
4074
|
+
performanceSequentialAwaits,
|
|
4075
|
+
performanceStateRaw
|
|
3340
4076
|
];
|
|
3341
4077
|
function explainRule(id) {
|
|
3342
|
-
const
|
|
3343
|
-
const rule = allRules.find((r) => r.id === target);
|
|
4078
|
+
const rule = allRules.find((r) => r.id === id);
|
|
3344
4079
|
if (!rule) return void 0;
|
|
3345
4080
|
return {
|
|
3346
4081
|
id: rule.id,
|
|
@@ -3547,7 +4282,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
3547
4282
|
const p = options.palette ?? noColorPalette;
|
|
3548
4283
|
const summary = summarize(results, config);
|
|
3549
4284
|
const { health, categories: byCat } = computeHealth(results, config);
|
|
3550
|
-
const
|
|
4285
|
+
const present3 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
3551
4286
|
const lines = [];
|
|
3552
4287
|
if (!options.omitHeader) {
|
|
3553
4288
|
lines.push(
|
|
@@ -3556,7 +4291,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
3556
4291
|
`${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
|
|
3557
4292
|
);
|
|
3558
4293
|
}
|
|
3559
|
-
for (const c of
|
|
4294
|
+
for (const c of present3) {
|
|
3560
4295
|
lines.push(scoreLine(p, CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
3561
4296
|
}
|
|
3562
4297
|
lines.push("");
|
|
@@ -3874,6 +4609,10 @@ function formatMarkdownReport(results, config, meta) {
|
|
|
3874
4609
|
lines.push("");
|
|
3875
4610
|
lines.push(`\u2026and ${findings.length - MAX_FINDINGS} more (run \`npx svelte-vitals\` locally for the full report)`);
|
|
3876
4611
|
}
|
|
4612
|
+
lines.push("");
|
|
4613
|
+
lines.push(
|
|
4614
|
+
"_Expected findings (e.g. routes behind auth)? See [Excluding routes or rules](https://oekazuma.github.io/svelte-vitals/guides/ci/#excluding-routes-or-rules)._"
|
|
4615
|
+
);
|
|
3877
4616
|
}
|
|
3878
4617
|
return lines.join("\n");
|
|
3879
4618
|
}
|
|
@@ -4575,6 +5314,38 @@ function applyRuleSeverities(results, config) {
|
|
|
4575
5314
|
return setting && setting !== "off" ? { ...result, severity: setting } : result;
|
|
4576
5315
|
});
|
|
4577
5316
|
}
|
|
5317
|
+
function routeGlobToRegExp(pattern) {
|
|
5318
|
+
const body = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").split("\0").join(".*");
|
|
5319
|
+
const source = body.endsWith("/.*") ? `${body.slice(0, -3)}(/.*)?` : body;
|
|
5320
|
+
return new RegExp(`^${source}$`);
|
|
5321
|
+
}
|
|
5322
|
+
function toPatterns(globs) {
|
|
5323
|
+
if (globs === void 0) return [];
|
|
5324
|
+
return (Array.isArray(globs) ? globs : [globs]).map(routeGlobToRegExp);
|
|
5325
|
+
}
|
|
5326
|
+
function applyOverrides(results, config) {
|
|
5327
|
+
const overrides = config.overrides;
|
|
5328
|
+
if (!overrides || overrides.length === 0) return results;
|
|
5329
|
+
const compiled = overrides.map((o) => ({
|
|
5330
|
+
routes: toPatterns(o.route),
|
|
5331
|
+
files: toPatterns(o.files),
|
|
5332
|
+
rules: o.rules
|
|
5333
|
+
}));
|
|
5334
|
+
const out = [];
|
|
5335
|
+
for (const result of results) {
|
|
5336
|
+
const { route, location } = result;
|
|
5337
|
+
let setting;
|
|
5338
|
+
for (const o of compiled) {
|
|
5339
|
+
const matched = route !== void 0 && o.routes.some((p) => p.test(route)) || location !== void 0 && o.files.some((p) => p.test(location));
|
|
5340
|
+
if (!matched) continue;
|
|
5341
|
+
const s = o.rules[result.id] ?? o.rules[result.category ?? "seo"];
|
|
5342
|
+
if (s !== void 0) setting = s;
|
|
5343
|
+
}
|
|
5344
|
+
if (setting === void 0) out.push(result);
|
|
5345
|
+
else if (setting !== "off") out.push({ ...result, severity: setting });
|
|
5346
|
+
}
|
|
5347
|
+
return out;
|
|
5348
|
+
}
|
|
4578
5349
|
export {
|
|
4579
5350
|
APP_SCRIPT,
|
|
4580
5351
|
APP_STYLE,
|
|
@@ -4583,9 +5354,10 @@ export {
|
|
|
4583
5354
|
ROBOTS_SOURCE_PATHS,
|
|
4584
5355
|
SITEMAP_SOURCE_PATHS,
|
|
4585
5356
|
allRules,
|
|
5357
|
+
applyOverrides,
|
|
4586
5358
|
applyRuleSeverities,
|
|
4587
|
-
|
|
4588
|
-
|
|
5359
|
+
architectureComponentSize,
|
|
5360
|
+
architecturePropCount,
|
|
4589
5361
|
attrText,
|
|
4590
5362
|
attrTextOf,
|
|
4591
5363
|
attrValue,
|
|
@@ -4597,15 +5369,17 @@ export {
|
|
|
4597
5369
|
collectKitModuleFacts,
|
|
4598
5370
|
computeHealth,
|
|
4599
5371
|
computeScore,
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
5372
|
+
correctnessEachIndexKey,
|
|
5373
|
+
correctnessEachKey,
|
|
5374
|
+
correctnessEffectAsDerived,
|
|
5375
|
+
correctnessEffectAsOnMount,
|
|
5376
|
+
correctnessInstanceBrowserGlobal,
|
|
5377
|
+
correctnessOrphanEffect,
|
|
5378
|
+
correctnessOrphanLifecycle,
|
|
5379
|
+
correctnessPropMutation,
|
|
5380
|
+
correctnessServerBrowserGlobal,
|
|
5381
|
+
correctnessStalePropDerivation,
|
|
5382
|
+
correctnessUnmutatedState,
|
|
4609
5383
|
defaultConfig,
|
|
4610
5384
|
defaultProject,
|
|
4611
5385
|
defineConfig,
|
|
@@ -4616,6 +5390,7 @@ export {
|
|
|
4616
5390
|
escapeHtml,
|
|
4617
5391
|
explainRule,
|
|
4618
5392
|
findAttr,
|
|
5393
|
+
findMinifyDisabled,
|
|
4619
5394
|
formatAgentReport,
|
|
4620
5395
|
formatConsoleReport,
|
|
4621
5396
|
formatGithubReport,
|
|
@@ -4632,16 +5407,20 @@ export {
|
|
|
4632
5407
|
noColorPalette,
|
|
4633
5408
|
parseComponentFacts,
|
|
4634
5409
|
parseKitModuleFacts,
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
5410
|
+
performanceFontPreloadCrossorigin,
|
|
5411
|
+
performanceHeavyImport,
|
|
5412
|
+
performanceImageDimensions,
|
|
5413
|
+
performanceImageLoadingHint,
|
|
5414
|
+
performanceLcpImage,
|
|
5415
|
+
performanceLoadWaterfall,
|
|
5416
|
+
performanceMinifyDisabled,
|
|
5417
|
+
performanceNamespaceImport,
|
|
5418
|
+
performancePreconnect,
|
|
5419
|
+
performancePreloadMissingAs,
|
|
5420
|
+
performanceRenderBlockingScript,
|
|
5421
|
+
performanceResponsiveImage,
|
|
5422
|
+
performanceSequentialAwaits,
|
|
5423
|
+
performanceStateRaw,
|
|
4645
5424
|
renderAppShell,
|
|
4646
5425
|
resolveRunesModuleSpecifier,
|
|
4647
5426
|
runRules,
|
|
@@ -4649,42 +5428,43 @@ export {
|
|
|
4649
5428
|
scoreBand,
|
|
4650
5429
|
scoreColor,
|
|
4651
5430
|
scoresByCategory,
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
5431
|
+
securityHandlerStateWrite,
|
|
5432
|
+
securityJavascriptUrl,
|
|
5433
|
+
securityRawHtml,
|
|
5434
|
+
securityServerModuleState,
|
|
5435
|
+
securitySharedStateImport,
|
|
4657
5436
|
selectRules,
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5437
|
+
seoCanonicalUrl,
|
|
5438
|
+
seoCharset,
|
|
5439
|
+
seoDescriptionLength,
|
|
5440
|
+
seoDescriptionPresence,
|
|
5441
|
+
seoDuplicateDescription,
|
|
5442
|
+
seoDuplicateTitle,
|
|
5443
|
+
seoHeadingLevelSkip,
|
|
5444
|
+
seoHreflang,
|
|
5445
|
+
seoHtmlLang,
|
|
5446
|
+
seoImageAlt,
|
|
5447
|
+
seoIndexability,
|
|
5448
|
+
seoJsonLd,
|
|
5449
|
+
seoJsonLdDateFormat,
|
|
5450
|
+
seoJsonLdDeprecatedType,
|
|
5451
|
+
seoJsonLdPlaceholder,
|
|
5452
|
+
seoJsonLdRelativeUrl,
|
|
5453
|
+
seoJsonLdRequiredProps,
|
|
5454
|
+
seoJsonLdValidity,
|
|
5455
|
+
seoOgDescription,
|
|
5456
|
+
seoOgImage,
|
|
5457
|
+
seoOgTitle,
|
|
5458
|
+
seoOgUrl,
|
|
5459
|
+
seoRobotsTxt,
|
|
5460
|
+
seoSingleH1,
|
|
5461
|
+
seoSitemapInRobots,
|
|
5462
|
+
seoSitemapXml,
|
|
5463
|
+
seoSsrDisabled,
|
|
5464
|
+
seoTitleLength,
|
|
5465
|
+
seoTitlePresence,
|
|
5466
|
+
seoTwitterCard,
|
|
5467
|
+
seoViewport,
|
|
4688
5468
|
summarize,
|
|
4689
5469
|
textFromNodes,
|
|
4690
5470
|
valueFromNodes
|