From 474c588067fa75f6785d9cfe5ac15ff8bbc829fe Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 27 Feb 2025 17:42:36 +0100 Subject: [PATCH 1/3] fix: disallow `bind:group` to snippet parameters (#15401) --- .changeset/hip-oranges-hang.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ .../svelte/messages/compile-errors/template.md | 4 ++++ packages/svelte/src/compiler/errors.js | 9 +++++++++ .../phases/2-analyze/visitors/BindDirective.js | 4 ++++ .../bind-group-snippet-parameter/errors.json | 14 ++++++++++++++ .../bind-group-snippet-parameter/input.svelte | 3 +++ 7 files changed, 45 insertions(+) create mode 100644 .changeset/hip-oranges-hang.md create mode 100644 packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json create mode 100644 packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte diff --git a/.changeset/hip-oranges-hang.md b/.changeset/hip-oranges-hang.md new file mode 100644 index 000000000000..addbeafa9cdf --- /dev/null +++ b/.changeset/hip-oranges-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disallow `bind:group` to snippet parameters diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index a4ecbb31d569..ea116014e7b1 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -84,6 +84,12 @@ Attribute values containing `{...}` must be enclosed in quote marks, unless the `bind:group` can only bind to an Identifier or MemberExpression ``` +### bind_group_invalid_snippet_parameter + +``` +Cannot `bind:group` to a snippet parameter +``` + ### bind_invalid_expression ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index d061416dbd1a..0569f63ad30d 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -54,6 +54,10 @@ > `bind:group` can only bind to an Identifier or MemberExpression +## bind_group_invalid_snippet_parameter + +> Cannot `bind:group` to a snippet parameter + ## bind_invalid_expression > Can only bind to an Identifier or MemberExpression or a `{get, set}` pair diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 93eeee539cc3..677b99fcff81 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -752,6 +752,15 @@ export function bind_group_invalid_expression(node) { e(node, 'bind_group_invalid_expression', `\`bind:group\` can only bind to an Identifier or MemberExpression\nhttps://svelte.dev/e/bind_group_invalid_expression`); } +/** + * Cannot `bind:group` to a snippet parameter + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function bind_group_invalid_snippet_parameter(node) { + e(node, 'bind_group_invalid_snippet_parameter', `Cannot \`bind:group\` to a snippet parameter\nhttps://svelte.dev/e/bind_group_invalid_snippet_parameter`); +} + /** * Can only bind to an Identifier or MemberExpression or a `{get, set}` pair * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 509fecf301cc..18ea79262b50 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -191,6 +191,10 @@ export function BindDirective(node, context) { throw new Error('Cannot find declaration for bind:group'); } + if (binding.kind === 'snippet') { + e.bind_group_invalid_snippet_parameter(node); + } + // Traverse the path upwards and find all EachBlocks who are (indirectly) contributing to bind:group, // i.e. one of their declarations is referenced in the binding. This allows group bindings to work // correctly when referencing a variable declared in an EachBlock by using the index of the each block diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json new file mode 100644 index 000000000000..15e762419f1d --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "bind_group_invalid_snippet_parameter", + "end": { + "column": 44, + "line": 2 + }, + "message": "Cannot `bind:group` to a snippet parameter", + "start": { + "column": 21, + "line": 2 + } + } +] diff --git a/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte new file mode 100644 index 000000000000..368484788a1f --- /dev/null +++ b/packages/svelte/tests/validator/samples/bind-group-snippet-parameter/input.svelte @@ -0,0 +1,3 @@ +{#snippet test(group)} + +{/snippet} \ No newline at end of file From 2032049e47b97098c874195d837a16acea5bb42e Mon Sep 17 00:00:00 2001 From: adiGuba Date: Mon, 3 Mar 2025 17:38:41 +0100 Subject: [PATCH 2/3] chore: Reduce hydration comment for {:else if} (#15250) * rewrite else/if hydration * fix: bad index * reduce args on if_block() * restore the block * don't use isNan() * changeset --- .changeset/tough-steaks-travel.md | 5 +++ .../3-transform/client/visitors/Fragment.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 25 +++++------ .../3-transform/server/visitors/IfBlock.js | 30 +++++++++----- .../src/internal/client/dom/blocks/if.js | 41 ++++++++++++++----- 5 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 .changeset/tough-steaks-travel.md diff --git a/.changeset/tough-steaks-travel.md b/.changeset/tough-steaks-travel.md new file mode 100644 index 000000000000..6a9468b609cb --- /dev/null +++ b/.changeset/tough-steaks-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +chore: Reduce hydration comment for {:else if} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index e9cfd9c50684..389a694741fc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -48,7 +48,9 @@ export function Fragment(node, context) { const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement'; const is_single_child_not_needing_template = trimmed.length === 1 && - (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); + (trimmed[0].type === 'SvelteFragment' || + trimmed[0].type === 'TitleElement' || + (trimmed[0].type === 'IfBlock' && trimmed[0].elseif)); const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index d658f9eaf819..0876fa30b6a5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; @@ -19,14 +19,21 @@ export function IfBlock(node, context) { let alternate_id; if (node.alternate) { - const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); alternate_id = context.state.scope.generate('alternate'); - statements.push(b.var(b.id(alternate_id), b.arrow([b.id('$$anchor')], alternate))); + const alternate = /** @type {BlockStatement} */ (context.visit(node.alternate)); + const nodes = node.alternate.nodes; + + let alternate_args = [b.id('$$anchor')]; + if (nodes.length === 1 && nodes[0].type === 'IfBlock' && nodes[0].elseif) { + alternate_args.push(b.id('$$elseif')); + } + + statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate))); } /** @type {Expression[]} */ const args = [ - context.state.node, + node.elseif ? b.id('$$anchor') : context.state.node, b.arrow( [b.id('$$render')], b.block([ @@ -34,13 +41,7 @@ export function IfBlock(node, context) { /** @type {Expression} */ (context.visit(node.test)), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), alternate_id - ? b.stmt( - b.call( - b.id('$$render'), - b.id(alternate_id), - node.alternate ? b.literal(false) : undefined - ) - ) + ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false))) : undefined ) ]) @@ -69,7 +70,7 @@ export function IfBlock(node, context) { // ...even though they're logically equivalent. In the first case, the // transition will only play when `y` changes, but in the second it // should play when `x` or `y` change — both are considered 'local' - args.push(b.literal(true)); + args.push(b.id('$$elseif')); } statements.push(b.stmt(b.call('$.if', ...args))); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 4df09aa8b948..cbdd2cd8cc2a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { BlockStatement, Expression, IfStatement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js'; @@ -10,19 +10,29 @@ import { block_close, block_open } from './shared/utils.js'; * @param {ComponentContext} context */ export function IfBlock(node, context) { - const test = /** @type {Expression} */ (context.visit(node.test)); - const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let if_statement = b.if(/** @type {Expression} */ (context.visit(node.test)), consequent); - const alternate = node.alternate - ? /** @type {BlockStatement} */ (context.visit(node.alternate)) - : b.block([]); + context.state.template.push(if_statement, block_close); - consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + let index = 1; + let alt = node.alternate; + while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) { + const elseif = alt.nodes[0]; + const alternate = /** @type {BlockStatement} */ (context.visit(elseif.consequent)); + alternate.body.unshift( + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(``))) + ); + if_statement = if_statement.alternate = b.if( + /** @type {Expression} */ (context.visit(elseif.test)), + alternate + ); + alt = elseif.alternate; + } - alternate.body.unshift( + if_statement.alternate = alt ? /** @type {BlockStatement} */ (context.visit(alt)) : b.block([]); + if_statement.alternate.body.unshift( b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) ); - - context.state.template.push(b.if(test, consequent, alternate), block_close); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 36790c05c135..423c436fe4ef 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -9,16 +9,16 @@ import { set_hydrating } from '../hydration.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; +import { HYDRATION_START, HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; /** * @param {TemplateNode} node - * @param {(branch: (fn: (anchor: Node) => void, flag?: boolean) => void) => void} fn - * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' + * @param {(branch: (fn: (anchor: Node, elseif?: [number,number]) => void, flag?: boolean) => void) => void} fn + * @param {[number,number]} [elseif] * @returns {void} */ -export function if_block(node, fn, elseif = false) { - if (hydrating) { +export function if_block(node, fn, [root_index, hydrate_index] = [0, 0]) { + if (hydrating && root_index === 0) { hydrate_next(); } @@ -33,26 +33,44 @@ export function if_block(node, fn, elseif = false) { /** @type {UNINITIALIZED | boolean | null} */ var condition = UNINITIALIZED; - var flags = elseif ? EFFECT_TRANSPARENT : 0; + var flags = root_index > 0 ? EFFECT_TRANSPARENT : 0; var has_branch = false; - const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => { + const set_branch = ( + /** @type {(anchor: Node, elseif?: [number,number]) => void} */ fn, + flag = true + ) => { has_branch = true; update_branch(flag, fn); }; const update_branch = ( /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node) => void)} */ fn + /** @type {null | ((anchor: Node, elseif?: [number,number]) => void)} */ fn ) => { if (condition === (condition = new_condition)) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ let mismatch = false; - if (hydrating) { - const is_else = /** @type {Comment} */ (anchor).data === HYDRATION_START_ELSE; + if (hydrating && hydrate_index !== -1) { + if (root_index === 0) { + const data = /** @type {Comment} */ (anchor).data; + if (data === HYDRATION_START) { + hydrate_index = 0; + } else if (data === HYDRATION_START_ELSE) { + hydrate_index = Infinity; + } else { + hydrate_index = parseInt(data.substring(1)); + if (hydrate_index !== hydrate_index) { + // if hydrate_index is NaN + // we set an invalid index to force mismatch + hydrate_index = condition ? Infinity : -1; + } + } + } + const is_else = hydrate_index > root_index; if (!!condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. @@ -62,6 +80,7 @@ export function if_block(node, fn, elseif = false) { set_hydrate_node(anchor); set_hydrating(false); mismatch = true; + hydrate_index = -1; // ignore hydration in next else if } } @@ -81,7 +100,7 @@ export function if_block(node, fn, elseif = false) { if (alternate_effect) { resume_effect(alternate_effect); } else if (fn) { - alternate_effect = branch(() => fn(anchor)); + alternate_effect = branch(() => fn(anchor, [root_index + 1, hydrate_index])); } if (consequent_effect) { From 7ce2dfc62297a41b8e403803cd54a86caf177418 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 16:40:08 +0000 Subject: [PATCH 3/3] Version Packages (#15402) Co-authored-by: github-actions[bot] --- .changeset/hip-oranges-hang.md | 5 ----- .changeset/tough-steaks-travel.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/hip-oranges-hang.md delete mode 100644 .changeset/tough-steaks-travel.md diff --git a/.changeset/hip-oranges-hang.md b/.changeset/hip-oranges-hang.md deleted file mode 100644 index addbeafa9cdf..000000000000 --- a/.changeset/hip-oranges-hang.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: disallow `bind:group` to snippet parameters diff --git a/.changeset/tough-steaks-travel.md b/.changeset/tough-steaks-travel.md deleted file mode 100644 index 6a9468b609cb..000000000000 --- a/.changeset/tough-steaks-travel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -chore: Reduce hydration comment for {:else if} diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index cb33bc0d3916..f32f872ee082 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.21.0 + +### Minor Changes + +- chore: Reduce hydration comment for {:else if} ([#15250](https://github.com/sveltejs/svelte/pull/15250)) + +### Patch Changes + +- fix: disallow `bind:group` to snippet parameters ([#15401](https://github.com/sveltejs/svelte/pull/15401)) + ## 5.20.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index bff6b5adc04f..f32466ce85fc 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.20.5", + "version": "5.21.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b77e0ea49edd..46fbec674cff 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.5'; +export const VERSION = '5.21.0'; export const PUBLIC_VERSION = '5';