diff --git a/.changeset/fast-ligers-repeat.md b/.changeset/fast-ligers-repeat.md new file mode 100644 index 0000000000..c6b35c4373 --- /dev/null +++ b/.changeset/fast-ligers-repeat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow invalid attributes for `` and `` diff --git a/.changeset/fresh-pigs-divide.md b/.changeset/fresh-pigs-divide.md new file mode 100644 index 0000000000..8aa3d74ce9 --- /dev/null +++ b/.changeset/fresh-pigs-divide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure props passed to components via mount are updateable diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 1e8c7ed3a8..5c6b96326a 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -412,6 +412,12 @@ Expected whitespace `$host()` can only be used inside custom element component instances ``` +### illegal_element_attribute + +``` +`<%name%>` does not support non-event attributes or spread attributes +``` + ### import_svelte_internal_forbidden ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index d18cbfba86..143bcc4c50 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -172,6 +172,10 @@ > Expected whitespace +## illegal_element_attribute + +> `<%name%>` does not support non-event attributes or spread attributes + ## invalid_bind_directive > Bind directive getter/setter values (`bind:thing={ getter, setter }`) must be two JavaScript expressions separated by a single comma and enclosed in curly braces diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index c08ff1860b..2d1c9d8918 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -993,6 +993,16 @@ export function invalid_bind_directive(node) { e(node, "invalid_bind_directive", "Bind directive getter/setter values (`bind:thing={ getter, setter }`) must be two JavaScript expressions separated by a single comma and enclosed in curly braces"); } +/** + * `<%name%>` does not support non-event attributes or spread attributes + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function illegal_element_attribute(node, name) { + e(node, "illegal_element_attribute", `\`<${name}>\` does not support non-event attributes or spread attributes`); +} + /** * %message% * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteDocument.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteDocument.js index a44baef9e4..fe54ebf30d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteDocument.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteDocument.js @@ -1,6 +1,8 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { disallow_children } from './shared/special-element.js'; +import * as e from '../../../errors.js'; +import { is_event_attribute } from '../../../utils/ast.js'; /** * @param {AST.SvelteDocument} node @@ -8,5 +10,15 @@ import { disallow_children } from './shared/special-element.js'; */ export function SvelteDocument(node, context) { disallow_children(node); + + for (const attribute of node.attributes) { + if ( + attribute.type === 'SpreadAttribute' || + (attribute.type === 'Attribute' && !is_event_attribute(attribute)) + ) { + e.illegal_element_attribute(attribute, 'svelte:document'); + } + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteWindow.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteWindow.js index c5392a477d..20f5abc5d6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteWindow.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteWindow.js @@ -1,6 +1,8 @@ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ import { disallow_children } from './shared/special-element.js'; +import * as e from '../../../errors.js'; +import { is_event_attribute } from '../../../utils/ast.js'; /** * @param {AST.SvelteWindow} node @@ -8,5 +10,15 @@ import { disallow_children } from './shared/special-element.js'; */ export function SvelteWindow(node, context) { disallow_children(node); + + for (const attribute of node.attributes) { + if ( + attribute.type === 'SpreadAttribute' || + (attribute.type === 'Attribute' && !is_event_attribute(attribute)) + ) { + e.illegal_element_attribute(attribute, 'svelte:window'); + } + } + context.next(); } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 53df86126a..bcf30e78db 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -22,4 +22,5 @@ export const EFFECT_HAS_DERIVED = 1 << 19; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); +export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index dbfb8f0337..8fb13f7e2c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -20,7 +20,13 @@ import { } from '../runtime.js'; import { safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { BRANCH_EFFECT, LEGACY_DERIVED_PROP, ROOT_EFFECT } from '../constants.js'; +import { + BRANCH_EFFECT, + LEGACY_DERIVED_PROP, + LEGACY_PROPS, + ROOT_EFFECT, + STATE_SYMBOL +} from '../constants.js'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -209,6 +215,9 @@ const spread_props_handler = { } }, has(target, key) { + // To prevent a false positive `is_entry_props` in the `prop` function + if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false; + for (let p of target.props) { if (is_function(p)) p = p(); if (p != null && key in p) return true; @@ -282,7 +291,14 @@ export function prop(props, key, flags, fallback) { } else { prop_value = /** @type {V} */ (props[key]); } - var setter = get_descriptor(props, key)?.set; + + // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` + // or `createClassComponent(Component, props)` + var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; + + var setter = + get_descriptor(props, key)?.set ?? + (is_entry_props && bindable && key in props ? (v) => (props[key] = v) : undefined); var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index eddcb69d9e..951feee33b 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,4 +1,4 @@ -import { dev_current_component_function, untrack } from './runtime.js'; +import { dev_current_component_function } from './runtime.js'; import { get_descriptor, is_array } from '../shared/utils.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; @@ -6,15 +6,6 @@ import { render_effect } from './reactivity/effects.js'; import * as w from './warnings.js'; import { capture_store_binding } from './reactivity/store.js'; -/** regex of all html void element names */ -const void_element_names = - /^(?:area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$/; - -/** @param {string} tag */ -function is_void(tag) { - return void_element_names.test(tag) || tag.toLowerCase() === '!doctype'; -} - /** * @param {() => any} collection * @param {(item: any, index: number) => string} key_fn diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index c7684570bb..9e1cd888ad 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -1,5 +1,5 @@ /** @import { ComponentConstructorOptions, ComponentType, SvelteComponent, Component } from 'svelte' */ -import { DIRTY, MAYBE_DIRTY } from '../internal/client/constants.js'; +import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.js'; import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; @@ -89,7 +89,7 @@ class Svelte4Component { }; // Replicate coarse-grained props through a proxy that has a version source for - // each property, which is increment on updates to the property itself. Do not + // each property, which is incremented on updates to the property itself. Do not // use our $state proxy because that one has fine-grained reactivity. const props = new Proxy( { ...(options.props || {}), $$events: {} }, @@ -98,6 +98,9 @@ class Svelte4Component { return get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop))); }, has(target, prop) { + // Necessary to not throw "invalid binding" validation errors on the component side + if (prop === LEGACY_PROPS) return true; + get(sources.get(prop) ?? add_source(prop, Reflect.get(target, prop))); return Reflect.has(target, prop); }, diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js new file mode 100644 index 0000000000..ff7af2d524 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js @@ -0,0 +1,47 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + // The buz fallback does not propagate back up + ` + foo baz +
foo bar baz buz
+
foo bar baz buz
+ ` + ); + + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + + btn2.click(); + btn3.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + // bar is not set in the parent because it's a readonly property + // baz is not set in the parent because while it's a bindable property, + // it wasn't set initially so it's treated as a readonly proeprty + ` + foo 3 +
1 2 3 4
+
1 2 3 4
+ ` + ); + + btn1.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + // Because foo is a readonly property, component.svelte diverges locally from it, + // and the passed in property keeps the initial value of foo. This is why it stays + // at 1, because foo is not updated to a different value. + ` + foo bar baz buz +
1 bar baz buz
+
1 bar baz buz
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/component.svelte b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/component.svelte new file mode 100644 index 0000000000..214cb7bf72 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/component.svelte @@ -0,0 +1,17 @@ + + + + +{foo} +{bar} +{baz} +{buz} diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/main.svelte b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/main.svelte new file mode 100644 index 0000000000..0769830afb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/main.svelte @@ -0,0 +1,30 @@ + + + {props.foo} {props.bar} {props.baz} {props.buz} +
+
diff --git a/packages/svelte/tests/validator/samples/illegal_spread_element-document/errors.json b/packages/svelte/tests/validator/samples/illegal_spread_element-document/errors.json new file mode 100644 index 0000000000..2e15b152e7 --- /dev/null +++ b/packages/svelte/tests/validator/samples/illegal_spread_element-document/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "illegal_element_attribute", + "message": "`` does not support non-event attributes or spread attributes", + "start": { + "line": 4, + "column": 17 + }, + "end": { + "line": 4, + "column": 23 + } + } +] diff --git a/packages/svelte/tests/validator/samples/illegal_spread_element-document/input.svelte b/packages/svelte/tests/validator/samples/illegal_spread_element-document/input.svelte new file mode 100644 index 0000000000..8980eea012 --- /dev/null +++ b/packages/svelte/tests/validator/samples/illegal_spread_element-document/input.svelte @@ -0,0 +1,4 @@ + + diff --git a/packages/svelte/tests/validator/samples/illegal_spread_element-window/errors.json b/packages/svelte/tests/validator/samples/illegal_spread_element-window/errors.json new file mode 100644 index 0000000000..c59045e852 --- /dev/null +++ b/packages/svelte/tests/validator/samples/illegal_spread_element-window/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "illegal_element_attribute", + "message": "`` does not support non-event attributes or spread attributes", + "start": { + "line": 4, + "column": 15 + }, + "end": { + "line": 4, + "column": 21 + } + } +] diff --git a/packages/svelte/tests/validator/samples/illegal_spread_element-window/input.svelte b/packages/svelte/tests/validator/samples/illegal_spread_element-window/input.svelte new file mode 100644 index 0000000000..c0076cc6bf --- /dev/null +++ b/packages/svelte/tests/validator/samples/illegal_spread_element-window/input.svelte @@ -0,0 +1,4 @@ + +