@svelte-vitals/core 0.27.0 → 0.28.0

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