fix: replicate Svelte 4 props update detection in legacy mode (#11577)

* fix: replicate Svelte 4 props update detection in legacy mode

fixes #11448 by wrapping props in deriveds

* fix test

* Update packages/svelte/src/compiler/phases/3-transform/client/utils.js

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* dedicated flag

* prettier

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/11595/head
Simon H 2 months ago committed by GitHub
parent a0bdac8cd7
commit d408d20cdd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: replicate Svelte 4 props update detection in legacy mode

@ -88,11 +88,7 @@ export function serialize_get_binding(node, state) {
} }
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if ( if (!state.analysis.runes || binding.reassigned || binding.initial) {
state.analysis.accessors ||
(state.analysis.immutable ? binding.reassigned : binding.mutated) ||
binding.initial
) {
return b.call(node); return b.call(node);
} }

@ -93,24 +93,17 @@ export const javascript_visitors_legacy = {
state.scope.get(declarator.id.name) state.scope.get(declarator.id.name)
); );
if ( declarations.push(
state.analysis.accessors || b.declarator(
(state.analysis.immutable ? binding.reassigned : binding.mutated) || declarator.id,
declarator.init get_prop_source(
) { binding,
declarations.push( state,
b.declarator( binding.prop_alias ?? declarator.id.name,
declarator.id, declarator.init && /** @type {import('estree').Expression} */ (visit(declarator.init))
get_prop_source(
binding,
state,
binding.prop_alias ?? declarator.id.name,
declarator.init &&
/** @type {import('estree').Expression} */ (visit(declarator.init))
)
) )
); )
} );
continue; continue;
} }

@ -11,8 +11,9 @@ export const MAYBE_DIRTY = 1 << 10;
export const INERT = 1 << 11; export const INERT = 1 << 11;
export const DESTROYED = 1 << 12; export const DESTROYED = 1 << 12;
export const EFFECT_RAN = 1 << 13; export const EFFECT_RAN = 1 << 13;
/** 'Transparent' effects do not create a transition boundary */ /** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 14; export const EFFECT_TRANSPARENT = 1 << 14;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 15;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');

@ -7,11 +7,12 @@ import {
} from '../../../constants.js'; } from '../../../constants.js';
import { get_descriptor, is_function } from '../utils.js'; import { get_descriptor, is_function } from '../utils.js';
import { mutable_source, set, source } from './sources.js'; import { mutable_source, set, source } from './sources.js';
import { derived } from './deriveds.js'; import { derived, derived_safe_equal } from './deriveds.js';
import { get, is_signals_recorded, untrack, update } from '../runtime.js'; import { get, is_signals_recorded, untrack, update } from '../runtime.js';
import { safe_equals } from './equality.js'; import { safe_equals } from './equality.js';
import { inspect_fn } from '../dev/inspect.js'; import { inspect_fn } from '../dev/inspect.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { LEGACY_DERIVED_PROP } from '../constants.js';
/** /**
* @param {((value?: number) => number)} fn * @param {((value?: number) => number)} fn
@ -236,18 +237,28 @@ export function prop(props, key, flags, fallback) {
if (setter) setter(prop_value); if (setter) setter(prop_value);
} }
var getter = runes /** @type {() => V} */
? () => { var getter;
var value = /** @type {V} */ (props[key]); if (runes) {
if (value === undefined) return get_fallback(); getter = () => {
fallback_dirty = true; var value = /** @type {V} */ (props[key]);
return value; if (value === undefined) return get_fallback();
} fallback_dirty = true;
: () => { return value;
var value = /** @type {V} */ (props[key]); };
if (value !== undefined) fallback_value = /** @type {V} */ (undefined); } else {
return value === undefined ? fallback_value : value; // Svelte 4 did not trigger updates when a primitive value was updated to the same value.
}; // Replicate that behavior through using a derived
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
var value = get(derived_getter);
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
return value === undefined ? fallback_value : value;
};
}
// easy mode — prop is never written to // easy mode — prop is never written to
if ((flags & PROPS_IS_UPDATED) === 0) { if ((flags & PROPS_IS_UPDATED) === 0) {

@ -15,7 +15,8 @@ import {
BRANCH_EFFECT, BRANCH_EFFECT,
STATE_SYMBOL, STATE_SYMBOL,
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT ROOT_EFFECT,
LEGACY_DERIVED_PROP
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -835,7 +836,16 @@ export function invalidate_inner_signals(fn) {
captured_signals = previous_captured_signals; captured_signals = previous_captured_signals;
} }
for (signal of captured) { for (signal of captured) {
mutate(signal, null /* doesnt matter */); // Go one level up because derived signals created as part of props in legacy mode
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {import('#client').Derived} */ (signal).deps || []) {
if ((dep.f & DERIVED) === 0) {
mutate(dep, null /* doesnt matter */);
}
}
} else {
mutate(signal, null /* doesnt matter */);
}
} }
} }

@ -0,0 +1,6 @@
<script>
export let primitive;
export let object;
$: primitive && console.log('primitive');
$: object && console.log('object');
</script>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
async test({ assert, logs, target }) {
assert.deepEqual(logs, ['primitive', 'object']);
await target.querySelector('button')?.click();
assert.deepEqual(logs, ['primitive', 'object', 'object']);
}
});

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
let value = { count: 1 };
</script>
<button on:click={() => value = { count: 1 }}>reassign</button>
<Nested primitive={value.count} object={value} />

@ -1,6 +1,6 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
client: ['foo.bar.baz'], client: ['bar.baz'],
server: ['foo.bar.baz'] server: ['bar.baz']
}); });

Loading…
Cancel
Save