@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 +33 -6
- package/dist/index.js +155 -13
- package/package.json +1 -1
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
|
-
*
|
|
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"
|
|
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
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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))
|
|
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: "
|
|
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(...)
|
|
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,
|