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/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/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/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/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/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) { 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'; 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy