From b93adb7d00ec58b7a4eaaa1046e98ce188b2c1ef Mon Sep 17 00:00:00 2001 From: Ahmad S Date: Thu, 27 Jun 2024 10:49:48 +0300 Subject: [PATCH 1/8] docs: correct $effect examples (#12196) --- .../src/routes/docs/content/01-api/02-runes.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 4a84e3a0f7..b83d347d2b 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 @@ -337,7 +337,7 @@ In general, `$effect` is best considered something of an escape hatch — useful > For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by). -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRy2rDMBD8lWXJwYE0dg-9KFYg31H3oNirIJBlYa1DjPG_F8l1XEop9LgzOzP7mFAbSwHF-4ROtYQCL97jAXn0sQh3skx4wNANfR2RMtS98XyuXMWWGLhjZUHCa1GcVix4cgwSdoEVU1bsn4wl_Y1I2kS6inekNdWcZXuQZ5giFDWpfwl5WYyT2fynbB1g1UWbTVbm2w6utOpKNq1TGucHhri6rLBX7kYVwtW4RtyVHUhOyXeGVj3klLxnyJP0i8lXNJUx6en-v6A48K85kTimpi0sYj-yAo-Wlh9FcL1LY4K3ahSgLT1OC3ZTXkBxfKN2uVC6T5LjAduuMdpQg4L7geaP-RNHPuClMQIAAA==)): +You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)): ```svelte + + + diff --git a/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js new file mode 100644 index 0000000000..c2718e2850 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/_config.js @@ -0,0 +1,29 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const audio = target.querySelector('audio'); + const button = target.querySelector('button'); + ok(audio); + + assert.equal(audio.playbackRate, 0.5); + + audio.playbackRate = 1.0; + audio.dispatchEvent(new CustomEvent('ratechange')); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 1.0); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 2); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 3); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.playbackRate, 4); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte new file mode 100644 index 0000000000..07c2080637 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-playbackrate/main.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js new file mode 100644 index 0000000000..61f3db9692 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-volume/_config.js @@ -0,0 +1,29 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const audio = target.querySelector('audio'); + const button = target.querySelector('button'); + ok(audio); + + assert.equal(audio.volume, 0.1); + + audio.volume = 0.2; + audio.dispatchEvent(new CustomEvent('volumechange')); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1); // JavaScript can't add floating point numbers correctly + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1 + 0.1); + + button?.click(); + await new Promise((r) => setTimeout(r, 100)); + assert.equal(audio.volume, 0.2 + 0.1 + 0.1 + 0.1); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte new file mode 100644 index 0000000000..21755e0cba --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-volume/main.svelte @@ -0,0 +1,6 @@ + + + + From 33e44ea697356ea3f21fc6649919cf7fb4948b22 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 28 Jun 2024 09:20:41 +0200 Subject: [PATCH 7/8] feat: allow `let props = $props()`, optimize prop read access (#12201) - allow to write `let props = $props()` - optimize read access of props.x to use `$$props` argument directly; closes #11055 --- .changeset/six-gorillas-obey.md | 5 ++ .../src/compiler/phases/2-analyze/index.js | 65 ++++++++------ .../compiler/phases/2-analyze/validation.js | 22 ++--- .../3-transform/client/visitors/global.js | 21 +++++ .../client/visitors/javascript-runes.js | 86 +++++++++++-------- .../src/internal/client/reactivity/props.js | 1 + .../_expected/client/index.svelte.js | 17 ++++ .../_expected/server/index.svelte.js | 16 ++++ .../samples/props-identifier/index.svelte | 10 +++ .../routes/docs/content/01-api/02-runes.md | 6 ++ 10 files changed, 174 insertions(+), 75 deletions(-) create mode 100644 .changeset/six-gorillas-obey.md create mode 100644 packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-identifier/index.svelte diff --git a/.changeset/six-gorillas-obey.md b/.changeset/six-gorillas-obey.md new file mode 100644 index 0000000000..258505a381 --- /dev/null +++ b/.changeset/six-gorillas-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: allow `let props = $props()` and optimize prop read access diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2e46d18c6f..f0f0178144 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -31,6 +31,7 @@ import { hash } from './utils.js'; import { warn_unused } from './css/css-warn.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js'; +import { equal } from '../../utils/assert.js'; /** * @param {import('#compiler').Script | null} script @@ -969,34 +970,42 @@ const runes_scope_tweaker = { if (rune === '$props') { state.analysis.needs_props = true; - for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { - if (property.type !== 'Property') continue; - - const name = - property.value.type === 'AssignmentPattern' - ? /** @type {import('estree').Identifier} */ (property.value.left).name - : /** @type {import('estree').Identifier} */ (property.value).name; - const alias = - property.key.type === 'Identifier' - ? property.key.name - : String(/** @type {import('estree').Literal} */ (property.key).value); - let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; - - const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); - binding.prop_alias = alias; - - // 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; + if (node.id.type === 'Identifier') { + const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name)); + binding.initial = null; // else would be $props() + binding.kind = 'rest_prop'; + } else { + equal(node.id.type, 'ObjectPattern'); + + for (const property of node.id.properties) { + if (property.type !== 'Property') continue; + + const name = + property.value.type === 'AssignmentPattern' + ? /** @type {import('estree').Identifier} */ (property.value.left).name + : /** @type {import('estree').Identifier} */ (property.value).name; + const alias = + property.key.type === 'Identifier' + ? property.key.name + : String(/** @type {import('estree').Literal} */ (property.key).value); + let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; + + const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); + binding.prop_alias = alias; + + // 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/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index f4778e4322..45f87935e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1240,7 +1240,7 @@ export const validation_runes = merge(validation, a11y_validators, { e.rune_invalid_arguments(node, rune); } - if (node.id.type !== 'ObjectPattern') { + if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { e.props_invalid_identifier(node); } @@ -1248,17 +1248,19 @@ export const validation_runes = merge(validation, a11y_validators, { e.props_invalid_placement(node); } - for (const property of node.id.properties) { - if (property.type === 'Property') { - if (property.computed) { - e.props_invalid_pattern(property); - } + if (node.id.type === 'ObjectPattern') { + for (const property of node.id.properties) { + if (property.type === 'Property') { + if (property.computed) { + e.props_invalid_pattern(property); + } - const value = - property.value.type === 'AssignmentPattern' ? property.value.left : property.value; + const value = + property.value.type === 'AssignmentPattern' ? property.value.left : property.value; - if (value.type !== 'Identifier') { - e.props_invalid_pattern(property); + if (value.type !== 'Identifier') { + e.props_invalid_pattern(property); + } } } } 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 37dd02855f..a640fb01b0 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 @@ -9,6 +9,27 @@ export const global_visitors = { if (node.name === '$$props') { return b.id('$$sanitized_props'); } + + // Optimize prop access: If it's a member read access, we can use the $$props object directly + const binding = state.scope.get(node.name); + if ( + state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write + binding !== null && + node !== binding.node && + binding.kind === 'rest_prop' + ) { + const parent = path.at(-1); + const grand_parent = path.at(-2); + if ( + parent?.type === 'MemberExpression' && + !parent.computed && + grand_parent?.type !== 'AssignmentExpression' && + grand_parent?.type !== 'UpdateExpression' + ) { + return b.id('$$props'); + } + } + return serialize_get_binding(node, state); } }, 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 91934738b0..4588f71d47 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 @@ -238,8 +238,6 @@ export const javascript_visitors_runes = { } if (rune === '$props') { - assert.equal(declarator.id.type, 'ObjectPattern'); - /** @type {string[]} */ const seen = ['$$slots', '$$events', '$$legacy']; @@ -247,44 +245,58 @@ export const javascript_visitors_runes = { seen.push('$$host'); } - for (const property of declarator.id.properties) { - if (property.type === 'Property') { - const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( - property.key - ); - const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value); - - seen.push(name); - - 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 = - binding.initial && - /** @type {import('estree').Expression} */ (visit(binding.initial)); - // We're adding proxy here on demand and not within the prop runtime function so that - // people not using proxied state anywhere in their code don't have to pay the additional bundle size cost - if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) { - initial = b.call('$.proxy', initial); - } + if (declarator.id.type === 'Identifier') { + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; - if (is_prop_source(binding, state)) { - declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); - } - } else { - // RestElement - /** @type {import('estree').Expression[]} */ - const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; - - if (state.options.dev) { - // include rest name, so we can provide informative error messages - args.push( - b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) + if (state.options.dev) { + // include rest name, so we can provide informative error messages + args.push(b.literal(declarator.id.name)); + } + + declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args))); + } else { + assert.equal(declarator.id.type, 'ObjectPattern'); + + for (const property of declarator.id.properties) { + if (property.type === 'Property') { + const key = /** @type {import('estree').Identifier | import('estree').Literal} */ ( + property.key ); + const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value); + + seen.push(name); + + 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 = + binding.initial && + /** @type {import('estree').Expression} */ (visit(binding.initial)); + // We're adding proxy here on demand and not within the prop runtime function so that + // people not using proxied state anywhere in their code don't have to pay the additional bundle size cost + if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) { + initial = b.call('$.proxy', initial); + } + + if (is_prop_source(binding, state)) { + declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); + } + } else { + // RestElement + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + + if (state.options.dev) { + // include rest name, so we can provide informative error messages + args.push( + b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) + ); + } + + declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } - - 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 3232239a75..c6b73ee6b5 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -78,6 +78,7 @@ const rest_props_handler = { * @param {string} [name] * @returns {Record} */ +/*#__NO_SIDE_EFFECTS__*/ export function rest_props(props, exclude, name) { return new Proxy( DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js new file mode 100644 index 0000000000..2a10dbc1b1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client/index.svelte.js @@ -0,0 +1,17 @@ +import "svelte/internal/disclose-version"; +import * as $ from "svelte/internal/client"; + +export default function Props_identifier($$anchor, $$props) { + $.push($$props, true); + + let props = $.rest_props($$props, ["$$slots", "$$events", "$$legacy"]); + + $$props.a; + props[a]; + $$props.a.b; + $$props.a.b = true; + props.a = true; + props[a] = true; + props; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js new file mode 100644 index 0000000000..362d773be1 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/server/index.svelte.js @@ -0,0 +1,16 @@ +import * as $ from "svelte/internal/server"; + +export default function Props_identifier($$payload, $$props) { + $.push(); + + let props = $$props; + + props.a; + props[a]; + props.a.b; + props.a.b = true; + props.a = true; + props[a] = true; + props; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte b/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte new file mode 100644 index 0000000000..ebd9d09dca --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-identifier/index.svelte @@ -0,0 +1,10 @@ + 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 84010d4f9b..06b4f97834 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 @@ -548,6 +548,12 @@ To get all properties, use rest syntax: let { a, b, c, ...everythingElse } = $props(); ``` +You can also use an identifier: + +```js +let props = $props(); +``` + If you're using TypeScript, you can declare the prop types: ```ts From c42bb04276af0024b49aa46918eec69ad56570a5 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:25:09 +0200 Subject: [PATCH 8/8] fix: disallow accessing internal Svelte props (#12207) closes #12184 --- .changeset/olive-cobras-wonder.md | 5 +++++ packages/svelte/messages/compile-errors/script.md | 4 ++++ packages/svelte/src/compiler/errors.js | 9 +++++++++ .../src/compiler/phases/2-analyze/validation.js | 12 ++++++++++++ .../samples/runes-props-illegal-name-1/_config.js | 9 +++++++++ .../samples/runes-props-illegal-name-1/main.svelte | 3 +++ .../samples/runes-props-illegal-name-2/_config.js | 9 +++++++++ .../samples/runes-props-illegal-name-2/main.svelte | 4 ++++ 8 files changed, 55 insertions(+) create mode 100644 .changeset/olive-cobras-wonder.md create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte diff --git a/.changeset/olive-cobras-wonder.md b/.changeset/olive-cobras-wonder.md new file mode 100644 index 0000000000..331d61a404 --- /dev/null +++ b/.changeset/olive-cobras-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow accessing internal Svelte props diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 57fa1ed700..d31c04b99c 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -78,6 +78,10 @@ > Cannot use `$props()` more than once +## props_illegal_name + +> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals) + ## props_invalid_identifier > `$props()` can only be used with an object destructuring pattern diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index d6d3d51d7c..1a60f99000 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -276,6 +276,15 @@ export function props_duplicate(node) { e(node, "props_duplicate", "Cannot use `$props()` more than once"); } +/** + * Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals) + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function props_illegal_name(node) { + e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)"); +} + /** * `$props()` can only be used with an object destructuring pattern * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 45f87935e1..93d12bfeda 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -341,6 +341,14 @@ function validate_block_not_empty(node, context) { * @type {import('zimmerframe').Visitors} */ const validation = { + MemberExpression(node, context) { + if (node.object.type === 'Identifier' && node.property.type === 'Identifier') { + const binding = context.state.scope.get(node.object.name); + if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) { + e.props_illegal_name(node.property); + } + } + }, AssignmentExpression(node, context) { validate_assignment(node, node.left, context.state); }, @@ -1255,6 +1263,10 @@ export const validation_runes = merge(validation, a11y_validators, { e.props_invalid_pattern(property); } + if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) { + e.props_illegal_name(property); + } + const value = property.value.type === 'AssignmentPattern' ? property.value.left : property.value; diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js new file mode 100644 index 0000000000..b205e0de38 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'props_illegal_name', + message: + 'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte new file mode 100644 index 0000000000..dd581833fd --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-1/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js new file mode 100644 index 0000000000..b205e0de38 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'props_illegal_name', + message: + 'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte new file mode 100644 index 0000000000..3b287f829e --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-props-illegal-name-2/main.svelte @@ -0,0 +1,4 @@ +