@svelte-vitals/core 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -328,21 +328,29 @@ interface ComponentFacts {
328
328
  name: string;
329
329
  line: number;
330
330
  }[];
331
- /** Mutations of a non-`$bindable` prop from `$props()` — member writes, `delete`, or a mutating method call (correctness/prop-mutation). */
331
+ /** Mutations of a non-`$bindable` prop from `$props()`, or a legacy `export let` prop — member writes, `delete`, or a mutating method call (correctness/prop-mutation). `legacy` distinguishes which mode the prop was declared in (absent/false: `$props()`), since the fix differs — optional so existing external constructors of `ComponentFacts` are unaffected. */
332
332
  mutatedProps: {
333
333
  name: string;
334
334
  line: number;
335
+ legacy?: boolean;
335
336
  }[];
336
- /** Top-level const/let bindings computed from a $props() prop without $derived, never reassigned or escaped, and referenced (eagerly) in the template — frozen at init (correctness/stale-prop-derivation). */
337
+ /** Top-level const/let bindings computed from a $props() or legacy `export let` prop without $derived (or `$:`), never reassigned or escaped, and referenced (eagerly) in the template — frozen at init (correctness/stale-prop-derivation). `legacy` distinguishes which mode the prop was declared in, since the fix differs — optional so existing external constructors of `ComponentFacts` are unaffected. */
337
338
  stalePropDerivations: {
338
339
  name: string;
339
340
  line: number;
341
+ legacy?: boolean;
340
342
  }[];
341
343
  /** Object/array-literal $state bindings reassigned at least once but never mutated, escaped, aliased, or item-edited — $state.raw candidates (performance/state-raw). */
342
344
  rawableStates: {
343
345
  name: string;
344
346
  line: number;
345
347
  }[];
348
+ /** Plain built-in instances (Map/Set/Date/URL/URLSearchParams) in $state whose type-specific mutations were observed inside functions, with no exempting reassignment — untracked by reactivity (correctness/nonreactive-builtin-state). */
349
+ nonreactiveBuiltinStates: {
350
+ name: string;
351
+ type: string;
352
+ line: number;
353
+ }[];
346
354
  /** `$effect` calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-effect). */
347
355
  orphanEffects: OrphanEffectFact[];
348
356
  /** Svelte lifecycle/context calls guaranteed to run outside component initialisation — module scope in `.svelte.ts`/`.svelte.js` or `<script module>` (correctness/orphan-lifecycle). */
@@ -715,15 +723,34 @@ declare const correctnessEffectAsOnMount: Rule;
715
723
 
716
724
  declare const correctnessUnmutatedState: Rule;
717
725
 
726
+ /**
727
+ * correctness/prop-mutation — mutating a prop directly is a silent bug in both Svelte modes,
728
+ * for different reasons: in runes mode, a non-$bindable prop mutation doesn't propagate to the
729
+ * parent; in legacy mode (export let), Svelte's reactivity is assignment-based, so a mutating
730
+ * method call (`.push(...)`, etc.) doesn't trigger an update at all without a following
731
+ * reassignment. The two modes can't be mixed in one component, so a given finding is always
732
+ * exactly one or the other — see `legacy` on `ComponentFacts.mutatedProps` (component-parse.ts).
733
+ */
718
734
  declare const correctnessPropMutation: Rule;
719
735
 
720
736
  /**
721
- * correctness/stale-prop-derivation — a value computed from a prop without
722
- * $derived is evaluated once, at init, and silently stops tracking the parent.
723
- * Svelte's own guidance: treat props as though they will change.
737
+ * correctness/stale-prop-derivation — a value computed from a prop without $derived (runes
738
+ * mode) or $: (legacy mode) is evaluated once, at init, and silently stops tracking the
739
+ * parent. Svelte's own guidance: treat props as though they will change. The two modes can't
740
+ * be mixed in one component, so a given finding is always exactly one or the other — see
741
+ * `legacy` on `ComponentFacts.stalePropDerivations` (component-parse.ts).
724
742
  */
725
743
  declare const correctnessStalePropDerivation: Rule;
726
744
 
