Skip to content

Commit 84e2dd3

Browse files
committed
make it a dev-time validation error that also deals with ...rest props
1 parent ed670eb commit 84e2dd3

File tree

9 files changed

+72
-38
lines changed

9 files changed

+72
-38
lines changed

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export function client_component(source, analysis, options) {
239239
);
240240
});
241241

242-
const properties = analysis.exports.map(({ name, alias }) => {
242+
const component_returned_object = analysis.exports.map(({ name, alias }) => {
243243
const expression = serialize_get_binding(b.id(name), instance_state);
244244

245245
if (expression.type === 'Identifier' && !options.dev) {
@@ -249,11 +249,26 @@ export function client_component(source, analysis, options) {
249249
return b.get(alias ?? name, [b.return(expression)]);
250250
});
251251

252-
if (analysis.accessors) {
253-
for (const [name, binding] of analysis.instance.scope.declarations) {
254-
if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$'))
255-
continue;
252+
const properties = [...analysis.instance.scope.declarations].filter(
253+
([name, binding]) =>
254+
(binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
255+
);
256256

257+
if (analysis.runes && options.dev) {
258+
/** @type {import('estree').Literal[]} */
259+
const bindable = [];
260+
for (const [name, binding] of properties) {
261+
if (binding.kind === 'bindable_prop') {
262+
bindable.push(b.literal(binding.prop_alias ?? name));
263+
}
264+
}
265+
instance.body.unshift(
266+
b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable)))
267+
);
268+
}
269+
270+
if (analysis.accessors) {
271+
for (const [name, binding] of properties) {
257272
const key = binding.prop_alias ?? name;
258273
if (
259274
binding.kind === 'prop' &&
@@ -265,15 +280,15 @@ export function client_component(source, analysis, options) {
265280
continue;
266281
}
267282

268-
properties.push(
283+
component_returned_object.push(
269284
b.get(key, [b.return(b.call(b.id(name)))]),
270285
b.set(key, [b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call('$.flushSync'))])
271286
);
272287
}
273288
}
274289

275290
if (options.legacy.componentApi) {
276-
properties.push(
291+
component_returned_object.push(
277292
b.init('$set', b.id('$.update_legacy_props')),
278293
b.init(
279294
'$on',
@@ -289,7 +304,7 @@ export function client_component(source, analysis, options) {
289304
)
290305
);
291306
} else if (options.dev) {
292-
properties.push(
307+
component_returned_object.push(
293308
b.init(
294309
'$set',
295310
b.thunk(
@@ -357,8 +372,8 @@ export function client_component(source, analysis, options) {
357372

358373
append_styles();
359374
component_block.body.push(
360-
properties.length > 0
361-
? b.return(b.call('$.pop', b.object(properties)))
375+
component_returned_object.length > 0
376+
? b.return(b.call('$.pop', b.object(component_returned_object)))
362377
: b.stmt(b.call('$.pop'))
363378
);
364379

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

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import {
55
PROPS_IS_LAZY_INITIAL,
66
PROPS_IS_IMMUTABLE,
77
PROPS_IS_RUNES,
8-
PROPS_IS_UPDATED,
9-
PROPS_IS_BINDABLE
8+
PROPS_IS_UPDATED
109
} from '../../../../constants.js';
1110

1211
/**
@@ -640,19 +639,6 @@ export function get_prop_source(binding, state, name, initial) {
640639
flags |= PROPS_IS_RUNES;
641640
}
642641

643-
if (
644-
binding.kind === 'bindable_prop' ||
645-
// Make sure that
646-
// let { foo: _, ...rest } = $props();
647-
// let { foo } = $props.bindable();
648-
// marks both `foo` and `_` as bindable to prevent false-positive runtime validation errors
649-
[...state.scope.declarations.values()].some(
650-
(d) => d.kind === 'bindable_prop' && d.prop_alias === name
651-
)
652-
) {
653-
flags |= PROPS_IS_BINDABLE;
654-
}
655-
656642
if (
657643
state.analysis.accessors ||
658644
(state.analysis.immutable ? binding.reassigned : binding.mutated)

packages/svelte/src/constants.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export const PROPS_IS_IMMUTABLE = 1;
1111
export const PROPS_IS_RUNES = 1 << 1;
1212
export const PROPS_IS_UPDATED = 1 << 2;
1313
export const PROPS_IS_LAZY_INITIAL = 1 << 3;
14-
export const PROPS_IS_BINDABLE = 1 << 4;
1514

1615
/** List of Element events that will be delegated */
1716
export const DelegatedEvents = [

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

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { DEV } from 'esm-env';
22
import {
3-
PROPS_IS_BINDABLE,
43
PROPS_IS_IMMUTABLE,
54
PROPS_IS_LAZY_INITIAL,
65
PROPS_IS_RUNES,
@@ -143,15 +142,6 @@ export function prop(props, key, flags, initial) {
143142
var prop_value = /** @type {V} */ (props[key]);
144143
var setter = get_descriptor(props, key)?.set;
145144

146-
if ((flags & PROPS_IS_BINDABLE) === 0 && setter) {
147-
throw new Error(
148-
'ERR_SVELTE_NOT_BINDABLE' +
149-
(DEV
150-
? `: Cannot bind:${key} because the property was not declared as bindable. To mark a property as bindable, use let \`{ ${key} } = $props.bindable()\` within the component.`
151-
: '')
152-
);
153-
}
154-
155145
if (prop_value === undefined && initial !== undefined) {
156146
if (setter && runes) {
157147
// TODO consolidate all these random runtime errors

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { untrack } from './runtime.js';
2-
import { is_array } from './utils.js';
2+
import { get_descriptor, is_array } from './utils.js';
33

44
/** regex of all html void element names */
55
const void_element_names =
@@ -137,3 +137,22 @@ export function validate_component(component_fn) {
137137
}
138138
return component_fn;
139139
}
140+
141+
/**
142+
* @param {Record<string, any>} $$props
143+
* @param {string[]} bindable
144+
*/
145+
export function validate_prop_bindings($$props, bindable) {
146+
for (const key in $$props) {
147+
if (!bindable.includes(key)) {
148+
var setter = get_descriptor($$props, key)?.set;
149+
150+
if (setter) {
151+
throw new Error(
152+
`Cannot use bind:${key} on this component because the property was not declared as bindable. ` +
153+
`To mark a property as bindable, use let \`{ ${key} } = $props.bindable()\` within the component.`
154+
);
155+
}
156+
}
157+
}
158+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script>
2+
let { ...rest } = $props();
3+
</script>
4+
5+
{rest.count}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
compileOptions: {
5+
dev: true
6+
},
7+
error:
8+
'Cannot use bind:count on this component because the property was not declared as bindable. To mark a property as bindable, use let `{ count } = $props.bindable()` within the component.',
9+
html: `0`
10+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
import Counter from './Counter.svelte';
3+
4+
let count = $state(0);
5+
</script>
6+
7+
<Counter bind:count />
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { test } from '../../test';
22

33
export default test({
4+
compileOptions: {
5+
dev: true
6+
},
47
error:
5-
'ERR_SVELTE_NOT_BINDABLE: Cannot bind:count because the property was not declared as bindable. To mark a property as bindable, use let `{ count } = $props.bindable()` within the component.',
8+
'Cannot use bind:count on this component because the property was not declared as bindable. To mark a property as bindable, use let `{ count } = $props.bindable()` within the component.',
69
html: `0`
710
});

0 commit comments

Comments
 (0)