From 001a8314fa5b27126bf3494de4189f0cb67ee212 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 16:24:35 +0100 Subject: [PATCH 01/26] add $props.bindable() --- .../src/compiler/phases/2-analyze/index.js | 9 ++++--- .../src/compiler/phases/2-analyze/types.d.ts | 2 +- .../compiler/phases/2-analyze/validation.js | 22 ++++++++++----- .../client/visitors/javascript-runes.js | 2 +- .../3-transform/server/transform-server.js | 2 +- .../svelte/src/compiler/phases/constants.js | 1 + .../svelte/src/internal/client/runtime.js | 27 +++++++++++++------ packages/svelte/src/main/ambient.d.ts | 15 +++++++++++ packages/svelte/types/index.d.ts | 15 +++++++++++ .../src/lib/CodeMirror.svelte | 1 + .../routes/docs/content/01-api/02-runes.md | 6 ++++- 11 files changed, 79 insertions(+), 23 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 3ddb68237c04..5f278dc7925e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -422,7 +422,7 @@ export function analyze_component(root, options) { options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', parent_element: null, - has_props_rune: false, + has_props_rune: [false, false], component_slots: new Set(), expression: null, private_derived_state: [], @@ -446,7 +446,7 @@ export function analyze_component(root, options) { analysis, options, parent_element: null, - has_props_rune: false, + has_props_rune: [false, false], ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', instance_scope: instance.scope, reactive_statement: null, @@ -854,7 +854,8 @@ const runes_scope_tweaker = { rune !== '$state.frozen' && rune !== '$derived' && rune !== '$derived.by' && - rune !== '$props' + rune !== '$props' && + rune !== '$props.bindable' ) return; @@ -873,7 +874,7 @@ const runes_scope_tweaker = { : 'prop'; } - if (rune === '$props') { + if (rune === '$props' || rune === '$props.bindable') { for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { if (property.type !== 'Property') continue; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index d2c503e8b108..75d84d3e7a65 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -15,7 +15,7 @@ export interface AnalysisState { options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; parent_element: string | null; - has_props_rune: boolean; + has_props_rune: [props: boolean, bindings: boolean]; /** Which slots the current parent component has */ component_slots: Set; /** The current {expression}, if any */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index cf6f00637f7a..52e5786824bc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -775,12 +775,17 @@ function validate_call_expression(node, scope, path) { const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1)); - if (rune === '$props') { + if (rune === '$props' || rune === '$props.bindable') { if (parent.type === 'VariableDeclarator') return; error(node, 'invalid-props-location'); } - if (rune === '$state' || rune === '$derived' || rune === '$derived.by') { + if ( + rune === '$state' || + rune === '$state.frozen' || + rune === '$derived' || + rune === '$derived.by' + ) { if (parent.type === 'VariableDeclarator') return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; error(node, 'invalid-state-location', rune); @@ -871,7 +876,7 @@ export const validation_runes_js = { error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { error(node, 'invalid-rune-args-length', rune, [0, 1]); - } else if (rune === '$props') { + } else if (rune === '$props' || rune === '$props.bindable') { error(node, 'invalid-props-location'); } }, @@ -1054,15 +1059,18 @@ export const validation_runes = merge(validation, a11y_validators, { error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { error(node, 'invalid-rune-args-length', rune, [0, 1]); - } else if (rune === '$props') { - if (state.has_props_rune) { + } else if (rune === '$props' || rune === '$props.bindable') { + if ( + (rune === '$props' && state.has_props_rune[0]) || + (rune === '$props.bindable' && state.has_props_rune[1]) + ) { error(node, 'duplicate-props-rune'); } - state.has_props_rune = true; + state.has_props_rune[rune === '$props' ? 0 : 1] = true; if (args.length > 0) { - error(node, 'invalid-rune-args-length', '$props', [0]); + error(node, 'invalid-rune-args-length', rune, [0]); } if (node.id.type !== 'ObjectPattern') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 3eb43322b7d5..bed57cfa862e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -192,7 +192,7 @@ export const javascript_visitors_runes = { continue; } - if (rune === '$props') { + if (rune === '$props' || rune === '$props.bindable') { assert.equal(declarator.id.type, 'ObjectPattern'); /** @type {string[]} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 8ff7d2c31687..2173df9fc443 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -689,7 +689,7 @@ const javascript_visitors_runes = { continue; } - if (rune === '$props') { + if (rune === '$props' || rune === '$props.bindable') { declarations.push(b.declarator(declarator.id, b.id('$$props'))); continue; } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index eaf01b7f341a..affa67aa4f10 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', '$props', + '$props.bindable', '$derived', '$derived.by', '$effect', diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 745342bf6e8b..566ae38f3687 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1253,22 +1253,33 @@ export function unwrap(value) { } if (DEV) { - /** @param {string} rune */ - function throw_rune_error(rune) { + /** + * @param {string} rune + * @param {string[]} [variants] + */ + function throw_rune_error(rune, variants = []) { if (!(rune in globalThis)) { + // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message // @ts-ignore globalThis[rune] = () => { - // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message - throw new Error(`${rune} is only available inside .svelte and .svelte.js/ts files`); + throw new Error(`${rune}() is only available inside .svelte and .svelte.js/ts files`); }; + for (const variant of variants) { + // @ts-ignore + globalThis[rune][variant] = () => { + throw new Error( + `${rune}.${variant}() is only available inside .svelte and .svelte.js/ts files` + ); + }; + } } } - throw_rune_error('$state'); - throw_rune_error('$effect'); - throw_rune_error('$derived'); + throw_rune_error('$state', ['frozen']); + throw_rune_error('$effect', ['pre', 'root', 'active']); + throw_rune_error('$derived', ['by']); throw_rune_error('$inspect'); - throw_rune_error('$props'); + throw_rune_error('$props', ['bindable']); } /** diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index 1cb02f9f4d16..c9d4d9e8b867 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -175,10 +175,25 @@ declare namespace $effect { * let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props(); * ``` * + * Props declared with `$props()` cannot be used with `bind:`, use `$props.bindable()` for these instead. + * * https://svelte-5-preview.vercel.app/docs/runes#$props */ declare function $props(): any; +declare namespace $props { + /** + * Declares the props that a component accepts and which consumers can `bind:` to. Example: + * + * ```ts + * let { optionalProp, requiredProp }: { optionalProp?: number; requiredProps: string } = $props.bindable(); + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$props + */ + function bindable(): any; +} + /** * Inspects one or more values whenever they, or the properties they contain, change. Example: * diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f3325cfad0db..75ac1c0e9e16 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2462,10 +2462,25 @@ declare namespace $effect { * let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props(); * ``` * + * Props declared with `$props()` cannot be used with `bind:`, use `$props.bindable()` for these instead. + * * https://svelte-5-preview.vercel.app/docs/runes#$props */ declare function $props(): any; +declare namespace $props { + /** + * Declares the props that a component accepts and which consumers can `bind:` to. Example: + * + * ```ts + * let { optionalProp, requiredProp }: { optionalProp?: number; requiredProps: string } = $props.bindable(); + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$props + */ + function bindable(): any; +} + /** * Inspects one or more values whenever they, or the properties they contain, change. Example: * diff --git a/sites/svelte-5-preview/src/lib/CodeMirror.svelte b/sites/svelte-5-preview/src/lib/CodeMirror.svelte index 3f29dfe17623..b30394961293 100644 --- a/sites/svelte-5-preview/src/lib/CodeMirror.svelte +++ b/sites/svelte-5-preview/src/lib/CodeMirror.svelte @@ -220,6 +220,7 @@ boost: 5 }), { label: '$state.frozen', type: 'keyword', boost: 4 }, + { label: '$props.bindable', type: 'keyword', boost: 4 }, snip('$effect.root(() => {\n\t${}\n});', { label: '$effect.root', type: 'keyword', diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 7cb20245ff7a..ce32e57ebdfa 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -471,7 +471,7 @@ let { a, b, c, ...everythingElse }: MyProps = $props(); > > ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring. -Props cannot be mutated, unless the parent component uses `bind:`. During development, attempts to mutate props will result in an error. +Props declared with `$props()` cannot be mutated, and you cannot `bind:` to them in the parent. To declare props as bindable, use [`$props.bindable()`](#propsbindable). ### What this replaces @@ -479,6 +479,10 @@ Props cannot be mutated, unless the parent component uses `bind:`. During develo Note that you can still use `export const` and `export function` to expose things to users of your component (if they're using `bind:this`, for example). +### `$props.bindable()` + +To declare props as bindable, use `$props.bindable()`. Besides using them as regular props, the parent can then also `bind:` to these props. Works exactly like `$props()`. During development, attempts to mutate these props will result in a warning, unless the parent did `bind:` them. + ## `$inspect` The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its From e98c242d2dc54ddcf7da01ac37d6f8b5dc3389f9 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Thu, 14 Mar 2024 18:23:09 +0100 Subject: [PATCH 02/26] compile time mutation validation --- packages/svelte/src/compiler/errors.js | 2 ++ .../src/compiler/phases/2-analyze/index.js | 15 +++++++---- .../compiler/phases/2-analyze/validation.js | 27 +++++++++++++------ .../3-transform/client/transform-client.js | 8 +++--- .../phases/3-transform/client/utils.js | 9 ++++--- .../3-transform/client/visitors/global.js | 3 ++- .../client/visitors/javascript-legacy.js | 6 ++--- .../3-transform/client/visitors/template.js | 1 + .../3-transform/server/transform-server.js | 7 ++--- packages/svelte/src/compiler/types/index.d.ts | 4 ++- .../_config.js | 9 +++++++ .../main.svelte | 4 +++ .../samples/each-bind-this-member/main.svelte | 2 +- .../Counter.svelte | 2 +- .../sub.svelte | 3 ++- .../Counter.svelte | 2 +- .../props-bound-fallback/Counter.svelte | 2 +- .../props-default-reactivity/Counter.svelte | 2 +- .../samples/proxy-prop-bound/Counter.svelte | 2 +- 19 files changed, 75 insertions(+), 35 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 72397e1f6ef5..b1bc2c5c8d1c 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -182,6 +182,8 @@ const runes = { `$props() assignment must not contain nested properties or computed keys`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, + 'invalid-props-mutation': () => + 'Properties defined by $props() cannot be mutated. Use $props.bindable() instead, or make a copy of the value and reassign it.', /** @param {string} rune */ 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 5f278dc7925e..05eda25ac5ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -436,7 +436,7 @@ export function analyze_component(root, options) { ); } } else { - instance.scope.declare(b.id('$$props'), 'prop', 'synthetic'); + instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); for (const { ast, scope, scopes } of [module, instance, template]) { @@ -466,7 +466,10 @@ export function analyze_component(root, options) { } for (const [name, binding] of instance.scope.declarations) { - if (binding.kind === 'prop' && binding.node.name !== '$$props') { + if ( + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && + binding.node.name !== '$$props' + ) { const references = binding.references.filter( (r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier' ); @@ -758,7 +761,7 @@ const legacy_scope_tweaker = { (binding.kind === 'normal' && (binding.declaration_kind === 'let' || binding.declaration_kind === 'var')) ) { - binding.kind = 'prop'; + binding.kind = 'bindable_prop'; if (specifier.exported.name !== specifier.local.name) { binding.prop_alias = specifier.exported.name; } @@ -796,7 +799,7 @@ const legacy_scope_tweaker = { for (const declarator of node.declaration.declarations) { for (const id of extract_identifiers(declarator.id)) { const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); - binding.kind = 'prop'; + binding.kind = 'bindable_prop'; } } } @@ -871,7 +874,9 @@ const runes_scope_tweaker = { ? 'derived' : path.is_rest ? 'rest_prop' - : 'prop'; + : rune === '$props.bindable' + ? 'bindable_prop' + : 'prop'; } if (rune === '$props' || rune === '$props.bindable') { diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 52e5786824bc..dc48a10846e6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -299,17 +299,19 @@ const validation = { error(node, 'invalid-binding-expression'); } + const binding = context.state.scope.get(left.name); + if ( assignee.type === 'Identifier' && node.name !== 'this' // bind:this also works for regular variables ) { - const binding = context.state.scope.get(left.name); // reassignment if ( !binding || (binding.kind !== 'state' && binding.kind !== 'frozen_state' && binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'store_sub' && !binding.mutated) @@ -328,7 +330,9 @@ const validation = { // TODO handle mutations of non-state/props in runes mode } - const binding = context.state.scope.get(left.name); + if (assignee.type === 'MemberExpression' && binding?.kind === 'prop') { + error(node, 'invalid-props-mutation'); + } if (node.name === 'group') { if (!binding) { @@ -969,7 +973,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) { function validate_assignment(node, argument, state) { validate_no_const_assignment(node, argument, state.scope, false); - if (state.analysis.runes && argument.type === 'Identifier') { + if (!state.analysis.runes) return; + + if (argument.type === 'Identifier') { const binding = state.scope.get(argument.name); if (binding?.kind === 'derived') { error(node, 'invalid-derived-assignment'); @@ -978,19 +984,24 @@ function validate_assignment(node, argument, state) { if (binding?.kind === 'each') { error(node, 'invalid-each-assignment'); } + } else if (argument.type === 'MemberExpression') { + const id = object(argument); + if (id && state.scope.get(id.name)?.kind === 'prop') { + error(node, 'invalid-props-mutation'); + } } - let object = /** @type {import('estree').Expression | import('estree').Super} */ (argument); + let obj = /** @type {import('estree').Expression | import('estree').Super} */ (argument); /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ let property = null; - while (object.type === 'MemberExpression') { - property = object.property; - object = object.object; + while (obj.type === 'MemberExpression') { + property = obj.property; + obj = obj.object; } - if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { + if (obj.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { if (state.private_derived_state.includes(property.name)) { error(node, 'invalid-derived-assignment'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 3d378df563b5..4b09a2b3027f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -251,7 +251,8 @@ export function client_component(source, analysis, options) { if (analysis.accessors) { for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind !== 'prop' || name.startsWith('$$')) continue; + if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$')) + continue; const key = binding.prop_alias ?? name; @@ -356,7 +357,7 @@ export function client_component(source, analysis, options) { /** @type {string[]} */ const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); + if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); } component_block.body.unshift( @@ -464,7 +465,8 @@ export function client_component(source, analysis, options) { const props_str = []; for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind !== 'prop' || name.startsWith('$$')) continue; + if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$')) + continue; const key = binding.prop_alias ?? name; const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {}; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index d93f8f5b92c8..cd274eef2377 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -78,7 +78,7 @@ export function serialize_get_binding(node, state) { return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression; } - if (binding.kind === 'prop') { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (binding.node.name === '$$props') { // Special case for $$props which only exists in the old world // TODO this probably shouldn't have a 'prop' binding kind @@ -377,6 +377,7 @@ export function serialize_set_binding(node, context, fallback, options) { binding.kind !== 'state' && binding.kind !== 'frozen_state' && binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && !is_store @@ -389,7 +390,7 @@ export function serialize_set_binding(node, context, fallback, options) { const serialize = () => { if (left === node.left) { - if (binding.kind === 'prop') { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { return b.call(left, value); } else if (is_store) { return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); @@ -467,7 +468,7 @@ export function serialize_set_binding(node, context, fallback, options) { b.call('$.untrack', b.id('$' + left_name)) ); } else if (!state.analysis.runes) { - if (binding.kind === 'prop') { + if (binding.kind === 'bindable_prop') { return b.call( left, b.sequence([ @@ -571,7 +572,7 @@ function get_hoistable_params(node, context) { params.push(b.id(binding.expression.object.arguments[0].name)); } else if ( // If we are referencing a simple $$props value, then we need to reference the object property instead - binding.kind === 'prop' && + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !binding.reassigned && binding.initial === null && !context.state.analysis.accessors diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 5d1689cadcc3..c299dd99ef6f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -52,6 +52,7 @@ export const global_visitors = { binding?.kind === 'each' || binding?.kind === 'legacy_reactive' || binding?.kind === 'prop' || + binding?.kind === 'bindable_prop' || is_store ) { /** @type {import('estree').Expression[]} */ @@ -64,7 +65,7 @@ export const global_visitors = { fn += '_store'; args.push(serialize_get_binding(b.id(name), state), b.call('$' + name)); } else { - if (binding.kind === 'prop') fn += '_prop'; + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop'; args.push(b.id(name)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js index ed4c6e8474c8..ffb089bf83da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js @@ -40,7 +40,7 @@ export const javascript_visitors_legacy = { state.scope.get_bindings(declarator) ); const has_state = bindings.some((binding) => binding.kind === 'state'); - const has_props = bindings.some((binding) => binding.kind === 'prop'); + const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { const init = declarator.init; @@ -80,7 +80,7 @@ export const javascript_visitors_legacy = { declarations.push( b.declarator( path.node, - binding.kind === 'prop' + binding.kind === 'bindable_prop' ? get_prop_source(binding, state, binding.prop_alias ?? name, value) : value ) @@ -168,7 +168,7 @@ export const javascript_visitors_legacy = { // If the binding is a prop, we need to deep read it because it could be fine-grained $state // from a runes-component, where mutations don't trigger an update on the prop as a whole. - if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') { + if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') { serialized = b.call('$.deep_read_state', serialized); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index e5302a3a7c3b..06ba29c33df3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1367,6 +1367,7 @@ function serialize_event_handler(node, { state, visit }) { binding.kind === 'legacy_reactive' || binding.kind === 'derived' || binding.kind === 'prop' || + binding.kind === 'bindable_prop' || binding.kind === 'store_sub') ) { handler = dynamic_handler(); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 2173df9fc443..bac1f0929c7a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) { binding.kind !== 'state' && binding.kind !== 'frozen_state' && binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && !is_store @@ -1131,7 +1132,7 @@ const javascript_visitors_legacy = { state.scope.get_bindings(declarator) ); const has_state = bindings.some((binding) => binding.kind === 'state'); - const has_props = bindings.some((binding) => binding.kind === 'prop'); + const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); @@ -2258,7 +2259,7 @@ export function server_component(analysis, options) { /** @type {import('estree').Property[]} */ const props = []; for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop' && !name.startsWith('$$')) { + if (binding.kind === 'bindable_prop' && !name.startsWith('$$')) { props.push(b.init(binding.prop_alias ?? name, b.id(name))); } } @@ -2280,7 +2281,7 @@ export function server_component(analysis, options) { /** @type {string[]} */ const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); + if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); } component_block.body.unshift( diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 6d09effe9321..9f82c014c892 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -241,7 +241,8 @@ export interface Binding { node: Identifier; /** * - `normal`: A variable that is not in any way special - * - `prop`: A normal prop (possibly mutated) + * - `prop`: A normal prop (possibly reassigned) + * - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated) * - `rest_prop`: A rest prop * - `state`: A state variable * - `derived`: A derived variable @@ -253,6 +254,7 @@ export interface Binding { kind: | 'normal' | 'prop' + | 'bindable_prop' | 'rest_prop' | 'state' | 'frozen_state' diff --git a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js new file mode 100644 index 000000000000..5afbe53acd12 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-props-mutation', + message: + 'Properties defined by $props() cannot be mutated. Use $props.bindable() instead, or make a copy of the value and reassign it.' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte new file mode 100644 index 000000000000..10390e812dfc --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte index a685cc9c84df..c66525b3d2f1 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte @@ -1,5 +1,5 @@ {#each items as item, i} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte index d1be32683013..8b132280c613 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte index d1be32683013..8b132280c613 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte index 172b6992057c..b55b9c16a7c3 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte index a0a4ebd14cbf..9f5880666620 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte index 82b2f0648a68..12a31b34fcfd 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte index e9bcb945b280..690e0a38c2e5 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-behavior/inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-behavior/inner.svelte index 9c66d82b7f56..df6c7ce0152e 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-behavior/inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-behavior/inner.svelte @@ -1,5 +1,5 @@

From 695a3095679bed91d541f9401a78da32b0177cf4 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 16:35:21 +0100 Subject: [PATCH 07/26] remove compiler error due to potential of false positives --- packages/svelte/src/compiler/errors.js | 2 -- .../svelte/src/compiler/phases/2-analyze/validation.js | 9 --------- .../samples/runes-non-bindable-prop-mutated/_config.js | 9 --------- .../samples/runes-non-bindable-prop-mutated/main.svelte | 4 ---- 4 files changed, 24 deletions(-) delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index b1bc2c5c8d1c..72397e1f6ef5 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -182,8 +182,6 @@ const runes = { `$props() assignment must not contain nested properties or computed keys`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, - 'invalid-props-mutation': () => - 'Properties defined by $props() cannot be mutated. Use $props.bindable() instead, or make a copy of the value and reassign it.', /** @param {string} rune */ 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index dc48a10846e6..4e6dbb62395e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -330,10 +330,6 @@ const validation = { // TODO handle mutations of non-state/props in runes mode } - if (assignee.type === 'MemberExpression' && binding?.kind === 'prop') { - error(node, 'invalid-props-mutation'); - } - if (node.name === 'group') { if (!binding) { error(node, 'INTERNAL', 'Cannot find declaration for bind:group'); @@ -984,11 +980,6 @@ function validate_assignment(node, argument, state) { if (binding?.kind === 'each') { error(node, 'invalid-each-assignment'); } - } else if (argument.type === 'MemberExpression') { - const id = object(argument); - if (id && state.scope.get(id.name)?.kind === 'prop') { - error(node, 'invalid-props-mutation'); - } } let obj = /** @type {import('estree').Expression | import('estree').Super} */ (argument); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js deleted file mode 100644 index 5afbe53acd12..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'invalid-props-mutation', - message: - 'Properties defined by $props() cannot be mutated. Use $props.bindable() instead, or make a copy of the value and reassign it.' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte deleted file mode 100644 index 10390e812dfc..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-non-bindable-prop-mutated/main.svelte +++ /dev/null @@ -1,4 +0,0 @@ - From a472ccf828c85c4537dd1d54577c97854963b677 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:03:32 +0100 Subject: [PATCH 08/26] error on rest prop and duplicate props --- packages/svelte/src/compiler/errors.js | 3 +++ .../svelte/src/compiler/phases/2-analyze/index.js | 12 ++++++++++++ .../svelte/src/compiler/phases/2-analyze/types.d.ts | 2 ++ .../src/compiler/phases/2-analyze/validation.js | 10 +++++++++- .../samples/props-bindable-rest/_config.js | 9 +++++++++ .../samples/props-bindable-rest/main.svelte | 4 ++++ .../samples/props-duplicate-destructuring/_config.js | 9 +++++++++ .../props-duplicate-destructuring/main.svelte | 4 ++++ 8 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 72397e1f6ef5..0278308d13ba 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -180,6 +180,9 @@ const runes = { 'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`, 'invalid-props-pattern': () => `$props() assignment must not contain nested properties or computed keys`, + 'duplicate-prop-name': () => + `Cannot use the same prop name more than once across $props() and $props.bindable()`, + 'invalid-props-rest-element': () => `Cannot use ...rest parameter with $props.bindable()`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, /** @param {string} rune */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 05eda25ac5ed..22a7e35cb954 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -422,6 +422,7 @@ export function analyze_component(root, options) { options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', parent_element: null, + all_prop_names: new Set(), has_props_rune: [false, false], component_slots: new Set(), expression: null, @@ -446,6 +447,7 @@ export function analyze_component(root, options) { analysis, options, parent_element: null, + all_prop_names: new Set(), has_props_rune: [false, false], ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', instance_scope: instance.scope, @@ -525,6 +527,16 @@ export function analyze_component(root, options) { } } + if (analysis.runes) { + const props = new Set(); + for (const [name, binding] of instance.scope.declarations) { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { + if (props.has(binding)) { + } + } + } + } + if (analysis.css.ast) { analyze_css(analysis.css.ast, analysis); diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 75d84d3e7a65..867b3bae6564 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -15,6 +15,8 @@ export interface AnalysisState { options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; parent_element: string | null; + /** Names of $props() and $props.bindable() */ + all_prop_names: Set; has_props_rune: [props: boolean, bindings: boolean]; /** Which slots the current parent component has */ component_slots: Set; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 4e6dbb62395e..f54893c56ece 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1092,9 +1092,17 @@ export const validation_runes = merge(validation, a11y_validators, { const value = property.value.type === 'AssignmentPattern' ? property.value.left : property.value; - if (value.type !== 'Identifier') { + if (value.type !== 'Identifier' || property.key.type !== 'Identifier') { error(property, 'invalid-props-pattern'); } + + if (state.all_prop_names.has(property.key.name)) { + error(property, 'duplicate-prop-name'); + } + + state.all_prop_names.add(property.key.name); + } else if (rune === '$props.bindable') { + error(property, 'invalid-props-rest-element'); } } } diff --git a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js new file mode 100644 index 000000000000..9753642bc5e7 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-props-rest-element', + message: 'Cannot use ...rest parameter with $props.bindable()', + position: [53, 62] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte new file mode 100644 index 000000000000..3f103f66d970 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte @@ -0,0 +1,4 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js new file mode 100644 index 000000000000..4bdf1e629117 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'duplicate-prop-name', + message: 'Cannot use the same prop name more than once across $props() and $props.bindable()', + position: [44, 50] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte new file mode 100644 index 000000000000..733315cee0df --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte @@ -0,0 +1,4 @@ + From efcc5ac6b3d5ebd4559f0c0b11b53d577319ae59 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:19:21 +0100 Subject: [PATCH 09/26] revert duplicate prop name validation (valid use cases exist) --- .../svelte/src/compiler/phases/2-analyze/index.js | 12 ------------ .../svelte/src/compiler/phases/2-analyze/types.d.ts | 2 -- .../src/compiler/phases/2-analyze/validation.js | 6 ------ .../samples/bind-state-property/CheckBox.svelte | 3 ++- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 22a7e35cb954..05eda25ac5ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -422,7 +422,6 @@ export function analyze_component(root, options) { options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', parent_element: null, - all_prop_names: new Set(), has_props_rune: [false, false], component_slots: new Set(), expression: null, @@ -447,7 +446,6 @@ export function analyze_component(root, options) { analysis, options, parent_element: null, - all_prop_names: new Set(), has_props_rune: [false, false], ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', instance_scope: instance.scope, @@ -527,16 +525,6 @@ export function analyze_component(root, options) { } } - if (analysis.runes) { - const props = new Set(); - for (const [name, binding] of instance.scope.declarations) { - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { - if (props.has(binding)) { - } - } - } - } - if (analysis.css.ast) { analyze_css(analysis.css.ast, analysis); diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 867b3bae6564..75d84d3e7a65 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -15,8 +15,6 @@ export interface AnalysisState { options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; parent_element: string | null; - /** Names of $props() and $props.bindable() */ - all_prop_names: Set; has_props_rune: [props: boolean, bindings: boolean]; /** Which slots the current parent component has */ component_slots: Set; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index f54893c56ece..d0267fa70ea8 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1095,12 +1095,6 @@ export const validation_runes = merge(validation, a11y_validators, { if (value.type !== 'Identifier' || property.key.type !== 'Identifier') { error(property, 'invalid-props-pattern'); } - - if (state.all_prop_names.has(property.key.name)) { - error(property, 'duplicate-prop-name'); - } - - state.all_prop_names.add(property.key.name); } else if (rune === '$props.bindable') { error(property, 'invalid-props-rest-element'); } diff --git a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte index b55b9c16a7c3..41bf11f5daf7 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte @@ -1,5 +1,6 @@ From 4d92bfb7b0badf93d50cfe4445285a7164f95837 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:21:00 +0100 Subject: [PATCH 10/26] fix --- packages/svelte/src/compiler/phases/2-analyze/validation.js | 2 +- .../runtime-runes/samples/bind-and-spread/button.svelte | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index d0267fa70ea8..ff82e11ee415 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1092,7 +1092,7 @@ export const validation_runes = merge(validation, a11y_validators, { const value = property.value.type === 'AssignmentPattern' ? property.value.left : property.value; - if (value.type !== 'Identifier' || property.key.type !== 'Identifier') { + if (value.type !== 'Identifier') { error(property, 'invalid-props-pattern'); } } else if (rune === '$props.bindable') { diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte index 8530291abfdd..8f5dd0395f59 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte @@ -1,5 +1,6 @@ - + From b0a8aaf8551f3720495622b996a15b4f2b7379c2 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:43:39 +0100 Subject: [PATCH 11/26] more fixes --- .../src/compiler/phases/3-transform/client/utils.js | 11 ++++++++++- packages/svelte/src/compiler/types/index.d.ts | 2 +- .../samples/bind-and-spread/button.svelte | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 604bfdff6ae9..836ecaf8d037 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -640,7 +640,16 @@ export function get_prop_source(binding, state, name, initial) { flags |= PROPS_IS_RUNES; } - if (binding.kind === 'bindable_prop') { + 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; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 9f82c014c892..68a9751efb21 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -282,7 +282,7 @@ export interface Binding { scope: Scope; /** For `legacy_reactive`: its reactive dependencies */ legacy_dependencies: Binding[]; - /** Legacy props: the `class` in `{ export klass as class}` */ + /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */ prop_alias: string | null; /** * If this is set, all references should use this expression instead of the identifier name. diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte index 8f5dd0395f59..988f4f41865b 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte @@ -1,5 +1,5 @@ From 17b916c8cd6819e341e826e390569b0cea3010a3 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:46:59 +0100 Subject: [PATCH 12/26] fix another edge case --- .../phases/3-transform/client/transform-client.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 4b09a2b3027f..3358b78063be 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -255,6 +255,15 @@ export function client_component(source, analysis, options) { continue; const key = binding.prop_alias ?? name; + if ( + binding.kind === 'prop' && + [...analysis.instance.scope.declarations].some( + ([name, d]) => d.kind === 'bindable_prop' && (d.prop_alias ?? name) === key + ) + ) { + // bindable prop takes precedence + continue; + } properties.push( b.get(key, [b.return(b.call(b.id(name)))]), From cc90ffa0e3f28126f9cd072f7926513d591a77b5 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:55:51 +0100 Subject: [PATCH 13/26] woops --- packages/svelte/src/compiler/errors.js | 2 -- .../samples/props-duplicate-destructuring/_config.js | 9 --------- .../samples/props-duplicate-destructuring/main.svelte | 4 ---- 3 files changed, 15 deletions(-) delete mode 100644 packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 0278308d13ba..4a6ca214b530 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -180,8 +180,6 @@ const runes = { 'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`, 'invalid-props-pattern': () => `$props() assignment must not contain nested properties or computed keys`, - 'duplicate-prop-name': () => - `Cannot use the same prop name more than once across $props() and $props.bindable()`, 'invalid-props-rest-element': () => `Cannot use ...rest parameter with $props.bindable()`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, diff --git a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js deleted file mode 100644 index 4bdf1e629117..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'duplicate-prop-name', - message: 'Cannot use the same prop name more than once across $props() and $props.bindable()', - position: [44, 50] - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte b/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte deleted file mode 100644 index 733315cee0df..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/props-duplicate-destructuring/main.svelte +++ /dev/null @@ -1,4 +0,0 @@ - From 4f976399eaa99c0ca6cf63665869352365f9a1c6 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Mar 2024 17:56:28 +0100 Subject: [PATCH 14/26] regenerate --- packages/svelte/types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index e2f0e9f5f345..7ea6743cf0b8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -749,7 +749,7 @@ declare module 'svelte/compiler' { scope: Scope; /** For `legacy_reactive`: its reactive dependencies */ legacy_dependencies: Binding[]; - /** Legacy props: the `class` in `{ export klass as class}` */ + /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */ prop_alias: string | null; /** * If this is set, all references should use this expression instead of the identifier name. From 84e2dd313034c66bb29b874c6013b2b5a4eb5a69 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 19 Mar 2024 20:18:19 +0100 Subject: [PATCH 15/26] make it a dev-time validation error that also deals with ...rest props --- .../3-transform/client/transform-client.js | 35 +++++++++++++------ .../phases/3-transform/client/utils.js | 16 +-------- packages/svelte/src/constants.js | 1 - .../src/internal/client/reactivity/props.js | 10 ------ .../svelte/src/internal/client/validate.js | 21 ++++++++++- .../props-not-bindable-spread/Counter.svelte | 5 +++ .../props-not-bindable-spread/_config.js | 10 ++++++ .../props-not-bindable-spread/main.svelte | 7 ++++ .../samples/props-not-bindable/_config.js | 5 ++- 9 files changed, 72 insertions(+), 38 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/Counter.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/main.svelte diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 3358b78063be..eaef6aa549d1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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); 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)]); }); - if (analysis.accessors) { - for (const [name, binding] of analysis.instance.scope.declarations) { - if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$')) - continue; + const properties = [...analysis.instance.scope.declarations].filter( + ([name, binding]) => + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$') + ); + 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; if ( binding.kind === 'prop' && @@ -265,7 +280,7 @@ export function client_component(source, analysis, options) { continue; } - properties.push( + component_returned_object.push( 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'))]) ); @@ -273,7 +288,7 @@ export function client_component(source, analysis, options) { } if (options.legacy.componentApi) { - properties.push( + component_returned_object.push( b.init('$set', b.id('$.update_legacy_props')), b.init( '$on', @@ -289,7 +304,7 @@ export function client_component(source, analysis, options) { ) ); } else if (options.dev) { - properties.push( + component_returned_object.push( b.init( '$set', b.thunk( @@ -357,8 +372,8 @@ export function client_component(source, analysis, options) { append_styles(); component_block.body.push( - properties.length > 0 - ? b.return(b.call('$.pop', b.object(properties))) + component_returned_object.length > 0 + ? b.return(b.call('$.pop', b.object(component_returned_object))) : b.stmt(b.call('$.pop')) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 836ecaf8d037..cd274eef2377 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -5,8 +5,7 @@ import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - PROPS_IS_BINDABLE + PROPS_IS_UPDATED } from '../../../../constants.js'; /** @@ -640,19 +639,6 @@ export function get_prop_source(binding, state, name, initial) { 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 ( state.analysis.accessors || (state.analysis.immutable ? binding.reassigned : binding.mutated) diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 6f5c1673bc18..7996e3b2fa31 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -11,7 +11,6 @@ export const PROPS_IS_IMMUTABLE = 1; export const PROPS_IS_RUNES = 1 << 1; export const PROPS_IS_UPDATED = 1 << 2; export const PROPS_IS_LAZY_INITIAL = 1 << 3; -export const PROPS_IS_BINDABLE = 1 << 4; /** List of Element events that will be delegated */ export const DelegatedEvents = [ diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 56aa23101cca..586f0e64b1b6 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,6 +1,5 @@ import { DEV } from 'esm-env'; import { - PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, @@ -143,15 +142,6 @@ export function prop(props, key, flags, initial) { var prop_value = /** @type {V} */ (props[key]); 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 (setter && runes) { // TODO consolidate all these random runtime errors diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 442d91190b9a..e3ded5a50d2e 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,5 +1,5 @@ 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 */ const void_element_names = @@ -137,3 +137,22 @@ export function validate_component(component_fn) { } return component_fn; } + +/** + * @param {Record} $$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.` + ); + } + } + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/Counter.svelte new file mode 100644 index 000000000000..f22fd6e976dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/Counter.svelte @@ -0,0 +1,5 @@ + + +{rest.count} diff --git a/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/_config.js new file mode 100644 index 000000000000..4cc6e7ff759b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/_config.js @@ -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` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/main.svelte new file mode 100644 index 000000000000..80242b75c6dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-not-bindable/_config.js b/packages/svelte/tests/runtime-runes/samples/props-not-bindable/_config.js index e70b8067ccfa..4cc6e7ff759b 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-not-bindable/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/props-not-bindable/_config.js @@ -1,7 +1,10 @@ import { test } from '../../test'; export default test({ + compileOptions: { + dev: true + }, 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` }); From 1d818380dd30afc28f81b82adedc9fb091f1e531 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 19 Mar 2024 23:01:52 +0100 Subject: [PATCH 16/26] allow rest props on $props.bindable() and add related dev time validation, closes #10711 --- packages/svelte/src/compiler/errors.js | 1 - .../compiler/phases/2-analyze/validation.js | 2 - .../client/visitors/javascript-runes.js | 16 +++--- .../src/internal/client/reactivity/props.js | 35 ++++++++++--- .../samples/props-bindable-rest/_config.js | 9 ---- .../samples/props-bindable-rest/main.svelte | 4 -- .../props-bindable-spread/Counter.svelte | 5 ++ .../samples/props-bindable-spread/_config.js | 49 +++++++++++++++++++ .../samples/props-bindable-spread/main.svelte | 13 +++++ 9 files changed, 100 insertions(+), 34 deletions(-) delete mode 100644 packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 4a6ca214b530..72397e1f6ef5 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -180,7 +180,6 @@ const runes = { 'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`, 'invalid-props-pattern': () => `$props() assignment must not contain nested properties or computed keys`, - 'invalid-props-rest-element': () => `Cannot use ...rest parameter with $props.bindable()`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, /** @param {string} rune */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index ff82e11ee415..4e6dbb62395e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1095,8 +1095,6 @@ export const validation_runes = merge(validation, a11y_validators, { if (value.type !== 'Identifier') { error(property, 'invalid-props-pattern'); } - } else if (rune === '$props.bindable') { - error(property, 'invalid-props-rest-element'); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index bed57cfa862e..df2eca4b9636 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -224,16 +224,12 @@ export const javascript_visitors_runes = { } } else { // RestElement - declarations.push( - b.declarator( - property.argument, - b.call( - '$.rest_props', - b.id('$$props'), - b.array(seen.map((name) => b.literal(name))) - ) - ) - ); + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + if (rune === '$props.bindable') { + args.push(b.literal(true)); + } + declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 586f0e64b1b6..3961ca53adb6 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -36,21 +36,39 @@ export function update_pre_prop(fn, d = 1) { /** * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`). * Is passed the full `$$props` object and excludes the named props. - * @type {ProxyHandler<{ props: Record, exclude: Array }>}} + * @type {ProxyHandler<{ props: Record, exclude: Array, p: boolean }>}} */ const rest_props_handler = { get(target, key) { if (target.exclude.includes(key)) return; return target.props[key]; }, + set(target, key, value) { + if (target.exclude.includes(key) || !(key in target.props)) return false; + if (DEV) { + if (!target.p) { + throw new Error( + `Cannot set read-only property '${String(key)}' of rest element of $props(). Only rest elements from $props.bindable() can be written to.'` + ); + } else if (!get_descriptor(target.props, key)?.set) { + throw new Error( + `Cannot write to property '${String(key)}' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component.` + ); + } + } + target.props[key] = value; + return true; + }, getOwnPropertyDescriptor(target, key) { if (target.exclude.includes(key)) return; if (key in target.props) { - return { - enumerable: true, - configurable: true, - value: target.props[key] - }; + return target.p + ? get_descriptor(target.props, key) + : { + enumerable: true, + configurable: true, + value: target.props[key] + }; } }, has(target, key) { @@ -65,10 +83,11 @@ const rest_props_handler = { /** * @param {Record} props * @param {string[]} rest + * @param {boolean} [preserve_setters] * @returns {Record} */ -export function rest_props(props, rest) { - return new Proxy({ props, exclude: rest }, rest_props_handler); +export function rest_props(props, rest, preserve_setters = false) { + return new Proxy({ props, exclude: rest, p: preserve_setters }, rest_props_handler); } /** diff --git a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js deleted file mode 100644 index 9753642bc5e7..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/_config.js +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'invalid-props-rest-element', - message: 'Cannot use ...rest parameter with $props.bindable()', - position: [53, 62] - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte b/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte deleted file mode 100644 index 3f103f66d970..000000000000 --- a/packages/svelte/tests/compiler-errors/samples/props-bindable-rest/main.svelte +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte new file mode 100644 index 000000000000..91f1d7f3bbc1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js new file mode 100644 index 000000000000..6dc0d2cff708 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js @@ -0,0 +1,49 @@ +import { test } from '../../test'; + +let failed_too_soon = true; + +export default test({ + html: ` +

0 0 0

+ + + + `, + + before_test() { + failed_too_soon = true; + }, + async test({ assert, target }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + + b1.click(); + b2.click(); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +

1 1 0

+ + + + ` + ); + + failed_too_soon = false; + + b3.click(); + await Promise.resolve(); + }, + test_ssr() { + failed_too_soon = false; + }, + after_test() { + if (failed_too_soon) { + throw new Error('Test failed too soon'); + } + }, + + runtime_error: + "Cannot write to property 'count' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component." +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte new file mode 100644 index 000000000000..937416cedb17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte @@ -0,0 +1,13 @@ + + +

{bound} {bound_nested.count} {unbound}

+ + + + From 6f274ac9147fc5421cc4503d6e97ebfeac3c0edc Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 19 Mar 2024 23:12:11 +0100 Subject: [PATCH 17/26] tweak --- .../src/compiler/phases/2-analyze/validation.js | 14 ++++++-------- .../phases/3-transform/client/transform-client.js | 5 +---- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 4e6dbb62395e..34e86a6f29d0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -969,9 +969,7 @@ function validate_no_const_assignment(node, argument, scope, is_binding) { function validate_assignment(node, argument, state) { validate_no_const_assignment(node, argument, state.scope, false); - if (!state.analysis.runes) return; - - if (argument.type === 'Identifier') { + if (state.analysis.runes && argument.type === 'Identifier') { const binding = state.scope.get(argument.name); if (binding?.kind === 'derived') { error(node, 'invalid-derived-assignment'); @@ -982,17 +980,17 @@ function validate_assignment(node, argument, state) { } } - let obj = /** @type {import('estree').Expression | import('estree').Super} */ (argument); + let object = /** @type {import('estree').Expression | import('estree').Super} */ (argument); /** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */ let property = null; - while (obj.type === 'MemberExpression') { - property = obj.property; - obj = obj.object; + while (object.type === 'MemberExpression') { + property = object.property; + object = object.object; } - if (obj.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { + if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') { if (state.private_derived_state.includes(property.name)) { error(node, 'invalid-derived-assignment'); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index eaef6aa549d1..ae72d5e07068 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -488,10 +488,7 @@ export function client_component(source, analysis, options) { /** @type {import('estree').Property[]} */ const props_str = []; - for (const [name, binding] of analysis.instance.scope.declarations) { - if ((binding.kind !== 'prop' && binding.kind !== 'bindable_prop') || name.startsWith('$$')) - continue; - + for (const [name, binding] of properties) { const key = binding.prop_alias ?? name; const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {}; if ( From 6d6e94c3748c2b6e8cf65e2a857e41cb48a16acb Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 20 Mar 2024 23:30:57 +0100 Subject: [PATCH 18/26] breaking: add `$bindable()` rune to denote bindable props alternative to #10804 --- packages/svelte/src/compiler/errors.js | 1 + .../src/compiler/phases/2-analyze/index.js | 30 ++++++++---- .../src/compiler/phases/2-analyze/types.d.ts | 2 +- .../compiler/phases/2-analyze/validation.js | 26 +++++++--- .../client/visitors/javascript-runes.js | 20 +++----- .../3-transform/server/transform-server.js | 18 ++++++- .../svelte/src/compiler/phases/constants.js | 2 +- .../svelte/src/compiler/utils/builders.js | 9 ++++ .../src/internal/client/reactivity/props.js | 37 +++++--------- .../svelte/src/internal/client/validate.js | 2 +- packages/svelte/src/main/ambient.d.ts | 26 +++++----- .../samples/bind-and-spread/button.svelte | 3 +- .../bind-state-property/CheckBox.svelte | 3 +- .../samples/each-bind-this-member/main.svelte | 2 +- .../Counter.svelte | 2 +- .../sub.svelte | 3 +- .../Counter.svelte | 2 +- .../samples/props-alias/Counter.svelte | 2 +- .../props-bindable-spread/Counter.svelte | 5 -- .../samples/props-bindable-spread/_config.js | 49 ------------------- .../samples/props-bindable-spread/main.svelte | 13 ----- .../props-bound-fallback/Counter.svelte | 2 +- .../props-bound-to-normal/Inner.svelte | 2 +- .../samples/props-bound/Counter.svelte | 2 +- .../props-default-reactivity/Counter.svelte | 2 +- .../props-default-value-behavior/inner.svelte | 2 +- .../props-not-bindable-spread/_config.js | 3 +- .../samples/props-not-bindable/_config.js | 3 +- .../samples/proxy-prop-bound/Counter.svelte | 2 +- packages/svelte/types/index.d.ts | 26 +++++----- .../routes/docs/content/01-api/02-runes.md | 12 +++-- 31 files changed, 135 insertions(+), 178 deletions(-) delete mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js delete mode 100644 packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 72397e1f6ef5..8e38a95b6107 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -182,6 +182,7 @@ const runes = { `$props() assignment must not contain nested properties or computed keys`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, + 'invalid-bindable-location': () => `$bindable() can only be used as part of the $props() rune`, /** @param {string} rune */ 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 05eda25ac5ed..064a6bbb3471 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -422,7 +422,7 @@ export function analyze_component(root, options) { options, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', parent_element: null, - has_props_rune: [false, false], + has_props_rune: false, component_slots: new Set(), expression: null, private_derived_state: [], @@ -446,7 +446,7 @@ export function analyze_component(root, options) { analysis, options, parent_element: null, - has_props_rune: [false, false], + has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', instance_scope: instance.scope, reactive_statement: null, @@ -857,8 +857,7 @@ const runes_scope_tweaker = { rune !== '$state.frozen' && rune !== '$derived' && rune !== '$derived.by' && - rune !== '$props' && - rune !== '$props.bindable' + rune !== '$props' ) return; @@ -874,12 +873,10 @@ const runes_scope_tweaker = { ? 'derived' : path.is_rest ? 'rest_prop' - : rune === '$props.bindable' - ? 'bindable_prop' - : 'prop'; + : 'prop'; } - if (rune === '$props' || rune === '$props.bindable') { + if (rune === '$props') { for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { if (property.type !== 'Property') continue; @@ -891,11 +888,24 @@ const runes_scope_tweaker = { property.key.type === 'Identifier' ? property.key.name : /** @type {string} */ (/** @type {import('estree').Literal} */ (property.key).value); - const initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; + let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); binding.prop_alias = alias; - binding.initial = initial; // rewire initial from $props() to the actual initial value + + // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary + if ( + initial?.type === 'CallExpression' && + initial.callee.type === 'Identifier' && + initial.callee.name === '$bindable' + ) { + binding.initial = /** @type {import('estree').Expression | null} */ ( + initial.arguments[0] ?? null + ); + binding.kind = 'bindable_prop'; + } else { + binding.initial = initial; + } } } }, diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 75d84d3e7a65..d2c503e8b108 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -15,7 +15,7 @@ export interface AnalysisState { options: ValidatedCompileOptions; ast_type: 'instance' | 'template' | 'module'; parent_element: string | null; - has_props_rune: [props: boolean, bindings: boolean]; + has_props_rune: boolean; /** Which slots the current parent component has */ component_slots: Set; /** The current {expression}, if any */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 34e86a6f29d0..f20e07ebaa91 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -775,11 +775,24 @@ function validate_call_expression(node, scope, path) { const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1)); - if (rune === '$props' || rune === '$props.bindable') { + if (rune === '$props') { if (parent.type === 'VariableDeclarator') return; error(node, 'invalid-props-location'); } + if (rune === '$bindable') { + if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') { + const declarator = path.at(-4); + if ( + declarator?.type === 'VariableDeclarator' && + get_rune(declarator.init, scope) === '$props' + ) { + return; + } + } + error(node, 'invalid-bindable-location'); + } + if ( rune === '$state' || rune === '$state.frozen' || @@ -876,7 +889,7 @@ export const validation_runes_js = { error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { error(node, 'invalid-rune-args-length', rune, [0, 1]); - } else if (rune === '$props' || rune === '$props.bindable') { + } else if (rune === '$props') { error(node, 'invalid-props-location'); } }, @@ -1059,15 +1072,12 @@ export const validation_runes = merge(validation, a11y_validators, { error(node, 'invalid-rune-args-length', rune, [1]); } else if (rune === '$state' && args.length > 1) { error(node, 'invalid-rune-args-length', rune, [0, 1]); - } else if (rune === '$props' || rune === '$props.bindable') { - if ( - (rune === '$props' && state.has_props_rune[0]) || - (rune === '$props.bindable' && state.has_props_rune[1]) - ) { + } else if (rune === '$props') { + if (rune === '$props' && state.has_props_rune) { error(node, 'duplicate-props-rune'); } - state.has_props_rune[rune === '$props' ? 0 : 1] = true; + state.has_props_rune = true; if (args.length > 0) { error(node, 'invalid-rune-args-length', rune, [0]); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index df2eca4b9636..1bccfaec5eef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -192,7 +192,7 @@ export const javascript_visitors_runes = { continue; } - if (rune === '$props' || rune === '$props.bindable') { + if (rune === '$props') { assert.equal(declarator.id.type, 'ObjectPattern'); /** @type {string[]} */ @@ -207,17 +207,14 @@ export const javascript_visitors_runes = { seen.push(name); - let id = property.value; - let initial = undefined; - - if (property.value.type === 'AssignmentPattern') { - id = property.value.left; - initial = /** @type {import('estree').Expression} */ (visit(property.value.right)); - } - + let id = + property.value.type === 'AssignmentPattern' ? property.value.left : property.value; assert.equal(id.type, 'Identifier'); - const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); + let initial = /** @type {import('estree').Expression | null} */ (binding.initial); + if (initial) { + initial = /** @type {import('estree').Expression} */ (visit(initial)); + } if (binding.reassigned || state.analysis.accessors || initial) { declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); @@ -226,9 +223,6 @@ export const javascript_visitors_runes = { // RestElement /** @type {import('estree').Expression[]} */ const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; - if (rune === '$props.bindable') { - args.push(b.literal(true)); - } declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 65ced342a00b..fc5ef8e57e18 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -690,8 +690,22 @@ const javascript_visitors_runes = { continue; } - if (rune === '$props' || rune === '$props.bindable') { - declarations.push(b.declarator(declarator.id, b.id('$$props'))); + if (rune === '$props') { + // remove $bindable() from props declaration + const id = walk(declarator.id, null, { + AssignmentPattern(node) { + if ( + node.right.type === 'CallExpression' && + get_rune(node.right, state.scope) === '$bindable' + ) { + const right = node.right.arguments.length + ? /** @type {import('estree').Expression} */ (visit(node.right.arguments[0])) + : b.id('undefined'); + return b.assignment_pattern(node.left, right); + } + } + }); + declarations.push(b.declarator(id, b.id('$$props'))); continue; } diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index affa67aa4f10..822174d4dd1a 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -32,7 +32,7 @@ export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', '$props', - '$props.bindable', + '$bindable', '$derived', '$derived.by', '$effect', diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index bc6df3a8f061..e3a12493bb83 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -17,6 +17,15 @@ export function array_pattern(elements) { return { type: 'ArrayPattern', elements }; } +/** + * @param {import('estree').Pattern} left + * @param {import('estree').Expression} right + * @returns {import('estree').AssignmentPattern} + */ +export function assignment_pattern(left, right) { + return { type: 'AssignmentPattern', left, right }; +} + /** * @param {Array} params * @param {import('estree').BlockStatement | import('estree').Expression} body diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 3961ca53adb6..9a87893516b8 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -36,39 +36,29 @@ export function update_pre_prop(fn, d = 1) { /** * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`). * Is passed the full `$$props` object and excludes the named props. - * @type {ProxyHandler<{ props: Record, exclude: Array, p: boolean }>}} + * @type {ProxyHandler<{ props: Record, exclude: Array }>}} */ const rest_props_handler = { get(target, key) { if (target.exclude.includes(key)) return; return target.props[key]; }, - set(target, key, value) { - if (target.exclude.includes(key) || !(key in target.props)) return false; + set(_, key) { if (DEV) { - if (!target.p) { - throw new Error( - `Cannot set read-only property '${String(key)}' of rest element of $props(). Only rest elements from $props.bindable() can be written to.'` - ); - } else if (!get_descriptor(target.props, key)?.set) { - throw new Error( - `Cannot write to property '${String(key)}' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component.` - ); - } + throw new Error( + `Cannot write to property '${String(key)}' of rest element of $props(). It is always readonly.` + ); } - target.props[key] = value; - return true; + return false; }, getOwnPropertyDescriptor(target, key) { if (target.exclude.includes(key)) return; if (key in target.props) { - return target.p - ? get_descriptor(target.props, key) - : { - enumerable: true, - configurable: true, - value: target.props[key] - }; + return { + enumerable: true, + configurable: true, + value: target.props[key] + }; } }, has(target, key) { @@ -83,11 +73,10 @@ const rest_props_handler = { /** * @param {Record} props * @param {string[]} rest - * @param {boolean} [preserve_setters] * @returns {Record} */ -export function rest_props(props, rest, preserve_setters = false) { - return new Proxy({ props, exclude: rest, p: preserve_setters }, rest_props_handler); +export function rest_props(props, rest) { + return new Proxy({ props, exclude: rest }, rest_props_handler); } /** diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index e3ded5a50d2e..befd68b02758 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -150,7 +150,7 @@ export function validate_prop_bindings($$props, bindable) { 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.` + `To mark a property as bindable, use the $bindable() rune like this: \`let { ${key} = $bindable() } = $props()\`` ); } } diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index c9d4d9e8b867..a2ad6d63af9a 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -172,27 +172,23 @@ declare namespace $effect { * Declares the props that a component accepts. Example: * * ```ts - * let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props(); + * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props(); * ``` * - * Props declared with `$props()` cannot be used with `bind:`, use `$props.bindable()` for these instead. - * * https://svelte-5-preview.vercel.app/docs/runes#$props */ declare function $props(): any; -declare namespace $props { - /** - * Declares the props that a component accepts and which consumers can `bind:` to. Example: - * - * ```ts - * let { optionalProp, requiredProp }: { optionalProp?: number; requiredProps: string } = $props.bindable(); - * ``` - * - * https://svelte-5-preview.vercel.app/docs/runes#$props - */ - function bindable(): any; -} +/** + * Declares a prop as bindable, meaning the parent component can use `bind:propName={value}` to bind to it. + * + * ```ts + * let { propName = $bindable() }: { propName: boolean } = $props(); + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$bindable + */ +declare function $bindable(t?: T): T; /** * Inspects one or more values whenever they, or the properties they contain, change. Example: diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte index 988f4f41865b..7bb18ec5f7e0 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte @@ -1,6 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte index 41bf11f5daf7..22c46a363d47 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte @@ -1,6 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte index c66525b3d2f1..7d46ea90f084 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte @@ -1,5 +1,5 @@ {#each items as item, i} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte index 8b132280c613..57cbebde1259 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte index 8b132280c613..57cbebde1259 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte deleted file mode 100644 index 91f1d7f3bbc1..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/Counter.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js deleted file mode 100644 index 6dc0d2cff708..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/_config.js +++ /dev/null @@ -1,49 +0,0 @@ -import { test } from '../../test'; - -let failed_too_soon = true; - -export default test({ - html: ` -

0 0 0

- - - - `, - - before_test() { - failed_too_soon = true; - }, - async test({ assert, target }) { - const [b1, b2, b3] = target.querySelectorAll('button'); - - b1.click(); - b2.click(); - await Promise.resolve(); - - assert.htmlEqual( - target.innerHTML, - ` -

1 1 0

- - - - ` - ); - - failed_too_soon = false; - - b3.click(); - await Promise.resolve(); - }, - test_ssr() { - failed_too_soon = false; - }, - after_test() { - if (failed_too_soon) { - throw new Error('Test failed too soon'); - } - }, - - runtime_error: - "Cannot write to property 'count' of rest element of $props.bindable(). It is readonly because it was not declared using bind: on the consumer component." -}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte deleted file mode 100644 index 937416cedb17..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/props-bindable-spread/main.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -

{bound} {bound_nested.count} {unbound}

- - - - diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte index 4c5d96ee6886..a2bda4c70b5d 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte @@ -1,5 +1,5 @@ {count} diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte index 12a31b34fcfd..3d1261071e0f 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte index 690e0a38c2e5..67b08a561f13 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte index 973a634a29ac..077eda5709bc 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte @@ -1,6 +1,6 @@