745
+ /**
746
+ * correctness/nonreactive-builtin-state — $state's deep proxy covers plain
747
+ * objects and arrays only. A plain Map/Set/Date/URL/URLSearchParams in $state
748
+ * keeps working as data, but its mutations never reach effects, deriveds, or
749
+ * the template: the UI silently stops updating. svelte/reactivity ships
750
+ * drop-in reactive equivalents for exactly this.
751
+ */
752
+ declare const correctnessNonreactiveBuiltinState: Rule;
753
+
727
754
  declare const correctnessOrphanEffect: Rule;
728
755
 
729
756
  /**
@@ -1088,4 +1115,4 @@ declare function applyRuleSeverities(results: Result[], config: Config): Result[
1088
1115
  */
1089
1116
  declare function applyOverrides(results: Result[], config: Config): Result[];
1090
1117
 
1091
- export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type KitModuleFacts, type OrphanEffectFact, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleOverride, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyOverrides, applyRuleSeverities, architectureComponentSize, architecturePropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, computeHealth, computeScore, correctnessEachIndexKey, correctnessEachKey, correctnessEffectAsDerived, correctnessEffectAsOnMount, correctnessInstanceBrowserGlobal, correctnessOrphanEffect, correctnessOrphanLifecycle, correctnessPropMutation, correctnessServerBrowserGlobal, correctnessStalePropDerivation, correctnessUnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, findMinifyDisabled, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, parseKitModuleFacts, performanceFontPreloadCrossorigin, performanceHeavyImport, performanceImageDimensions, performanceImageLoadingHint, performanceLcpImage, performanceLoadWaterfall, performanceMinifyDisabled, performanceNamespaceImport, performancePreconnect, performancePreloadMissingAs, performanceRenderBlockingScript, performanceResponsiveImage, performanceSequentialAwaits, performanceStateRaw, renderAppShell, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, securityHandlerStateWrite, securityJavascriptUrl, securityRawHtml, securityServerModuleState, securitySharedStateImport, selectRules, seoCanonicalUrl, seoCharset, seoDescriptionLength, seoDescriptionPresence, seoDuplicateDescription, seoDuplicateTitle, seoHeadingLevelSkip, seoHreflang, seoHtmlLang, seoImageAlt, seoIndexability, seoJsonLd, seoJsonLdDateFormat, seoJsonLdDeprecatedType, seoJsonLdPlaceholder, seoJsonLdRelativeUrl, seoJsonLdRequiredProps, seoJsonLdValidity, seoOgDescription, seoOgImage, seoOgTitle, seoOgUrl, seoRobotsTxt, seoSingleH1, seoSitemapInRobots, seoSitemapXml, seoSsrDisabled, seoTitleLength, seoTitlePresence, seoTwitterCard, seoViewport, summarize, textFromNodes, valueFromNodes };
1118
+ export { APP_SCRIPT, APP_STYLE, type AppSnapshot, BAND_COLOR, CHILD_NODE_KEYS, type Category, type Classification, type ComponentFacts, type Config, type ConsoleReportOptions, type Detection, type EachBlockFact, type EffectFact, type Fix, type HeadProvider, type HeadTag, type HeadingInfo, type HealthResult, type ImageInfo, type JsonReport, type KitModuleFacts, type OrphanEffectFact, type Palette, type Presence, type Project, ROBOTS_SOURCE_PATHS, type ResolvedHead, type ResolvedHeadings, type ResolvedImages, type Result, type RouteBadge, type Rule, type RuleContext, type RuleInfo, type RuleOverride, type RuleSetting, type Runtime, SITEMAP_SOURCE_PATHS, type Scope, type ScoreModel, type ScoreOptions, type ScoreResult, type Severity, type SourceSpan, type Summary, type SuppressionDirective, type TreatDynamicAs, type Value, allRules, applyOverrides, applyRuleSeverities, architectureComponentSize, architecturePropCount, attrText, attrTextOf, attrValue, attrValueOf, buildHtmlDocument, buildJsonReport, classify, collectComponentFacts, collectKitModuleFacts, computeHealth, computeScore, correctnessEachIndexKey, correctnessEachKey, correctnessEffectAsDerived, correctnessEffectAsOnMount, correctnessInstanceBrowserGlobal, correctnessNonreactiveBuiltinState, correctnessOrphanEffect, correctnessOrphanLifecycle, correctnessPropMutation, correctnessServerBrowserGlobal, correctnessStalePropDerivation, correctnessUnmutatedState, defaultConfig, defaultProject, defineConfig, docsUrlFor, effectiveSeverity, emptyComponentFacts, emptyKitModuleFacts, escapeHtml, explainRule, findAttr, findMinifyDisabled, formatAgentReport, formatConsoleReport, formatGithubReport, formatHtmlReport, formatJsonReport, formatMarkdownReport, formatSarifReport, hasFailureAtOrAbove, headTagRule, imageRule, isPenalized, lineOf, linkRule, noColorPalette, parseComponentFacts, parseKitModuleFacts, performanceFontPreloadCrossorigin, performanceHeavyImport, performanceImageDimensions, performanceImageLoadingHint, performanceLcpImage, performanceLoadWaterfall, performanceMinifyDisabled, performanceNamespaceImport, performancePreconnect, performancePreloadMissingAs, performanceRenderBlockingScript, performanceResponsiveImage, performanceSequentialAwaits, performanceStateRaw, renderAppShell, resolveRunesModuleSpecifier, runRules, safeHref, scoreBand, scoreColor, scoresByCategory, securityHandlerStateWrite, securityJavascriptUrl, securityRawHtml, securityServerModuleState, securitySharedStateImport, selectRules, seoCanonicalUrl, seoCharset, seoDescriptionLength, seoDescriptionPresence, seoDuplicateDescription, seoDuplicateTitle, seoHeadingLevelSkip, seoHreflang, seoHtmlLang, seoImageAlt, seoIndexability, seoJsonLd, seoJsonLdDateFormat, seoJsonLdDeprecatedType, seoJsonLdPlaceholder, seoJsonLdRelativeUrl, seoJsonLdRequiredProps, seoJsonLdValidity, seoOgDescription, seoOgImage, seoOgTitle, seoOgUrl, seoRobotsTxt, seoSingleH1, seoSitemapInRobots, seoSitemapXml, seoSsrDisabled, seoTitleLength, seoTitlePresence, seoTwitterCard, seoViewport, summarize, textFromNodes, valueFromNodes };
package/dist/index.js CHANGED
@@ -245,8 +245,10 @@ function scopeIntroducedNames(node) {
245
245
  addBoundNames(node.param, introduced);
246
246
  } else if (node.type === "BlockStatement") {
247
247
  for (const stmt of node.body ?? []) {
248
- if (stmt?.type === "VariableDeclaration" && stmt.kind !== "var") {
248
+ if (stmt?.type === "VariableDeclaration") {
249
249
  for (const d of stmt.declarations ?? []) addBoundNames(d.id, introduced);
250
+ } else if ((stmt?.type === "FunctionDeclaration" || stmt?.type === "ClassDeclaration") && typeof stmt.id?.name === "string") {
251
+ introduced.add(stmt.id.name);
250
252
  }
251
253
  }
252
254
  } else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
@@ -330,6 +332,81 @@ function isDeferredBody(n) {
330
332
  function isPlainStateCall(node) {
331
333
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "$state";
332
334
  }
335
+ var BUILTIN_STATE_TYPES = /* @__PURE__ */ new Set(["Map", "Set", "Date", "URL", "URLSearchParams"]);
336
+ var BUILTIN_MUTATIONS = {
337
+ Map: /* @__PURE__ */ new Set(["set", "delete", "clear"]),
338
+ Set: /* @__PURE__ */ new Set(["add", "delete", "clear"]),
339
+ Date: /* @__PURE__ */ new Set([
340
+ "setTime",
341
+ "setFullYear",
342
+ "setMonth",
343
+ "setDate",
344
+ "setHours",
345
+ "setMinutes",
346
+ "setSeconds",
347
+ "setMilliseconds",
348
+ "setYear",
349
+ "setUTCFullYear",
350
+ "setUTCMonth",
351
+ "setUTCDate",
352
+ "setUTCHours",
353
+ "setUTCMinutes",
354
+ "setUTCSeconds",
355
+ "setUTCMilliseconds"
356
+ ]),
357
+ URL: /* @__PURE__ */ new Set(),
358
+ URLSearchParams: /* @__PURE__ */ new Set(["append", "set", "delete", "sort"])
359
+ };
360
+ function collectBuiltinStateSignals(node, candidates, mutated, reassigned, shadowed = /* @__PURE__ */ new Set(), inFunction = false) {
361
+ if (Array.isArray(node)) {
362
+ for (const child of node) collectBuiltinStateSignals(child, candidates, mutated, reassigned, shadowed, inFunction);
363
+ return;
364
+ }
365
+ if (!node || typeof node !== "object" || typeof node.type !== "string") return;
366
+ const introduced = scopeIntroducedNames(node);
367
+ const scope = introduced.size > 0 ? /* @__PURE__ */ new Set([...shadowed, ...introduced]) : shadowed;
368
+ const boundary = isDeferredBody(node) || node.type === "ClassDeclaration" || node.type === "ClassExpression";
369
+ const nextInFunction = inFunction || boundary;
370
+ const hit = (name) => typeof name === "string" && candidates.has(name) && !scope.has(name) ? name : void 0;
371
+ if (node.type === "AssignmentExpression") {
372
+ if (node.left?.type === "Identifier") {
373
+ const n = hit(node.left.name);
374
+ const isBareSelfAssign = node.right?.type === "Identifier" && node.right.name === n;
375
+ if (n && !isBareSelfAssign) reassigned.add(n);
376
+ } else if (node.left?.type === "ObjectPattern" || node.left?.type === "ArrayPattern") {
377
+ const bound = /* @__PURE__ */ new Set();
378
+ addBoundNames(node.left, bound);
379
+ for (const name of bound) {
380
+ const n = hit(name);
381
+ if (n) reassigned.add(n);
382
+ }
383
+ } else if (node.left?.type === "MemberExpression" && inFunction) {
384
+ const n = hit(rootObjectName(node.left));
385
+ if (n && candidates.get(n) === "URL") mutated.add(n);
386
+ }
387
+ } else if (node.type === "UpdateExpression" && node.argument?.type === "MemberExpression" && inFunction) {
388
+ const n = hit(rootObjectName(node.argument));
389
+ if (n && candidates.get(n) === "URL") mutated.add(n);
390
+ } else if (node.type === "UnaryExpression" && node.operator === "delete" && inFunction) {
391
+ const n = hit(rootObjectName(node.argument));
392
+ if (n && candidates.get(n) === "URL") mutated.add(n);
393
+ } else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && inFunction) {
394
+ const method = node.callee.property?.name;
395
+ if (typeof method === "string") {
396
+ if (node.callee.object?.type === "Identifier") {
397
+ const n = hit(node.callee.object.name);
398
+ if (n && BUILTIN_MUTATIONS[candidates.get(n)]?.has(method)) mutated.add(n);
399
+ } else if (node.callee.object?.type === "MemberExpression") {
400
+ const n = hit(rootObjectName(node.callee));
401
+ if (n && candidates.get(n) === "URL" && BUILTIN_MUTATIONS.URLSearchParams.has(method)) mutated.add(n);
402
+ }
403
+ }
404
+ }
405
+ for (const key of Object.keys(node)) {
406
+ if (WALK_IGNORED_KEYS.has(key)) continue;
407
+ collectBuiltinStateSignals(node[key], candidates, mutated, reassigned, scope, nextInFunction);
408
+ }
409
+ }
333
410
  function collectPatternAliasRefs(node, names, acc, scope, ownRhs) {
334
411
  if (!node || typeof node !== "object" || typeof node.type !== "string") return;
335
412
  if (node.type === "Identifier") return;
@@ -651,6 +728,16 @@ function collectPropNames(program, includeBindable) {
651
728
  });
652
729
  return ambiguous || seen > 1 ? /* @__PURE__ */ new Set() : names;
653
730
  }
