@svelte-vitals/core 0.27.0 → 0.29.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 +267 -124
- package/dist/index.js +1429 -544
- 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);
|
|
@@ -188,8 +245,10 @@ function scopeIntroducedNames(node) {
|
|
|
188
245
|
addBoundNames(node.param, introduced);
|
|
189
246
|
} else if (node.type === "BlockStatement") {
|
|
190
247
|
for (const stmt of node.body ?? []) {
|
|
191
|
-
if (stmt?.type === "VariableDeclaration"
|
|
248
|
+
if (stmt?.type === "VariableDeclaration") {
|
|
192
249
|
for (const d of stmt.declarations ?? []) addBoundNames(d.id, introduced);
|
|
250
|
+
} else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && typeof stmt.id?.name === "string") {
|
|
251
|
+
introduced.add(stmt.id.name);
|
|
193
252
|
}
|
|
194
253
|
}
|
|
195
254
|
} else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
|
|
@@ -199,6 +258,12 @@ function scopeIntroducedNames(node) {
|
|
|
199
258
|
}
|
|
200
259
|
} else if (node.type === "EachBlock" && node.context) {
|
|
201
260
|
addBoundNames(node.context, introduced);
|
|
261
|
+
if (typeof node.index === "string") introduced.add(node.index);
|
|
262
|
+
} else if (node.type === "SnippetBlock") {
|
|
263
|
+
for (const p of node.parameters ?? []) addBoundNames(p, introduced);
|
|
264
|
+
} else if (node.type === "AwaitBlock") {
|
|
265
|
+
if (node.value) addBoundNames(node.value, introduced);
|
|
266
|
+
if (node.error) addBoundNames(node.error, introduced);
|
|
202
267
|
}
|
|
203
268
|
return introduced;
|
|
204
269
|
}
|
|
@@ -216,43 +281,315 @@ function walkScoped(node, visit, shadowed = /* @__PURE__ */ new Set()) {
|
|
|
216
281
|
walkScoped(node[key], visit, scope);
|
|
217
282
|
}
|
|
218
283
|
}
|
|
219
|
-
function collectStateWrites(root, stateNames, acc) {
|
|
284
|
+
function collectStateWrites(root, stateNames, acc, kinds) {
|
|
285
|
+
const record = (name, kind) => {
|
|
286
|
+
acc.add(name);
|
|
287
|
+
if (kinds) {
|
|
288
|
+
let set = kinds.get(name);
|
|
289
|
+
if (!set) kinds.set(name, set = /* @__PURE__ */ new Set());
|
|
290
|
+
set.add(kind);
|
|
291
|
+
}
|
|
292
|
+
};
|
|
220
293
|
walkScoped(root, (n, scope) => {
|
|
221
294
|
const shadowed = (name) => name === void 0 || scope.has(name);
|
|
222
295
|
if (n?.type === "AssignmentExpression") {
|
|
223
296
|
if (n.left?.type === "Identifier" && stateNames.has(n.left.name) && !shadowed(n.left.name)) {
|
|
224
|
-
|
|
297
|
+
record(n.left.name, "reassign");
|
|
225
298
|
} else if (n.left?.type === "MemberExpression") {
|
|
226
299
|
const r = rootObjectName(n.left);
|
|
227
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
300
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
228
301
|
} else if (n.left?.type === "ObjectPattern" || n.left?.type === "ArrayPattern") {
|
|
229
302
|
const bound = /* @__PURE__ */ new Set();
|
|
230
303
|
addBoundNames(n.left, bound);
|
|
231
|
-
for (const name of bound) if (stateNames.has(name) && !shadowed(name))
|
|
304
|
+
for (const name of bound) if (stateNames.has(name) && !shadowed(name)) record(name, "reassign");
|
|
232
305
|
}
|
|
233
306
|
} else if (n?.type === "UpdateExpression") {
|
|
234
|
-
|
|
235
|
-
|
|
307
|
+
if (n.argument?.type === "Identifier") {
|
|
308
|
+
if (stateNames.has(n.argument.name) && !shadowed(n.argument.name)) record(n.argument.name, "reassign");
|
|
309
|
+
} else {
|
|
310
|
+
const r = rootObjectName(n.argument);
|
|
311
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
312
|
+
}
|
|
236
313
|
} else if (n?.type === "UnaryExpression" && n.operator === "delete") {
|
|
237
314
|
const r = rootObjectName(n.argument);
|
|
238
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
315
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
239
316
|
} else if (n?.type === "CallExpression") {
|
|
240
317
|
if (n.callee?.type === "MemberExpression") {
|
|
241
318
|
const r = rootObjectName(n.callee);
|
|
242
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
319
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "mutate");
|
|
243
320
|
}
|
|
244
321
|
for (const a of n.arguments ?? []) {
|
|
245
322
|
const arg = a?.type === "SpreadElement" ? a.argument : a;
|
|
246
323
|
const r = rootObjectName(arg);
|
|
247
|
-
if (r && stateNames.has(r) && !shadowed(r))
|
|
324
|
+
if (r && stateNames.has(r) && !shadowed(r)) record(r, "escape");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function isDeferredBody(n) {
|
|
330
|
+
return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
|
|
331
|
+
}
|
|
332
|
+
function isPlainStateCall(node) {
|
|
333
|
+
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$state";
|
|
334
|
+
}
|
|
335
|
+
var BUILTIN_STATE_TYPES = /* @__PURE__ */ new Set(["Map", "Set", "Date", "URL", "URLSearchParams"]);
|
|
336
|
+
var BUILTIN_MUTATIONS = {
|
|
337
|
+
Map: /* @__PURE__ */ new Set(["set", "delete", "clear"]),
|
|
338
|
+
Set: /* @__PURE__ */ new Set(["add", "delete", "clear"]),
|
|
339
|
+
Date: /* @__PURE__ */ new Set([
|
|
340
|
+
"setTime",
|
|
341
|
+
"setFullYear",
|
|
342
|
+
"setMonth",
|
|
343
|
+
"setDate",
|
|
344
|
+
"setHours",
|
|
345
|
+
"setMinutes",
|
|
346
|
+
"setSeconds",
|
|
347
|
+
"setMilliseconds",
|
|
348
|
+
"setYear",
|
|
349
|
+
"setUTCFullYear",
|
|
350
|
+
"setUTCMonth",
|
|
351
|
+
"setUTCDate",
|
|
352
|
+
"setUTCHours",
|
|
353
|
+
"setUTCMinutes",
|
|
354
|
+
"setUTCSeconds",
|
|
355
|
+
"setUTCMilliseconds"
|
|
356
|
+
]),
|
|
357
|
+
URL: /* @__PURE__ */ new Set(),
|
|
358
|
+
URLSearchParams: /* @__PURE__ */ new Set(["append", "set", "delete", "sort"])
|
|
359
|
+
};
|
|
360
|
+
function collectBuiltinStateSignals(node, candidates, mutated, reassigned, shadowed = /* @__PURE__ */ new Set(), inFunction = false) {
|
|
361
|
+
if (Array.isArray(node)) {
|
|
362
|
+
for (const child of node) collectBuiltinStateSignals(child, candidates, mutated, reassigned, shadowed, inFunction);
|
|
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
|
+
const boundary = isDeferredBody(node) || node.type === "ClassDeclaration" || node.type === "ClassExpression";
|
|
369
|
+
const nextInFunction = inFunction || boundary;
|
|
370
|
+
const hit = (name) => typeof name === "string" && candidates.has(name) && !scope.has(name) ? name : void 0;
|
|
371
|
+
if (node.type === "AssignmentExpression") {
|
|
372
|
+
if (node.left?.type === "Identifier") {
|
|
373
|
+
const n = hit(node.left.name);
|
|
374
|
+
const isBareSelfAssign = node.right?.type === "Identifier" && node.right.name === n;
|
|
375
|
+
if (n && !isBareSelfAssign) reassigned.add(n);
|
|
376
|
+
} else if (node.left?.type === "ObjectPattern" || node.left?.type === "ArrayPattern") {
|
|
377
|
+
const bound = /* @__PURE__ */ new Set();
|
|
378
|
+
addBoundNames(node.left, bound);
|
|
379
|
+
for (const name of bound) {
|
|
380
|
+
const n = hit(name);
|
|
381
|
+
if (n) reassigned.add(n);
|
|
382
|
+
}
|
|
383
|
+
} else if (node.left?.type === "MemberExpression" && inFunction) {
|
|
384
|
+
const n = hit(rootObjectName(node.left));
|
|
385
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
386
|
+
}
|
|
387
|
+
} else if (node.type === "UpdateExpression" && node.argument?.type === "MemberExpression" && inFunction) {
|
|
388
|
+
const n = hit(rootObjectName(node.argument));
|
|
389
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
390
|
+
} else if (node.type === "UnaryExpression" && node.operator === "delete" && inFunction) {
|
|
391
|
+
const n = hit(rootObjectName(node.argument));
|
|
392
|
+
if (n && candidates.get(n) === "URL") mutated.add(n);
|
|
393
|
+
} else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && inFunction) {
|
|
394
|
+
const method = node.callee.property?.name;
|
|
395
|
+
if (typeof method === "string") {
|
|
396
|
+
if (node.callee.object?.type === "Identifier") {
|
|
397
|
+
const n = hit(node.callee.object.name);
|
|
398
|
+
if (n && BUILTIN_MUTATIONS[candidates.get(n)]?.has(method)) mutated.add(n);
|
|
399
|
+
} else if (node.callee.object?.type === "MemberExpression") {
|
|
400
|
+
const n = hit(rootObjectName(node.callee));
|
|
401
|
+
if (n && candidates.get(n) === "URL" && BUILTIN_MUTATIONS.URLSearchParams.has(method)) mutated.add(n);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const key of Object.keys(node)) {
|
|
406
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
407
|
+
collectBuiltinStateSignals(node[key], candidates, mutated, reassigned, scope, nextInFunction);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function collectPatternAliasRefs(node, names, acc, scope, ownRhs) {
|
|
411
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
412
|
+
if (node.type === "Identifier") return;
|
|
413
|
+
if (node.type === "ObjectPattern") {
|
|
414
|
+
for (const prop of node.properties ?? []) {
|
|
415
|
+
if (prop?.type === "RestElement") {
|
|
416
|
+
collectPatternAliasRefs(prop.argument, names, acc, scope, ownRhs);
|
|
417
|
+
} else if (prop?.type === "Property") {
|
|
418
|
+
if (prop.computed) collectAliasRefs(prop.key, names, acc, scope, ownRhs);
|
|
419
|
+
collectPatternAliasRefs(prop.value, names, acc, scope, ownRhs);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (node.type === "ArrayPattern") {
|
|
425
|
+
for (const el of node.elements ?? []) collectPatternAliasRefs(el, names, acc, scope, ownRhs);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (node.type === "AssignmentPattern") {
|
|
429
|
+
collectPatternAliasRefs(node.left, names, acc, scope, ownRhs);
|
|
430
|
+
collectAliasRefs(node.right, names, acc, scope, ownRhs);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (node.type === "RestElement") {
|
|
434
|
+
collectPatternAliasRefs(node.argument, names, acc, scope, ownRhs);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function collectAliasRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set(), ownRhs = null) {
|
|
438
|
+
if (Array.isArray(node)) {
|
|
439
|
+
for (const child of node) collectAliasRefs(child, names, acc, shadowed, ownRhs);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
443
|
+
const introduced = scopeIntroducedNames(node);
|
|
444
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
445
|
+
if (node.type === "AssignmentExpression") {
|
|
446
|
+
const lhsIsCandidate = node.left?.type === "Identifier" && names.has(node.left.name) && !scope.has(node.left.name);
|
|
447
|
+
if (!lhsIsCandidate) collectAliasRefs(node.left, names, acc, scope, null);
|
|
448
|
+
collectAliasRefs(node.right, names, acc, scope, lhsIsCandidate ? node.left.name : null);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (node.type === "VariableDeclarator") {
|
|
452
|
+
collectPatternAliasRefs(node.id, names, acc, scope, ownRhs);
|
|
453
|
+
if (node.init) collectAliasRefs(node.init, names, acc, scope, ownRhs);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name) && node.name !== ownRhs) {
|
|
457
|
+
acc.add(node.name);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
for (const key of Object.keys(node)) {
|
|
461
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
462
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
463
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
464
|
+
collectAliasRefs(node[key], names, acc, scope, ownRhs);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function collectFragmentAliasRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
468
|
+
if (Array.isArray(node)) {
|
|
469
|
+
for (const child of node) collectFragmentAliasRefs(child, names, acc, shadowed);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
473
|
+
if (isDeferredBody(node)) {
|
|
474
|
+
const introduced2 = scopeIntroducedNames(node);
|
|
475
|
+
const scope2 = introduced2.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced2]) : shadowed;
|
|
476
|
+
collectAliasRefs(node.body, names, acc, scope2, null);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const introduced = scopeIntroducedNames(node);
|
|
480
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
481
|
+
if (Array.isArray(node.attributes)) collectFragmentAliasRefs(node.attributes, names, acc, scope);
|
|
482
|
+
for (const key of Object.keys(node)) {
|
|
483
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "attributes") continue;
|
|
484
|
+
collectFragmentAliasRefs(node[key], names, acc, scope);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function collectEachContextTaint(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
488
|
+
if (Array.isArray(node)) {
|
|
489
|
+
for (const child of node) collectEachContextTaint(child, names, acc, shadowed);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
493
|
+
const introduced = scopeIntroducedNames(node);
|
|
494
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
495
|
+
if (node.type === "EachBlock") {
|
|
496
|
+
const expr = unwrapTs(node.expression);
|
|
497
|
+
const target = expr?.type === "Identifier" ? expr.name : expr?.type === "MemberExpression" ? rootObjectName(expr) : void 0;
|
|
498
|
+
if (target !== void 0 && names.has(target) && !shadowed.has(target)) {
|
|
499
|
+
const ctxNames = /* @__PURE__ */ new Set();
|
|
500
|
+
addBoundNames(node.context, ctxNames);
|
|
501
|
+
if (typeof node.index === "string") ctxNames.add(node.index);
|
|
502
|
+
if (ctxNames.size > 0) {
|
|
503
|
+
const union = /* @__PURE__ */ new Set();
|
|
504
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
505
|
+
collectStateWrites(node.body, ctxNames, union, kinds);
|
|
506
|
+
collectTemplateEscapes(node.body, ctxNames, union, kinds);
|
|
507
|
+
const dirty = [...union].some((n) => {
|
|
508
|
+
const k = kinds.get(n);
|
|
509
|
+
return !k || [...k].some((kind) => kind !== "reassign");
|
|
510
|
+
});
|
|
511
|
+
if (dirty) acc.add(target);
|
|
248
512
|
}
|
|
249
513
|
}
|
|
514
|
+
}
|
|
515
|
+
for (const key of Object.keys(node)) {
|
|
516
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
517
|
+
collectEachContextTaint(node[key], names, acc, scope);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function refsNamesEagerly(node, names, shadowed = /* @__PURE__ */ new Set()) {
|
|
521
|
+
if (Array.isArray(node)) return node.some((c) => refsNamesEagerly(c, names, shadowed));
|
|
522
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return false;
|
|
523
|
+
if (isDeferredBody(node)) return false;
|
|
524
|
+
const introduced = scopeIntroducedNames(node);
|
|
525
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
526
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name)) return true;
|
|
527
|
+
for (const key of Object.keys(node)) {
|
|
528
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
529
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
530
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
531
|
+
if (refsNamesEagerly(node[key], names, scope)) return true;
|
|
532
|
+
}
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
function containsCallLike(node) {
|
|
536
|
+
let found = false;
|
|
537
|
+
walkEstree(node, (n) => {
|
|
538
|
+
if (n?.type === "CallExpression" || n?.type === "NewExpression" || n?.type === "AwaitExpression") found = true;
|
|
250
539
|
});
|
|
540
|
+
return found;
|
|
541
|
+
}
|
|
542
|
+
function collectFragmentRefs(node, names, acc, shadowed = /* @__PURE__ */ new Set()) {
|
|
543
|
+
if (Array.isArray(node)) {
|
|
544
|
+
for (const c of node) collectFragmentRefs(c, names, acc, shadowed);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
548
|
+
if (isDeferredBody(node)) return;
|
|
549
|
+
const introduced = scopeIntroducedNames(node);
|
|
550
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
551
|
+
if (node.type === "Identifier" && names.has(node.name) && !scope.has(node.name)) acc.add(node.name);
|
|
552
|
+
if (node.type === "EachBlock" || node.type === "AwaitBlock") {
|
|
553
|
+
collectFragmentRefs(node.expression, names, acc, shadowed);
|
|
554
|
+
for (const key of Object.keys(node)) {
|
|
555
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "expression") continue;
|
|
556
|
+
collectFragmentRefs(node[key], names, acc, scope);
|
|
557
|
+
}
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (Array.isArray(node.attributes)) collectFragmentRefs(node.attributes, names, acc, scope);
|
|
561
|
+
for (const key of Object.keys(node)) {
|
|
562
|
+
if (WALK_IGNORED_KEYS.has(key) || key === "attributes") continue;
|
|
563
|
+
if (node.type === "MemberExpression" && key === "property" && !node.computed) continue;
|
|
564
|
+
if (node.type === "Property" && key === "key" && !node.computed) continue;
|
|
565
|
+
collectFragmentRefs(node[key], names, acc, scope);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function collectStalePropCandidates(program, propNames, source) {
|
|
569
|
+
const out = [];
|
|
570
|
+
for (const stmt of program.body ?? []) {
|
|
571
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
572
|
+
for (const d of stmt.declarations ?? []) {
|
|
573
|
+
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
574
|
+
if (containsCallLike(d.init)) continue;
|
|
575
|
+
if (!refsNamesEagerly(d.init, propNames)) continue;
|
|
576
|
+
out.push({ name: d.id.name, line: lineOf(source, d.start) });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return out;
|
|
251
580
|
}
|
|
252
581
|
var COMPONENT_LIKE_TYPES = /* @__PURE__ */ new Set(["Component", "SvelteComponent", "SvelteSelf"]);
|
|
253
|
-
function collectTemplateEscapes(node, stateNames, acc) {
|
|
582
|
+
function collectTemplateEscapes(node, stateNames, acc, kinds) {
|
|
583
|
+
const record = (name) => {
|
|
584
|
+
acc.add(name);
|
|
585
|
+
if (kinds) {
|
|
586
|
+
let set = kinds.get(name);
|
|
587
|
+
if (!set) kinds.set(name, set = /* @__PURE__ */ new Set());
|
|
588
|
+
set.add("escape");
|
|
589
|
+
}
|
|
590
|
+
};
|
|
254
591
|
if (Array.isArray(node)) {
|
|
255
|
-
for (const c of node) collectTemplateEscapes(c, stateNames, acc);
|
|
592
|
+
for (const c of node) collectTemplateEscapes(c, stateNames, acc, kinds);
|
|
256
593
|
return;
|
|
257
594
|
}
|
|
258
595
|
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
@@ -260,16 +597,36 @@ function collectTemplateEscapes(node, stateNames, acc) {
|
|
|
260
597
|
for (const attr of node.attributes) {
|
|
261
598
|
if (attr?.type === "BindDirective") {
|
|
262
599
|
const r = rootObjectName(attr.expression);
|
|
263
|
-
if (r && stateNames.has(r))
|
|
600
|
+
if (r && stateNames.has(r)) record(r);
|
|
264
601
|
} else if (COMPONENT_LIKE_TYPES.has(node.type)) {
|
|
265
602
|
walkEstree(attr, (m) => {
|
|
266
|
-
if (m?.type === "Identifier" && stateNames.has(m.name))
|
|
603
|
+
if (m?.type === "Identifier" && stateNames.has(m.name)) record(m.name);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
for (const key of CHILD_NODE_KEYS) {
|
|
609
|
+
if (key in node) collectTemplateEscapes(node[key], stateNames, acc, kinds);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
var DIRECTIVE_ESCAPE_TYPES = /* @__PURE__ */ new Set(["UseDirective", "TransitionDirective", "AnimateDirective"]);
|
|
613
|
+
function collectDirectiveEscapes(node, names, acc) {
|
|
614
|
+
if (Array.isArray(node)) {
|
|
615
|
+
for (const c of node) collectDirectiveEscapes(c, names, acc);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
619
|
+
if (Array.isArray(node.attributes)) {
|
|
620
|
+
for (const attr of node.attributes) {
|
|
621
|
+
if (DIRECTIVE_ESCAPE_TYPES.has(attr?.type) && attr.expression) {
|
|
622
|
+
walkEstree(attr.expression, (m) => {
|
|
623
|
+
if (m?.type === "Identifier" && names.has(m.name)) acc.add(m.name);
|
|
267
624
|
});
|
|
268
625
|
}
|
|
269
626
|
}
|
|
270
627
|
}
|
|
271
628
|
for (const key of CHILD_NODE_KEYS) {
|
|
272
|
-
if (key in node)
|
|
629
|
+
if (key in node) collectDirectiveEscapes(node[key], names, acc);
|
|
273
630
|
}
|
|
274
631
|
}
|
|
275
632
|
var RUNE_NAMES = /* @__PURE__ */ new Set(["$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host"]);
|
|
@@ -341,7 +698,7 @@ function isPropsCall(node) {
|
|
|
341
698
|
function isBindableCall(node) {
|
|
342
699
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$bindable";
|
|
343
700
|
}
|
|
344
|
-
function
|
|
701
|
+
function collectPropNames(program, includeBindable) {
|
|
345
702
|
const names = /* @__PURE__ */ new Set();
|
|
346
703
|
let seen = 0;
|
|
347
704
|
let ambiguous = false;
|
|
@@ -361,7 +718,8 @@ function collectNonBindableProps(program) {
|
|
|
361
718
|
addBoundNames(p.argument, names);
|
|
362
719
|
} else if (p?.type === "Property") {
|
|
363
720
|
if (p.value?.type === "AssignmentPattern") {
|
|
364
|
-
if (!isBindableCall(p.value.right) && p.value.left?.type === "Identifier")
|
|
721
|
+
if ((includeBindable || !isBindableCall(p.value.right)) && p.value.left?.type === "Identifier")
|
|
722
|
+
names.add(p.value.left.name);
|
|
365
723
|
} else if (p.value?.type === "Identifier") {
|
|
366
724
|
names.add(p.value.name);
|
|
367
725
|
}
|
|
@@ -370,6 +728,16 @@ function collectNonBindableProps(program) {
|
|
|
370
728
|
});
|
|
371
729
|
return ambiguous || seen > 1 ? /* @__PURE__ */ new Set() : names;
|
|
372
730
|
}
|
|
731
|
+
function collectLegacyPropNames(program) {
|
|
732
|
+
const names = /* @__PURE__ */ new Set();
|
|
733
|
+
for (const stmt of program.body ?? []) {
|
|
734
|
+
if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
|
|
735
|
+
for (const d of stmt.declaration.declarations ?? []) {
|
|
736
|
+
if (d?.id?.type === "Identifier") names.add(d.id.name);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return names;
|
|
740
|
+
}
|
|
373
741
|
var MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
374
742
|
"push",
|
|
375
743
|
"pop",
|
|
@@ -443,15 +811,20 @@ function collectNamespaceImports(program, source, acc) {
|
|
|
443
811
|
}
|
|
444
812
|
});
|
|
445
813
|
}
|
|
446
|
-
var
|
|
447
|
-
var
|
|
814
|
+
var RULE_ID_RE = "[a-z]+\\/[a-z][a-z0-9-]*";
|
|
815
|
+
var JS_DIRECTIVE = new RegExp(
|
|
816
|
+
`^\\s*//\\s*svelte-vitals-disable-next-line(?:\\s+(${RULE_ID_RE}(?:\\s*,\\s*${RULE_ID_RE})*))?\\s*$`
|
|
817
|
+
);
|
|
818
|
+
var HTML_DIRECTIVE = new RegExp(
|
|
819
|
+
`^\\s*<!--\\s*svelte-vitals-disable-next-line(?:\\s+(${RULE_ID_RE}(?:\\s*,\\s*${RULE_ID_RE})*))?\\s*-->\\s*$`
|
|
820
|
+
);
|
|
448
821
|
function collectSuppressions(source) {
|
|
449
822
|
const out = [];
|
|
450
823
|
const lines = source.split("\n");
|
|
451
824
|
lines.forEach((line, i) => {
|
|
452
825
|
const m = JS_DIRECTIVE.exec(line) ?? HTML_DIRECTIVE.exec(line);
|
|
453
826
|
if (!m) return;
|
|
454
|
-
const ruleIds = m[1]?.split(",").map((s) => s.trim()
|
|
827
|
+
const ruleIds = m[1]?.split(",").map((s) => s.trim());
|
|
455
828
|
out.push({ line: i + 2, ruleIds });
|
|
456
829
|
});
|
|
457
830
|
return out;
|
|
@@ -807,6 +1180,9 @@ function parseModuleFacts(source, filename) {
|
|
|
807
1180
|
namespaceImports: [],
|
|
808
1181
|
constableStates: [],
|
|
809
1182
|
mutatedProps: [],
|
|
1183
|
+
stalePropDerivations: [],
|
|
1184
|
+
rawableStates: [],
|
|
1185
|
+
nonreactiveBuiltinStates: [],
|
|
810
1186
|
suppressions: collectSuppressions(source),
|
|
811
1187
|
orphanEffects,
|
|
812
1188
|
orphanLifecycleCalls,
|
|
@@ -842,15 +1218,42 @@ function parseComponentFacts(source, filename) {
|
|
|
842
1218
|
const effects = [];
|
|
843
1219
|
const constableStates = [];
|
|
844
1220
|
const mutatedProps = [];
|
|
1221
|
+
const stalePropDerivations = [];
|
|
1222
|
+
const rawableStates = [];
|
|
1223
|
+
const nonreactiveBuiltinStates = [];
|
|
845
1224
|
let propCount = 0;
|
|
846
1225
|
const program = ast.instance?.content;
|
|
847
1226
|
if (program) {
|
|
848
1227
|
collectImportSources(program, source, importSpans);
|
|
849
1228
|
collectNamespaceImports(program, source, namespaceImports);
|
|
850
1229
|
propCount = countProps(program);
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
1230
|
+
const legacyPropNames = collectLegacyPropNames(program);
|
|
1231
|
+
const nonBindableProps = /* @__PURE__ */ new Set([...collectPropNames(program, false), ...legacyPropNames]);
|
|
1232
|
+
const rawMutations = [];
|
|
1233
|
+
collectPropMutations(program, nonBindableProps, source, rawMutations);
|
|
1234
|
+
if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, rawMutations);
|
|
1235
|
+
for (const m of rawMutations) mutatedProps.push(legacyPropNames.has(m.name) ? { ...m, legacy: true } : m);
|
|
1236
|
+
const allPropNames = /* @__PURE__ */ new Set([...collectPropNames(program, true), ...legacyPropNames]);
|
|
1237
|
+
if (allPropNames.size > 0) {
|
|
1238
|
+
const candidates = collectStalePropCandidates(program, allPropNames, source);
|
|
1239
|
+
if (candidates.length > 0) {
|
|
1240
|
+
const candidateNames = new Set(candidates.map((c) => c.name));
|
|
1241
|
+
const disqualified = /* @__PURE__ */ new Set();
|
|
1242
|
+
collectStateWrites(program, candidateNames, disqualified);
|
|
1243
|
+
if (ast.fragment) {
|
|
1244
|
+
collectStateWrites(ast.fragment, candidateNames, disqualified);
|
|
1245
|
+
collectTemplateEscapes(ast.fragment, candidateNames, disqualified);
|
|
1246
|
+
}
|
|
1247
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
1248
|
+
if (ast.fragment) collectFragmentRefs(ast.fragment, candidateNames, referenced);
|
|
1249
|
+
const isLegacy = legacyPropNames.size > 0;
|
|
1250
|
+
for (const c of candidates) {
|
|
1251
|
+
if (!disqualified.has(c.name) && referenced.has(c.name)) {
|
|
1252
|
+
stalePropDerivations.push(isLegacy ? { ...c, legacy: true } : c);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
854
1257
|
const stateNames = /* @__PURE__ */ new Set();
|
|
855
1258
|
const reactiveNames = /* @__PURE__ */ new Set();
|
|
856
1259
|
const stateDecls = [];
|
|
@@ -878,10 +1281,69 @@ function parseComponentFacts(source, filename) {
|
|
|
878
1281
|
if (ast.fragment) {
|
|
879
1282
|
collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
|
|
880
1283
|
collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
1284
|
+
collectDirectiveEscapes(ast.fragment, stateNames, writtenOrEscaped);
|
|
881
1285
|
}
|
|
882
1286
|
for (const d of stateDecls) {
|
|
883
1287
|
if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
|
|
884
1288
|
}
|
|
1289
|
+
const rawableCandidates = [];
|
|
1290
|
+
for (const stmt of program.body ?? []) {
|
|
1291
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1292
|
+
for (const d of stmt.declarations ?? []) {
|
|
1293
|
+
if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
|
|
1294
|
+
const arg = unwrapTs(d.init.arguments?.[0]);
|
|
1295
|
+
if (arg?.type === "ObjectExpression" || arg?.type === "ArrayExpression") {
|
|
1296
|
+
rawableCandidates.push({ name: d.id.name, line: lineOf(source, d.start) });
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (rawableCandidates.length > 0) {
|
|
1301
|
+
const candNames = new Set(rawableCandidates.map((c) => c.name));
|
|
1302
|
+
const union = /* @__PURE__ */ new Set();
|
|
1303
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
1304
|
+
collectStateWrites(program, candNames, union, kinds);
|
|
1305
|
+
if (ast.fragment) {
|
|
1306
|
+
collectStateWrites(ast.fragment, candNames, union, kinds);
|
|
1307
|
+
collectTemplateEscapes(ast.fragment, candNames, union, kinds);
|
|
1308
|
+
}
|
|
1309
|
+
const aliasEscapes = /* @__PURE__ */ new Set();
|
|
1310
|
+
collectAliasRefs(program, candNames, aliasEscapes);
|
|
1311
|
+
const eachTaint = /* @__PURE__ */ new Set();
|
|
1312
|
+
if (ast.fragment) {
|
|
1313
|
+
collectFragmentAliasRefs(ast.fragment, candNames, aliasEscapes);
|
|
1314
|
+
collectDirectiveEscapes(ast.fragment, candNames, aliasEscapes);
|
|
1315
|
+
collectEachContextTaint(ast.fragment, candNames, eachTaint);
|
|
1316
|
+
}
|
|
1317
|
+
for (const c of rawableCandidates) {
|
|
1318
|
+
const k = kinds.get(c.name);
|
|
1319
|
+
const reassigned = k?.has("reassign") ?? false;
|
|
1320
|
+
const dirty = k !== void 0 && [...k].some((kind) => kind !== "reassign") || aliasEscapes.has(c.name) || eachTaint.has(c.name);
|
|
1321
|
+
if (reassigned && !dirty) rawableStates.push(c);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const builtinCandidates = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const stmt of program.body ?? []) {
|
|
1326
|
+
if (stmt?.type !== "VariableDeclaration") continue;
|
|
1327
|
+
for (const d of stmt.declarations ?? []) {
|
|
1328
|
+
if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
|
|
1329
|
+
const arg = unwrapTs(d.init.arguments?.[0]);
|
|
1330
|
+
if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
|
|
1331
|
+
builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
if (builtinCandidates.size > 0) {
|
|
1336
|
+
const types = new Map([...builtinCandidates].map(([n, meta]) => [n, meta.type]));
|
|
1337
|
+
const mutatedBuiltins = /* @__PURE__ */ new Set();
|
|
1338
|
+
const reassignedBuiltins = /* @__PURE__ */ new Set();
|
|
1339
|
+
collectBuiltinStateSignals(program, types, mutatedBuiltins, reassignedBuiltins);
|
|
1340
|
+
if (ast.fragment) collectBuiltinStateSignals(ast.fragment, types, mutatedBuiltins, reassignedBuiltins);
|
|
1341
|
+
for (const [name, meta] of builtinCandidates) {
|
|
1342
|
+
if (mutatedBuiltins.has(name) && !reassignedBuiltins.has(name)) {
|
|
1343
|
+
nonreactiveBuiltinStates.push({ name, type: meta.type, line: meta.line });
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
885
1347
|
let moduleExtra;
|
|
886
1348
|
if (moduleProgram) {
|
|
887
1349
|
const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
|
|
@@ -907,6 +1369,9 @@ function parseComponentFacts(source, filename) {
|
|
|
907
1369
|
namespaceImports,
|
|
908
1370
|
constableStates,
|
|
909
1371
|
mutatedProps,
|
|
1372
|
+
stalePropDerivations,
|
|
1373
|
+
rawableStates,
|
|
1374
|
+
nonreactiveBuiltinStates,
|
|
910
1375
|
orphanEffects,
|
|
911
1376
|
orphanLifecycleCalls,
|
|
912
1377
|
browserGlobalRefs,
|
|
@@ -930,6 +1395,9 @@ function emptyComponentFacts(file) {
|
|
|
930
1395
|
namespaceImports: [],
|
|
931
1396
|
constableStates: [],
|
|
932
1397
|
mutatedProps: [],
|
|
1398
|
+
stalePropDerivations: [],
|
|
1399
|
+
rawableStates: [],
|
|
1400
|
+
nonreactiveBuiltinStates: [],
|
|
933
1401
|
orphanEffects: [],
|
|
934
1402
|
orphanLifecycleCalls: [],
|
|
935
1403
|
browserGlobalRefs: [],
|
|
@@ -966,11 +1434,6 @@ var HANDLER_NAMES = /* @__PURE__ */ new Set([
|
|
|
966
1434
|
"OPTIONS",
|
|
967
1435
|
"fallback"
|
|
968
1436
|
]);
|
|
969
|
-
function unwrapTs(expr) {
|
|
970
|
-
let cur = expr;
|
|
971
|
-
while (cur?.type === "TSSatisfiesExpression" || cur?.type === "TSAsExpression") cur = cur.expression;
|
|
972
|
-
return cur;
|
|
973
|
-
}
|
|
974
1437
|
function isFunctionNode(n) {
|
|
975
1438
|
return n?.type === "FunctionDeclaration" || n?.type === "FunctionExpression" || n?.type === "ArrowFunctionExpression";
|
|
976
1439
|
}
|
|
@@ -995,102 +1458,201 @@ function addActionsMembers(obj, handlers) {
|
|
|
995
1458
|
if (isFunctionNode(v)) handlers.add(v);
|
|
996
1459
|
}
|
|
997
1460
|
}
|
|
998
|
-
function
|
|
1461
|
+
function forEachNamedExport(program, visit) {
|
|
999
1462
|
for (const stmt of program.body ?? []) {
|
|
1000
|
-
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.
|
|
1463
|
+
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
|
|
1464
|
+
const decl = stmt.declaration;
|
|
1465
|
+
if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier") {
|
|
1466
|
+
if (visit(decl.id.name, decl, decl)) return;
|
|
1001
1467
|
continue;
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
if (
|
|
1007
|
-
handlers.add(resolved);
|
|
1008
|
-
} else if (exportedName === "actions" && resolved?.type === "ObjectExpression") {
|
|
1009
|
-
addActionsMembers(resolved, handlers);
|
|
1010
|
-
}
|
|
1468
|
+
}
|
|
1469
|
+
if (decl.type !== "VariableDeclaration") continue;
|
|
1470
|
+
for (const d of decl.declarations ?? []) {
|
|
1471
|
+
if (d?.id?.type !== "Identifier" || !d.init) continue;
|
|
1472
|
+
if (visit(d.id.name, unwrapTs(d.init), d)) return;
|
|
1011
1473
|
}
|
|
1012
1474
|
}
|
|
1013
|
-
|
|
1014
|
-
function resolveAliasStartupExports(program, bindings, startup) {
|
|
1475
|
+
let bindings;
|
|
1015
1476
|
for (const stmt of program.body ?? []) {
|
|
1016
1477
|
if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
|
|
1017
1478
|
continue;
|
|
1018
1479
|
for (const s of stmt.specifiers) {
|
|
1019
1480
|
if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
|
|
1020
|
-
|
|
1481
|
+
bindings ??= collectTopLevelBindings(program);
|
|
1021
1482
|
const resolved = bindings.get(s.local.name);
|
|
1022
|
-
if (
|
|
1483
|
+
if (resolved === void 0) continue;
|
|
1484
|
+
if (visit(s.exported.name, resolved, resolved)) return;
|
|
1023
1485
|
}
|
|
1024
1486
|
}
|
|
1025
1487
|
}
|
|
1026
1488
|
function collectHandlerFunctions(program) {
|
|
1027
1489
|
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);
|
|
1490
|
+
forEachNamedExport(program, (name, value) => {
|
|
1491
|
+
if (HANDLER_NAMES.has(name) && isFunctionNode(value)) handlers.add(value);
|
|
1492
|
+
else if (name === "actions" && value?.type === "ObjectExpression") addActionsMembers(value, handlers);
|
|
1493
|
+
return void 0;
|
|
1494
|
+
});
|
|
1047
1495
|
return handlers;
|
|
1048
1496
|
}
|
|
1049
1497
|
function collectStartupFunctions(program) {
|
|
1050
1498
|
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);
|
|
1499
|
+
forEachNamedExport(program, (name, value) => {
|
|
1500
|
+
if (name === "init" && isFunctionNode(value)) startup.add(value);
|
|
1501
|
+
return void 0;
|
|
1502
|
+
});
|
|
1066
1503
|
return startup;
|
|
1067
1504
|
}
|
|
1068
|
-
function
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1505
|
+
function findFalseOptOut(program, source, name) {
|
|
1506
|
+
let hit;
|
|
1507
|
+
forEachNamedExport(program, (exported, value, anchor) => {
|
|
1508
|
+
if (exported !== name || value?.type !== "Literal" || value.value !== false) return void 0;
|
|
1509
|
+
hit = { line: lineOf(source, anchor.start) };
|
|
1510
|
+
return true;
|
|
1511
|
+
});
|
|
1512
|
+
return hit;
|
|
1513
|
+
}
|
|
1514
|
+
function findLoadFunction(program) {
|
|
1515
|
+
let load;
|
|
1516
|
+
forEachNamedExport(program, (name, value) => {
|
|
1517
|
+
if (name !== "load" || !isFunctionNode(value)) return void 0;
|
|
1518
|
+
load = value;
|
|
1519
|
+
return true;
|
|
1520
|
+
});
|
|
1521
|
+
return load;
|
|
1522
|
+
}
|
|
1523
|
+
function collectAwaits(node, out = []) {
|
|
1524
|
+
if (Array.isArray(node)) {
|
|
1525
|
+
for (const child of node) collectAwaits(child, out);
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return out;
|
|
1529
|
+
if (isFunctionNode(node)) return out;
|
|
1530
|
+
if (node.type === "AwaitExpression") out.push(node);
|
|
1531
|
+
for (const key of Object.keys(node)) {
|
|
1532
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1533
|
+
collectAwaits(node[key], out);
|
|
1534
|
+
}
|
|
1535
|
+
return out;
|
|
1536
|
+
}
|
|
1537
|
+
function isParentCall(arg) {
|
|
1538
|
+
const e = unwrapTs(arg);
|
|
1539
|
+
if (e?.type !== "CallExpression") return false;
|
|
1540
|
+
const callee = e.callee;
|
|
1541
|
+
if (callee?.type === "Identifier" && callee.name === "parent") return true;
|
|
1542
|
+
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && callee.property.name === "parent";
|
|
1543
|
+
}
|
|
1544
|
+
var BODY_METHODS = /* @__PURE__ */ new Set(["json", "text", "blob", "arrayBuffer", "formData", "bytes"]);
|
|
1545
|
+
function isBodyParseCall(arg) {
|
|
1546
|
+
const e = unwrapTs(arg);
|
|
1547
|
+
if (e?.type !== "CallExpression" || e.arguments?.length) return false;
|
|
1548
|
+
const callee = e.callee;
|
|
1549
|
+
return callee?.type === "MemberExpression" && !callee.computed && callee.property.type === "Identifier" && BODY_METHODS.has(callee.property.name);
|
|
1550
|
+
}
|
|
1551
|
+
function refsTainted(node, tainted) {
|
|
1552
|
+
let hit = false;
|
|
1553
|
+
const walk = (n, shadowed) => {
|
|
1554
|
+
if (hit) return;
|
|
1555
|
+
if (Array.isArray(n)) {
|
|
1556
|
+
for (const child of n) walk(child, shadowed);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (!n || typeof n !== "object" || typeof n.type !== "string") return;
|
|
1560
|
+
const introduced = scopeIntroducedNames(n);
|
|
1561
|
+
const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
|
|
1562
|
+
if (n.type === "Identifier" && tainted.has(n.name) && !scope.has(n.name)) {
|
|
1563
|
+
hit = true;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
for (const key of Object.keys(n)) {
|
|
1567
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1568
|
+
if (n.type === "MemberExpression" && key === "property" && !n.computed) continue;
|
|
1569
|
+
if (n.type === "Property" && key === "key" && !n.computed) continue;
|
|
1570
|
+
walk(n[key], scope);
|
|
1571
|
+
}
|
|
1072
1572
|
};
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1573
|
+
walk(node, /* @__PURE__ */ new Set());
|
|
1574
|
+
return hit;
|
|
1575
|
+
}
|
|
1576
|
+
function collectLoadWaterfalls(program, wrapped) {
|
|
1577
|
+
const dependentLines = [];
|
|
1578
|
+
const independentLines = [];
|
|
1579
|
+
const load = findLoadFunction(program);
|
|
1580
|
+
if (!load?.body || load.body.type !== "BlockStatement") return { dependentLines, independentLines };
|
|
1581
|
+
const line = (start) => Math.max(0, lineOf(wrapped, start) - 1);
|
|
1582
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
1583
|
+
let sawAwaitSite = false;
|
|
1584
|
+
const taintAssignTarget = (left) => {
|
|
1585
|
+
if (left?.type === "MemberExpression") {
|
|
1586
|
+
const root = rootObjectName(left);
|
|
1587
|
+
if (root) tainted.add(root);
|
|
1588
|
+
} else {
|
|
1589
|
+
addBoundNames(left, tainted);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
const taintOnly = (node) => {
|
|
1593
|
+
if (Array.isArray(node)) {
|
|
1594
|
+
for (const child of node) taintOnly(child);
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (!node || typeof node !== "object" || typeof node.type !== "string") return;
|
|
1598
|
+
if (isFunctionNode(node)) return;
|
|
1599
|
+
if (node.type === "AssignmentExpression") {
|
|
1600
|
+
if (collectAwaits(node.right).length > 0 || refsTainted(node.right, tainted)) taintAssignTarget(node.left);
|
|
1601
|
+
} else if (node.type === "VariableDeclaration") {
|
|
1602
|
+
for (const d of node.declarations ?? []) {
|
|
1603
|
+
if (d?.id && d.init && (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted))) {
|
|
1604
|
+
addBoundNames(d.id, tainted);
|
|
1605
|
+
}
|
|
1079
1606
|
}
|
|
1080
1607
|
}
|
|
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;
|
|
1608
|
+
for (const key of Object.keys(node)) {
|
|
1609
|
+
if (WALK_IGNORED_KEYS.has(key)) continue;
|
|
1610
|
+
taintOnly(node[key]);
|
|
1091
1611
|
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1612
|
+
};
|
|
1613
|
+
const processStatements = (body) => {
|
|
1614
|
+
for (const stmt of body ?? []) {
|
|
1615
|
+
if (!stmt) continue;
|
|
1616
|
+
if (stmt.type === "TryStatement") {
|
|
1617
|
+
if (stmt.block?.type === "BlockStatement") processStatements(stmt.block.body);
|
|
1618
|
+
if (stmt.handler) taintOnly(stmt.handler);
|
|
1619
|
+
if (stmt.finalizer) taintOnly(stmt.finalizer);
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
if (stmt.type === "VariableDeclaration" || stmt.type === "ExpressionStatement" || stmt.type === "ReturnStatement") {
|
|
1623
|
+
const sites = collectAwaits(stmt).filter((a) => !isParentCall(a.argument) && !isBodyParseCall(a.argument));
|
|
1624
|
+
if (sites.length > 0) {
|
|
1625
|
+
const dependent = sites.filter((a) => refsTainted(a.argument, tainted));
|
|
1626
|
+
if (dependent.length > 0) {
|
|
1627
|
+
const anchor = dependent.reduce((m, a) => a.start < m.start ? a : m);
|
|
1628
|
+
dependentLines.push(line(anchor.start));
|
|
1629
|
+
} else if (sawAwaitSite) {
|
|
1630
|
+
const workSites = sites.filter((a) => unwrapTs(a.argument)?.type !== "Identifier");
|
|
1631
|
+
if (workSites.length > 0) {
|
|
1632
|
+
const anchor = workSites.reduce((m, a) => a.start < m.start ? a : m);
|
|
1633
|
+
independentLines.push(line(anchor.start));
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
sawAwaitSite = true;
|
|
1637
|
+
}
|
|
1638
|
+
if (stmt.type === "VariableDeclaration") {
|
|
1639
|
+
for (const d of stmt.declarations ?? []) {
|
|
1640
|
+
if (!d?.id || !d.init) continue;
|
|
1641
|
+
if (collectAwaits(d.init).length > 0 || refsTainted(d.init, tainted)) addBoundNames(d.id, tainted);
|
|
1642
|
+
}
|
|
1643
|
+
} else if (stmt.type === "ExpressionStatement") {
|
|
1644
|
+
const expr = unwrapTs(stmt.expression);
|
|
1645
|
+
if (expr?.type === "AssignmentExpression") {
|
|
1646
|
+
if (collectAwaits(expr.right).length > 0 || refsTainted(expr.right, tainted)) taintAssignTarget(expr.left);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
} else {
|
|
1650
|
+
taintOnly(stmt);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
processStatements(load.body.body);
|
|
1655
|
+
return { dependentLines, independentLines };
|
|
1094
1656
|
}
|
|
1095
1657
|
function walkKit(node, handlerFns, startupFns, visit, shadowed = /* @__PURE__ */ new Set(), inFunction = false, inHandler = false, inStartup = false) {
|
|
1096
1658
|
if (Array.isArray(node)) {
|
|
@@ -1187,7 +1749,10 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1187
1749
|
const handlerFns = collectHandlerFunctions(program);
|
|
1188
1750
|
const startupFns = collectStartupFunctions(program);
|
|
1189
1751
|
const svelteImports = collectSvelteLifecycleImports(program);
|
|
1190
|
-
|
|
1752
|
+
const ssrOptOut = findFalseOptOut(program, wrapped, "ssr");
|
|
1753
|
+
const csrOptOut = findFalseOptOut(program, wrapped, "csr");
|
|
1754
|
+
const waterfalls = collectLoadWaterfalls(program, wrapped);
|
|
1755
|
+
if (!ssrOptOut) {
|
|
1191
1756
|
const shiftLine = (l) => Math.max(0, l - 1);
|
|
1192
1757
|
const browserImports = collectBrowserGuardImports(program);
|
|
1193
1758
|
const guards = /* @__PURE__ */ new Set([...browserImports, ...collectDerivedGuardBindings(program, browserImports)]);
|
|
@@ -1287,6 +1852,9 @@ function parseKitModuleFacts(source, filename) {
|
|
|
1287
1852
|
runesModuleImports: byLine(runesModuleImports),
|
|
1288
1853
|
lifecycleCalls: byLine(lifecycleCalls),
|
|
1289
1854
|
browserGlobalRefs: byLine(browserGlobalRefs),
|
|
1855
|
+
...ssrOptOut ? { ssrDisabled: { line: Math.max(0, ssrOptOut.line - 1) } } : {},
|
|
1856
|
+
...csrOptOut ? { csrDisabled: { line: Math.max(0, csrOptOut.line - 1) } } : {},
|
|
1857
|
+
...waterfalls.dependentLines.length > 0 || waterfalls.independentLines.length > 0 ? { loadWaterfalls: waterfalls } : {},
|
|
1290
1858
|
suppressions
|
|
1291
1859
|
};
|
|
1292
1860
|
}
|
|
@@ -1331,7 +1899,82 @@ async function collectKitModuleFacts(rt, cwd) {
|
|
|
1331
1899
|
);
|
|
1332
1900
|
}
|
|
1333
1901
|
|
|
1334
|
-
// src/
|
|
1902
|
+
// src/vite-config-parse.ts
|
|
1903
|
+
function propOf(obj, name) {
|
|
1904
|
+
let found;
|
|
1905
|
+
for (const p of obj.properties) {
|
|
1906
|
+
if (p.type === "SpreadElement") {
|
|
1907
|
+
if (found) found = void 0;
|
|
1908
|
+
continue;
|
|
1909
|
+
}
|
|
1910
|
+
if (p.type !== "Property" || p.computed) continue;
|
|
1911
|
+
if (p.key.type === "Identifier" && p.key.name === name) found = p;
|
|
1912
|
+
else if (p.key.type === "Literal" && p.key.value === name) found = p;
|
|
1913
|
+
}
|
|
1914
|
+
return found;
|
|
1915
|
+
}
|
|
1916
|
+
function unwrapToObjectExpression(expr, bindings) {
|
|
1917
|
+
let current = expr;
|
|
1918
|
+
for (let i = 0; i < 4 && current; i++) {
|
|
1919
|
+
const e = unwrapTs(current);
|
|
1920
|
+
if (e.type === "ObjectExpression") return e;
|
|
1921
|
+
if (e.type === "Identifier") {
|
|
1922
|
+
current = bindings.get(e.name);
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
if (e.type === "CallExpression") {
|
|
1926
|
+
current = e.arguments[0];
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
return void 0;
|
|
1930
|
+
}
|
|
1931
|
+
const final = current ? unwrapTs(current) : void 0;
|
|
1932
|
+
return final?.type === "ObjectExpression" ? final : void 0;
|
|
1933
|
+
}
|
|
1934
|
+
function findExportedExpression(program) {
|
|
1935
|
+
let exported;
|
|
1936
|
+
for (const stmt of program.body) {
|
|
1937
|
+
if (stmt.type === "ExportDefaultDeclaration") exported = stmt.declaration;
|
|
1938
|
+
}
|
|
1939
|
+
if (exported) return exported;
|
|
1940
|
+
let cjsExported;
|
|
1941
|
+
for (const stmt of program.body) {
|
|
1942
|
+
if (stmt.type !== "ExpressionStatement") continue;
|
|
1943
|
+
const expr = stmt.expression;
|
|
1944
|
+
if (expr.type !== "AssignmentExpression" || expr.operator !== "=") continue;
|
|
1945
|
+
const left = expr.left;
|
|
1946
|
+
if (left.type === "MemberExpression" && !left.computed && left.object.type === "Identifier" && left.object.name === "module" && left.property.type === "Identifier" && left.property.name === "exports") {
|
|
1947
|
+
cjsExported = expr.right;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
return cjsExported;
|
|
1951
|
+
}
|
|
1952
|
+
function resolveConfigObject(program) {
|
|
1953
|
+
const exported = findExportedExpression(program);
|
|
1954
|
+
if (!exported) return void 0;
|
|
1955
|
+
return unwrapToObjectExpression(exported, collectTopLevelBindings(program));
|
|
1956
|
+
}
|
|
1957
|
+
function findMinifyDisabled(source) {
|
|
1958
|
+
let program;
|
|
1959
|
+
let wrapped;
|
|
1960
|
+
try {
|
|
1961
|
+
({ program, wrapped } = parseModuleProgram(source, "vite.config.ts"));
|
|
1962
|
+
} catch {
|
|
1963
|
+
return void 0;
|
|
1964
|
+
}
|
|
1965
|
+
if (!program) return void 0;
|
|
1966
|
+
const config = resolveConfigObject(program);
|
|
1967
|
+
if (!config) return void 0;
|
|
1968
|
+
const build = propOf(config, "build");
|
|
1969
|
+
const buildValue = build ? unwrapTs(build.value) : void 0;
|
|
1970
|
+
if (buildValue?.type !== "ObjectExpression") return void 0;
|
|
1971
|
+
const minify = propOf(buildValue, "minify");
|
|
1972
|
+
const minifyValue = minify ? unwrapTs(minify.value) : void 0;
|
|
1973
|
+
if (!minify || minifyValue?.type !== "Literal" || minifyValue.value !== false) return void 0;
|
|
1974
|
+
return { line: Math.max(0, lineOf(wrapped, minify.start) - 1) };
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// src/project-paths.ts
|
|
1335
1978
|
var ROBOTS_SOURCE_PATHS = [
|
|
1336
1979
|
"static/robots.txt",
|
|
1337
1980
|
"src/routes/robots.txt/+server.ts",
|
|
@@ -1360,7 +2003,7 @@ async function runRules(rules, ctx) {
|
|
|
1360
2003
|
return perRule.flat();
|
|
1361
2004
|
}
|
|
1362
2005
|
|
|
1363
|
-
// src/rules/seo/
|
|
2006
|
+
// src/rules/seo/title-presence.ts
|
|
1364
2007
|
var FIX = {
|
|
1365
2008
|
description: "Add a <title> inside <svelte:head> (a dynamic title is fine).",
|
|
1366
2009
|
snippet: "<svelte:head>\n <title>{data.title}</title>\n</svelte:head>",
|
|
@@ -1378,8 +2021,8 @@ function messageFor(detection) {
|
|
|
1378
2021
|
if (detection.value === "absent") return "Empty <title>";
|
|
1379
2022
|
return "<title>";
|
|
1380
2023
|
}
|
|
1381
|
-
var
|
|
1382
|
-
id: "
|
|
2024
|
+
var seoTitlePresence = {
|
|
2025
|
+
id: "seo/title-presence",
|
|
1383
2026
|
title: "Title presence",
|
|
1384
2027
|
category: "seo",
|
|
1385
2028
|
severity: "critical",
|
|
@@ -1390,7 +2033,7 @@ var seo001Title = {
|
|
|
1390
2033
|
return ctx.heads.map((head) => {
|
|
1391
2034
|
const detection = detectTitle(head);
|
|
1392
2035
|
return {
|
|
1393
|
-
id: "
|
|
2036
|
+
id: "seo/title-presence",
|
|
1394
2037
|
category: "seo",
|
|
1395
2038
|
severity: "critical",
|
|
1396
2039
|
detection,
|
|
@@ -1398,7 +2041,7 @@ var seo001Title = {
|
|
|
1398
2041
|
location: head.file,
|
|
1399
2042
|
message: messageFor(detection),
|
|
1400
2043
|
recommendation: "Add a <title> inside <svelte:head>, e.g. <title>{data.title}</title>, or set it via your meta component.",
|
|
1401
|
-
docsUrl: docsUrlFor("
|
|
2044
|
+
docsUrl: docsUrlFor("seo/title-presence"),
|
|
1402
2045
|
fix: { ...FIX }
|
|
1403
2046
|
};
|
|
1404
2047
|
});
|
|
@@ -1444,9 +2087,9 @@ function headTagRule(opts) {
|
|
|
1444
2087
|
};
|
|
1445
2088
|
}
|
|
1446
2089
|
|
|
1447
|
-
// src/rules/seo/
|
|
1448
|
-
var
|
|
1449
|
-
id: "
|
|
2090
|
+
// src/rules/seo/description-presence.ts
|
|
2091
|
+
var seoDescriptionPresence = headTagRule({
|
|
2092
|
+
id: "seo/description-presence",
|
|
1450
2093
|
title: "Description presence",
|
|
1451
2094
|
severity: "critical",
|
|
1452
2095
|
match: (t) => t.kind === "meta" && t.name === "description",
|
|
@@ -1459,8 +2102,10 @@ var seo002Description = headTagRule({
|
|
|
1459
2102
|
lang: "svelte"
|
|
1460
2103
|
}
|
|
1461
2104
|
});
|
|
1462
|
-
|
|
1463
|
-
|
|
2105
|
+
|
|
2106
|
+
// src/rules/seo/canonical-url.ts
|
|
2107
|
+
var seoCanonicalUrl = headTagRule({
|
|
2108
|
+
id: "seo/canonical-url",
|
|
1464
2109
|
title: "Canonical URL",
|
|
1465
2110
|
severity: "warning",
|
|
1466
2111
|
match: (t) => t.kind === "link" && t.rel === "canonical",
|
|
@@ -1473,8 +2118,10 @@ var seo003Canonical = headTagRule({
|
|
|
1473
2118
|
lang: "svelte"
|
|
1474
2119
|
}
|
|
1475
2120
|
});
|
|
1476
|
-
|
|
1477
|
-
|
|
2121
|
+
|
|
2122
|
+
// src/rules/seo/og-image.ts
|
|
2123
|
+
var seoOgImage = headTagRule({
|
|
2124
|
+
id: "seo/og-image",
|
|
1478
2125
|
title: "Open Graph image",
|
|
1479
2126
|
severity: "warning",
|
|
1480
2127
|
match: (t) => t.kind === "meta" && t.property === "og:image",
|
|
@@ -1487,8 +2134,10 @@ var seo004OgImage = headTagRule({
|
|
|
1487
2134
|
lang: "svelte"
|
|
1488
2135
|
}
|
|
1489
2136
|
});
|
|
1490
|
-
|
|
1491
|
-
|
|
2137
|
+
|
|
2138
|
+
// src/rules/seo/og-title.ts
|
|
2139
|
+
var seoOgTitle = headTagRule({
|
|
2140
|
+
id: "seo/og-title",
|
|
1492
2141
|
title: "Open Graph title",
|
|
1493
2142
|
severity: "warning",
|
|
1494
2143
|
match: (t) => t.kind === "meta" && t.property === "og:title",
|
|
@@ -1501,8 +2150,10 @@ var seo005OgTitle = headTagRule({
|
|
|
1501
2150
|
lang: "svelte"
|
|
1502
2151
|
}
|
|
1503
2152
|
});
|
|
1504
|
-
|
|
1505
|
-
|
|
2153
|
+
|
|
2154
|
+
// src/rules/seo/json-ld.ts
|
|
2155
|
+
var seoJsonLd = headTagRule({
|
|
2156
|
+
id: "seo/json-ld",
|
|
1506
2157
|
title: "JSON-LD structured data",
|
|
1507
2158
|
severity: "info",
|
|
1508
2159
|
match: (t) => t.kind === "jsonld",
|
|
@@ -1519,93 +2170,99 @@ var seo008JsonLd = headTagRule({
|
|
|
1519
2170
|
}
|
|
1520
2171
|
});
|
|
1521
2172
|
|
|
1522
|
-
// src/rules/seo/
|
|
2173
|
+
// src/rules/seo/robots-txt.ts
|
|
1523
2174
|
var present = { presence: "own", value: "static" };
|
|
1524
2175
|
var absent = { presence: "none", value: "absent" };
|
|
1525
|
-
var
|
|
2176
|
+
var FIX2 = {
|
|
1526
2177
|
description: "Create static/robots.txt (or a src/routes/robots.txt/+server endpoint).",
|
|
1527
2178
|
snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
|
|
1528
2179
|
lang: "text"
|
|
1529
2180
|
};
|
|
1530
|
-
var
|
|
1531
|
-
id: "
|
|
2181
|
+
var seoRobotsTxt = {
|
|
2182
|
+
id: "seo/robots-txt",
|
|
1532
2183
|
title: "robots.txt",
|
|
1533
2184
|
category: "seo",
|
|
1534
2185
|
severity: "warning",
|
|
1535
2186
|
scope: "project",
|
|
1536
2187
|
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:
|
|
2188
|
+
fix: FIX2,
|
|
1538
2189
|
async check(ctx) {
|
|
1539
2190
|
const detection = ctx.project.hasRobotsTxt ? present : absent;
|
|
1540
2191
|
return [
|
|
1541
2192
|
{
|
|
1542
|
-
id: "
|
|
2193
|
+
id: "seo/robots-txt",
|
|
1543
2194
|
category: "seo",
|
|
1544
2195
|
severity: "warning",
|
|
1545
2196
|
detection,
|
|
1546
2197
|
message: ctx.project.hasRobotsTxt ? "robots.txt" : "Missing robots.txt",
|
|
1547
2198
|
recommendation: "Add static/robots.txt or a src/routes/robots.txt/+server endpoint.",
|
|
1548
|
-
docsUrl: docsUrlFor("
|
|
1549
|
-
fix: { ...
|
|
2199
|
+
docsUrl: docsUrlFor("seo/robots-txt"),
|
|
2200
|
+
fix: { ...FIX2 }
|
|
1550
2201
|
}
|
|
1551
2202
|
];
|
|
1552
2203
|
}
|
|
1553
2204
|
};
|
|
1554
|
-
|
|
2205
|
+
|
|
2206
|
+
// src/rules/seo/sitemap-xml.ts
|
|
2207
|
+
var present2 = { presence: "own", value: "static" };
|
|
2208
|
+
var absent2 = { presence: "none", value: "absent" };
|
|
2209
|
+
var FIX3 = {
|
|
1555
2210
|
description: "Create static/sitemap.xml (or a src/routes/sitemap.xml/+server endpoint).",
|
|
1556
2211
|
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
2212
|
lang: "xml"
|
|
1558
2213
|
};
|
|
1559
|
-
var
|
|
1560
|
-
id: "
|
|
2214
|
+
var seoSitemapXml = {
|
|
2215
|
+
id: "seo/sitemap-xml",
|
|
1561
2216
|
title: "sitemap.xml",
|
|
1562
2217
|
category: "seo",
|
|
1563
2218
|
severity: "warning",
|
|
1564
2219
|
scope: "project",
|
|
1565
2220
|
rationale: "A sitemap.xml lists your URLs so search engines can discover and prioritise them, especially pages not well linked internally.",
|
|
1566
|
-
fix:
|
|
2221
|
+
fix: FIX3,
|
|
1567
2222
|
async check(ctx) {
|
|
1568
|
-
const detection = ctx.project.hasSitemap ?
|
|
2223
|
+
const detection = ctx.project.hasSitemap ? present2 : absent2;
|
|
1569
2224
|
return [
|
|
1570
2225
|
{
|
|
1571
|
-
id: "
|
|
2226
|
+
id: "seo/sitemap-xml",
|
|
1572
2227
|
category: "seo",
|
|
1573
2228
|
severity: "warning",
|
|
1574
2229
|
detection,
|
|
1575
2230
|
message: ctx.project.hasSitemap ? "sitemap.xml" : "Missing sitemap.xml",
|
|
1576
2231
|
recommendation: "Add static/sitemap.xml or a src/routes/sitemap.xml/+server endpoint.",
|
|
1577
|
-
docsUrl: docsUrlFor("
|
|
1578
|
-
fix: { ...
|
|
2232
|
+
docsUrl: docsUrlFor("seo/sitemap-xml"),
|
|
2233
|
+
fix: { ...FIX3 }
|
|
1579
2234
|
}
|
|
1580
2235
|
];
|
|
1581
2236
|
}
|
|
1582
2237
|
};
|
|
1583
|
-
|
|
2238
|
+
|
|
2239
|
+
// src/rules/seo/html-lang.ts
|
|
2240
|
+
var FIX4 = {
|
|
1584
2241
|
description: "Set the lang attribute on <html> in src/app.html.",
|
|
1585
2242
|
snippet: '<html lang="en">',
|
|
1586
2243
|
lang: "html"
|
|
1587
2244
|
};
|
|
1588
|
-
var
|
|
1589
|
-
id: "
|
|
2245
|
+
var seoHtmlLang = {
|
|
2246
|
+
id: "seo/html-lang",
|
|
1590
2247
|
title: "<html lang>",
|
|
1591
2248
|
category: "seo",
|
|
1592
2249
|
severity: "warning",
|
|
1593
2250
|
scope: "project",
|
|
1594
2251
|
rationale: "The <html lang> attribute declares the page language for search engines, screen readers, and translation tools.",
|
|
1595
|
-
fix:
|
|
2252
|
+
fix: FIX4,
|
|
1596
2253
|
async check(ctx) {
|
|
1597
2254
|
const detection = ctx.project.htmlLang;
|
|
1598
2255
|
const message = detection.presence === "none" ? "Missing <html lang>" : detection.value === "absent" ? "Empty <html lang>" : "<html lang>";
|
|
1599
2256
|
return [
|
|
1600
2257
|
{
|
|
1601
|
-
id: "
|
|
2258
|
+
id: "seo/html-lang",
|
|
1602
2259
|
category: "seo",
|
|
1603
2260
|
severity: "warning",
|
|
1604
2261
|
detection,
|
|
1605
2262
|
message,
|
|
1606
2263
|
recommendation: 'Set <html lang="..."> in src/app.html.',
|
|
1607
|
-
docsUrl: docsUrlFor("
|
|
1608
|
-
fix: { ...
|
|
2264
|
+
docsUrl: docsUrlFor("seo/html-lang"),
|
|
2265
|
+
fix: { ...FIX4 }
|
|
1609
2266
|
}
|
|
1610
2267
|
];
|
|
1611
2268
|
}
|
|
@@ -1662,9 +2319,9 @@ function imageRule(opts) {
|
|
|
1662
2319
|
};
|
|
1663
2320
|
}
|
|
1664
2321
|
|
|
1665
|
-
// src/rules/perf/
|
|
1666
|
-
var
|
|
1667
|
-
id: "
|
|
2322
|
+
// src/rules/perf/image-dimensions.ts
|
|
2323
|
+
var performanceImageDimensions = imageRule({
|
|
2324
|
+
id: "performance/image-dimensions",
|
|
1668
2325
|
title: "Image dimensions",
|
|
1669
2326
|
severity: "warning",
|
|
1670
2327
|
label: "<img> width/height",
|
|
@@ -1677,8 +2334,10 @@ var perf001ImageDimensions = imageRule({
|
|
|
1677
2334
|
},
|
|
1678
2335
|
ok: (img) => img.hasWidth && img.hasHeight
|
|
1679
2336
|
});
|
|
1680
|
-
|
|
1681
|
-
|
|
2337
|
+
|
|
2338
|
+
// src/rules/perf/image-loading-hint.ts
|
|
2339
|
+
var performanceImageLoadingHint = imageRule({
|
|
2340
|
+
id: "performance/image-loading-hint",
|
|
1682
2341
|
title: "Image loading hint",
|
|
1683
2342
|
severity: "info",
|
|
1684
2343
|
label: "<img> loading attribute",
|
|
@@ -1691,8 +2350,10 @@ var perf002ImageLoading = imageRule({
|
|
|
1691
2350
|
},
|
|
1692
2351
|
ok: (img) => img.hasLoading
|
|
1693
2352
|
});
|
|
1694
|
-
|
|
1695
|
-
|
|
2353
|
+
|
|
2354
|
+
// src/rules/perf/responsive-image.ts
|
|
2355
|
+
var performanceResponsiveImage = imageRule({
|
|
2356
|
+
id: "performance/responsive-image",
|
|
1696
2357
|
title: "Responsive image",
|
|
1697
2358
|
severity: "info",
|
|
1698
2359
|
label: "<img> srcset",
|
|
@@ -1759,9 +2420,9 @@ function linkRule(opts) {
|
|
|
1759
2420
|
};
|
|
1760
2421
|
}
|
|
1761
2422
|
|
|
1762
|
-
// src/rules/perf/
|
|
1763
|
-
var
|
|
1764
|
-
id: "
|
|
2423
|
+
// src/rules/perf/preload-missing-as.ts
|
|
2424
|
+
var performancePreloadMissingAs = linkRule({
|
|
2425
|
+
id: "performance/preload-missing-as",
|
|
1765
2426
|
title: "Preload missing as",
|
|
1766
2427
|
severity: "warning",
|
|
1767
2428
|
label: "`as` on a preloaded `<link>`",
|
|
@@ -1775,8 +2436,10 @@ var perf003PreloadAs = linkRule({
|
|
|
1775
2436
|
relevant: (t) => t.rel === "preload",
|
|
1776
2437
|
ok: (t) => t.hasAs === true
|
|
1777
2438
|
});
|
|
1778
|
-
|
|
1779
|
-
|
|
2439
|
+
|
|
2440
|
+
// src/rules/perf/font-preload-crossorigin.ts
|
|
2441
|
+
var performanceFontPreloadCrossorigin = linkRule({
|
|
2442
|
+
id: "performance/font-preload-crossorigin",
|
|
1780
2443
|
title: "Font preload missing crossorigin",
|
|
1781
2444
|
severity: "warning",
|
|
1782
2445
|
label: "`crossorigin` on a font preload",
|
|
@@ -1791,11 +2454,11 @@ var perf004FontPreloadCrossorigin = linkRule({
|
|
|
1791
2454
|
ok: (t) => t.hasCrossorigin === true
|
|
1792
2455
|
});
|
|
1793
2456
|
|
|
1794
|
-
// src/rules/perf/
|
|
1795
|
-
var docsUrl = docsUrlFor("
|
|
2457
|
+
// src/rules/perf/lcp-image.ts
|
|
2458
|
+
var docsUrl = docsUrlFor("performance/lcp-image");
|
|
1796
2459
|
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: "
|
|
2460
|
+
var performanceLcpImage = {
|
|
2461
|
+
id: "performance/lcp-image",
|
|
1799
2462
|
title: "LCP image eager loading",
|
|
1800
2463
|
category: "performance",
|
|
1801
2464
|
severity: "warning",
|
|
@@ -1813,7 +2476,7 @@ var perf005LcpImage = {
|
|
|
1813
2476
|
if (!first) continue;
|
|
1814
2477
|
out.push(
|
|
1815
2478
|
first.lazy ? {
|
|
1816
|
-
id: "
|
|
2479
|
+
id: "performance/lcp-image",
|
|
1817
2480
|
category: "performance",
|
|
1818
2481
|
severity: "warning",
|
|
1819
2482
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1823,9 +2486,9 @@ var perf005LcpImage = {
|
|
|
1823
2486
|
message: 'First image (likely LCP) is loading="lazy"',
|
|
1824
2487
|
recommendation,
|
|
1825
2488
|
docsUrl,
|
|
1826
|
-
fix: { ...
|
|
2489
|
+
fix: { ...performanceLcpImage.fix }
|
|
1827
2490
|
} : {
|
|
1828
|
-
id: "
|
|
2491
|
+
id: "performance/lcp-image",
|
|
1829
2492
|
category: "performance",
|
|
1830
2493
|
severity: "warning",
|
|
1831
2494
|
detection: { presence: "own", value: "static" },
|
|
@@ -1840,11 +2503,11 @@ var perf005LcpImage = {
|
|
|
1840
2503
|
}
|
|
1841
2504
|
};
|
|
1842
2505
|
|
|
1843
|
-
// src/rules/perf/
|
|
1844
|
-
var docsUrl2 = docsUrlFor("
|
|
2506
|
+
// src/rules/perf/render-blocking-script.ts
|
|
2507
|
+
var docsUrl2 = docsUrlFor("performance/render-blocking-script");
|
|
1845
2508
|
var recommendation2 = 'Add defer (or type="module"), or async, to the <script> so it does not block HTML parsing.';
|
|
1846
|
-
var
|
|
1847
|
-
id: "
|
|
2509
|
+
var performanceRenderBlockingScript = {
|
|
2510
|
+
id: "performance/render-blocking-script",
|
|
1848
2511
|
title: "Render-blocking script",
|
|
1849
2512
|
category: "performance",
|
|
1850
2513
|
severity: "warning",
|
|
@@ -1864,7 +2527,7 @@ var perf007RenderBlockingScript = {
|
|
|
1864
2527
|
if (blocking.length > 0) {
|
|
1865
2528
|
for (const tag of blocking) {
|
|
1866
2529
|
out.push({
|
|
1867
|
-
id: "
|
|
2530
|
+
id: "performance/render-blocking-script",
|
|
1868
2531
|
category: "performance",
|
|
1869
2532
|
severity: "warning",
|
|
1870
2533
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1874,12 +2537,12 @@ var perf007RenderBlockingScript = {
|
|
|
1874
2537
|
message: `Render-blocking <script>${tag.href ? ` (${tag.href})` : ""} in <head>`,
|
|
1875
2538
|
recommendation: recommendation2,
|
|
1876
2539
|
docsUrl: docsUrl2,
|
|
1877
|
-
fix: { ...
|
|
2540
|
+
fix: { ...performanceRenderBlockingScript.fix }
|
|
1878
2541
|
});
|
|
1879
2542
|
}
|
|
1880
2543
|
} else {
|
|
1881
2544
|
out.push({
|
|
1882
|
-
id: "
|
|
2545
|
+
id: "performance/render-blocking-script",
|
|
1883
2546
|
category: "performance",
|
|
1884
2547
|
severity: "warning",
|
|
1885
2548
|
detection: { presence: "own", value: "static" },
|
|
@@ -1894,16 +2557,16 @@ var perf007RenderBlockingScript = {
|
|
|
1894
2557
|
}
|
|
1895
2558
|
};
|
|
1896
2559
|
|
|
1897
|
-
// src/rules/perf/
|
|
1898
|
-
var docsUrl3 = docsUrlFor("
|
|
2560
|
+
// src/rules/perf/preconnect.ts
|
|
2561
|
+
var docsUrl3 = docsUrlFor("performance/preconnect");
|
|
1899
2562
|
var recommendation3 = 'Add <link rel="preconnect"> (or dns-prefetch) for the third-party origin so the connection is set up early.';
|
|
1900
2563
|
var THIRD_PARTY_ORIGINS = /* @__PURE__ */ new Set(["fonts.googleapis.com", "fonts.gstatic.com"]);
|
|
1901
2564
|
function hostOf(href) {
|
|
1902
2565
|
const m = /^(?:https?:)?\/\/([^/?#]+)/i.exec(href);
|
|
1903
2566
|
return m ? m[1].toLowerCase() : void 0;
|
|
1904
2567
|
}
|
|
1905
|
-
var
|
|
1906
|
-
id: "
|
|
2568
|
+
var performancePreconnect = {
|
|
2569
|
+
id: "performance/preconnect",
|
|
1907
2570
|
title: "Preconnect third-party origin",
|
|
1908
2571
|
category: "performance",
|
|
1909
2572
|
severity: "info",
|
|
@@ -1930,7 +2593,7 @@ var perf008Preconnect = {
|
|
|
1930
2593
|
const missing = [...referenced].filter(([host]) => !covered.has(host));
|
|
1931
2594
|
if (missing.length === 0) {
|
|
1932
2595
|
out.push({
|
|
1933
|
-
id: "
|
|
2596
|
+
id: "performance/preconnect",
|
|
1934
2597
|
category: "performance",
|
|
1935
2598
|
severity: "info",
|
|
1936
2599
|
detection: { presence: "own", value: "static" },
|
|
@@ -1943,7 +2606,7 @@ var perf008Preconnect = {
|
|
|
1943
2606
|
}
|
|
1944
2607
|
for (const [host, file] of missing) {
|
|
1945
2608
|
out.push({
|
|
1946
|
-
id: "
|
|
2609
|
+
id: "performance/preconnect",
|
|
1947
2610
|
category: "performance",
|
|
1948
2611
|
severity: "info",
|
|
1949
2612
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1952,7 +2615,7 @@ var perf008Preconnect = {
|
|
|
1952
2615
|
message: `Third-party origin ${host} used without a preconnect`,
|
|
1953
2616
|
recommendation: recommendation3,
|
|
1954
2617
|
docsUrl: docsUrl3,
|
|
1955
|
-
fix: { ...
|
|
2618
|
+
fix: { ...performancePreconnect.fix }
|
|
1956
2619
|
});
|
|
1957
2620
|
}
|
|
1958
2621
|
}
|
|
@@ -1960,28 +2623,28 @@ var perf008Preconnect = {
|
|
|
1960
2623
|
}
|
|
1961
2624
|
};
|
|
1962
2625
|
|
|
1963
|
-
// src/rules/seo/
|
|
1964
|
-
var
|
|
2626
|
+
// src/rules/seo/indexability.ts
|
|
2627
|
+
var FIX5 = {
|
|
1965
2628
|
description: 'If this route should be indexed, drop noindex from its <meta name="robots">.',
|
|
1966
2629
|
snippet: '<svelte:head>\n <meta name="robots" content="index, follow" />\n</svelte:head>',
|
|
1967
2630
|
lang: "svelte"
|
|
1968
2631
|
};
|
|
1969
|
-
var
|
|
1970
|
-
id: "
|
|
2632
|
+
var seoIndexability = {
|
|
2633
|
+
id: "seo/indexability",
|
|
1971
2634
|
title: "Indexability",
|
|
1972
2635
|
category: "seo",
|
|
1973
2636
|
severity: "info",
|
|
1974
2637
|
scope: "route",
|
|
1975
2638
|
rationale: "A noindex directive removes the page from search results; an accidental noindex on a public route silently deindexes it.",
|
|
1976
|
-
fix:
|
|
2639
|
+
fix: FIX5,
|
|
1977
2640
|
async check(ctx) {
|
|
1978
|
-
const docsUrl7 = docsUrlFor("
|
|
2641
|
+
const docsUrl7 = docsUrlFor("seo/indexability");
|
|
1979
2642
|
const out = [];
|
|
1980
2643
|
for (const head of ctx.heads) {
|
|
1981
2644
|
const noindexed = head.tags.some((t) => t.kind === "meta" && t.name === "robots" && t.noindex === true);
|
|
1982
2645
|
if (!noindexed) continue;
|
|
1983
2646
|
out.push({
|
|
1984
|
-
id: "
|
|
2647
|
+
id: "seo/indexability",
|
|
1985
2648
|
category: "seo",
|
|
1986
2649
|
severity: "info",
|
|
1987
2650
|
detection: { presence: "none", value: "absent" },
|
|
@@ -1991,14 +2654,16 @@ var seo010Indexability = {
|
|
|
1991
2654
|
message: "Route is noindex \u2014 verify this is intentional",
|
|
1992
2655
|
recommendation: 'If this route should be indexed, remove noindex from its <meta name="robots">.',
|
|
1993
2656
|
docsUrl: docsUrl7,
|
|
1994
|
-
fix: { ...
|
|
2657
|
+
fix: { ...FIX5 }
|
|
1995
2658
|
});
|
|
1996
2659
|
}
|
|
1997
2660
|
return out;
|
|
1998
2661
|
}
|
|
1999
2662
|
};
|
|
2000
|
-
|
|
2001
|
-
|
|
2663
|
+
|
|
2664
|
+
// src/rules/seo/twitter-card.ts
|
|
2665
|
+
var seoTwitterCard = headTagRule({
|
|
2666
|
+
id: "seo/twitter-card",
|
|
2002
2667
|
title: "Twitter Card",
|
|
2003
2668
|
severity: "info",
|
|
2004
2669
|
match: (t) => t.kind === "meta" && t.name === "twitter:card",
|
|
@@ -2011,8 +2676,10 @@ var seo011TwitterCard = headTagRule({
|
|
|
2011
2676
|
lang: "svelte"
|
|
2012
2677
|
}
|
|
2013
2678
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2679
|
+
|
|
2680
|
+
// src/rules/seo/og-description.ts
|
|
2681
|
+
var seoOgDescription = headTagRule({
|
|
2682
|
+
id: "seo/og-description",
|
|
2016
2683
|
title: "Open Graph description",
|
|
2017
2684
|
severity: "warning",
|
|
2018
2685
|
match: (t) => t.kind === "meta" && t.property === "og:description",
|
|
@@ -2025,8 +2692,10 @@ var seo012OgDescription = headTagRule({
|
|
|
2025
2692
|
lang: "svelte"
|
|
2026
2693
|
}
|
|
2027
2694
|
});
|
|
2028
|
-
|
|
2029
|
-
|
|
2695
|
+
|
|
2696
|
+
// src/rules/seo/og-url.ts
|
|
2697
|
+
var seoOgUrl = headTagRule({
|
|
2698
|
+
id: "seo/og-url",
|
|
2030
2699
|
title: "Open Graph URL",
|
|
2031
2700
|
severity: "info",
|
|
2032
2701
|
match: (t) => t.kind === "meta" && t.property === "og:url",
|
|
@@ -2039,8 +2708,10 @@ var seo013OgUrl = headTagRule({
|
|
|
2039
2708
|
lang: "svelte"
|
|
2040
2709
|
}
|
|
2041
2710
|
});
|
|
2042
|
-
|
|
2043
|
-
|
|
2711
|
+
|
|
2712
|
+
// src/rules/seo/viewport.ts
|
|
2713
|
+
var seoViewport = headTagRule({
|
|
2714
|
+
id: "seo/viewport",
|
|
2044
2715
|
title: "Viewport",
|
|
2045
2716
|
severity: "warning",
|
|
2046
2717
|
match: (t) => t.kind === "meta" && t.name === "viewport",
|
|
@@ -2057,37 +2728,43 @@ var seo014Viewport = headTagRule({
|
|
|
2057
2728
|
lang: "html"
|
|
2058
2729
|
}
|
|
2059
2730
|
});
|
|
2060
|
-
|
|
2731
|
+
|
|
2732
|
+
// src/rules/seo/sitemap-in-robots.ts
|
|
2733
|
+
var FIX6 = {
|
|
2061
2734
|
description: "Add a Sitemap: line to static/robots.txt.",
|
|
2062
2735
|
snippet: "User-agent: *\nAllow: /\n\nSitemap: https://example.com/sitemap.xml",
|
|
2063
2736
|
lang: "text"
|
|
2064
2737
|
};
|
|
2065
|
-
var
|
|
2066
|
-
id: "
|
|
2738
|
+
var seoSitemapInRobots = {
|
|
2739
|
+
id: "seo/sitemap-in-robots",
|
|
2067
2740
|
title: "Sitemap referenced in robots.txt",
|
|
2068
2741
|
category: "seo",
|
|
2069
2742
|
severity: "info",
|
|
2070
2743
|
scope: "project",
|
|
2071
2744
|
rationale: "A Sitemap: line in robots.txt helps crawlers discover your sitemap; without it discovery relies on manual submission.",
|
|
2072
|
-
fix:
|
|
2745
|
+
fix: FIX6,
|
|
2073
2746
|
async check(ctx) {
|
|
2074
2747
|
const { hasRobotsTxt, hasSitemap, robotsReferencesSitemap } = ctx.project;
|
|
2075
2748
|
if (!(hasRobotsTxt && hasSitemap && robotsReferencesSitemap === false)) return [];
|
|
2076
2749
|
return [
|
|
2077
2750
|
{
|
|
2078
|
-
id: "
|
|
2751
|
+
id: "seo/sitemap-in-robots",
|
|
2079
2752
|
category: "seo",
|
|
2080
2753
|
severity: "info",
|
|
2081
2754
|
detection: { presence: "none", value: "absent" },
|
|
2082
2755
|
message: "robots.txt does not reference your sitemap",
|
|
2083
2756
|
recommendation: "Add a Sitemap: line to static/robots.txt pointing at your sitemap.xml.",
|
|
2084
|
-
docsUrl: docsUrlFor("
|
|
2085
|
-
fix: { ...
|
|
2757
|
+
docsUrl: docsUrlFor("seo/sitemap-in-robots"),
|
|
2758
|
+
fix: { ...FIX6 }
|
|
2086
2759
|
}
|
|
2087
2760
|
];
|
|
2088
2761
|
}
|
|
2089
2762
|
};
|
|
2090
2763
|
|
|
2764
|
+
// src/rules/seo/detection.ts
|
|
2765
|
+
var PENALIZED = { presence: "none", value: "absent" };
|
|
2766
|
+
var PASS = { presence: "own", value: "static" };
|
|
2767
|
+
|
|
2091
2768
|
// src/rules/seo/jsonld-engine.ts
|
|
2092
2769
|
function parseJsonLd(raw) {
|
|
2093
2770
|
let data;
|
|
@@ -2223,65 +2900,9 @@ var REQUIRED_PROPS = {
|
|
|
2223
2900
|
VideoObject: ["name", "description", "thumbnailUrl", "uploadDate"],
|
|
2224
2901
|
LocalBusiness: ["name", "address"]
|
|
2225
2902
|
};
|
|
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
2903
|
function jsonldTags(head) {
|
|
2233
2904
|
return head.tags.filter((t) => t.kind === "jsonld" && typeof t.jsonld === "string");
|
|
2234
2905
|
}
|
|
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
|
-
}
|
|
2284
|
-
};
|
|
2285
2906
|
function jsonldRule(opts) {
|
|
2286
2907
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2287
2908
|
return {
|
|
@@ -2330,8 +2951,62 @@ function jsonldRule(opts) {
|
|
|
2330
2951
|
}
|
|
2331
2952
|
};
|
|
2332
2953
|
}
|
|
2333
|
-
|
|
2334
|
-
|
|
2954
|
+
|
|
2955
|
+
// src/rules/seo/json-ld-validity.ts
|
|
2956
|
+
var seoJsonLdValidity = {
|
|
2957
|
+
id: "seo/json-ld-validity",
|
|
2958
|
+
title: "JSON-LD validity",
|
|
2959
|
+
category: "seo",
|
|
2960
|
+
severity: "warning",
|
|
2961
|
+
scope: "route",
|
|
2962
|
+
rationale: "Invalid JSON-LD \u2014 unparseable, or missing @context/@type \u2014 is silently ignored by search engines, so the structured data does nothing.",
|
|
2963
|
+
fix: {
|
|
2964
|
+
description: "Make the JSON-LD valid: parseable JSON with both @context (schema.org) and @type.",
|
|
2965
|
+
snippet: '<svelte:head>\n <script type="application/ld+json">\n {"@context":"https://schema.org","@type":"WebPage","name":"\u2026"}\n </script>\n</svelte:head>',
|
|
2966
|
+
lang: "svelte"
|
|
2967
|
+
},
|
|
2968
|
+
async check(ctx) {
|
|
2969
|
+
const docsUrl7 = docsUrlFor("seo/json-ld-validity");
|
|
2970
|
+
const out = [];
|
|
2971
|
+
for (const head of ctx.heads) {
|
|
2972
|
+
for (const tag of jsonldTags(head)) {
|
|
2973
|
+
const parsed = parseJsonLd(tag.jsonld);
|
|
2974
|
+
let problem;
|
|
2975
|
+
if (!parsed.ok) problem = "JSON-LD is not valid JSON";
|
|
2976
|
+
else if (!parsed.nodes.some((n) => "@context" in n)) problem = "JSON-LD is missing @context";
|
|
2977
|
+
else if (!parsed.nodes.some((n) => typeOf(n).length > 0)) problem = "JSON-LD is missing @type";
|
|
2978
|
+
out.push(
|
|
2979
|
+
problem ? {
|
|
2980
|
+
id: "seo/json-ld-validity",
|
|
2981
|
+
category: "seo",
|
|
2982
|
+
severity: "warning",
|
|
2983
|
+
detection: PENALIZED,
|
|
2984
|
+
route: head.route,
|
|
2985
|
+
location: head.file,
|
|
2986
|
+
message: problem,
|
|
2987
|
+
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2988
|
+
docsUrl: docsUrl7,
|
|
2989
|
+
fix: { ...seoJsonLdValidity.fix }
|
|
2990
|
+
} : {
|
|
2991
|
+
id: "seo/json-ld-validity",
|
|
2992
|
+
category: "seo",
|
|
2993
|
+
severity: "warning",
|
|
2994
|
+
detection: PASS,
|
|
2995
|
+
route: head.route,
|
|
2996
|
+
message: "JSON-LD validity",
|
|
2997
|
+
recommendation: "Make the JSON-LD valid JSON with both @context and @type.",
|
|
2998
|
+
docsUrl: docsUrl7
|
|
2999
|
+
}
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
return out;
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
|
|
3007
|
+
// src/rules/seo/json-ld-deprecated-type.ts
|
|
3008
|
+
var seoJsonLdDeprecatedType = jsonldRule({
|
|
3009
|
+
id: "seo/json-ld-deprecated-type",
|
|
2335
3010
|
title: "Deprecated structured-data type",
|
|
2336
3011
|
severity: "info",
|
|
2337
3012
|
label: "Structured-data type",
|
|
@@ -2342,8 +3017,10 @@ var seo017DeprecatedType = jsonldRule({
|
|
|
2342
3017
|
return dep ? `@type "${dep}" no longer reliably produces a Google rich result` : void 0;
|
|
2343
3018
|
}
|
|
2344
3019
|
});
|
|
2345
|
-
|
|
2346
|
-
|
|
3020
|
+
|
|
3021
|
+
// src/rules/seo/json-ld-relative-url.ts
|
|
3022
|
+
var seoJsonLdRelativeUrl = jsonldRule({
|
|
3023
|
+
id: "seo/json-ld-relative-url",
|
|
2347
3024
|
title: "JSON-LD relative URL",
|
|
2348
3025
|
severity: "warning",
|
|
2349
3026
|
label: "JSON-LD URLs",
|
|
@@ -2359,8 +3036,10 @@ var seo018RelativeUrl = jsonldRule({
|
|
|
2359
3036
|
return bad ? `Relative URL in JSON-LD: "${bad}" \u2014 use an absolute URL` : void 0;
|
|
2360
3037
|
}
|
|
2361
3038
|
});
|
|
2362
|
-
|
|
2363
|
-
|
|
3039
|
+
|
|
3040
|
+
// src/rules/seo/json-ld-date-format.ts
|
|
3041
|
+
var seoJsonLdDateFormat = jsonldRule({
|
|
3042
|
+
id: "seo/json-ld-date-format",
|
|
2364
3043
|
title: "JSON-LD date format",
|
|
2365
3044
|
severity: "info",
|
|
2366
3045
|
label: "JSON-LD dates",
|
|
@@ -2376,8 +3055,10 @@ var seo019DateFormat = jsonldRule({
|
|
|
2376
3055
|
return bad ? `Non-ISO-8601 date in JSON-LD: "${bad}"` : void 0;
|
|
2377
3056
|
}
|
|
2378
3057
|
});
|
|
2379
|
-
|
|
2380
|
-
|
|
3058
|
+
|
|
3059
|
+
// src/rules/seo/json-ld-placeholder.ts
|
|
3060
|
+
var seoJsonLdPlaceholder = jsonldRule({
|
|
3061
|
+
id: "seo/json-ld-placeholder",
|
|
2381
3062
|
title: "JSON-LD placeholder text",
|
|
2382
3063
|
severity: "info",
|
|
2383
3064
|
label: "JSON-LD content",
|
|
@@ -2388,8 +3069,10 @@ var seo020Placeholder = jsonldRule({
|
|
|
2388
3069
|
return bad ? `Placeholder text in JSON-LD: "${bad}"` : void 0;
|
|
2389
3070
|
}
|
|
2390
3071
|
});
|
|
2391
|
-
|
|
2392
|
-
|
|
3072
|
+
|
|
3073
|
+
// src/rules/seo/json-ld-required-props.ts
|
|
3074
|
+
var seoJsonLdRequiredProps = jsonldRule({
|
|
3075
|
+
id: "seo/json-ld-required-props",
|
|
2393
3076
|
title: "JSON-LD required properties",
|
|
2394
3077
|
severity: "warning",
|
|
2395
3078
|
label: "JSON-LD required properties",
|
|
@@ -2421,7 +3104,7 @@ function visibleLength(s) {
|
|
|
2421
3104
|
return [...segmenter.segment(collapsed)].length;
|
|
2422
3105
|
}
|
|
2423
3106
|
|
|
2424
|
-
// src/rules/seo/
|
|
3107
|
+
// src/rules/seo/length-rule.ts
|
|
2425
3108
|
function lengthRule(opts) {
|
|
2426
3109
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2427
3110
|
return {
|
|
@@ -2467,8 +3150,10 @@ function lengthRule(opts) {
|
|
|
2467
3150
|
}
|
|
2468
3151
|
};
|
|
2469
3152
|
}
|
|
2470
|
-
|
|
2471
|
-
|
|
3153
|
+
|
|
3154
|
+
// src/rules/seo/title-length.ts
|
|
3155
|
+
var seoTitleLength = lengthRule({
|
|
3156
|
+
id: "seo/title-length",
|
|
2472
3157
|
title: "Title length",
|
|
2473
3158
|
label: "Title length",
|
|
2474
3159
|
noun: "Title",
|
|
@@ -2478,8 +3163,10 @@ var seo022TitleLength = lengthRule({
|
|
|
2478
3163
|
recommendation: "Aim for a title of 30\u201360 characters so it is not truncated in search results.",
|
|
2479
3164
|
rationale: "A title that is too short wastes the strongest on-page signal; one that is too long is truncated in the SERP."
|
|
2480
3165
|
});
|
|
2481
|
-
|
|
2482
|
-
|
|
3166
|
+
|
|
3167
|
+
// src/rules/seo/description-length.ts
|
|
3168
|
+
var seoDescriptionLength = lengthRule({
|
|
3169
|
+
id: "seo/description-length",
|
|
2483
3170
|
title: "Description length",
|
|
2484
3171
|
label: "Description length",
|
|
2485
3172
|
noun: "Description",
|
|
@@ -2490,9 +3177,9 @@ var seo023DescriptionLength = lengthRule({
|
|
|
2490
3177
|
rationale: "A description that is too short under-uses the SERP snippet; one that is too long is truncated by search engines."
|
|
2491
3178
|
});
|
|
2492
3179
|
|
|
2493
|
-
// src/rules/seo/
|
|
2494
|
-
var
|
|
2495
|
-
id: "
|
|
3180
|
+
// src/rules/seo/charset.ts
|
|
3181
|
+
var seoCharset = headTagRule({
|
|
3182
|
+
id: "seo/charset",
|
|
2496
3183
|
title: "Character encoding",
|
|
2497
3184
|
severity: "warning",
|
|
2498
3185
|
match: (t) => t.kind === "meta" && t.name === "charset",
|
|
@@ -2507,9 +3194,9 @@ var seo024Charset = headTagRule({
|
|
|
2507
3194
|
}
|
|
2508
3195
|
});
|
|
2509
3196
|
|
|
2510
|
-
// src/rules/seo/
|
|
2511
|
-
var
|
|
2512
|
-
id: "
|
|
3197
|
+
// src/rules/seo/image-alt.ts
|
|
3198
|
+
var seoImageAlt = imageRule({
|
|
3199
|
+
id: "seo/image-alt",
|
|
2513
3200
|
title: "Image alt text",
|
|
2514
3201
|
category: "seo",
|
|
2515
3202
|
severity: "warning",
|
|
@@ -2524,15 +3211,15 @@ var seo025ImageAlt = imageRule({
|
|
|
2524
3211
|
ok: (img) => img.hasAlt
|
|
2525
3212
|
});
|
|
2526
3213
|
|
|
2527
|
-
// src/rules/seo/
|
|
2528
|
-
var docsUrl4 = docsUrlFor("
|
|
3214
|
+
// src/rules/seo/hreflang.ts
|
|
3215
|
+
var docsUrl4 = docsUrlFor("seo/hreflang");
|
|
2529
3216
|
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
3217
|
var HREFLANG_RE = /^[a-z]{2,3}(-[a-z]{4})?(-([a-z]{2}|\d{3}))?$/i;
|
|
2531
3218
|
function isValidHreflang(v) {
|
|
2532
3219
|
return v.toLowerCase() === "x-default" || HREFLANG_RE.test(v);
|
|
2533
3220
|
}
|
|
2534
|
-
var
|
|
2535
|
-
id: "
|
|
3221
|
+
var seoHreflang = {
|
|
3222
|
+
id: "seo/hreflang",
|
|
2536
3223
|
title: "hreflang validity",
|
|
2537
3224
|
category: "seo",
|
|
2538
3225
|
severity: "warning",
|
|
@@ -2557,7 +3244,7 @@ var seo026Hreflang = {
|
|
|
2557
3244
|
}
|
|
2558
3245
|
out.push(
|
|
2559
3246
|
problem ? {
|
|
2560
|
-
id: "
|
|
3247
|
+
id: "seo/hreflang",
|
|
2561
3248
|
category: "seo",
|
|
2562
3249
|
severity: "warning",
|
|
2563
3250
|
detection: PENALIZED,
|
|
@@ -2567,7 +3254,7 @@ var seo026Hreflang = {
|
|
|
2567
3254
|
recommendation: recommendation4,
|
|
2568
3255
|
docsUrl: docsUrl4
|
|
2569
3256
|
} : {
|
|
2570
|
-
id: "
|
|
3257
|
+
id: "seo/hreflang",
|
|
2571
3258
|
category: "seo",
|
|
2572
3259
|
severity: "warning",
|
|
2573
3260
|
detection: PASS,
|
|
@@ -2582,11 +3269,11 @@ var seo026Hreflang = {
|
|
|
2582
3269
|
}
|
|
2583
3270
|
};
|
|
2584
3271
|
|
|
2585
|
-
// src/rules/seo/
|
|
2586
|
-
var docsUrl5 = docsUrlFor("
|
|
3272
|
+
// src/rules/seo/single-h1.ts
|
|
3273
|
+
var docsUrl5 = docsUrlFor("seo/single-h1");
|
|
2587
3274
|
var recommendation5 = "Use exactly one <h1> per page for its main topic; demote extra top-level headings to <h2>+.";
|
|
2588
|
-
var
|
|
2589
|
-
id: "
|
|
3275
|
+
var seoSingleH1 = {
|
|
3276
|
+
id: "seo/single-h1",
|
|
2590
3277
|
title: "Heading hierarchy",
|
|
2591
3278
|
category: "seo",
|
|
2592
3279
|
severity: "warning",
|
|
@@ -2609,7 +3296,7 @@ var seo027Heading = {
|
|
|
2609
3296
|
}
|
|
2610
3297
|
out.push(
|
|
2611
3298
|
problem ? {
|
|
2612
|
-
id: "
|
|
3299
|
+
id: "seo/single-h1",
|
|
2613
3300
|
category: "seo",
|
|
2614
3301
|
severity: "warning",
|
|
2615
3302
|
detection: PENALIZED,
|
|
@@ -2619,7 +3306,7 @@ var seo027Heading = {
|
|
|
2619
3306
|
recommendation: recommendation5,
|
|
2620
3307
|
docsUrl: docsUrl5
|
|
2621
3308
|
} : {
|
|
2622
|
-
id: "
|
|
3309
|
+
id: "seo/single-h1",
|
|
2623
3310
|
category: "seo",
|
|
2624
3311
|
severity: "warning",
|
|
2625
3312
|
detection: PASS,
|
|
@@ -2634,7 +3321,7 @@ var seo027Heading = {
|
|
|
2634
3321
|
}
|
|
2635
3322
|
};
|
|
2636
3323
|
|
|
2637
|
-
// src/rules/seo/
|
|
3324
|
+
// src/rules/seo/uniqueness-rule.ts
|
|
2638
3325
|
function uniquenessRule(opts) {
|
|
2639
3326
|
const docsUrl7 = docsUrlFor(opts.id);
|
|
2640
3327
|
return {
|
|
@@ -2681,8 +3368,10 @@ function uniquenessRule(opts) {
|
|
|
2681
3368
|
}
|
|
2682
3369
|
};
|
|
2683
3370
|
}
|
|
2684
|
-
|
|
2685
|
-
|
|
3371
|
+
|
|
3372
|
+
// src/rules/seo/duplicate-title.ts
|
|
3373
|
+
var seoDuplicateTitle = uniquenessRule({
|
|
3374
|
+
id: "seo/duplicate-title",
|
|
2686
3375
|
title: "Duplicate title",
|
|
2687
3376
|
label: "Unique title",
|
|
2688
3377
|
noun: "Title",
|
|
@@ -2690,8 +3379,10 @@ var seo028TitleUnique = uniquenessRule({
|
|
|
2690
3379
|
recommendation: "Give each route a unique <title> that describes that page specifically.",
|
|
2691
3380
|
rationale: "Duplicate titles across pages make them compete in search results and weaken each page\u2019s relevance signal."
|
|
2692
3381
|
});
|
|
2693
|
-
|
|
2694
|
-
|
|
3382
|
+
|
|
3383
|
+
// src/rules/seo/duplicate-description.ts
|
|
3384
|
+
var seoDuplicateDescription = uniquenessRule({
|
|
3385
|
+
id: "seo/duplicate-description",
|
|
2695
3386
|
title: "Duplicate description",
|
|
2696
3387
|
label: "Unique description",
|
|
2697
3388
|
noun: "Description",
|
|
@@ -2700,11 +3391,11 @@ var seo029DescriptionUnique = uniquenessRule({
|
|
|
2700
3391
|
rationale: "Duplicate meta descriptions give search engines no per-page summary, so they are often ignored or rewritten."
|
|
2701
3392
|
});
|
|
2702
3393
|
|
|
2703
|
-
// src/rules/seo/
|
|
2704
|
-
var docsUrl6 = docsUrlFor("
|
|
3394
|
+
// src/rules/seo/heading-level-skip.ts
|
|
3395
|
+
var docsUrl6 = docsUrlFor("seo/heading-level-skip");
|
|
2705
3396
|
var recommendation6 = "Increase heading levels one step at a time (do not jump, e.g. from <h2> straight to <h4>).";
|
|
2706
|
-
var
|
|
2707
|
-
id: "
|
|
3397
|
+
var seoHeadingLevelSkip = {
|
|
3398
|
+
id: "seo/heading-level-skip",
|
|
2708
3399
|
title: "Heading order",
|
|
2709
3400
|
category: "seo",
|
|
2710
3401
|
severity: "info",
|
|
@@ -2726,7 +3417,7 @@ var seo030HeadingOrder = {
|
|
|
2726
3417
|
}
|
|
2727
3418
|
out.push(
|
|
2728
3419
|
skip ? {
|
|
2729
|
-
id: "
|
|
3420
|
+
id: "seo/heading-level-skip",
|
|
2730
3421
|
category: "seo",
|
|
2731
3422
|
severity: "info",
|
|
2732
3423
|
detection: PENALIZED,
|
|
@@ -2737,7 +3428,7 @@ var seo030HeadingOrder = {
|
|
|
2737
3428
|
recommendation: recommendation6,
|
|
2738
3429
|
docsUrl: docsUrl6
|
|
2739
3430
|
} : {
|
|
2740
|
-
id: "
|
|
3431
|
+
id: "seo/heading-level-skip",
|
|
2741
3432
|
category: "seo",
|
|
2742
3433
|
severity: "info",
|
|
2743
3434
|
detection: PASS,
|
|
@@ -2752,10 +3443,85 @@ var seo030HeadingOrder = {
|
|
|
2752
3443
|
}
|
|
2753
3444
|
};
|
|
2754
3445
|
|
|
2755
|
-
// src/rules/
|
|
3446
|
+
// src/rules/kit-module-rule.ts
|
|
2756
3447
|
var PENALIZED2 = { presence: "none", value: "absent" };
|
|
2757
3448
|
var PASS2 = { presence: "own", value: "static" };
|
|
2758
|
-
function isSuppressed(
|
|
3449
|
+
function isSuppressed(m, ruleId, line) {
|
|
3450
|
+
return (m.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
3451
|
+
}
|
|
3452
|
+
function kitModuleRule(opts) {
|
|
3453
|
+
const docsUrl7 = docsUrlFor(opts.id);
|
|
3454
|
+
const severity = opts.severity ?? "warning";
|
|
3455
|
+
return {
|
|
3456
|
+
id: opts.id,
|
|
3457
|
+
title: opts.title,
|
|
3458
|
+
category: opts.category,
|
|
3459
|
+
severity,
|
|
3460
|
+
scope: "component",
|
|
3461
|
+
rationale: opts.rationale,
|
|
3462
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
3463
|
+
async check(ctx) {
|
|
3464
|
+
const out = [];
|
|
3465
|
+
for (const m of ctx.kitModules ?? []) {
|
|
3466
|
+
if (!opts.applies(m, ctx)) continue;
|
|
3467
|
+
const bad = opts.bad(m, ctx).filter((b) => !(b.line > 0 && isSuppressed(m, opts.id, b.line)));
|
|
3468
|
+
if (bad.length === 0) {
|
|
3469
|
+
out.push({
|
|
3470
|
+
id: opts.id,
|
|
3471
|
+
category: opts.category,
|
|
3472
|
+
severity,
|
|
3473
|
+
detection: PASS2,
|
|
3474
|
+
route: m.file,
|
|
3475
|
+
message: opts.label,
|
|
3476
|
+
recommendation: opts.recommendation,
|
|
3477
|
+
docsUrl: docsUrl7
|
|
3478
|
+
});
|
|
3479
|
+
continue;
|
|
3480
|
+
}
|
|
3481
|
+
for (const b of bad) {
|
|
3482
|
+
out.push({
|
|
3483
|
+
id: opts.id,
|
|
3484
|
+
category: opts.category,
|
|
3485
|
+
severity,
|
|
3486
|
+
detection: PENALIZED2,
|
|
3487
|
+
route: m.file,
|
|
3488
|
+
location: m.file,
|
|
3489
|
+
...b.line > 0 ? { line: b.line } : {},
|
|
3490
|
+
message: b.message,
|
|
3491
|
+
recommendation: opts.recommendation,
|
|
3492
|
+
docsUrl: docsUrl7,
|
|
3493
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
return out;
|
|
3498
|
+
}
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// src/rules/seo/ssr-disabled.ts
|
|
3503
|
+
var ROOT_LAYOUT_RE = /^src\/routes\/\+layout(\.server)?\.(ts|js)$/;
|
|
3504
|
+
var PAGE_OPTION_FILE_RE = /\+(page|layout)(\.server)?\.(ts|js)$/;
|
|
3505
|
+
var seoSsrDisabled = kitModuleRule({
|
|
3506
|
+
id: "seo/ssr-disabled",
|
|
3507
|
+
title: "SSR disabled",
|
|
3508
|
+
category: "seo",
|
|
3509
|
+
label: "SSR enabled",
|
|
3510
|
+
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.",
|
|
3511
|
+
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.",
|
|
3512
|
+
applies: (m) => m.ssrDisabled !== void 0 && PAGE_OPTION_FILE_RE.test(m.file),
|
|
3513
|
+
bad: (m) => [
|
|
3514
|
+
{
|
|
3515
|
+
line: m.ssrDisabled.line,
|
|
3516
|
+
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"
|
|
3517
|
+
}
|
|
3518
|
+
]
|
|
3519
|
+
});
|
|
3520
|
+
|
|
3521
|
+
// src/rules/component-rule.ts
|
|
3522
|
+
var PENALIZED3 = { presence: "none", value: "absent" };
|
|
3523
|
+
var PASS3 = { presence: "own", value: "static" };
|
|
3524
|
+
function isSuppressed2(c, ruleId, line) {
|
|
2759
3525
|
return (c.suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ruleId)));
|
|
2760
3526
|
}
|
|
2761
3527
|
function componentRule(opts) {
|
|
@@ -2768,17 +3534,18 @@ function componentRule(opts) {
|
|
|
2768
3534
|
severity,
|
|
2769
3535
|
scope: "component",
|
|
2770
3536
|
rationale: opts.rationale,
|
|
3537
|
+
...opts.fix ? { fix: opts.fix } : {},
|
|
2771
3538
|
async check(ctx) {
|
|
2772
3539
|
const out = [];
|
|
2773
3540
|
for (const c of ctx.components ?? []) {
|
|
2774
3541
|
if (!opts.applies(c)) continue;
|
|
2775
|
-
const bad = opts.bad(c).filter((b) => !(b.line > 0 &&
|
|
3542
|
+
const bad = opts.bad(c).filter((b) => !(b.line > 0 && isSuppressed2(c, opts.id, b.line)));
|
|
2776
3543
|
if (bad.length === 0) {
|
|
2777
3544
|
out.push({
|
|
2778
3545
|
id: opts.id,
|
|
2779
3546
|
category: opts.category,
|
|
2780
3547
|
severity,
|
|
2781
|
-
detection:
|
|
3548
|
+
detection: PASS3,
|
|
2782
3549
|
route: c.file,
|
|
2783
3550
|
message: opts.label,
|
|
2784
3551
|
recommendation: opts.recommendation,
|
|
@@ -2791,13 +3558,14 @@ function componentRule(opts) {
|
|
|
2791
3558
|
id: opts.id,
|
|
2792
3559
|
category: opts.category,
|
|
2793
3560
|
severity,
|
|
2794
|
-
detection:
|
|
3561
|
+
detection: PENALIZED3,
|
|
2795
3562
|
route: c.file,
|
|
2796
3563
|
location: c.file,
|
|
2797
3564
|
...b.line > 0 ? { line: b.line } : {},
|
|
2798
3565
|
message: b.message,
|
|
2799
3566
|
recommendation: opts.recommendation,
|
|
2800
|
-
docsUrl: docsUrl7
|
|
3567
|
+
docsUrl: docsUrl7,
|
|
3568
|
+
...opts.fix ? { fix: { ...opts.fix } } : {}
|
|
2801
3569
|
});
|
|
2802
3570
|
}
|
|
2803
3571
|
}
|
|
@@ -2806,9 +3574,9 @@ function componentRule(opts) {
|
|
|
2806
3574
|
};
|
|
2807
3575
|
}
|
|
2808
3576
|
|
|
2809
|
-
// src/rules/correctness/
|
|
2810
|
-
var
|
|
2811
|
-
id: "
|
|
3577
|
+
// src/rules/correctness/each-key.ts
|
|
3578
|
+
var correctnessEachKey = componentRule({
|
|
3579
|
+
id: "correctness/each-key",
|
|
2812
3580
|
title: "Keyed each block",
|
|
2813
3581
|
category: "correctness",
|
|
2814
3582
|
label: "Keyed {#each}",
|
|
@@ -2817,8 +3585,25 @@ var correct001EachKey = componentRule({
|
|
|
2817
3585
|
applies: (c) => c.eachBlocks.length > 0,
|
|
2818
3586
|
bad: (c) => c.eachBlocks.filter((e) => !e.hasKey).map((e) => ({ line: e.line, message: "{#each} block has no key" }))
|
|
2819
3587
|
});
|
|
2820
|
-
|
|
2821
|
-
|
|
3588
|
+
|
|
3589
|
+
// src/rules/correctness/each-index-key.ts
|
|
3590
|
+
var correctnessEachIndexKey = componentRule({
|
|
3591
|
+
id: "correctness/each-index-key",
|
|
3592
|
+
title: "Index used as each key",
|
|
3593
|
+
category: "correctness",
|
|
3594
|
+
label: "Item-keyed {#each}",
|
|
3595
|
+
recommendation: "Key by a value that uniquely identifies the item, e.g. (item.id).",
|
|
3596
|
+
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.",
|
|
3597
|
+
applies: (c) => c.eachBlocks.some((e) => e.indexKey),
|
|
3598
|
+
bad: (c) => c.eachBlocks.filter((e) => e.indexKey).map((e) => ({
|
|
3599
|
+
line: e.line,
|
|
3600
|
+
message: "{#each} is keyed by its index \u2014 identity follows position, exactly like an unkeyed block, but the key makes it look safe."
|
|
3601
|
+
}))
|
|
3602
|
+
});
|
|
3603
|
+
|
|
3604
|
+
// src/rules/correctness/effect-as-derived.ts
|
|
3605
|
+
var correctnessEffectAsDerived = componentRule({
|
|
3606
|
+
id: "correctness/effect-as-derived",
|
|
2822
3607
|
title: "Effect used to derive state",
|
|
2823
3608
|
category: "correctness",
|
|
2824
3609
|
label: "$effect usage",
|
|
@@ -2827,8 +3612,10 @@ var correct002EffectDerived = componentRule({
|
|
|
2827
3612
|
applies: (c) => c.effects.length > 0,
|
|
2828
3613
|
bad: (c) => c.effects.filter((e) => e.assignsOnlyState).map((e) => ({ line: e.line, message: "$effect only assigns state \u2014 use $derived instead" }))
|
|
2829
3614
|
});
|
|
2830
|
-
|
|
2831
|
-
|
|
3615
|
+
|
|
3616
|
+
// src/rules/correctness/effect-as-onmount.ts
|
|
3617
|
+
var correctnessEffectAsOnMount = componentRule({
|
|
3618
|
+
id: "correctness/effect-as-onmount",
|
|
2832
3619
|
title: "Effect used as onMount",
|
|
2833
3620
|
category: "correctness",
|
|
2834
3621
|
label: "$effect usage",
|
|
@@ -2838,14 +3625,14 @@ var correct003EffectAsOnMount = componentRule({
|
|
|
2838
3625
|
bad: (c) => c.effects.filter((e) => e.mountOnly).map((e) => ({ line: e.line, message: "$effect reads no reactive value \u2014 use onMount instead" }))
|
|
2839
3626
|
});
|
|
2840
3627
|
|
|
2841
|
-
// src/rules/correctness/
|
|
2842
|
-
var
|
|
2843
|
-
id: "
|
|
3628
|
+
// src/rules/correctness/unmutated-state.ts
|
|
3629
|
+
var correctnessUnmutatedState = componentRule({
|
|
3630
|
+
id: "correctness/unmutated-state",
|
|
2844
3631
|
title: "Unmutated $state",
|
|
2845
3632
|
category: "correctness",
|
|
2846
3633
|
severity: "info",
|
|
2847
3634
|
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.",
|
|
3635
|
+
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
3636
|
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
3637
|
applies: (c) => c.constableStates.length > 0,
|
|
2851
3638
|
bad: (c) => c.constableStates.map((s) => ({
|
|
@@ -2854,24 +3641,62 @@ var correct004UnmutatedState = componentRule({
|
|
|
2854
3641
|
}))
|
|
2855
3642
|
});
|
|
2856
3643
|
|
|
2857
|
-
// src/rules/correctness/
|
|
2858
|
-
var
|
|
2859
|
-
id: "
|
|
3644
|
+
// src/rules/correctness/prop-mutation.ts
|
|
3645
|
+
var correctnessPropMutation = componentRule({
|
|
3646
|
+
id: "correctness/prop-mutation",
|
|
2860
3647
|
title: "Mutated non-bindable prop",
|
|
2861
3648
|
category: "correctness",
|
|
2862
3649
|
label: "Prop mutation",
|
|
2863
|
-
recommendation: "
|
|
2864
|
-
rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.",
|
|
3650
|
+
recommendation: "Runes mode: clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it. Legacy mode: reassign the prop after mutating it (e.g. `list = list`) so Svelte's assignment-based reactivity picks up the change.",
|
|
3651
|
+
rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own \u2014 Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically.",
|
|
2865
3652
|
applies: (c) => c.mutatedProps.length > 0,
|
|
2866
3653
|
bad: (c) => c.mutatedProps.map((m) => ({
|
|
2867
3654
|
line: m.line,
|
|
2868
|
-
message: `Prop "${m.name}" is mutated, but it is not declared $bindable`
|
|
3655
|
+
message: m.legacy ? `Prop "${m.name}" is mutated directly \u2014 Svelte's legacy-mode reactivity is assignment-based, so this alone will not update the UI. Reassign it after mutating (e.g. "${m.name} = ${m.name}").` : `Prop "${m.name}" is mutated, but it is not declared $bindable`
|
|
3656
|
+
}))
|
|
3657
|
+
});
|
|
3658
|
+
|
|
3659
|
+
// src/rules/correctness/stale-prop-derivation.ts
|
|
3660
|
+
var correctnessStalePropDerivation = componentRule({
|
|
3661
|
+
id: "correctness/stale-prop-derivation",
|
|
3662
|
+
title: "Stale prop derivation",
|
|
3663
|
+
category: "correctness",
|
|
3664
|
+
severity: "warning",
|
|
3665
|
+
label: "Props derived reactively",
|
|
3666
|
+
recommendation: "Wrap the computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes-mode components; prefix the assignment with $: in legacy-mode components.",
|
|
3667
|
+
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. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job.",
|
|
3668
|
+
fix: {
|
|
3669
|
+
description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression."
|
|
3670
|
+
},
|
|
3671
|
+
applies: (c) => c.stalePropDerivations.length > 0,
|
|
3672
|
+
bad: (c) => c.stalePropDerivations.map((s) => ({
|
|
3673
|
+
line: s.line,
|
|
3674
|
+
message: s.legacy ? `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Prefix the assignment with $: to make it a reactive statement.` : `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
|
|
2869
3675
|
}))
|
|
2870
3676
|
});
|
|
2871
3677
|
|
|
2872
|
-
// src/rules/correctness/
|
|
2873
|
-
var
|
|
2874
|
-
id: "
|
|
3678
|
+
// src/rules/correctness/nonreactive-builtin-state.ts
|
|
3679
|
+
var correctnessNonreactiveBuiltinState = componentRule({
|
|
3680
|
+
id: "correctness/nonreactive-builtin-state",
|
|
3681
|
+
title: "Non-reactive built-in in $state",
|
|
3682
|
+
category: "correctness",
|
|
3683
|
+
severity: "warning",
|
|
3684
|
+
label: "Reactive collections in $state",
|
|
3685
|
+
recommendation: "Import the reactive equivalent from 'svelte/reactivity' (SvelteMap, SvelteSet, SvelteDate, SvelteURL, SvelteURLSearchParams) and construct that instead.",
|
|
3686
|
+
rationale: "$state deep-proxies plain objects and arrays only; built-in collection, date, and URL instances stay untracked, so property-level changes never reach effects, deriveds, or the template. Svelte's own answer is the drop-in classes in svelte/reactivity.",
|
|
3687
|
+
fix: {
|
|
3688
|
+
description: "Import Svelte<Type> from 'svelte/reactivity' and replace new <Type>(...) with new Svelte<Type>(...) \u2014 the API is identical."
|
|
3689
|
+
},
|
|
3690
|
+
applies: (c) => c.nonreactiveBuiltinStates.length > 0,
|
|
3691
|
+
bad: (c) => c.nonreactiveBuiltinStates.map((s) => ({
|
|
3692
|
+
line: s.line,
|
|
3693
|
+
message: `"${s.name}" is a plain ${s.type} in $state \u2014 its mutations are not tracked, so the UI silently stops updating when it changes. Use Svelte${s.type} from 'svelte/reactivity'.`
|
|
3694
|
+
}))
|
|
3695
|
+
});
|
|
3696
|
+
|
|
3697
|
+
// src/rules/correctness/orphan-effect.ts
|
|
3698
|
+
var correctnessOrphanEffect = componentRule({
|
|
3699
|
+
id: "correctness/orphan-effect",
|
|
2875
3700
|
title: "Orphan $effect",
|
|
2876
3701
|
category: "correctness",
|
|
2877
3702
|
severity: "critical",
|
|
@@ -2888,25 +3713,25 @@ var correct006OrphanEffect = componentRule({
|
|
|
2888
3713
|
}))
|
|
2889
3714
|
});
|
|
2890
3715
|
|
|
2891
|
-
// src/rules/correctness/
|
|
2892
|
-
var
|
|
2893
|
-
var
|
|
2894
|
-
var ID = "
|
|
3716
|
+
// src/rules/correctness/orphan-lifecycle.ts
|
|
3717
|
+
var PENALIZED4 = { presence: "none", value: "absent" };
|
|
3718
|
+
var PASS4 = { presence: "own", value: "static" };
|
|
3719
|
+
var ID = "correctness/orphan-lifecycle";
|
|
2895
3720
|
var DOCS_URL = docsUrlFor(ID);
|
|
2896
3721
|
var LABEL = "Lifecycle-call context";
|
|
2897
3722
|
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
3723
|
var topLevelMessage = (name) => `${name}() runs at module evaluation, outside component initialisation \u2014 it throws lifecycle_outside_component at runtime`;
|
|
2899
|
-
function
|
|
3724
|
+
function isSuppressed3(suppressions, line) {
|
|
2900
3725
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID)));
|
|
2901
3726
|
}
|
|
2902
3727
|
function emitFile(out, file, issues, suppressions) {
|
|
2903
|
-
const bad = issues.filter((b) => !(b.line > 0 &&
|
|
3728
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed3(suppressions, b.line)));
|
|
2904
3729
|
if (bad.length === 0) {
|
|
2905
3730
|
out.push({
|
|
2906
3731
|
id: ID,
|
|
2907
3732
|
category: "correctness",
|
|
2908
3733
|
severity: "critical",
|
|
2909
|
-
detection:
|
|
3734
|
+
detection: PASS4,
|
|
2910
3735
|
route: file,
|
|
2911
3736
|
message: LABEL,
|
|
2912
3737
|
recommendation: RECOMMENDATION,
|
|
@@ -2919,7 +3744,7 @@ function emitFile(out, file, issues, suppressions) {
|
|
|
2919
3744
|
id: ID,
|
|
2920
3745
|
category: "correctness",
|
|
2921
3746
|
severity: "critical",
|
|
2922
|
-
detection:
|
|
3747
|
+
detection: PENALIZED4,
|
|
2923
3748
|
route: file,
|
|
2924
3749
|
location: file,
|
|
2925
3750
|
...b.line > 0 ? { line: b.line } : {},
|
|
@@ -2929,7 +3754,7 @@ function emitFile(out, file, issues, suppressions) {
|
|
|
2929
3754
|
});
|
|
2930
3755
|
}
|
|
2931
3756
|
}
|
|
2932
|
-
var
|
|
3757
|
+
var correctnessOrphanLifecycle = {
|
|
2933
3758
|
id: ID,
|
|
2934
3759
|
title: "Lifecycle call outside component initialisation",
|
|
2935
3760
|
category: "correctness",
|
|
@@ -2968,25 +3793,25 @@ var correct007OrphanLifecycle = {
|
|
|
2968
3793
|
}
|
|
2969
3794
|
};
|
|
2970
3795
|
|
|
2971
|
-
// src/rules/correctness/
|
|
2972
|
-
var
|
|
2973
|
-
var
|
|
2974
|
-
var ID2 = "
|
|
3796
|
+
// src/rules/correctness/server-browser-global.ts
|
|
3797
|
+
var PENALIZED5 = { presence: "none", value: "absent" };
|
|
3798
|
+
var PASS5 = { presence: "own", value: "static" };
|
|
3799
|
+
var ID2 = "correctness/server-browser-global";
|
|
2975
3800
|
var DOCS_URL2 = docsUrlFor(ID2);
|
|
2976
3801
|
var LABEL2 = "Server-safe module code";
|
|
2977
3802
|
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
3803
|
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
|
|
3804
|
+
function isSuppressed4(suppressions, line) {
|
|
2980
3805
|
return (suppressions ?? []).some((s) => s.line === line && (!s.ruleIds || s.ruleIds.includes(ID2)));
|
|
2981
3806
|
}
|
|
2982
3807
|
function emitFile2(out, file, issues, suppressions) {
|
|
2983
|
-
const bad = issues.filter((b) => !(b.line > 0 &&
|
|
3808
|
+
const bad = issues.filter((b) => !(b.line > 0 && isSuppressed4(suppressions, b.line)));
|
|
2984
3809
|
if (bad.length === 0) {
|
|
2985
3810
|
out.push({
|
|
2986
3811
|
id: ID2,
|
|
2987
3812
|
category: "correctness",
|
|
2988
3813
|
severity: "critical",
|
|
2989
|
-
detection:
|
|
3814
|
+
detection: PASS5,
|
|
2990
3815
|
route: file,
|
|
2991
3816
|
message: LABEL2,
|
|
2992
3817
|
recommendation: RECOMMENDATION2,
|
|
@@ -2999,7 +3824,7 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
2999
3824
|
id: ID2,
|
|
3000
3825
|
category: "correctness",
|
|
3001
3826
|
severity: "critical",
|
|
3002
|
-
detection:
|
|
3827
|
+
detection: PENALIZED5,
|
|
3003
3828
|
route: file,
|
|
3004
3829
|
location: file,
|
|
3005
3830
|
...b.line > 0 ? { line: b.line } : {},
|
|
@@ -3009,7 +3834,7 @@ function emitFile2(out, file, issues, suppressions) {
|
|
|
3009
3834
|
});
|
|
3010
3835
|
}
|
|
3011
3836
|
}
|
|
3012
|
-
var
|
|
3837
|
+
var correctnessServerBrowserGlobal = {
|
|
3013
3838
|
id: ID2,
|
|
3014
3839
|
title: "Browser global in server module code",
|
|
3015
3840
|
category: "correctness",
|
|
@@ -3045,9 +3870,9 @@ var correct008BrowserGlobals = {
|
|
|
3045
3870
|
}
|
|
3046
3871
|
};
|
|
3047
3872
|
|
|
3048
|
-
// src/rules/correctness/
|
|
3049
|
-
var
|
|
3050
|
-
id: "
|
|
3873
|
+
// src/rules/correctness/instance-browser-global.ts
|
|
3874
|
+
var correctnessInstanceBrowserGlobal = componentRule({
|
|
3875
|
+
id: "correctness/instance-browser-global",
|
|
3051
3876
|
title: "Browser global during component initialisation",
|
|
3052
3877
|
category: "correctness",
|
|
3053
3878
|
label: "Server-safe component init",
|
|
@@ -3060,9 +3885,9 @@ var correct009InstanceBrowserGlobals = componentRule({
|
|
|
3060
3885
|
}))
|
|
3061
3886
|
});
|
|
3062
3887
|
|
|
3063
|
-
// src/rules/security/
|
|
3064
|
-
var
|
|
3065
|
-
id: "
|
|
3888
|
+
// src/rules/security/raw-html.ts
|
|
3889
|
+
var securityRawHtml = componentRule({
|
|
3890
|
+
id: "security/raw-html",
|
|
3066
3891
|
title: "Raw HTML render",
|
|
3067
3892
|
category: "security",
|
|
3068
3893
|
label: "{@html} usage",
|
|
@@ -3071,8 +3896,10 @@ var sec001Html = componentRule({
|
|
|
3071
3896
|
applies: (c) => c.htmlTags.length > 0,
|
|
3072
3897
|
bad: (c) => c.htmlTags.map((h) => ({ line: h.line, message: "{@html} renders unescaped HTML \u2014 ensure it is sanitized" }))
|
|
3073
3898
|
});
|
|
3074
|
-
|
|
3075
|
-
|
|
3899
|
+
|
|
3900
|
+
// src/rules/security/javascript-url.ts
|
|
3901
|
+
var securityJavascriptUrl = componentRule({
|
|
3902
|
+
id: "security/javascript-url",
|
|
3076
3903
|
title: "javascript: URL",
|
|
3077
3904
|
category: "security",
|
|
3078
3905
|
label: "No javascript: URLs",
|
|
@@ -3082,63 +3909,9 @@ var sec002JavascriptUrl = componentRule({
|
|
|
3082
3909
|
bad: (c) => c.javascriptUrls.map((u) => ({ line: u.line, message: "javascript: URL in an attribute" }))
|
|
3083
3910
|
});
|
|
3084
3911
|
|
|
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",
|
|
3912
|
+
// src/rules/security/handler-state-write.ts
|
|
3913
|
+
var securityHandlerStateWrite = kitModuleRule({
|
|
3914
|
+
id: "security/handler-state-write",
|
|
3142
3915
|
title: "Handler writes imported state",
|
|
3143
3916
|
category: "security",
|
|
3144
3917
|
severity: "critical",
|
|
@@ -3152,9 +3925,9 @@ var sec003LoadStateWrite = kitModuleRule({
|
|
|
3152
3925
|
}))
|
|
3153
3926
|
});
|
|
3154
3927
|
|
|
3155
|
-
// src/rules/security/
|
|
3156
|
-
var
|
|
3157
|
-
id: "
|
|
3928
|
+
// src/rules/security/server-module-state.ts
|
|
3929
|
+
var securityServerModuleState = kitModuleRule({
|
|
3930
|
+
id: "security/server-module-state",
|
|
3158
3931
|
title: "Server module-scope state",
|
|
3159
3932
|
category: "security",
|
|
3160
3933
|
label: "Server module state",
|
|
@@ -3167,12 +3940,12 @@ var sec004ServerModuleState = kitModuleRule({
|
|
|
3167
3940
|
}))
|
|
3168
3941
|
});
|
|
3169
3942
|
|
|
3170
|
-
// src/rules/security/
|
|
3943
|
+
// src/rules/security/shared-state-import.ts
|
|
3171
3944
|
function extSibling(path) {
|
|
3172
3945
|
return path.endsWith(".svelte.ts") ? path.replace(/\.svelte\.ts$/, ".svelte.js") : path.replace(/\.svelte\.js$/, ".svelte.ts");
|
|
3173
3946
|
}
|
|
3174
|
-
var
|
|
3175
|
-
id: "
|
|
3947
|
+
var securitySharedStateImport = kitModuleRule({
|
|
3948
|
+
id: "security/shared-state-import",
|
|
3176
3949
|
title: "Shared runes-state import on the server",
|
|
3177
3950
|
category: "security",
|
|
3178
3951
|
label: "Server state imports",
|
|
@@ -3198,11 +3971,10 @@ var sec005SharedStateImport = kitModuleRule({
|
|
|
3198
3971
|
}
|
|
3199
3972
|
});
|
|
3200
3973
|
|
|
3201
|
-
// src/rules/architecture/
|
|
3974
|
+
// src/rules/architecture/component-size.ts
|
|
3202
3975
|
var MAX_LOC = 400;
|
|
3203
|
-
var
|
|
3204
|
-
|
|
3205
|
-
id: "ARCH001",
|
|
3976
|
+
var architectureComponentSize = componentRule({
|
|
3977
|
+
id: "architecture/component-size",
|
|
3206
3978
|
title: "Component size",
|
|
3207
3979
|
category: "architecture",
|
|
3208
3980
|
severity: "info",
|
|
@@ -3213,8 +3985,11 @@ var arch001ComponentSize = componentRule({
|
|
|
3213
3985
|
// skip unanalyzable files (loc 0 = read/parse failure), don't PASS them
|
|
3214
3986
|
bad: (c) => c.loc > MAX_LOC ? [{ line: 1, message: `Component is ${c.loc} lines (over ${MAX_LOC})` }] : []
|
|
3215
3987
|
});
|
|
3216
|
-
|
|
3217
|
-
|
|
3988
|
+
|
|
3989
|
+
// src/rules/architecture/prop-count.ts
|
|
3990
|
+
var MAX_PROPS = 10;
|
|
3991
|
+
var architecturePropCount = componentRule({
|
|
3992
|
+
id: "architecture/prop-count",
|
|
3218
3993
|
title: "Prop count",
|
|
3219
3994
|
category: "architecture",
|
|
3220
3995
|
severity: "info",
|
|
@@ -3226,13 +4001,13 @@ var arch002PropCount = componentRule({
|
|
|
3226
4001
|
bad: (c) => c.propCount > MAX_PROPS ? [{ line: 1, message: `Component takes ${c.propCount} props (over ${MAX_PROPS})` }] : []
|
|
3227
4002
|
});
|
|
3228
4003
|
|
|
3229
|
-
// src/rules/perf/
|
|
4004
|
+
// src/rules/perf/heavy-import.ts
|
|
3230
4005
|
var HEAVY_PACKAGES = {
|
|
3231
4006
|
lodash: "import a submodule (lodash/debounce) or use lodash-es for tree-shaking",
|
|
3232
4007
|
moment: "use a lighter date library (date-fns or dayjs) \u2014 moment is large and not tree-shakeable"
|
|
3233
4008
|
};
|
|
3234
|
-
var
|
|
3235
|
-
id: "
|
|
4009
|
+
var performanceHeavyImport = componentRule({
|
|
4010
|
+
id: "performance/heavy-import",
|
|
3236
4011
|
title: "Heavy dependency import",
|
|
3237
4012
|
category: "performance",
|
|
3238
4013
|
severity: "info",
|
|
@@ -3256,9 +4031,9 @@ var perf009HeavyImport = componentRule({
|
|
|
3256
4031
|
}
|
|
3257
4032
|
});
|
|
3258
4033
|
|
|
3259
|
-
// src/rules/perf/
|
|
3260
|
-
var
|
|
3261
|
-
id: "
|
|
4034
|
+
// src/rules/perf/namespace-import.ts
|
|
4035
|
+
var performanceNamespaceImport = componentRule({
|
|
4036
|
+
id: "performance/namespace-import",
|
|
3262
4037
|
title: "Namespace import",
|
|
3263
4038
|
category: "performance",
|
|
3264
4039
|
severity: "info",
|
|
@@ -3279,68 +4054,169 @@ var perf010NamespaceImport = componentRule({
|
|
|
3279
4054
|
}
|
|
3280
4055
|
});
|
|
3281
4056
|
|
|
4057
|
+
// src/rules/perf/minify-disabled.ts
|
|
4058
|
+
var PENALIZED6 = { presence: "none", value: "absent" };
|
|
4059
|
+
var MINIFY_DISABLED_FIX = {
|
|
4060
|
+
description: "Remove the minify: false override from vite.config (Vite minifies with esbuild by default), or scope it to non-production builds.",
|
|
4061
|
+
snippet: "export default defineConfig({\n build: {\n minify: 'esbuild'\n }\n});",
|
|
4062
|
+
lang: "ts"
|
|
4063
|
+
};
|
|
4064
|
+
var RECOMMENDATION3 = "Remove build.minify: false from vite.config, or scope it to non-production builds if it is intentional.";
|
|
4065
|
+
var performanceMinifyDisabled = {
|
|
4066
|
+
id: "performance/minify-disabled",
|
|
4067
|
+
title: "Minification disabled",
|
|
4068
|
+
category: "performance",
|
|
4069
|
+
severity: "warning",
|
|
4070
|
+
scope: "project",
|
|
4071
|
+
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.",
|
|
4072
|
+
fix: MINIFY_DISABLED_FIX,
|
|
4073
|
+
async check(ctx) {
|
|
4074
|
+
const hit = ctx.project.viteMinifyDisabled;
|
|
4075
|
+
if (!hit) return [];
|
|
4076
|
+
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." : "";
|
|
4077
|
+
return [
|
|
4078
|
+
{
|
|
4079
|
+
id: "performance/minify-disabled",
|
|
4080
|
+
category: "performance",
|
|
4081
|
+
severity: "warning",
|
|
4082
|
+
detection: PENALIZED6,
|
|
4083
|
+
...hit.file !== void 0 ? { location: hit.file } : {},
|
|
4084
|
+
...hit.line !== void 0 ? { line: hit.line } : {},
|
|
4085
|
+
message: "JS/CSS minification is disabled (build.minify: false) \u2014 production bundles ship unminified and several times larger." + provenance,
|
|
4086
|
+
recommendation: RECOMMENDATION3,
|
|
4087
|
+
docsUrl: docsUrlFor("performance/minify-disabled"),
|
|
4088
|
+
fix: { ...MINIFY_DISABLED_FIX }
|
|
4089
|
+
}
|
|
4090
|
+
];
|
|
4091
|
+
}
|
|
4092
|
+
};
|
|
4093
|
+
|
|
4094
|
+
// src/rules/perf/load-waterfall.ts
|
|
4095
|
+
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.";
|
|
4096
|
+
var performanceLoadWaterfall = kitModuleRule({
|
|
4097
|
+
id: "performance/load-waterfall",
|
|
4098
|
+
title: "Load waterfall",
|
|
4099
|
+
category: "performance",
|
|
4100
|
+
severity: "warning",
|
|
4101
|
+
label: "No load waterfalls",
|
|
4102
|
+
recommendation: "Move the dependent await chain into a server load (+page.server.ts / +layout.server.ts), where the hops run server-to-server.",
|
|
4103
|
+
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.",
|
|
4104
|
+
fix: {
|
|
4105
|
+
description: "Move the dependent await chain into a server load (+page.server.ts), where hops run server-to-server.",
|
|
4106
|
+
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}",
|
|
4107
|
+
lang: "ts"
|
|
4108
|
+
},
|
|
4109
|
+
applies: (m) => m.kind === "universal" && m.csrDisabled === void 0 && (m.loadWaterfalls?.dependentLines.length ?? 0) > 0,
|
|
4110
|
+
bad: (m) => m.loadWaterfalls.dependentLines.map((line) => ({ line, message: MESSAGE }))
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
// src/rules/perf/sequential-awaits.ts
|
|
4114
|
+
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.";
|
|
4115
|
+
var performanceSequentialAwaits = kitModuleRule({
|
|
4116
|
+
id: "performance/sequential-awaits",
|
|
4117
|
+
title: "Sequential independent awaits",
|
|
4118
|
+
category: "performance",
|
|
4119
|
+
severity: "info",
|
|
4120
|
+
label: "No needlessly sequential awaits",
|
|
4121
|
+
recommendation: "Start the independent requests together and await them with Promise.all.",
|
|
4122
|
+
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.",
|
|
4123
|
+
fix: {
|
|
4124
|
+
description: "Start the independent requests together and await them with Promise.all.",
|
|
4125
|
+
snippet: "const [a, b] = await Promise.all([fetchA(), fetchB()]);",
|
|
4126
|
+
lang: "ts"
|
|
4127
|
+
},
|
|
4128
|
+
applies: (m) => (m.loadWaterfalls?.independentLines.length ?? 0) > 0,
|
|
4129
|
+
bad: (m) => m.loadWaterfalls.independentLines.map((line) => ({ line, message: MESSAGE2 }))
|
|
4130
|
+
});
|
|
4131
|
+
|
|
4132
|
+
// src/rules/perf/state-raw.ts
|
|
4133
|
+
var performanceStateRaw = componentRule({
|
|
4134
|
+
id: "performance/state-raw",
|
|
4135
|
+
title: "Raw state opportunity",
|
|
4136
|
+
category: "performance",
|
|
4137
|
+
severity: "info",
|
|
4138
|
+
label: "Deep reactivity only where mutated",
|
|
4139
|
+
recommendation: "Declare it with $state.raw(...) \u2014 reassignment stays reactive; only property-level mutation needs the deep proxy.",
|
|
4140
|
+
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.",
|
|
4141
|
+
fix: {
|
|
4142
|
+
description: "Replace $state(...) with $state.raw(...); keep the same initializer."
|
|
4143
|
+
},
|
|
4144
|
+
applies: (c) => c.rawableStates.length > 0,
|
|
4145
|
+
bad: (c) => c.rawableStates.map((s) => ({
|
|
4146
|
+
line: s.line,
|
|
4147
|
+
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).`
|
|
4148
|
+
}))
|
|
4149
|
+
});
|
|
4150
|
+
|
|
3282
4151
|
// src/rules/index.ts
|
|
3283
4152
|
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
|
-
|
|
4153
|
+
seoTitlePresence,
|
|
4154
|
+
seoDescriptionPresence,
|
|
4155
|
+
seoCanonicalUrl,
|
|
4156
|
+
seoOgImage,
|
|
4157
|
+
seoOgTitle,
|
|
4158
|
+
seoRobotsTxt,
|
|
4159
|
+
seoSitemapXml,
|
|
4160
|
+
seoJsonLd,
|
|
4161
|
+
seoHtmlLang,
|
|
4162
|
+
performanceImageDimensions,
|
|
4163
|
+
performanceImageLoadingHint,
|
|
4164
|
+
performancePreloadMissingAs,
|
|
4165
|
+
performanceFontPreloadCrossorigin,
|
|
4166
|
+
seoIndexability,
|
|
4167
|
+
seoTwitterCard,
|
|
4168
|
+
seoOgDescription,
|
|
4169
|
+
seoOgUrl,
|
|
4170
|
+
seoViewport,
|
|
4171
|
+
seoSitemapInRobots,
|
|
4172
|
+
seoJsonLdValidity,
|
|
4173
|
+
seoJsonLdDeprecatedType,
|
|
4174
|
+
seoJsonLdRelativeUrl,
|
|
4175
|
+
seoJsonLdDateFormat,
|
|
4176
|
+
seoJsonLdPlaceholder,
|
|
4177
|
+
seoJsonLdRequiredProps,
|
|
4178
|
+
seoTitleLength,
|
|
4179
|
+
seoDescriptionLength,
|
|
4180
|
+
seoCharset,
|
|
4181
|
+
seoImageAlt,
|
|
4182
|
+
seoHreflang,
|
|
4183
|
+
seoSingleH1,
|
|
4184
|
+
performanceLcpImage,
|
|
4185
|
+
performanceResponsiveImage,
|
|
4186
|
+
performanceRenderBlockingScript,
|
|
4187
|
+
performancePreconnect,
|
|
4188
|
+
seoDuplicateTitle,
|
|
4189
|
+
seoDuplicateDescription,
|
|
4190
|
+
seoHeadingLevelSkip,
|
|
4191
|
+
seoSsrDisabled,
|
|
4192
|
+
correctnessEachKey,
|
|
4193
|
+
correctnessEachIndexKey,
|
|
4194
|
+
correctnessEffectAsDerived,
|
|
4195
|
+
correctnessEffectAsOnMount,
|
|
4196
|
+
correctnessUnmutatedState,
|
|
4197
|
+
correctnessPropMutation,
|
|
4198
|
+
correctnessStalePropDerivation,
|
|
4199
|
+
correctnessNonreactiveBuiltinState,
|
|
4200
|
+
correctnessOrphanEffect,
|
|
4201
|
+
correctnessOrphanLifecycle,
|
|
4202
|
+
correctnessServerBrowserGlobal,
|
|
4203
|
+
correctnessInstanceBrowserGlobal,
|
|
4204
|
+
securityRawHtml,
|
|
4205
|
+
securityJavascriptUrl,
|
|
4206
|
+
securityHandlerStateWrite,
|
|
4207
|
+
securityServerModuleState,
|
|
4208
|
+
securitySharedStateImport,
|
|
4209
|
+
architectureComponentSize,
|
|
4210
|
+
architecturePropCount,
|
|
4211
|
+
performanceHeavyImport,
|
|
4212
|
+
performanceNamespaceImport,
|
|
4213
|
+
performanceMinifyDisabled,
|
|
4214
|
+
performanceLoadWaterfall,
|
|
4215
|
+
performanceSequentialAwaits,
|
|
4216
|
+
performanceStateRaw
|
|
3340
4217
|
];
|
|
3341
4218
|
function explainRule(id) {
|
|
3342
|
-
const
|
|
3343
|
-
const rule = allRules.find((r) => r.id === target);
|
|
4219
|
+
const rule = allRules.find((r) => r.id === id);
|
|
3344
4220
|
if (!rule) return void 0;
|
|
3345
4221
|
return {
|
|
3346
4222
|
id: rule.id,
|
|
@@ -3547,7 +4423,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
3547
4423
|
const p = options.palette ?? noColorPalette;
|
|
3548
4424
|
const summary = summarize(results, config);
|
|
3549
4425
|
const { health, categories: byCat } = computeHealth(results, config);
|
|
3550
|
-
const
|
|
4426
|
+
const present3 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
|
|
3551
4427
|
const lines = [];
|
|
3552
4428
|
if (!options.omitHeader) {
|
|
3553
4429
|
lines.push(
|
|
@@ -3556,7 +4432,7 @@ function formatConsoleReport(results, config, options = {}) {
|
|
|
3556
4432
|
`${p.bold("Health:")} ${scoreColor(p, health)(`${health}/100`)}`
|
|
3557
4433
|
);
|
|
3558
4434
|
}
|
|
3559
|
-
for (const c of
|
|
4435
|
+
for (const c of present3) {
|
|
3560
4436
|
lines.push(scoreLine(p, CATEGORY_LABEL[c] ?? c, byCat[c]));
|
|
3561
4437
|
}
|
|
3562
4438
|
lines.push("");
|
|
@@ -4621,8 +5497,8 @@ export {
|
|
|
4621
5497
|
allRules,
|
|
4622
5498
|
applyOverrides,
|
|
4623
5499
|
applyRuleSeverities,
|
|
4624
|
-
|
|
4625
|
-
|
|
5500
|
+
architectureComponentSize,
|
|
5501
|
+
architecturePropCount,
|
|
4626
5502
|
attrText,
|
|
4627
5503
|
attrTextOf,
|
|
4628
5504
|
attrValue,
|
|
@@ -4634,15 +5510,18 @@ export {
|
|
|
4634
5510
|
collectKitModuleFacts,
|
|
4635
5511
|
computeHealth,
|
|
4636
5512
|
computeScore,
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
5513
|
+
correctnessEachIndexKey,
|
|
5514
|
+
correctnessEachKey,
|
|
5515
|
+
correctnessEffectAsDerived,
|
|
5516
|
+
correctnessEffectAsOnMount,
|
|
5517
|
+
correctnessInstanceBrowserGlobal,
|
|
5518
|
+
correctnessNonreactiveBuiltinState,
|
|
5519
|
+
correctnessOrphanEffect,
|
|
5520
|
+
correctnessOrphanLifecycle,
|
|
5521
|
+
correctnessPropMutation,
|
|
5522
|
+
correctnessServerBrowserGlobal,
|
|
5523
|
+
correctnessStalePropDerivation,
|
|
5524
|
+
correctnessUnmutatedState,
|
|
4646
5525
|
defaultConfig,
|
|
4647
5526
|
defaultProject,
|
|
4648
5527
|
defineConfig,
|
|
@@ -4653,6 +5532,7 @@ export {
|
|
|
4653
5532
|
escapeHtml,
|
|
4654
5533
|
explainRule,
|
|
4655
5534
|
findAttr,
|
|
5535
|
+
findMinifyDisabled,
|
|
4656
5536
|
formatAgentReport,
|
|
4657
5537
|
formatConsoleReport,
|
|
4658
5538
|
formatGithubReport,
|
|
@@ -4669,16 +5549,20 @@ export {
|
|
|
4669
5549
|
noColorPalette,
|
|
4670
5550
|
parseComponentFacts,
|
|
4671
5551
|
parseKitModuleFacts,
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
5552
|
+
performanceFontPreloadCrossorigin,
|
|
5553
|
+
performanceHeavyImport,
|
|
5554
|
+
performanceImageDimensions,
|
|
5555
|
+
performanceImageLoadingHint,
|
|
5556
|
+
performanceLcpImage,
|
|
5557
|
+
performanceLoadWaterfall,
|
|
5558
|
+
performanceMinifyDisabled,
|
|
5559
|
+
performanceNamespaceImport,
|
|
5560
|
+
performancePreconnect,
|
|
5561
|
+
performancePreloadMissingAs,
|
|
5562
|
+
performanceRenderBlockingScript,
|
|
5563
|
+
performanceResponsiveImage,
|
|
5564
|
+
performanceSequentialAwaits,
|
|
5565
|
+
performanceStateRaw,
|
|
4682
5566
|
renderAppShell,
|
|
4683
5567
|
resolveRunesModuleSpecifier,
|
|
4684
5568
|
runRules,
|
|
@@ -4686,42 +5570,43 @@ export {
|
|
|
4686
5570
|
scoreBand,
|
|
4687
5571
|
scoreColor,
|
|
4688
5572
|
scoresByCategory,
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
5573
|
+
securityHandlerStateWrite,
|
|
5574
|
+
securityJavascriptUrl,
|
|
5575
|
+
securityRawHtml,
|
|
5576
|
+
securityServerModuleState,
|
|
5577
|
+
securitySharedStateImport,
|
|
4694
5578
|
selectRules,
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
5579
|
+
seoCanonicalUrl,
|
|
5580
|
+
seoCharset,
|
|
5581
|
+
seoDescriptionLength,
|
|
5582
|
+
seoDescriptionPresence,
|
|
5583
|
+
seoDuplicateDescription,
|
|
5584
|
+
seoDuplicateTitle,
|
|
5585
|
+
seoHeadingLevelSkip,
|
|
5586
|
+
seoHreflang,
|
|
5587
|
+
seoHtmlLang,
|
|
5588
|
+
seoImageAlt,
|
|
5589
|
+
seoIndexability,
|
|
5590
|
+
seoJsonLd,
|
|
5591
|
+
seoJsonLdDateFormat,
|
|
5592
|
+
seoJsonLdDeprecatedType,
|
|
5593
|
+
seoJsonLdPlaceholder,
|
|
5594
|
+
seoJsonLdRelativeUrl,
|
|
5595
|
+
seoJsonLdRequiredProps,
|
|
5596
|
+
seoJsonLdValidity,
|
|
5597
|
+
seoOgDescription,
|
|
5598
|
+
seoOgImage,
|
|
5599
|
+
seoOgTitle,
|
|
5600
|
+
seoOgUrl,
|
|
5601
|
+
seoRobotsTxt,
|
|
5602
|
+
seoSingleH1,
|
|
5603
|
+
seoSitemapInRobots,
|
|
5604
|
+
seoSitemapXml,
|
|
5605
|
+
seoSsrDisabled,
|
|
5606
|
+
seoTitleLength,
|
|
5607
|
+
seoTitlePresence,
|
|
5608
|
+
seoTwitterCard,
|
|
5609
|
+
seoViewport,
|
|
4725
5610
|
summarize,
|
|
4726
5611
|
textFromNodes,
|
|
4727
5612
|
valueFromNodes
|