From 885f3d61536c22bb560ff3ce1695e4045524320d Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:27:39 -0700 Subject: [PATCH] add error message if source doesn't exist, cleanup code --- documentation/docs/02-runes/02-$state.md | 11 ++++- .../98-reference/.generated/client-errors.md | 6 +++ .../svelte/messages/client-errors/errors.md | 4 ++ .../client/visitors/CallExpression.js | 43 +++++++++++++++---- packages/svelte/src/internal/client/errors.js | 15 +++++++ .../src/internal/client/reactivity/sources.js | 4 +- 6 files changed, 70 insertions(+), 13 deletions(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 8415ac9947..8742e78369 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -184,7 +184,7 @@ In the case that you aren't using a proxied `$state` via use of `$state.raw` or ``` -`$state.invalidate` can also be used with reactive class fields: +`$state.invalidate` can also be used with reactive class fields, and properties of `$state` objects: ```js class Box { @@ -199,10 +199,17 @@ class Counter { count = $state(new Box(0)); increment() { - this.count.value++; + this.count.value += 1; $state.invalidate(this.count); } } + +let counter = $state({count: new Box(0)}); + +function increment() { + counter.count.value += 1; + $state.invalidate(counter.count); +} ``` ## Passing state into functions diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 32348bb781..cf992c8cab 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -116,6 +116,12 @@ The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. ``` +### state_invalidate_invalid_source + +``` +The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object. +``` + ### state_prototype_fixed ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index c4e68f8fee..7511abf8d6 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -76,6 +76,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`. +## state_invalidate_invalid_source + +> The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object. + ## state_prototype_fixed > Cannot set prototype of `$state` object diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index e29ec86948..d18f9ff7d9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -1,17 +1,31 @@ -/** @import { CallExpression, Expression, Identifier } from 'estree' */ +/** @import { CallExpression, Expression, Identifier, MemberExpression, Node } from 'estree' */ /** @import { Context } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; import { transform_inspect_rune } from '../../utils.js'; import * as e from '../../../../errors.js'; -import { object } from '../../../../utils/ast.js'; /** * @param {CallExpression} node * @param {Context} context */ export function CallExpression(node, context) { + /** + * Some nodes that get replaced should keep their locations (for better source maps and such) + * @template {Node} N + * @param {N} node + * @param {N} replacement + * @returns {N} + */ + function attach_locations(node, replacement) { + return { + ...replacement, + start: node.start, + end: node.end, + loc: node.loc + }; + } switch (get_rune(node, context.state.scope)) { case '$host': return b.id('$$props.$$host'); @@ -28,11 +42,13 @@ export function CallExpression(node, context) { /* eslint-disable no-fallthrough */ case '$state.invalidate': if (node.arguments[0].type === 'Identifier') { - return b.call('$.invalidate', node.arguments[0]); + return b.call( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + node.arguments[0] + ); } else if (node.arguments[0].type === 'MemberExpression') { - const { object: obj, property } = node.arguments[0]; - const root = object(node.arguments[0]); - if (obj.type === 'ThisExpression') { + const { object, property } = node.arguments[0]; + if (object.type === 'ThisExpression') { let field; switch (property.type) { case 'Identifier': @@ -45,17 +61,26 @@ export function CallExpression(node, context) { if (!field || (field.kind !== 'state' && field.kind !== 'raw_state')) { e.state_invalidate_nonreactive_argument(node); } - return b.call('$.invalidate', b.member(b.this, field.id)); + return b.call( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + attach_locations(node.arguments[0], b.member(object, field.id)) + ); } /** @type {Expression[]} */ const source_args = /** @type {Expression[]} */ ([ - context.visit(obj), + context.visit(object), node.arguments[0].computed ? context.visit(property) : b.literal(/** @type {Identifier} */ (property).name) ]); const arg = b.call('$.lookup_source', ...source_args); - return b.call('$.invalidate', arg); + return b.call( + attach_locations(/** @type {Expression} */ (node.callee), b.id('$.invalidate')), + attach_locations( + /** @type {Expression} */ (node.arguments[0]), + /** @type {Expression} */ (arg) + ) + ); } case '$effect.root': diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 429dd99da9..c869e82a50 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -291,6 +291,21 @@ export function state_descriptors_fixed() { } } +/** + * The argument passed to `$state.invalidate` must be a variable or class field declared with `$state` or `$state.raw`, or a property of a `$state` object. + * @returns {never} + */ +export function state_invalidate_invalid_source() { + if (DEV) { + const error = new Error(`state_invalidate_invalid_source\nThe argument passed to \`$state.invalidate\` must be a variable or class field declared with \`$state\` or \`$state.raw\`, or a property of a \`$state\` object.\nhttps://svelte.dev/e/state_invalidate_invalid_source`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/state_invalidate_invalid_source`); + } +} + /** * Cannot set prototype of `$state` object * @returns {never} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 8aa7b65019..20fa1106ba 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -224,8 +224,8 @@ export function internal_set(source, value) { * @param {Source | null} source */ export function invalidate(source) { - if (source === null) { - return; + if (source === null || (source.f & DERIVED) !== 0) { + e.state_invalidate_invalid_source(); } if ( active_reaction !== null &&