731
+ function collectLegacyPropNames(program) {
732
+ const names = /* @__PURE__ */ new Set();
733
+ for (const stmt of program.body ?? []) {
734
+ if (stmt?.type !== "ExportNamedDeclaration" || stmt.declaration?.type !== "VariableDeclaration") continue;
735
+ for (const d of stmt.declaration.declarations ?? []) {
736
+ if (d?.id?.type === "Identifier") names.add(d.id.name);
737
+ }
738
+ }
739
+ return names;
740
+ }
654
741
  var MUTATING_METHODS = /* @__PURE__ */ new Set([
655
742
  "push",
656
743
  "pop",
@@ -1095,6 +1182,7 @@ function parseModuleFacts(source, filename) {
1095
1182
  mutatedProps: [],
1096
1183
  stalePropDerivations: [],
1097
1184
  rawableStates: [],
1185
+ nonreactiveBuiltinStates: [],
1098
1186
  suppressions: collectSuppressions(source),
1099
1187
  orphanEffects,
1100
1188
  orphanLifecycleCalls,
@@ -1132,16 +1220,20 @@ function parseComponentFacts(source, filename) {
1132
1220
  const mutatedProps = [];
1133
1221
  const stalePropDerivations = [];
1134
1222
  const rawableStates = [];
1223
+ const nonreactiveBuiltinStates = [];
1135
1224
  let propCount = 0;
1136
1225
  const program = ast.instance?.content;
1137
1226
  if (program) {
1138
1227
  collectImportSources(program, source, importSpans);
1139
1228
  collectNamespaceImports(program, source, namespaceImports);
1140
1229
  propCount = countProps(program);
1141
- const nonBindableProps = collectPropNames(program, false);
1142
- collectPropMutations(program, nonBindableProps, source, mutatedProps);
1143
- if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, mutatedProps);
1144
- const allPropNames = collectPropNames(program, true);
1230
+ const legacyPropNames = collectLegacyPropNames(program);
1231
+ const nonBindableProps = /* @__PURE__ */ new Set([...collectPropNames(program, false), ...legacyPropNames]);
1232
+ const rawMutations = [];
1233
+ collectPropMutations(program, nonBindableProps, source, rawMutations);
1234
+ if (ast.fragment) collectPropMutations(ast.fragment, nonBindableProps, source, rawMutations);
1235
+ for (const m of rawMutations) mutatedProps.push(legacyPropNames.has(m.name) ? { ...m, legacy: true } : m);
1236
+ const allPropNames = /* @__PURE__ */ new Set([...collectPropNames(program, true), ...legacyPropNames]);
1145
1237
  if (allPropNames.size > 0) {
1146
1238
  const candidates = collectStalePropCandidates(program, allPropNames, source);
1147
1239
  if (candidates.length > 0) {
@@ -1154,8 +1246,11 @@ function parseComponentFacts(source, filename) {
1154
1246
  }
1155
1247
  const referenced = /* @__PURE__ */ new Set();
1156
1248
  if (ast.fragment) collectFragmentRefs(ast.fragment, candidateNames, referenced);
1249
+ const isLegacy = legacyPropNames.size > 0;
1157
1250
  for (const c of candidates) {
1158
- if (!disqualified.has(c.name) && referenced.has(c.name)) stalePropDerivations.push(c);
1251
+ if (!disqualified.has(c.name) && referenced.has(c.name)) {
1252
+ stalePropDerivations.push(isLegacy ? { ...c, legacy: true } : c);
1253
+ }
1159
1254
  }
1160
1255
  }
1161
1256
  }
@@ -1186,6 +1281,7 @@ function parseComponentFacts(source, filename) {
1186
1281
  if (ast.fragment) {
1187
1282
  collectStateWrites(ast.fragment, stateNames, writtenOrEscaped);
1188
1283
  collectTemplateEscapes(ast.fragment, stateNames, writtenOrEscaped);
1284
+ collectDirectiveEscapes(ast.fragment, stateNames, writtenOrEscaped);
1189
1285
  }
1190
1286
  for (const d of stateDecls) {
1191
1287
  if (!writtenOrEscaped.has(d.name)) constableStates.push(d);
@@ -1225,6 +1321,29 @@ function parseComponentFacts(source, filename) {
1225
1321
  if (reassigned && !dirty) rawableStates.push(c);
1226
1322
  }
1227
1323
  }
1324
+ const builtinCandidates = /* @__PURE__ */ new Map();
1325
+ for (const stmt of program.body ?? []) {
1326
+ if (stmt?.type !== "VariableDeclaration") continue;
1327
+ for (const d of stmt.declarations ?? []) {
1328
+ if (d?.id?.type !== "Identifier" || !d.init || !isPlainStateCall(d.init)) continue;
1329
+ const arg = unwrapTs(d.init.arguments?.[0]);
1330
+ if (arg?.type === "NewExpression" && arg.callee?.type === "Identifier" && BUILTIN_STATE_TYPES.has(arg.callee.name)) {
1331
+ builtinCandidates.set(d.id.name, { type: arg.callee.name, line: lineOf(source, d.start) });
1332
+ }
1333
+ }
1334
+ }
1335
+ if (builtinCandidates.size > 0) {
1336
+ const types = new Map([...builtinCandidates].map(([n, meta]) => [n, meta.type]));
1337
+ const mutatedBuiltins = /* @__PURE__ */ new Set();
1338
+ const reassignedBuiltins = /* @__PURE__ */ new Set();
1339
+ collectBuiltinStateSignals(program, types, mutatedBuiltins, reassignedBuiltins);
1340
+ if (ast.fragment) collectBuiltinStateSignals(ast.fragment, types, mutatedBuiltins, reassignedBuiltins);
1341
+ for (const [name, meta] of builtinCandidates) {
1342
+ if (mutatedBuiltins.has(name) && !reassignedBuiltins.has(name)) {
1343
+ nonreactiveBuiltinStates.push({ name, type: meta.type, line: meta.line });
1344
+ }
1345
+ }
1346
+ }
1228
1347
  let moduleExtra;
1229
1348
  if (moduleProgram) {
1230
1349
  const moduleBrowserImports = collectBrowserGuardImports(moduleProgram);
@@ -1252,6 +1371,7 @@ function parseComponentFacts(source, filename) {
1252
1371
  mutatedProps,
1253
1372
  stalePropDerivations,
1254
1373
  rawableStates,
1374
+ nonreactiveBuiltinStates,
1255
1375
  orphanEffects,
1256
1376
  orphanLifecycleCalls,
1257
1377
  browserGlobalRefs,
@@ -1277,6 +1397,7 @@ function emptyComponentFacts(file) {
1277
1397
  mutatedProps: [],
1278
1398
  stalePropDerivations: [],
1279
1399
  rawableStates: [],
1400
+ nonreactiveBuiltinStates: [],
1280
1401
  orphanEffects: [],
1281
1402
  orphanLifecycleCalls: [],
1282
1403
  browserGlobalRefs: [],
@@ -3526,12 +3647,12 @@ var correctnessPropMutation = componentRule({
3526
3647
  title: "Mutated non-bindable prop",
3527
3648
  category: "correctness",
3528
3649
  label: "Prop mutation",
3529
- recommendation: "Clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it.",
3530
- rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. Neither is caught by the compiler, so this rule catches both statically.",
3650
+ recommendation: "Runes mode: clone the value before mutating it, communicate the change via a callback prop, or declare the prop $bindable if the parent and child should share it. Legacy mode: reassign the prop after mutating it (e.g. `list = list`) so Svelte's assignment-based reactivity picks up the change.",
3651
+ rationale: "Svelte's docs say plainly: don't mutate props unless they are $bindable. A plain-object prop mutation is a silent no-op (the object isn't a state proxy); a reactive-state-proxy prop mutation works but triggers the ownership_invalid_mutation dev warning only when that code path actually runs. In legacy mode, mutating methods like .push()/.splice() never trigger an update on their own \u2014 Svelte's reactivity there is based on assignments, not mutations. Neither case is caught by the compiler, so this rule catches both statically.",
3531
3652
  applies: (c) => c.mutatedProps.length > 0,
3532
3653
  bad: (c) => c.mutatedProps.map((m) => ({
3533
3654
  line: m.line,
3534
- message: `Prop "${m.name}" is mutated, but it is not declared $bindable`
3655
+ message: m.legacy ? `Prop "${m.name}" is mutated directly \u2014 Svelte's legacy-mode reactivity is assignment-based, so this alone will not update the UI. Reassign it after mutating (e.g. "${m.name} = ${m.name}").` : `Prop "${m.name}" is mutated, but it is not declared $bindable`
3535
3656
  }))
3536
3657
  });
3537
3658
 
@@ -3542,15 +3663,34 @@ var correctnessStalePropDerivation = componentRule({
3542
3663
  category: "correctness",
3543
3664
  severity: "warning",
3544
3665
  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.",
3666
+ recommendation: "Wrap the computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes-mode components; prefix the assignment with $: in legacy-mode components.",
3667
+ rationale: "Svelte's guidance is to treat props as though they will change: a plain `let color = type === 'danger' ? 'red' : 'green'` freezes the first render's value, so the UI silently stops tracking the parent when the prop changes. In runes mode, $derived keeps the computation live at no cost; in legacy mode (export let props), a $: reactive statement does the same job.",
3547
3668
  fix: {
3548
- description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body), keeping the same expression."
3669
+ description: "Wrap the prop-derived computation in $derived(...) (or $derived.by(() => ...) for a function body) in runes mode, or prefix the assignment with $: in legacy mode, keeping the same expression."
3549
3670
  },
3550
3671
  applies: (c) => c.stalePropDerivations.length > 0,
3551
3672
  bad: (c) => c.stalePropDerivations.map((s) => ({
3552
3673
  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.`
3674
+ message: s.legacy ? `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Prefix the assignment with $: to make it a reactive statement.` : `"${s.name}" is computed from a prop once, at initialization \u2014 it will not update when the prop changes. Wrap it in $derived.`
3675
+ }))
3676
+ });
3677
+
3678
+ // src/rules/correctness/nonreactive-builtin-state.ts
3679
+ var correctnessNonreactiveBuiltinState = componentRule({
3680
+ id: "correctness/nonreactive-builtin-state",
3681
+ title: "Non-reactive built-in in $state",
3682
+ category: "correctness",
3683
+ severity: "warning",
3684
+ label: "Reactive collections in $state",
3685
+ recommendation: "Import the reactive equivalent from 'svelte/reactivity' (SvelteMap, SvelteSet, SvelteDate, SvelteURL, SvelteURLSearchParams) and construct that instead.",
3686
+ rationale: "$state deep-proxies plain objects and arrays only; built-in collection, date, and URL instances stay untracked, so property-level changes never reach effects, deriveds, or the template. Svelte's own answer is the drop-in classes in svelte/reactivity.",
3687
+ fix: {
3688
+ description: "Import Svelte<Type> from 'svelte/reactivity' and replace new <Type>(...) with new Svelte<Type>(...) \u2014 the API is identical."
3689
+ },
3690
+ applies: (c) => c.nonreactiveBuiltinStates.length > 0,
3691
+ bad: (c) => c.nonreactiveBuiltinStates.map((s) => ({
3692
+ line: s.line,
3693
+ message: `"${s.name}" is a plain ${s.type} in $state \u2014 its mutations are not tracked, so the UI silently stops updating when it changes. Use Svelte${s.type} from 'svelte/reactivity'.`
3554
3694
  }))
3555
3695
  });
3556
3696
 
@@ -4056,6 +4196,7 @@ var allRules = [
4056
4196
  correctnessUnmutatedState,
4057
4197
  correctnessPropMutation,
4058
4198
  correctnessStalePropDerivation,
4199
+ correctnessNonreactiveBuiltinState,
4059
4200
  correctnessOrphanEffect,
4060
4201
  correctnessOrphanLifecycle,
4061
4202
  correctnessServerBrowserGlobal,
@@ -5374,6 +5515,7 @@ export {
5374
5515
  correctnessEffectAsDerived,
5375
5516
  correctnessEffectAsOnMount,
5376
5517
  correctnessInstanceBrowserGlobal,
5518
+ correctnessNonreactiveBuiltinState,
5377
5519
  correctnessOrphanEffect,
5378
5520
  correctnessOrphanLifecycle,
5379
5521
  correctnessPropMutation,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svelte-vitals/core",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Shared, runtime-agnostic core for svelte-vitals (types, rule engine, scorer, reporter).",
5
5
  "type": "module",
6
6
  "license": "MIT",