Skip to content

Commit 351b3e7

Browse files
fix: warn instead of error when binding a non-existent prop
Co-authored-by: Paolo Ricciuti <[email protected]>
1 parent 35ebbe6 commit 351b3e7

File tree

9 files changed

+94
-4
lines changed

9 files changed

+94
-4
lines changed

.changeset/nervous-pans-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: warn instead of error when binding a non-existent prop

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## binding_non_existent_prop
2+
3+
> A component is attempting to bind to a non-existent property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`)
4+
15
## binding_property_non_reactive
26

37
> `%binding%` is binding to a non-reactive property

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,16 +286,26 @@ export function client_component(analysis, options) {
286286

287287
const properties = [...analysis.instance.scope.declarations].filter(
288288
([name, binding]) =>
289-
(binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
289+
(binding.kind === 'prop' ||
290+
binding.kind === 'bindable_prop' ||
291+
binding.kind === 'rest_prop') &&
292+
!name.startsWith('$$')
290293
);
291294

292295
if (dev && analysis.runes) {
293296
const exports = analysis.exports.map(({ name, alias }) => b.literal(alias ?? name));
294297
/** @type {ESTree.Literal[]} */
295298
const bindable = [];
299+
const non_bindable = [];
300+
let has_rest = false;
301+
296302
for (const [name, binding] of properties) {
297303
if (binding.kind === 'bindable_prop') {
298304
bindable.push(b.literal(binding.prop_alias ?? name));
305+
} else if (binding.kind === 'rest_prop') {
306+
has_rest = true;
307+
} else {
308+
non_bindable.push(b.literal(binding.prop_alias ?? name));
299309
}
300310
}
301311
instance.body.unshift(
@@ -304,8 +314,10 @@ export function client_component(analysis, options) {
304314
'$.validate_prop_bindings',
305315
b.id('$$props'),
306316
b.array(bindable),
317+
b.array(non_bindable),
307318
b.array(exports),
308-
b.id(`${analysis.name}`)
319+
b.id(`${analysis.name}`),
320+
b.literal(has_rest)
309321
)
310322
)
311323
);

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,19 @@ export function validate_each_keys(collection, key_fn) {
4949
/**
5050
* @param {Record<string, any>} $$props
5151
* @param {string[]} bindable
52+
* @param {string[]} non_bindable
5253
* @param {string[]} exports
5354
* @param {Function & { [FILENAME]: string }} component
55+
* @param {boolean} has_rest
5456
*/
55-
export function validate_prop_bindings($$props, bindable, exports, component) {
57+
export function validate_prop_bindings(
58+
$$props,
59+
bindable,
60+
non_bindable,
61+
exports,
62+
component,
63+
has_rest
64+
) {
5665
for (const key in $$props) {
5766
var setter = get_descriptor($$props, key)?.set;
5867
var name = component.name;
@@ -63,7 +72,10 @@ export function validate_prop_bindings($$props, bindable, exports, component) {
6372
}
6473

6574
if (!bindable.includes(key)) {
66-
e.bind_not_bindable(key, component[FILENAME], name);
75+
if (has_rest || non_bindable.includes(key)) {
76+
e.bind_not_bindable(key, component[FILENAME], name);
77+
}
78+
w.binding_non_existent_prop(key, component[FILENAME], name);
6779
}
6880
}
6981
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import { DEV } from 'esm-env';
55
var bold = 'font-weight: bold';
66
var normal = 'font-weight: normal';
77

8+
/**
9+
* A component is attempting to bind to a non-existent property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`)
10+
* @param {string} key
11+
* @param {string} component
12+
* @param {string} name
13+
*/
14+
export function binding_non_existent_prop(key, component, name) {
15+
if (DEV) {
16+
console.warn(`%c[svelte] binding_non_existent_prop\n%cA component is attempting to bind to a non-existent property \`${key}\` belonging to ${component} (i.e. \`<${name} bind:${key}={...}>\`)`, bold, normal);
17+
} else {
18+
// TODO print a link to the documentation
19+
console.warn("binding_non_existent_prop");
20+
}
21+
}
22+
823
/**
924
* `%binding%` (%location%) is binding to a non-reactive property
1025
* @param {string} binding
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let {
3+
value = $bindable()
4+
} = $props()
5+
</script>
6+
7+
<input type="text" bind:value />
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script>
2+
let {
3+
value = $bindable(),
4+
content = $bindable(),
5+
} = $props()
6+
</script>
7+
<div bind:innerText={content} contenteditable>
8+
</div>
9+
<input type="text" bind:value />
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
compileOptions: {
5+
dev: true
6+
},
7+
8+
warnings: [
9+
'A component is attempting to bind to a non-existent property `content` belonging to Input.svelte (i.e. `<Input bind:content={...}>`)'
10+
]
11+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script>
2+
import Input from './Input.svelte';
3+
import InputEditable from './InputEditable.svelte';
4+
5+
const components = [
6+
Input,
7+
InputEditable
8+
]
9+
10+
const results = $state([{}, {}]);
11+
</script>
12+
13+
{#each components as Component, i}
14+
<Component bind:content={results[i].content} bind:value={results[i].value} />
15+
{/each}

0 commit comments

Comments
 (0)