make it a dev-time validation error that also deals with ...rest props

props-bindable
Simon Holthausen 2 years ago
parent ed670ebdd2
commit 84e2dd3130

@ -239,7 +239,7 @@ export function client_component(source, analysis, options) {
); );
}); });
const properties = analysis.exports.map(({ name, alias }) => { const component_returned_object = analysis.exports.map(({ name, alias }) => {
const expression = serialize_get_binding(b.id(name), instance_state); const expression = serialize_get_binding(b.id(name), instance_state);
if (expression.type === 'Identifier' && !options.dev) { if (expression.type === 'Identifier' && !options.dev) {
@ -249,11 +249,26 @@ export function client_component(source, analysis, options) {
return b.get(alias ?? name, [b.return(expression)]); return b.get(alias ?? name, [b.return(expression)]);
}); });
if (analysis.accessors) { const properties = [...analysis.instance.scope.declarations].filter(
for (const [name, binding] of analysis.instance.scope.declarations) { ([name, binding]) =>
if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$')) (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$')
continue; );
if (analysis.runes && options.dev) {
/** @type {import('estree').Literal[]} */
const bindable = [];
for (const [name, binding] of properties) {
if (binding.kind === 'bindable_prop') {
bindable.push(b.literal(binding.prop_alias ?? name));
}
}
instance.body.unshift(
b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable)))
);
}
if (analysis.accessors) {
for (const [name, binding] of properties) {
const key = binding.prop_alias ?? name; const key = binding.prop_alias ?? name;
if ( if (
binding.kind === 'prop' && binding.kind === 'prop' &&
@ -265,7 +280,7 @@ export function client_component(source, analysis, options) {
continue; continue;
} }
properties.push( component_returned_object.push(
b.get(key, [b.return(b.call(b.id(name)))]), b.get(key, [b.return(b.call(b.id(name)))]),
b.set(key, [b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call('$.flushSync'))]) b.set(key, [b.stmt(b.call(b.id(name), b.id('$$value'))), b.stmt(b.call('$.flushSync'))])
); );
@ -273,7 +288,7 @@ export function client_component(source, analysis, options) {
} }
if (options.legacy.componentApi) { if (options.legacy.componentApi) {
properties.push( component_returned_object.push(
b.init('$set', b.id('$.update_legacy_props')), b.init('$set', b.id('$.update_legacy_props')),
b.init( b.init(
'$on', '$on',
@ -289,7 +304,7 @@ export function client_component(source, analysis, options) {
) )
); );
} else if (options.dev) { } else if (options.dev) {
properties.push( component_returned_object.push(
b.init( b.init(
'$set', '$set',
b.thunk( b.thunk(
@ -357,8 +372,8 @@ export function client_component(source, analysis, options) {
append_styles(); append_styles();
component_block.body.push( component_block.body.push(
properties.length > 0 component_returned_object.length > 0
? b.return(b.call('$.pop', b.object(properties))) ? b.return(b.call('$.pop', b.object(component_returned_object)))
: b.stmt(b.call('$.pop')) : b.stmt(b.call('$.pop'))
); );

@ -5,8 +5,7 @@ import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES, PROPS_IS_RUNES,
PROPS_IS_UPDATED, PROPS_IS_UPDATED
PROPS_IS_BINDABLE
} from '../../../../constants.js'; } from '../../../../constants.js';
/** /**
@ -640,19 +639,6 @@ export function get_prop_source(binding, state, name, initial) {
flags |= PROPS_IS_RUNES; flags |= PROPS_IS_RUNES;
} }
if (
binding.kind === 'bindable_prop' ||
// Make sure that
// let { foo: _, ...rest } = $props();
// let { foo } = $props.bindable();
// marks both `foo` and `_` as bindable to prevent false-positive runtime validation errors
[...state.scope.declarations.values()].some(
(d) => d.kind === 'bindable_prop' && d.prop_alias === name
)
) {
flags |= PROPS_IS_BINDABLE;
}
if ( if (
state.analysis.accessors || state.analysis.accessors ||
(state.analysis.immutable ? binding.reassigned : binding.mutated) (state.analysis.immutable ? binding.reassigned : binding.mutated)

@ -11,7 +11,6 @@ export const PROPS_IS_IMMUTABLE = 1;
export const PROPS_IS_RUNES = 1 << 1; export const PROPS_IS_RUNES = 1 << 1;
export const PROPS_IS_UPDATED = 1 << 2; export const PROPS_IS_UPDATED = 1 << 2;
export const PROPS_IS_LAZY_INITIAL = 1 << 3; export const PROPS_IS_LAZY_INITIAL = 1 << 3;
export const PROPS_IS_BINDABLE = 1 << 4;
/** List of Element events that will be delegated */ /** List of Element events that will be delegated */
export const DelegatedEvents = [ export const DelegatedEvents = [

@ -1,6 +1,5 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES, PROPS_IS_RUNES,
@ -143,15 +142,6 @@ export function prop(props, key, flags, initial) {
var prop_value = /** @type {V} */ (props[key]); var prop_value = /** @type {V} */ (props[key]);
var setter = get_descriptor(props, key)?.set; var setter = get_descriptor(props, key)?.set;
if ((flags & PROPS_IS_BINDABLE) === 0 && setter) {
throw new Error(
'ERR_SVELTE_NOT_BINDABLE' +
(DEV
? `: 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.`
: '')
);
}
if (prop_value === undefined && initial !== undefined) { if (prop_value === undefined && initial !== undefined) {
if (setter && runes) { if (setter && runes) {
// TODO consolidate all these random runtime errors // TODO consolidate all these random runtime errors

@ -1,5 +1,5 @@
import { untrack } from './runtime.js'; import { untrack } from './runtime.js';
import { is_array } from './utils.js'; import { get_descriptor, is_array } from './utils.js';
/** regex of all html void element names */ /** regex of all html void element names */
const void_element_names = const void_element_names =
@ -137,3 +137,22 @@ export function validate_component(component_fn) {
} }
return component_fn; return component_fn;
} }
/**
* @param {Record<string, any>} $$props
* @param {string[]} bindable
*/
export function validate_prop_bindings($$props, bindable) {
for (const key in $$props) {
if (!bindable.includes(key)) {
var setter = get_descriptor($$props, key)?.set;
if (setter) {
throw new Error(
`Cannot use bind:${key} on this component because the property was not declared as bindable. ` +
`To mark a property as bindable, use let \`{ ${key} } = $props.bindable()\` within the component.`
);
}
}
}
}

@ -0,0 +1,5 @@
<script>
let { ...rest } = $props();
</script>
{rest.count}

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error:
'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.',
html: `0`
});

@ -0,0 +1,7 @@
<script>
import Counter from './Counter.svelte';
let count = $state(0);
</script>
<Counter bind:count />

@ -1,7 +1,10 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
compileOptions: {
dev: true
},
error: error:
'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.', '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.',
html: `0` html: `0`
}); });

Loading…
Cancel
Save