diff --git a/.changeset/beige-rabbits-shave.md b/.changeset/beige-rabbits-shave.md new file mode 100644 index 000000000000..b63c13d06c0f --- /dev/null +++ b/.changeset/beige-rabbits-shave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve props aliasing diff --git a/.changeset/chatty-cups-drop.md b/.changeset/chatty-cups-drop.md new file mode 100644 index 000000000000..282a9b25ded2 --- /dev/null +++ b/.changeset/chatty-cups-drop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add support for `{@const}` inside snippet block diff --git a/.changeset/dull-mangos-wave.md b/.changeset/dull-mangos-wave.md new file mode 100644 index 000000000000..18ba4e3c8c59 --- /dev/null +++ b/.changeset/dull-mangos-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve whitespace handling diff --git a/.changeset/four-flies-hammer.md b/.changeset/four-flies-hammer.md new file mode 100644 index 000000000000..b6d3770a4c95 --- /dev/null +++ b/.changeset/four-flies-hammer.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve each block fallback handling diff --git a/.changeset/happy-suits-film.md b/.changeset/happy-suits-film.md new file mode 100644 index 000000000000..84fdcbdb82e4 --- /dev/null +++ b/.changeset/happy-suits-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve attribute directive reactivity detection diff --git a/.changeset/heavy-ears-rule.md b/.changeset/heavy-ears-rule.md new file mode 100644 index 000000000000..7dcd27070ad2 --- /dev/null +++ b/.changeset/heavy-ears-rule.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve $inspect batching diff --git a/.changeset/large-turkeys-deny.md b/.changeset/large-turkeys-deny.md new file mode 100644 index 000000000000..6dcd227f7f5b --- /dev/null +++ b/.changeset/large-turkeys-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: improve readonly prop messaging diff --git a/.changeset/late-crabs-lay.md b/.changeset/late-crabs-lay.md new file mode 100644 index 000000000000..26a2504a3768 --- /dev/null +++ b/.changeset/late-crabs-lay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add `gamepadconnected` and `gamepaddisconnected` events diff --git a/.changeset/lovely-carpets-lick.md b/.changeset/lovely-carpets-lick.md new file mode 100644 index 000000000000..bd86e2ef447b --- /dev/null +++ b/.changeset/lovely-carpets-lick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better handle array property deletion reactivity diff --git a/.changeset/pre.json b/.changeset/pre.json index e8a33f2f70d1..c978e2128138 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -11,8 +11,10 @@ "changesets": [ "afraid-moose-matter", "beige-flies-wash", + "beige-rabbits-shave", "brave-walls-destroy", "brown-spoons-boil", + "chatty-cups-drop", "chatty-taxis-juggle", "chilled-pumas-invite", "chilly-dolphins-lick", @@ -24,6 +26,7 @@ "curly-lizards-dream", "dirty-garlics-design", "dirty-tips-add", + "dull-mangos-wave", "early-ads-tie", "eight-steaks-shout", "eighty-bikes-camp", @@ -36,6 +39,7 @@ "five-tigers-search", "flat-melons-protect", "forty-comics-invent", + "four-flies-hammer", "fresh-weeks-trade", "friendly-lies-camp", "funny-wombats-argue", @@ -45,7 +49,9 @@ "great-icons-retire", "green-eggs-approve", "green-hounds-play", + "happy-suits-film", "healthy-planes-vanish", + "heavy-ears-rule", "honest-icons-change", "hungry-dots-fry", "hungry-tips-unite", @@ -56,12 +62,15 @@ "kind-deers-lay", "kind-eagles-join", "large-clouds-carry", + "large-turkeys-deny", + "late-crabs-lay", "lazy-masks-sit", "lazy-months-knock", "lazy-spiders-think", "lemon-geese-drum", "light-pens-watch", "long-crews-return", + "lovely-carpets-lick", "lovely-items-turn", "lovely-rules-eat", "lucky-schools-hang", @@ -81,10 +90,13 @@ "poor-eggs-enjoy", "poor-seahorses-flash", "popular-mangos-rest", + "purple-dragons-peel", "quiet-camels-mate", "rare-pears-whisper", + "real-guests-do", "rich-sheep-burn", "rich-tables-sing", + "rotten-bags-type", "rotten-buckets-develop", "selfish-tools-hide", "serious-socks-cover", @@ -96,6 +108,7 @@ "shiny-baboons-play", "shiny-shrimps-march", "slimy-clouds-talk", + "slow-chefs-dream", "small-papayas-laugh", "smart-parents-swim", "soft-clocks-remember", @@ -104,15 +117,18 @@ "sour-rules-march", "spicy-plums-admire", "stale-comics-look", + "strong-gifts-smoke", "strong-lemons-provide", "sweet-mangos-beg", "swift-donkeys-perform", "swift-ravens-hunt", "swift-seahorses-deliver", + "tall-books-grin", "tall-shrimps-worry", "tall-tigers-wait", "tasty-numbers-perform", "ten-foxes-repeat", + "ten-peaches-sleep", "ten-worms-reflect", "thin-foxes-lick", "thirty-flowers-sit", @@ -128,6 +144,8 @@ "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train", + "wild-foxes-wonder", + "wise-dancers-hang", "wise-donkeys-marry", "witty-camels-warn" ] diff --git a/.changeset/purple-dragons-peel.md b/.changeset/purple-dragons-peel.md new file mode 100644 index 000000000000..a7e2574d20f9 --- /dev/null +++ b/.changeset/purple-dragons-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve unstate type definition diff --git a/.changeset/real-guests-do.md b/.changeset/real-guests-do.md new file mode 100644 index 000000000000..3f0c26a477d5 --- /dev/null +++ b/.changeset/real-guests-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve event delegation handler hoisting diff --git a/.changeset/rotten-bags-type.md b/.changeset/rotten-bags-type.md new file mode 100644 index 000000000000..0a69089e9345 --- /dev/null +++ b/.changeset/rotten-bags-type.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly reflect readonly proxy marker diff --git a/.changeset/slow-chefs-dream.md b/.changeset/slow-chefs-dream.md new file mode 100644 index 000000000000..9cd0de887b31 --- /dev/null +++ b/.changeset/slow-chefs-dream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: improve each block fast-path heuristic diff --git a/.changeset/strong-gifts-smoke.md b/.changeset/strong-gifts-smoke.md new file mode 100644 index 000000000000..202efbb48094 --- /dev/null +++ b/.changeset/strong-gifts-smoke.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve html tag svg behaviour diff --git a/.changeset/tall-books-grin.md b/.changeset/tall-books-grin.md new file mode 100644 index 000000000000..5587b6103c78 --- /dev/null +++ b/.changeset/tall-books-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure class constructor values are proxied diff --git a/.changeset/ten-peaches-sleep.md b/.changeset/ten-peaches-sleep.md new file mode 100644 index 000000000000..fa130171122e --- /dev/null +++ b/.changeset/ten-peaches-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better support for top-level snippet declarations diff --git a/.changeset/wild-foxes-wonder.md b/.changeset/wild-foxes-wonder.md new file mode 100644 index 000000000000..b7e57608686d --- /dev/null +++ b/.changeset/wild-foxes-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: cleanup each block animations on destroy diff --git a/.changeset/wise-dancers-hang.md b/.changeset/wise-dancers-hang.md new file mode 100644 index 000000000000..0ed0f40a7590 --- /dev/null +++ b/.changeset/wise-dancers-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve each block index handling diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 7512da7db9a5..c6e4e7555b33 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,57 @@ # svelte +## 5.0.0-next.26 + +### Patch Changes + +- fix: better handle array property deletion reactivity ([#9921](https://github.com/sveltejs/svelte/pull/9921)) + +- fix: improve event delegation handler hoisting ([#9929](https://github.com/sveltejs/svelte/pull/9929)) + +## 5.0.0-next.25 + +### Patch Changes + +- fix: improve whitespace handling ([#9912](https://github.com/sveltejs/svelte/pull/9912)) + +- fix: improve each block fallback handling ([#9914](https://github.com/sveltejs/svelte/pull/9914)) + +- fix: cleanup each block animations on destroy ([#9917](https://github.com/sveltejs/svelte/pull/9917)) + +## 5.0.0-next.24 + +### Patch Changes + +- fix: improve props aliasing ([#9900](https://github.com/sveltejs/svelte/pull/9900)) + +- feat: add support for `{@const}` inside snippet block ([#9904](https://github.com/sveltejs/svelte/pull/9904)) + +- fix: improve attribute directive reactivity detection ([#9907](https://github.com/sveltejs/svelte/pull/9907)) + +- fix: improve $inspect batching ([#9902](https://github.com/sveltejs/svelte/pull/9902)) + +- chore: improve readonly prop messaging ([#9901](https://github.com/sveltejs/svelte/pull/9901)) + +- fix: better support for top-level snippet declarations ([#9898](https://github.com/sveltejs/svelte/pull/9898)) + +## 5.0.0-next.23 + +### Patch Changes + +- feat: add `gamepadconnected` and `gamepaddisconnected` events ([#9861](https://github.com/sveltejs/svelte/pull/9861)) + +- fix: improve unstate type definition ([#9895](https://github.com/sveltejs/svelte/pull/9895)) + +- fix: correctly reflect readonly proxy marker ([#9893](https://github.com/sveltejs/svelte/pull/9893)) + +- chore: improve each block fast-path heuristic ([#9855](https://github.com/sveltejs/svelte/pull/9855)) + +- fix: improve html tag svg behaviour ([#9894](https://github.com/sveltejs/svelte/pull/9894)) + +- fix: ensure class constructor values are proxied ([#9888](https://github.com/sveltejs/svelte/pull/9888)) + +- fix: improve each block index handling ([#9889](https://github.com/sveltejs/svelte/pull/9889)) + ## 5.0.0-next.22 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index a45634028309..09df6ddbc8ef 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -53,6 +53,7 @@ export type KeyboardEventHandler = EventHandler = EventHandler; export type TouchEventHandler = EventHandler; export type PointerEventHandler = EventHandler; +export type GamepadEventHandler = EventHandler; export type UIEventHandler = EventHandler; export type WheelEventHandler = EventHandler; export type AnimationEventHandler = EventHandler; @@ -336,6 +337,12 @@ export interface DOMAttributes { onlostpointercapture?: PointerEventHandler | undefined | null; onlostpointercapturecapture?: PointerEventHandler | undefined | null; + // Gamepad Events + 'on:gamepadconnected'?: GamepadEventHandler | undefined | null; + ongamepadconnected?: GamepadEventHandler | undefined | null; + 'on:gamepaddisconnected'?: GamepadEventHandler | undefined | null; + ongamepaddisconnected?: GamepadEventHandler | undefined | null; + // UI Events 'on:scroll'?: UIEventHandler | undefined | null; onscroll?: UIEventHandler | undefined | null; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d2d8422eb478..6947dbc16920 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.22", + "version": "5.0.0-next.26", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 73b6f2e86d1d..c083604b87c1 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -333,7 +333,7 @@ const compiler_options = { /** @satisfies {Errors} */ const const_tag = { 'invalid-const-placement': () => - `{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ` + `{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ` }; /** @satisfies {Errors} */ diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/html.js b/packages/svelte/src/compiler/phases/1-parse/utils/html.js index de2776942bc6..bb45f9d6db4b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/html.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/html.js @@ -121,6 +121,16 @@ function validate_code(code) { // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission +const interactive_elements = new Set([ + 'a', + 'button', + 'iframe', + 'embed', + 'input', + 'select', + 'textarea' +]); + /** @type {Record>} */ const disallowed_contents = { li: new Set(['li']), @@ -143,6 +153,10 @@ const disallowed_contents = { th: new Set(['td', 'th', 'tr']) }; +for (const interactive_element of interactive_elements) { + disallowed_contents[interactive_element] = interactive_elements; +} + // can this be a child of the parent element, or does it implicitly // close it, like `
  • one
  • two`? diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js index b0942d6572c3..2a8e4eff4037 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/Selector.js @@ -193,13 +193,7 @@ export default class Selector { } get_amount_class_specificity_increased() { - let count = 0; - for (const block of this.blocks) { - if (block.should_encapsulate) { - count++; - } - } - return count; + return this.blocks.filter((block) => block.should_encapsulate).length; } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index bd43aca39846..35b7bbc170dd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -166,6 +166,7 @@ function get_delegated_event(node, context) { return non_hoistable; } + const visited_references = new Set(); const scope = target_function.metadata.scope; for (const [reference] of scope.references) { // Bail-out if the arguments keyword is used @@ -174,6 +175,15 @@ function get_delegated_event(node, context) { } const binding = scope.get(reference); + // If we have multiple references to the same store using $ prefix, bail out. + if ( + binding !== null && + binding.kind === 'store_sub' && + visited_references.has(reference.slice(1)) + ) { + return non_hoistable; + } + if ( binding !== null && // Bail-out if the the binding is a rest param @@ -188,6 +198,7 @@ function get_delegated_event(node, context) { ) { return non_hoistable; } + visited_references.add(reference); } return { type: 'hoistable', function: target_function }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 8dad40e2386e..08402009abd5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -476,6 +476,7 @@ export const validation = { grand_parent?.type !== 'SvelteComponent' && grand_parent?.type !== 'EachBlock' && grand_parent?.type !== 'AwaitBlock' && + grand_parent?.type !== 'SnippetBlock' && ((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') || !grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot'))) ) { 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 f22bf8065be3..7a0f8af075cd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -81,6 +81,9 @@ export function serialize_get_binding(node, state) { return b.call(node); } + if (binding.prop_alias) { + return b.member(b.id('$$props'), b.id(binding.prop_alias)); + } return b.member(b.id('$$props'), node); } @@ -157,16 +160,45 @@ export function serialize_set_binding(node, context, fallback) { let left = node.left; + // Handle class private/public state assignment cases while (left.type === 'MemberExpression') { - if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') { - if (context.state.private_state.has(left.property.name) && !state.in_constructor) { - const value = get_assignment_value(node, context); + if ( + left.object.type === 'ThisExpression' && + left.property.type === 'PrivateIdentifier' && + context.state.private_state.has(left.property.name) + ) { + const value = get_assignment_value(node, context); + if (state.in_constructor) { + // See if we should wrap value in $.proxy + if (context.state.analysis.runes && should_proxy(value)) { + const assignment = fallback(); + if (assignment.type === 'AssignmentExpression') { + assignment.right = b.call('$.proxy', value); + return assignment; + } + } + } else { return b.call( '$.set', left, context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value ); } + } else if ( + left.object.type === 'ThisExpression' && + left.property.type === 'Identifier' && + context.state.public_state.has(left.property.name) && + state.in_constructor + ) { + const value = get_assignment_value(node, context); + // See if we should wrap value in $.proxy + if (context.state.analysis.runes && should_proxy(value)) { + const assignment = fallback(); + if (assignment.type === 'AssignmentExpression') { + assignment.right = b.call('$.proxy', value); + return assignment; + } + } } // @ts-expect-error left = left.object; @@ -331,8 +363,7 @@ function get_hoistable_params(node, context) { binding.kind === 'prop' && !binding.reassigned && binding.initial === null && - !context.state.analysis.accessors && - context.state.analysis.runes + !context.state.analysis.accessors ) { // Handle $$props.something use-cases if (!added_props) { 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 1330141a8e43..6ad1026cc744 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 @@ -63,32 +63,48 @@ function get_attribute_name(element, attribute, context) { * @param {boolean} is_attributes_reactive */ function serialize_style_directives(style_directives, element_id, context, is_attributes_reactive) { - if (style_directives.length > 0) { - const values = style_directives.map((directive) => { - let value = - directive.value === true - ? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state) - : serialize_attribute_value(directive.value, context)[1]; - return b.stmt( - b.call( - '$.style', - element_id, - b.literal(directive.name), - value, - /** @type {import('estree').Expression} */ ( - directive.modifiers.includes('important') ? b.true : undefined - ) + const state = context.state; + + for (const directive of style_directives) { + let value = + directive.value === true + ? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state) + : serialize_attribute_value(directive.value, context)[1]; + const grouped = b.stmt( + b.call( + '$.style', + element_id, + b.literal(directive.name), + value, + /** @type {import('estree').Expression} */ ( + directive.modifiers.includes('important') ? b.true : undefined + ) + ) + ); + const singular = b.stmt( + b.call( + '$.style_effect', + element_id, + b.literal(directive.name), + b.arrow([], value), + /** @type {import('estree').Expression} */ ( + directive.modifiers.includes('important') ? b.true : undefined ) + ) + ); + + const contains_call_expression = + Array.isArray(directive.value) && + directive.value.some( + (v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression ); - }); - if ( - is_attributes_reactive || - style_directives.some((directive) => directive.metadata.dynamic) - ) { - context.state.update.push(...values.map((v) => ({ grouped: v }))); + if (!is_attributes_reactive && contains_call_expression) { + state.update_effects.push(singular); + } else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { + state.update.push({ grouped, singular }); } else { - context.state.init.push(...values); + state.init.push(grouped); } } } @@ -123,21 +139,21 @@ function parse_directive_name(name) { * @param {boolean} is_attributes_reactive */ function serialize_class_directives(class_directives, element_id, context, is_attributes_reactive) { - if (class_directives.length > 0) { - const values = class_directives.map((directive) => { - const value = /** @type {import('estree').Expression} */ ( - context.visit(directive.expression) - ); - return b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value)); - }); + const state = context.state; + for (const directive of class_directives) { + const value = /** @type {import('estree').Expression} */ (context.visit(directive.expression)); + const grouped = b.stmt(b.call('$.class_toggle', element_id, b.literal(directive.name), value)); + const singular = b.stmt( + b.call('$.class_toggle_effect', element_id, b.literal(directive.name), b.arrow([], value)) + ); + const contains_call_expression = directive.expression.type === 'CallExpression'; - if ( - is_attributes_reactive || - class_directives.some((directive) => directive.metadata.dynamic) - ) { - context.state.update.push(...values.map((v) => ({ grouped: v }))); + if (!is_attributes_reactive && contains_call_expression) { + state.update_effects.push(singular); + } else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { + state.update.push({ grouped, singular }); } else { - context.state.init.push(...values); + state.init.push(grouped); } } } @@ -295,7 +311,9 @@ function serialize_element_spread_attributes(attributes, context, element, eleme values.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); } - is_reactive ||= attribute.metadata.dynamic; + is_reactive ||= + attribute.metadata.dynamic || + (attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression); } const lowercase_attributes = @@ -2137,11 +2155,10 @@ export const template_visitors = { } // The runtime needs to know what kind of each block this is in order to optimize for the - // immutable + key==entry case. In that case, the item doesn't need to be reactive, because - // the array as a whole is immutable, so if something changes, it either has to recreate the - // array or use nested reactivity through runes. - // TODO this feels a bit "hidden performance boost"-style, investigate if there's a way - // to make this apply in more cases + // key === item (we avoid extra allocations). In that case, the item doesn't need to be reactive. + // We can guarantee this by knowing that in order for the item of the each block to change, they + // would need to mutate the key/item directly in the array. Given that in runes mode we use === + // equality, we can apply a fast-path (as long as the index isn't reactive). let each_type = 0; if ( @@ -2149,21 +2166,22 @@ export const template_visitors = { (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index) ) { each_type |= EACH_KEYED; + // If there's a destructuring, then we likely need the generated $$index + if (node.index || node.context.type !== 'Identifier') { + each_type |= EACH_INDEX_REACTIVE; + } if ( + context.state.analysis.runes && node.key.type === 'Identifier' && node.context.type === 'Identifier' && node.context.name === node.key.name && - context.state.options.immutable + (each_type & EACH_INDEX_REACTIVE) === 0 ) { - // Fast-path + // Fast-path for when the key === item each_item_is_reactive = false; } else { each_type |= EACH_ITEM_REACTIVE; } - // If there's a destructuring, then we likely need the generated $$index - if (node.index || node.context.type !== 'Identifier') { - each_type |= EACH_INDEX_REACTIVE; - } } else { each_type |= EACH_ITEM_REACTIVE; } @@ -2236,6 +2254,12 @@ export const template_visitors = { const item = b.id(each_node_meta.item_name); const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name)); binding.expression = each_item_is_reactive ? b.call('$.unwrap', item) : item; + if (node.index) { + const index_binding = /** @type {import('#compiler').Binding} */ ( + context.state.scope.get(node.index) + ); + index_binding.expression = each_item_is_reactive ? b.call('$.unwrap', index) : index; + } /** @type {import('estree').Statement[]} */ const declarations = []; @@ -2289,9 +2313,9 @@ export const template_visitors = { ) : b.literal(null); const key_function = - node.key && (each_type & 1) /* EACH_ITEM_REACTIVE */ !== 0 + node.key && ((each_type & EACH_ITEM_REACTIVE) !== 0 || context.state.options.dev) ? b.arrow( - [node.context.type === 'Identifier' ? node.context : b.id('$$item')], + [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], b.block( declarations.concat( b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) @@ -2471,7 +2495,14 @@ export const template_visitors = { body = /** @type {import('estree').BlockStatement} */ (context.visit(node.body)); } - context.state.init.push(b.const(node.expression, b.arrow(args, body))); + const path = context.path; + // If we're top-level, then we can create a function for the snippet so that it can be referenced + // in the props declaration (default value pattern). + if (path.length === 1 && path[0].type === 'Fragment') { + context.state.init.push(b.function_declaration(node.expression, args, body)); + } else { + context.state.init.push(b.const(node.expression, b.arrow(args, body))); + } if (context.state.options.dev) { context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 5b3a5e7c857c..9c7a559394bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -177,8 +177,9 @@ export function clean_nodes( node.data = node.data.replace(regex_whitespaces_strict, ' '); node.raw = node.raw.replace(regex_whitespaces_strict, ' '); if ( - (last_text === null || !regex_ends_with_whitespaces.test(last_text.data)) && - (!can_remove_entirely || node.data !== ' ') + (last_text === null && !can_remove_entirely) || + node.data !== ' ' || + node.data.charCodeAt(0) === 160 // non-breaking space ) { trimmed.push(node); } diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/each.js index 0602d789f7a0..e493241feda7 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/each.js @@ -119,6 +119,14 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re current_fallback = fallback; }; + /** @param {import('./types.js').EachBlock} block */ + const clear_each = (block) => { + const flags = block.f; + const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; + const anchor_node = block.a; + reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys); + }; + const each = render_effect( () => { /** @type {V[]} */ @@ -137,7 +145,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re if (fallback_fn !== null) { if (length === 0) { if (block.v.length !== 0 || render === null) { + clear_each(block); create_fallback_effect(); + return; } } else if (block.v.length === 0 && current_fallback !== null) { const fallback = current_fallback; @@ -160,17 +170,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re false ); - render = render_effect( - /** @param {import('./types.js').EachBlock} block */ - (block) => { - const flags = block.f; - const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; - const anchor_node = block.a; - reconcile_fn(array, block, anchor_node, is_controlled, render_fn, flags, true, keys); - }, - block, - true - ); + render = render_effect(clear_each, block, true); push_destroy_fn(each, () => { const flags = block.f; @@ -744,17 +744,27 @@ export function destroy_each_item_block( const transitions = block.s; if (apply_transitions && transitions !== null) { - trigger_transitions(transitions, 'out'); - if (transition_block !== null) { - transition_block.push(block); + // We might have pending key transitions, if so remove them first + for (let other of transitions) { + if (other.r === 'key') { + transitions.delete(other); + } } - } else { - const dom = block.d; - if (!controlled && dom !== null) { - remove(dom); + if (transitions.size === 0) { + block.s = null; + } else { + trigger_transitions(transitions, 'out'); + if (transition_block !== null) { + transition_block.push(block); + } + return; } - destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); } + const dom = block.d; + if (!controlled && dom !== null) { + remove(dom); + } + destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); } /** diff --git a/packages/svelte/src/internal/client/proxy/proxy.js b/packages/svelte/src/internal/client/proxy/proxy.js index eb392749e058..ae993267d6be 100644 --- a/packages/svelte/src/internal/client/proxy/proxy.js +++ b/packages/svelte/src/internal/client/proxy/proxy.js @@ -7,7 +7,8 @@ import { source, updating_derived, UNINITIALIZED, - mutable_source + mutable_source, + batch_inspect } from '../runtime.js'; import { define_property, @@ -100,12 +101,12 @@ function unwrap(value, already_unwrapped = new Map()) { } /** - * @template {StateObject} T + * @template T * @param {T} value - * @returns {Record} + * @returns {T} */ export function unstate(value) { - return unwrap(value); + return /** @type {T} */ (unwrap(/** @type {StateObject} */ (value))); } /** @@ -139,18 +140,34 @@ const handler = { deleteProperty(target, prop) { const metadata = target[STATE_SYMBOL]; - const s = metadata.s.get(prop); + const is_array = metadata.a; + const boolean = delete target[prop]; + + // If we have mutated an array directly, and the deletion + // was successful we will also need to update the length + // before updating the field or the version. This is to + // ensure any effects observing length can execute before + // effects that listen to the fields – otherwise they will + // operate an an index that no longer exists. + if (is_array && boolean) { + const ls = metadata.s.get('length'); + const length = target.length - 1; + if (ls !== undefined && ls.v !== length) { + set(ls, length); + } + } if (s !== undefined) set(s, UNINITIALIZED); if (prop in target) update(metadata.v); - return delete target[prop]; + return boolean; }, get(target, prop, receiver) { - if (DEV && prop === READONLY_SYMBOL) return target[READONLY_SYMBOL]; - + if (DEV && prop === READONLY_SYMBOL) { + return Reflect.get(target, READONLY_SYMBOL); + } const metadata = target[STATE_SYMBOL]; let s = metadata.s.get(prop); @@ -165,8 +182,17 @@ const handler = { metadata.s.set(prop, s); } - const value = s !== undefined ? get(s) : Reflect.get(target, prop, receiver); - return value === UNINITIALIZED ? undefined : value; + if (s !== undefined) { + const value = get(s); + return value === UNINITIALIZED ? undefined : value; + } + + if (DEV) { + if (typeof target[prop] === 'function' && prop !== Symbol.iterator) { + return batch_inspect(target, prop, receiver); + } + } + return Reflect.get(target, prop, receiver); }, getOwnPropertyDescriptor(target, prop) { @@ -184,6 +210,9 @@ const handler = { }, has(target, prop) { + if (DEV && prop === READONLY_SYMBOL) { + return Reflect.has(target, READONLY_SYMBOL); + } if (prop === STATE_SYMBOL) { return true; } diff --git a/packages/svelte/src/internal/client/proxy/readonly.js b/packages/svelte/src/internal/client/proxy/readonly.js index 3fce78339769..abc2163d9724 100644 --- a/packages/svelte/src/internal/client/proxy/readonly.js +++ b/packages/svelte/src/internal/client/proxy/readonly.js @@ -42,7 +42,7 @@ export function readonly(value) { */ const readonly_error = (_, prop) => { throw new Error( - `Non-bound props cannot be mutated — use \`bind:={...}\` to make \`${prop}\` settable. Fallback values can never be mutated.` + `Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:={...}\`. Fallback values can never be mutated.` ); }; diff --git a/packages/svelte/src/internal/client/reconciler.js b/packages/svelte/src/internal/client/reconciler.js index c96d9460a941..80c221e448c5 100644 --- a/packages/svelte/src/internal/client/reconciler.js +++ b/packages/svelte/src/internal/client/reconciler.js @@ -89,6 +89,8 @@ export function reconcile_html(dom, value, svg) { } var clone = content.cloneNode(true); frag_nodes = Array.from(clone.childNodes); - target.before(svg ? /** @type {Node} */ (clone.firstChild) : clone); + frag_nodes.forEach((node) => { + target.before(node); + }); return /** @type {Array} */ (frag_nodes); } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 93c823ce3364..a963c832c381 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -444,6 +444,20 @@ export function class_toggle(dom, class_name, value) { dom.classList.remove(class_name); } } + +/** + * @param {Element} dom + * @param {string} class_name + * @param {() => boolean} value + * @returns {void} + */ +export function class_toggle_effect(dom, class_name, value) { + render_effect(() => { + const string = value(); + class_toggle(dom, class_name, string); + }); +} + /** * Selects the correct option(s) (depending on whether this is a multiple select) * @template V @@ -2359,13 +2373,31 @@ export function set_custom_element_data(node, prop, value) { * @param {boolean} [important] */ export function style(dom, key, value, important) { + const style = dom.style; + const prev_value = style.getPropertyValue(key); if (value == null) { - dom.style.removeProperty(key); - } else { - dom.style.setProperty(key, value, important ? 'important' : ''); + if (prev_value !== '') { + style.removeProperty(key); + } + } else if (prev_value !== value) { + style.setProperty(key, value, important ? 'important' : ''); } } +/** + * @param {HTMLElement} dom + * @param {string} key + * @param {() => string} value + * @param {boolean} [important] + * @returns {void} + */ +export function style_effect(dom, key, value, important) { + render_effect(() => { + const string = value(); + style(dom, key, string, important); + }); +} + /** * List of attributes that should always be set through the attr method, * because updating them through the property setter doesn't work reliably. diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 428c03322986..6fb4cc927a02 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -37,6 +37,8 @@ let current_scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; let is_task_queued = false; +// Used for $inspect +export let is_batching_effect = false; // Handle effect queues @@ -62,8 +64,8 @@ let current_dependencies = null; let current_dependencies_index = 0; /** @type {null | import('./types.js').Signal[]} */ let current_untracked_writes = null; -// Handling capturing of signals from object property getters -let current_should_capture_signal = false; +/** @type {null | import('./types.js').Signal} */ +let last_inspected_signal = null; /** If `true`, `get`ting the signal should not register it as a dependency */ export let current_untracking = false; /** Exists to opt out of the mutation validation for stores which may be set for the first time during a derivation */ @@ -110,6 +112,29 @@ function is_runes(context) { return component_context !== null && component_context.r; } +/** + * @param {import("./proxy/proxy.js").StateObject} target + * @param {string | symbol} prop + * @param {any} receiver + */ +export function batch_inspect(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + return function () { + const previously_batching_effect = is_batching_effect; + is_batching_effect = true; + try { + return Reflect.apply(value, receiver, arguments); + } finally { + is_batching_effect = previously_batching_effect; + if (last_inspected_signal !== null) { + // @ts-expect-error + for (const fn of last_inspected_signal.inspect) fn(); + last_inspected_signal = null; + } + } + }; +} + /** * @param {null | import('./types.js').ComponentContext} context_stack_item * @returns {void} @@ -1053,8 +1078,12 @@ export function set_signal_value(signal, value) { // @ts-expect-error if (DEV && signal.inspect) { - // @ts-expect-error - for (const fn of signal.inspect) fn(); + if (is_batching_effect) { + last_inspected_signal = signal; + } else { + // @ts-expect-error + for (const fn of signal.inspect) fn(); + } } } } diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 084d51d8f5f9..e3316891ebe3 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,4 +1,5 @@ -import { untrack } from './runtime.js'; +import { EACH_INDEX_REACTIVE } from '../../constants.js'; +import { source, untrack } from './runtime.js'; import { is_array } from './utils.js'; /** regex of all html void element names */ @@ -65,7 +66,7 @@ export function validate_dynamic_element_tag(tag_fn) { /** * @param {() => any} collection - * @param {(item: any) => string} key_fn + * @param {(item: any, index: number) => string} key_fn * @returns {void} */ export function validate_each_keys(collection, key_fn) { @@ -78,7 +79,7 @@ export function validate_each_keys(collection, key_fn) { : Array.from(maybe_array); const length = array.length; for (let i = 0; i < length; i++) { - const key = key_fn(array[i]); + const key = key_fn(array[i], i); if (keys.has(key)) { throw new Error( `Cannot have duplicate keys in a keyed each: Keys at index ${keys.get( diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 53b5b7a1f662..06e4186895a8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.22'; +export const VERSION = '5.0.0-next.26'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/_config.js new file mode 100644 index 000000000000..28238df40287 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/_config.js @@ -0,0 +1,60 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target, window }) { + const button = target.querySelector('button'); + ok(button); + + assert.htmlEqual( + target.innerHTML, + ` +
    ` + ); + + flushSync(() => { + button.click(); + }); + + assert.htmlEqual( + target.innerHTML, + ` +
    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/main.svelte new file mode 100644 index 000000000000..99795007e801 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-flip-2/main.svelte @@ -0,0 +1,28 @@ + + + + +
    + {#each todos as todo (todo.id)} + + {/each} +
    diff --git a/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/_config.js b/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/_config.js new file mode 100644 index 000000000000..9e4c77c8be3a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `
    0
    1
    ` +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/main.svelte b/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/main.svelte new file mode 100644 index 000000000000..0bda97bc691f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-block-keyed-index/main.svelte @@ -0,0 +1,3 @@ +{#each {length: 2} as item, i (`${i}`)} +
    {i}
    +{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/store-reference/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-reference/_config.js new file mode 100644 index 000000000000..eceba7a93f8e --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-reference/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { dev: true }, // tests `@validate_store` code generation + + html: `` +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/store-reference/main.svelte b/packages/svelte/tests/runtime-legacy/samples/store-reference/main.svelte new file mode 100644 index 000000000000..2afc46d257b1 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-reference/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/_config.js b/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/_config.js new file mode 100644 index 000000000000..1e59eab130e5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/_config.js @@ -0,0 +1,14 @@ +import { ok, test } from '../../test'; + +export default test({ + test({ assert, target, component }) { + let svg = target.querySelector('svg'); + ok(svg); + + assert.equal(svg.namespaceURI, 'http://www.w3.org/2000/svg'); + assert.htmlEqual( + svg.outerHTML, + '' + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/main.svelte b/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/main.svelte new file mode 100644 index 000000000000..c8d313689996 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/svg-html-tag4/main.svelte @@ -0,0 +1,7 @@ + + + + {@html content} + diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/_config.js new file mode 100644 index 000000000000..436ce9979876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/main.svelte new file mode 100644 index 000000000000..7359f153047b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-private-state-proxy/main.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-proxy/_config.js b/packages/svelte/tests/runtime-runes/samples/class-state-proxy/_config.js new file mode 100644 index 000000000000..436ce9979876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-proxy/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-state-proxy/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-state-proxy/main.svelte new file mode 100644 index 000000000000..bf24921321ba --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-state-proxy/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/each-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/each-fallback/_config.js new file mode 100644 index 000000000000..183184e34194 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-fallback/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, component }) { + const [b1] = target.querySelectorAll('button'); + assert.htmlEqual(target.innerHTML, '
    abc
    '); + flushSync(() => { + b1.click(); + }); + assert.htmlEqual(target.innerHTML, '
    Fallback
    '); + flushSync(() => { + b1.click(); + }); + assert.htmlEqual(target.innerHTML, '
    abc
    '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-fallback/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-fallback/main.svelte new file mode 100644 index 000000000000..b0fcc9a86f2a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-fallback/main.svelte @@ -0,0 +1,22 @@ + +
    + {#each Object.keys(data) as key} + {key} + {:else} + Fallback + {/each} +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/each-mutation-2/_config.js b/packages/svelte/tests/runtime-runes/samples/each-mutation-2/_config.js new file mode 100644 index 000000000000..34ffe2ee1491 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-mutation-2/_config.js @@ -0,0 +1,52 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

    1

    2

    3

    `, + + async test({ assert, target }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    1

    2

    3

    4

    ` + ); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    1

    2

    3

    4

    5

    ` + ); + + flushSync(() => { + btn2.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    1

    2

    3

    4

    ` + ); + + flushSync(() => { + btn2.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    1

    2

    3

    ` + ); + + flushSync(() => { + btn2.click(); + }); + + assert.htmlEqual(target.innerHTML, `

    1

    2

    `); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-mutation-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-mutation-2/main.svelte new file mode 100644 index 000000000000..530532ccf2f6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-mutation-2/main.svelte @@ -0,0 +1,15 @@ + + + + + + +{#each numbers as number} +

    {number.id}

    +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js new file mode 100644 index 000000000000..e9dae162770d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + html: `
    ' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/main.svelte new file mode 100644 index 000000000000..00c337bdfd0d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-call-expressions/main.svelte @@ -0,0 +1,18 @@ + + +
    +
    + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte new file mode 100644 index 000000000000..a0a4ebd14cbf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-alias/Counter.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-alias/_config.js b/packages/svelte/tests/runtime-runes/samples/props-alias/_config.js new file mode 100644 index 000000000000..0fc80f0df5b7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-alias/_config.js @@ -0,0 +1,32 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

    0 0 0 0

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

    1 1 0 0

    + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-alias/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-alias/main.svelte new file mode 100644 index 000000000000..c3908f72fa11 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-alias/main.svelte @@ -0,0 +1,15 @@ + + +

    {bound} {bound_nested.count} {unbound} {unbound_nested.count}

    + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js index 352d874648c2..d78db389d74e 100644 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-default-readonly/_config.js @@ -15,5 +15,5 @@ export default test({ }, runtime_error: - 'Non-bound props cannot be mutated — use `bind:={...}` to make `count` settable. Fallback values can never be mutated.' + 'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:={...}`. Fallback values can never be mutated.' }); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js index 352d874648c2..d78db389d74e 100644 --- a/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/proxy-prop-readonly/_config.js @@ -15,5 +15,5 @@ export default test({ }, runtime_error: - 'Non-bound props cannot be mutated — use `bind:={...}` to make `count` settable. Fallback values can never be mutated.' + 'Non-bound props cannot be mutated — to make the `count` settable, ensure the object it is used within is bound as a prop `bind:={...}`. Fallback values can never be mutated.' }); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-const/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-const/_config.js new file mode 100644 index 000000000000..8867993dbbc5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-const/_config.js @@ -0,0 +1,16 @@ +import { test } from '../../test'; + +export default test({ + html: ``, + async test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, ''); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ''); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-const/main.svelte new file mode 100644 index 000000000000..cad63519bf75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-const/main.svelte @@ -0,0 +1,10 @@ + + +{#snippet counter()} + {@const doubled = count * 2} + +{/snippet} + +{@render counter()} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-top-level/_config.js new file mode 100644 index 000000000000..129a5734028c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-top-level/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true // Render in dev mode to check that the validation error is not thrown + }, + html: `

    hello world

    ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-top-level/main.svelte new file mode 100644 index 000000000000..b5da34accba2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-top-level/main.svelte @@ -0,0 +1,7 @@ + +{@render children()} +{#snippet snippet()} +

    hello world

    +{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/_config.js new file mode 100644 index 000000000000..6c130a0bfbe0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true // Render in dev mode to check that the validation error is not thrown + }, + html: `A\nB\nC\nD` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte new file mode 100644 index 000000000000..a85a932602a5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-whitespace/main.svelte @@ -0,0 +1,5 @@ +A +{#snippet snip()}C{/snippet} +B +{@render snip()} +D diff --git a/packages/svelte/tests/runtime-runes/samples/state-readonly/Component.svelte b/packages/svelte/tests/runtime-runes/samples/state-readonly/Component.svelte new file mode 100644 index 000000000000..9916a9abe045 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-readonly/Component.svelte @@ -0,0 +1,11 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/state-readonly/Component2.svelte b/packages/svelte/tests/runtime-runes/samples/state-readonly/Component2.svelte new file mode 100644 index 000000000000..88575acd3c26 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-readonly/Component2.svelte @@ -0,0 +1,5 @@ + + +{state} diff --git a/packages/svelte/tests/runtime-runes/samples/state-readonly/_config.js b/packages/svelte/tests/runtime-runes/samples/state-readonly/_config.js new file mode 100644 index 000000000000..a1f7effeae94 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-readonly/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target }) { + const btn = target.querySelector('button'); + + await btn?.click(); + assert.htmlEqual(target.innerHTML, `\n[object Object]`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-readonly/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-readonly/main.svelte new file mode 100644 index 000000000000..ec6cc9aeba5b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-readonly/main.svelte @@ -0,0 +1,12 @@ + + + +{#if state} + +{/if} diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json index f90fcf80ab47..66ff86663f6d 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-placement-1/errors.json @@ -1,7 +1,7 @@ [ { "code": "invalid-const-placement", - "message": "{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ", + "message": "{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ", "start": { "line": 5, "column": 0 }, "end": { "line": 5, "column": 18 } } diff --git a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json index 1124d0b4f869..6c14fdbaf728 100644 --- a/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json +++ b/packages/svelte/tests/validator/samples/const-tag-placement-2/errors.json @@ -1,7 +1,7 @@ [ { "code": "invalid-const-placement", - "message": "{@const} must be the immediate child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ", + "message": "{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, or ", "start": { "line": 7, "column": 4 }, "end": { "line": 7, "column": 18 } } diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md index af431b850ec8..0acacd3522df 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/01-faq.md @@ -95,7 +95,7 @@ All other features, including stores, are still fully supported in runes mode. ### When is it coming out? -When it's done. The goal is 'sometime later this year'. +When it's done. The goal is sometime in early 2024. ### Should I prepare my code for Svelte 5?