Skip to content

Commit 5e02a98

Browse files
committed
fix: prevent ownership warnings if the fallback of a bindable is used
1 parent 475b5db commit 5e02a98

File tree

13 files changed

+126
-11
lines changed

13 files changed

+126
-11
lines changed

.changeset/wise-turkeys-yell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: prevent ownership warnings if the fallback of a bindable is used

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js

+1
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export function validate_mutation(node, context, expression) {
348348
b.literal(binding.prop_alias),
349349
b.array(path),
350350
expression,
351+
binding.prop_alias != null && b.id(binding.prop_alias),
351352
loc && b.literal(loc.line),
352353
loc && b.literal(loc.column)
353354
);

packages/svelte/src/internal/client/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ export const EFFECT_HAS_DERIVED = 1 << 20;
2323
export const EFFECT_IS_UPDATING = 1 << 21;
2424

2525
export const STATE_SYMBOL = Symbol('$state');
26+
export const BINDABLE_FALLBACK_SYMBOL = Symbol('bindable fallback');
2627
export const LEGACY_PROPS = Symbol('legacy props');
2728
export const LOADING_ATTR_SYMBOL = Symbol('');

packages/svelte/src/internal/client/dev/ownership.js

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/** @typedef {{ file: string, line: number, column: number }} Location */
22

