@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.
Files changed (3) hide show
  1. package/dist/index.d.ts +267 -124
  2. package/dist/index.js +1429 -544
  3. 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 isConstantListEach(node) {
90
- const expr = node?.expression;
91
- return expr?.type === "ArrayExpression" && Array.isArray(expr.elements) && !expr.elements.some((el) => el?.type === "SpreadElement");
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 && !isConstantListEach(node)) {
100
- acc.push({ hasKey: node.key != null, line: lineOf(source, node.start) });
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" && stmt.kind !== "var") {
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
- acc.add(n.left.name);
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)) acc.add(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)) acc.add(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
- const r = rootObjectName(n.argument);
235
- if (r && stateNames.has(r) && !shadowed(r)) acc.add(r);
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)) acc.add(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)) acc.add(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)) acc.add(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)) acc.add(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)) acc.add(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) collectTemplateEscapes(node[key], stateNames, acc);
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 collectNonBindableProps(program) {
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") names.add(p.value.left.name);
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 JS_DIRECTIVE = /^\s*\/\/\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*$/;
447
- var HTML_DIRECTIVE = /^\s*<!--\s*svelte-vitals-disable-next-line(?:\s+([A-Za-z]+\d+(?:\s*,\s*[A-Za-z]+\d+)*))?\s*-->\s*$/;
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().toUpperCase());
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 nonBindableProps = collectNonBindableProps(program);
852
- collectPropMutations(program, nonBindableProps, source, mutatedProps);
853
- if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, mutatedProps);
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 resolveAliasHandlerExports(program, bindings, handlers) {
1461
+ function forEachNamedExport(program, visit) {
999
1462
  for (const stmt of program.body ?? []) {
1000
- if (stmt?.type !== "ExportNamedDeclaration" || !stmt.specifiers || stmt.source || stmt.exportKind === "type")
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
- for (const s of stmt.specifiers) {
1003
- if (s?.exportKind === "type" || s?.exported?.type !== "Identifier" || s?.local?.type !== "Identifier") continue;
1004
- const exportedName = s.exported.name;
1005
- const resolved = bindings.get(s.local.name);
1006
- if (HANDLER_NAMES.has(exportedName) && isFunctionNode(resolved)) {
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
- if (s.exported.name !== "init") continue;
1481
+ bindings ??= collectTopLevelBindings(program);
1021
1482
  const resolved = bindings.get(s.local.name);
1022
- if (isFunctionNode(resolved)) startup.add(resolved);
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
- for (const stmt of program.body ?? []) {
1029
- if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
1030
- const decl = stmt.declaration;
1031
- if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier" && HANDLER_NAMES.has(decl.id.name)) {
1032
- handlers.add(decl);
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
- for (const stmt of program.body ?? []) {
1052
- if (stmt?.type !== "ExportNamedDeclaration" || !stmt.declaration) continue;
1053
- const decl = stmt.declaration;
1054
- if (decl.type === "FunctionDeclaration" && decl.id?.type === "Identifier" && decl.id.name === "init") {
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 hasSsrFalseOptOut(program) {
1069
- const isFalse = (init) => {
1070
- const v = unwrapTs(init);
1071
- return v?.type === "Literal" && v.value === false;
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
- for (const stmt of program.body ?? []) {
1074
- const decl = unwrapExport(stmt);
1075
- if (decl?.type !== "VariableDeclaration") continue;
1076
- for (const d of decl.declarations ?? []) {
1077
- if (d?.id?.type === "Identifier" && d.id.name === "ssr" && d.init && isFalse(d.init)) {
1078
- if (stmt.type === "ExportNamedDeclaration") return true;
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
- const bindings = collectTopLevelBindings(program);
1083
- for (const stmt of program.body ?? []) {
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
- return false;
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
- if (!hasSsrFalseOptOut(program)) {
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/project-paths.ts
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/seo001-title.ts
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 seo001Title = {
1382
- id: "SEO001",
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: "SEO001",
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("SEO001"),
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/seo002-005-008.ts
1448
- var seo002Description = headTagRule({
1449
- id: "SEO002",
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
- var seo003Canonical = headTagRule({
1463
- id: "SEO003",
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
- var seo004OgImage = headTagRule({
1477
- id: "SEO004",
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
- var seo005OgTitle = headTagRule({
1491
- id: "SEO005",
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
- var seo008JsonLd = headTagRule({
1505
- id: "SEO008",
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/project-rules.ts
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 SEO006_FIX = {
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 seo006Robots = {
1531
- id: "SEO006",
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: SEO006_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: "SEO006",
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("SEO006"),
1549
- fix: { ...SEO006_FIX }
2199
+ docsUrl: docsUrlFor("seo/robots-txt"),
2200
+ fix: { ...FIX2 }
1550
2201
  }
1551
2202
  ];
1552
2203
  }
1553
2204
  };
1554
- var SEO007_FIX = {
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 seo007Sitemap = {
1560
- id: "SEO007",
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: SEO007_FIX,
2221
+ fix: FIX3,
1567
2222
  async check(ctx) {
1568
- const detection = ctx.project.hasSitemap ? present : absent;
2223
+ const detection = ctx.project.hasSitemap ? present2 : absent2;
1569
2224
  return [
1570
2225
  {
1571
- id: "SEO007",
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("SEO007"),
1578
- fix: { ...SEO007_FIX }
2232
+ docsUrl: docsUrlFor("seo/sitemap-xml"),
2233
+ fix: { ...FIX3 }
1579
2234
  }
1580
2235
  ];
1581
2236
  }
1582
2237
  };
1583
- var SEO009_FIX = {
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 seo009HtmlLang = {
1589
- id: "SEO009",
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: SEO009_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: "SEO009",
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("SEO009"),
1608
- fix: { ...SEO009_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/images.ts
1666
- var perf001ImageDimensions = imageRule({
1667
- id: "PERF001",
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
- var perf002ImageLoading = imageRule({
1681
- id: "PERF002",
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
- var perf006ResponsiveImage = imageRule({
1695
- id: "PERF006",
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/resource-hints.ts
1763
- var perf003PreloadAs = linkRule({
1764
- id: "PERF003",
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
- var perf004FontPreloadCrossorigin = linkRule({
1779
- id: "PERF004",
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/perf005-lcp-image.ts
1795
- var docsUrl = docsUrlFor("PERF005");
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 perf005LcpImage = {
1798
- id: "PERF005",
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: "PERF005",
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: { ...perf005LcpImage.fix }
2489
+ fix: { ...performanceLcpImage.fix }
1827
2490
  } : {
1828
- id: "PERF005",
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/perf007-render-blocking.ts
1844
- var docsUrl2 = docsUrlFor("PERF007");
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 perf007RenderBlockingScript = {
1847
- id: "PERF007",
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: "PERF007",
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: { ...perf007RenderBlockingScript.fix }
2540
+ fix: { ...performanceRenderBlockingScript.fix }
1878
2541
  });
1879
2542
  }
1880
2543
  } else {
1881
2544
  out.push({
1882
- id: "PERF007",
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/perf008-preconnect.ts
1898
- var docsUrl3 = docsUrlFor("PERF008");
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 perf008Preconnect = {
1906
- id: "PERF008",
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: "PERF008",
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: "PERF008",
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: { ...perf008Preconnect.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/seo010-015.ts
1964
- var SEO010_FIX = {
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 seo010Indexability = {
1970
- id: "SEO010",
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: SEO010_FIX,
2639
+ fix: FIX5,
1977
2640
  async check(ctx) {
1978
- const docsUrl7 = docsUrlFor("SEO010");
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: "SEO010",
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: { ...SEO010_FIX }
2657
+ fix: { ...FIX5 }
1995
2658
  });
1996
2659
  }
1997
2660
  return out;
1998
2661
  }
1999
2662
  };
2000
- var seo011TwitterCard = headTagRule({
2001
- id: "SEO011",
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
- var seo012OgDescription = headTagRule({
2015
- id: "SEO012",
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
- var seo013OgUrl = headTagRule({
2029
- id: "SEO013",
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
- var seo014Viewport = headTagRule({
2043
- id: "SEO014",
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
- var SEO015_FIX = {
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 seo015SitemapInRobots = {
2066
- id: "SEO015",
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: SEO015_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: "SEO015",
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("SEO015"),
2085
- fix: { ...SEO015_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
- var seo017DeprecatedType = jsonldRule({
2334
- id: "SEO017",
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
- var seo018RelativeUrl = jsonldRule({
2346
- id: "SEO018",
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
- var seo019DateFormat = jsonldRule({
2363
- id: "SEO019",
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
- var seo020Placeholder = jsonldRule({
2380
- id: "SEO020",
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
- var seo021RequiredProps = jsonldRule({
2392
- id: "SEO021",
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/seo022-023.ts
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
- var seo022TitleLength = lengthRule({
2471
- id: "SEO022",
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
- var seo023DescriptionLength = lengthRule({
2482
- id: "SEO023",
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/seo024-charset.ts
2494
- var seo024Charset = headTagRule({
2495
- id: "SEO024",
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/seo025-image-alt.ts
2511
- var seo025ImageAlt = imageRule({
2512
- id: "SEO025",
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/seo026-hreflang.ts
2528
- var docsUrl4 = docsUrlFor("SEO026");
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 seo026Hreflang = {
2535
- id: "SEO026",
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: "SEO026",
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: "SEO026",
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/seo027-heading.ts
2586
- var docsUrl5 = docsUrlFor("SEO027");
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 seo027Heading = {
2589
- id: "SEO027",
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: "SEO027",
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: "SEO027",
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/seo028-029-uniqueness.ts
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
- var seo028TitleUnique = uniquenessRule({
2685
- id: "SEO028",
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
- var seo029DescriptionUnique = uniquenessRule({
2694
- id: "SEO029",
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/seo030-heading-order.ts
2704
- var docsUrl6 = docsUrlFor("SEO030");
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 seo030HeadingOrder = {
2707
- id: "SEO030",
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: "SEO030",
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: "SEO030",
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/component-rule.ts
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(c, ruleId, line) {
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 && isSuppressed(c, opts.id, b.line)));
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: PASS2,
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: PENALIZED2,
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/correct001-002.ts
2810
- var correct001EachKey = componentRule({
2811
- id: "CORRECT001",
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
- var correct002EffectDerived = componentRule({
2821
- id: "CORRECT002",
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
- var correct003EffectAsOnMount = componentRule({
2831
- id: "CORRECT003",
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/correct004-unmutated-state.ts
2842
- var correct004UnmutatedState = componentRule({
2843
- id: "CORRECT004",
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/correct005-prop-mutation.ts
2858
- var correct005PropMutation = componentRule({
2859
- id: "CORRECT005",
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: "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.",
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/correct006-orphan-effect.ts
2873
- var correct006OrphanEffect = componentRule({
2874
- id: "CORRECT006",
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/correct007-orphan-lifecycle.ts
2892
- var PENALIZED3 = { presence: "none", value: "absent" };
2893
- var PASS3 = { presence: "own", value: "static" };
2894
- var ID = "CORRECT007";
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 isSuppressed2(suppressions, line) {
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 && isSuppressed2(suppressions, b.line)));
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: PASS3,
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: PENALIZED3,
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 correct007OrphanLifecycle = {
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/correct008-browser-globals.ts
2972
- var PENALIZED4 = { presence: "none", value: "absent" };
2973
- var PASS4 = { presence: "own", value: "static" };
2974
- var ID2 = "CORRECT008";
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 isSuppressed3(suppressions, line) {
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 && isSuppressed3(suppressions, b.line)));
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: PASS4,
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: PENALIZED4,
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 correct008BrowserGlobals = {
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/correct009-instance-browser-globals.ts
3049
- var correct009InstanceBrowserGlobals = componentRule({
3050
- id: "CORRECT009",
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/sec001-002.ts
3064
- var sec001Html = componentRule({
3065
- id: "SEC001",
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
- var sec002JavascriptUrl = componentRule({
3075
- id: "SEC002",
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/kit-module-rule.ts
3086
- var PENALIZED5 = { presence: "none", value: "absent" };
3087
- var PASS5 = { presence: "own", value: "static" };
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/sec004-server-module-state.ts
3156
- var sec004ServerModuleState = kitModuleRule({
3157
- id: "SEC004",
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/sec005-shared-state-import.ts
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 sec005SharedStateImport = kitModuleRule({
3175
- id: "SEC005",
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/arch001-002.ts
3974
+ // src/rules/architecture/component-size.ts
3202
3975
  var MAX_LOC = 400;
3203
- var MAX_PROPS = 10;
3204
- var arch001ComponentSize = componentRule({
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
- var arch002PropCount = componentRule({
3217
- id: "ARCH002",
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/perf009-heavy-import.ts
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 perf009HeavyImport = componentRule({
3235
- id: "PERF009",
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/perf010-namespace-import.ts
3260
- var perf010NamespaceImport = componentRule({
3261
- id: "PERF010",
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
- seo001Title,
3285
- seo002Description,
3286
- seo003Canonical,
3287
- seo004OgImage,
3288
- seo005OgTitle,
3289
- seo006Robots,
3290
- seo007Sitemap,
3291
- seo008JsonLd,
3292
- seo009HtmlLang,
3293
- perf001ImageDimensions,
3294
- perf002ImageLoading,
3295
- perf003PreloadAs,
3296
- perf004FontPreloadCrossorigin,
3297
- seo010Indexability,
3298
- seo011TwitterCard,
3299
- seo012OgDescription,
3300
- seo013OgUrl,
3301
- seo014Viewport,
3302
- seo015SitemapInRobots,
3303
- seo016JsonLdValidity,
3304
- seo017DeprecatedType,
3305
- seo018RelativeUrl,
3306
- seo019DateFormat,
3307
- seo020Placeholder,
3308
- seo021RequiredProps,
3309
- seo022TitleLength,
3310
- seo023DescriptionLength,
3311
- seo024Charset,
3312
- seo025ImageAlt,
3313
- seo026Hreflang,
3314
- seo027Heading,
3315
- perf005LcpImage,
3316
- perf006ResponsiveImage,
3317
- perf007RenderBlockingScript,
3318
- perf008Preconnect,
3319
- seo028TitleUnique,
3320
- seo029DescriptionUnique,
3321
- seo030HeadingOrder,
3322
- correct001EachKey,
3323
- correct002EffectDerived,
3324
- correct003EffectAsOnMount,
3325
- correct004UnmutatedState,
3326
- correct005PropMutation,
3327
- correct006OrphanEffect,
3328
- correct007OrphanLifecycle,
3329
- correct008BrowserGlobals,
3330
- correct009InstanceBrowserGlobals,
3331
- sec001Html,
3332
- sec002JavascriptUrl,
3333
- sec003LoadStateWrite,
3334
- sec004ServerModuleState,
3335
- sec005SharedStateImport,
3336
- arch001ComponentSize,
3337
- arch002PropCount,
3338
- perf009HeavyImport,
3339
- perf010NamespaceImport
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 target = id.toUpperCase();
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 present2 = CATEGORY_ORDER.filter((c) => byCat[c] !== void 0);
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 present2) {
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
- arch001ComponentSize,
4625
- arch002PropCount,
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
- correct001EachKey,
4638
- correct002EffectDerived,
4639
- correct003EffectAsOnMount,
4640
- correct004UnmutatedState,
4641
- correct005PropMutation,
4642
- correct006OrphanEffect,
4643
- correct007OrphanLifecycle,
4644
- correct008BrowserGlobals,
4645
- correct009InstanceBrowserGlobals,
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
- perf001ImageDimensions,
4673
- perf002ImageLoading,
4674
- perf003PreloadAs,
4675
- perf004FontPreloadCrossorigin,
4676
- perf005LcpImage,
4677
- perf006ResponsiveImage,
4678
- perf007RenderBlockingScript,
4679
- perf008Preconnect,
4680
- perf009HeavyImport,
4681
- perf010NamespaceImport,
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
- sec001Html,
4690
- sec002JavascriptUrl,
4691
- sec003LoadStateWrite,
4692
- sec004ServerModuleState,
4693
- sec005SharedStateImport,
5573
+ securityHandlerStateWrite,
5574
+ securityJavascriptUrl,
5575
+ securityRawHtml,
5576
+ securityServerModuleState,
5577
+ securitySharedStateImport,
4694
5578
  selectRules,
4695
- seo001Title,
4696
- seo002Description,
4697
- seo003Canonical,
4698
- seo004OgImage,
4699
- seo005OgTitle,
4700
- seo006Robots,
4701
- seo007Sitemap,
4702
- seo008JsonLd,
4703
- seo009HtmlLang,
4704
- seo010Indexability,
4705
- seo011TwitterCard,
4706
- seo012OgDescription,
4707
- seo013OgUrl,
4708
- seo014Viewport,
4709
- seo015SitemapInRobots,
4710
- seo016JsonLdValidity,
4711
- seo017DeprecatedType,
4712
- seo018RelativeUrl,
4713
- seo019DateFormat,
4714
- seo020Placeholder,
4715
- seo021RequiredProps,
4716
- seo022TitleLength,
4717
- seo023DescriptionLength,
4718
- seo024Charset,
4719
- seo025ImageAlt,
4720
- seo026Hreflang,
4721
- seo027Heading,
4722
- seo028TitleUnique,
4723
- seo029DescriptionUnique,
4724
- seo030HeadingOrder,
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