33
import { get_descriptor } from '../../shared/utils.js';
4-
import { LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
4+
import { BINDABLE_FALLBACK_SYMBOL, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
55
import { FILENAME } from '../../../constants.js';
66
import { component_context } from '../context.js';
77
import * as w from '../warnings.js';
@@ -22,12 +22,13 @@ export function create_ownership_validator(props) {
2222
* @param {string} prop
2323
* @param {any[]} path
2424
* @param {any} result
25+
* @param {any} prop_value
2526
* @param {number} line
2627
* @param {number} column
2728
*/
28-
mutation: (prop, path, result, line, column) => {
29+
mutation: (prop, path, result, prop_value, line, column) => {
2930
const name = path[0];
30-
if (is_bound(props, name) || !parent) {
31+
if (is_bound(props, name) || !parent || prop_value()?.[BINDABLE_FALLBACK_SYMBOL]) {
3132
return result;
3233
}
3334

@@ -52,7 +53,14 @@ export function create_ownership_validator(props) {
5253
* @param {() => any} value
5354
*/
5455
binding: (key, child_component, value) => {
55-
if (!is_bound(props, key) && parent && value()?.[STATE_SYMBOL]) {
56+
var val;
57+
if (
58+
!is_bound(props, key) &&
59+
parent &&
60+
// we do this trick to prevent calling the value function twice
61+
(val = value())?.[STATE_SYMBOL] &&
62+
!val[BINDABLE_FALLBACK_SYMBOL]
63+
) {
5664
w.ownership_invalid_binding(
5765
component[FILENAME],
5866
key,

packages/svelte/src/internal/client/proxy.js

+8-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
object_prototype
1010
} from '../shared/utils.js';
1111
import { state as source, set } from './reactivity/sources.js';
12-
import { STATE_SYMBOL } from './constants.js';
12+
import { BINDABLE_FALLBACK_SYMBOL, STATE_SYMBOL } from './constants.js';
1313
import { UNINITIALIZED } from '../../constants.js';
1414
import * as e from './errors.js';
1515
import { get_stack } from './dev/tracing.js';
@@ -64,10 +64,13 @@ export function proxy(value) {
6464
return new Proxy(/** @type {any} */ (value), {
6565
defineProperty(_, prop, descriptor) {
6666
if (
67-
!('value' in descriptor) ||
68-
descriptor.configurable === false ||
69-
descriptor.enumerable === false ||
70-
descriptor.writable === false
67+
// we allow non enumerable/writable defines if the prop being set is our own symbol
68+
// this should be fine for the invariants since the user can't get a handle to the symbol
69+
(!DEV || prop !== BINDABLE_FALLBACK_SYMBOL) &&
70+
(!('value' in descriptor) ||
71+
descriptor.configurable === false ||
72+
descriptor.enumerable === false ||
73+
descriptor.writable === false)
7174
) {
7275
// we disallow non-basic descriptors, because unless they are applied to the
7376
// target object — which we avoid, so that state can be forked — we will run

packages/svelte/src/internal/client/reactivity/props.js

+35-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import {
77
PROPS_IS_RUNES,
88
PROPS_IS_UPDATED
99
} from '../../../constants.js';
10-
import { get_descriptor, is_function } from '../../shared/utils.js';
10+
import { define_property, get_descriptor, is_function } from '../../shared/utils.js';
1111
import { mutable_source, set, source, update } from './sources.js';
1212
import { derived, derived_safe_equal } from './deriveds.js';
1313
import { get, captured_signals, untrack } from '../runtime.js';
1414
import { safe_equals } from './equality.js';
1515
import * as e from '../errors.js';
16-
import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js';
16+
import {
17+
BINDABLE_FALLBACK_SYMBOL,
18+
LEGACY_DERIVED_PROP,
19+
LEGACY_PROPS,
20+
STATE_SYMBOL
21+
} from '../constants.js';
1722
import { proxy } from '../proxy.js';
1823
import { capture_store_binding } from './store.js';
1924
import { legacy_mode_flag } from '../../flags/index.js';
@@ -304,6 +309,29 @@ export function prop(props, key, flags, fallback) {
304309
if (setter) setter(prop_value);
305310
}
306311

312+
/**
313+
* @param {any} value
314+
*/
315+
function set_bindable_fallback(value) {
316+
if (DEV && !setter && fallback_used && value != null && typeof value === 'object') {
317+
// in dev we issue a warning if a bindable prop is passed with bindable
318+
// to a child if the prop doesn't have a setter but if it's a fallback
319+
// it's a false positive since the state it's actually created in this
320+
// component so we store the fact that this is a bindable fallback with
321+
// a symbol
322+
define_property(value, BINDABLE_FALLBACK_SYMBOL, {
323+
enumerable: false,
324+
// it needs to be configurable or the proxy will complain when we return true
325+
// for a non configurable property
326+
configurable: true,
327+
writable: false,
328+
value: true
329+
});
330+
}
331+
}
332+
333+
set_bindable_fallback(prop_value);
334+
307335
/** @type {() => V} */
308336
var getter;
309337
if (runes) {
@@ -399,6 +427,11 @@ export function prop(props, key, flags, fallback) {
399427
if (arguments.length > 0) {
400428
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value;
401429

430+
// we only care to add the symbol if the original prop is reassigned
431+
if (runes && bindable && !mutation) {
432+
set_bindable_fallback(new_value);
433+
}
434+
402435
if (!current_value.equals(new_value)) {
403436
from_child = true;
404437
set(inner_current_value, new_value);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
const { test = $bindable() } = $props();
3+
</script>
4+
5+
{test}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import Child from './Child.svelte';
3+
4+
let { test = $bindable({}) } = $props();
5+
</script>
6+
7+
<Child bind:test />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
mode: ['client'],
5+
compileOptions: {
6+
dev: true
7+
},
8+
async test({ warnings, assert }) {
9+
assert.deepEqual(warnings, []);
10+
}
11+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import Parent from './Parent.svelte';
3+
</script>
4+
5+
<Parent />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
let { test = $bindable({}) } = $props();
3+
</script>
4+
5+
<button onclick={()=>test = {}}></button>
6+
<button onclick={()=>test.test = {}}></button>
7+
8+
{test}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
mode: ['client'],
6+
compileOptions: {
7+
dev: true
8+
},
9+
async test({ warnings, assert, target }) {
10+
const [btn, btn2] = target.querySelectorAll('button');
11+
flushSync(() => {
12+
btn2.click();
13+
});
14+
assert.deepEqual(warnings, []);
15+
flushSync(() => {
16+
btn.click();
17+
});
18+
flushSync(() => {
19+
btn2.click();
20+
});
21+
assert.deepEqual(warnings, []);
22+
}
23+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
import Parent from './Parent.svelte';
3+
</script>
4+
5+
<Parent />

0 commit comments

Comments
 (0)