diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index c0bfe272e5ee..c9c94096f47b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -29,7 +29,7 @@ export function EachBlock(node, context) { scope: /** @type {Scope} */ (context.state.scope.parent) }; - const collection = build_expression( + let collection = build_expression( { ...context, state: parent_scope_state @@ -38,6 +38,30 @@ export function EachBlock(node, context) { node.metadata.expression ); + const destructured_pattern = get_destructured_pattern(node.context); + + if (destructured_pattern) { + if (destructured_pattern.type === 'ArrayPattern') { + // For array destructuring, we need to destructure immediately during iteration + // to match for...of behavior, capturing values before the generator mutates them + const indices = []; + for (let i = 0; i < destructured_pattern.elements.length; i++) { + const element = destructured_pattern.elements[i]; + if (element && element.type !== 'RestElement') { + indices.push(i); + } + } + collection = b.call( + '$.to_array_destructured', + collection, + b.array(indices.map((i) => b.literal(i))) + ); + } else { + // For object destructuring, we still need to snapshot to capture values + collection = b.call('$.snapshot_each_value', collection, create_object_snapshot_mapper()); + } + } + if (!each_node_meta.is_controlled) { context.state.template.push_comment(); } @@ -365,3 +389,21 @@ export function EachBlock(node, context) { function collect_parent_each_blocks(context) { return /** @type {AST.EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock')); } + +/** + * @param {import('estree').Pattern | null | undefined} pattern + * @returns {import('estree').ArrayPattern | import('estree').ObjectPattern | null} + */ +function get_destructured_pattern(pattern) { + if (!pattern) return null; + if (pattern.type === 'ArrayPattern' || pattern.type === 'ObjectPattern') { + return pattern; + } + + return null; +} + +function create_object_snapshot_mapper() { + const value = b.id('$$value'); + return b.arrow([value], b.call('$.snapshot_object', value)); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index 3c0a8c167696..1430c1071c32 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; @@ -12,7 +12,29 @@ export function EachBlock(node, context) { const state = context.state; const each_node_meta = node.metadata; - const collection = /** @type {Expression} */ (context.visit(node.expression)); + let collection = /** @type {Expression} */ (context.visit(node.expression)); + const destructured_pattern = get_destructured_pattern(node.context); + + if (destructured_pattern) { + if (destructured_pattern.type === 'ArrayPattern') { + // For array destructuring, destructure immediately during iteration + const indices = []; + for (let i = 0; i < destructured_pattern.elements.length; i++) { + const element = destructured_pattern.elements[i]; + if (element && element.type !== 'RestElement') { + indices.push(i); + } + } + collection = b.call( + '$.to_array_destructured', + collection, + b.array(indices.map((i) => b.literal(i))) + ); + } else { + // For object destructuring, we still need to snapshot + collection = b.call('$.snapshot_each_value', collection, create_object_snapshot_mapper()); + } + } const index = each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index); @@ -78,3 +100,21 @@ export function EachBlock(node, context) { state.template.push(...block.body, block_close); } } + +/** + * @param {Pattern | null} pattern + * @returns {import('estree').ArrayPattern | import('estree').ObjectPattern | null} + */ +function get_destructured_pattern(pattern) { + if (!pattern) return null; + if (pattern.type === 'ArrayPattern' || pattern.type === 'ObjectPattern') { + return pattern; + } + + return null; +} + +function create_object_snapshot_mapper() { + const value = b.id('$$value'); + return b.arrow([value], b.call('$.snapshot_object', value)); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a2add3ec5978..037592c1459d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -170,7 +170,7 @@ export { } from './dom/operations.js'; export { attr, clsx } from '../shared/attributes.js'; export { snapshot } from '../shared/clone.js'; -export { noop, fallback, to_array } from '../shared/utils.js'; +export { noop, fallback, to_array, to_array_destructured, snapshot_each_value, snapshot_object } from '../shared/utils.js'; export { invalid_default_snippet, validate_dynamic_element_tag, diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index c0dbdbda14f6..a85f9e68bca9 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -454,7 +454,7 @@ export { push_element, pop_element, validate_snippet_args } from './dev.js'; export { snapshot } from '../shared/clone.js'; -export { fallback, to_array } from '../shared/utils.js'; +export { fallback, to_array, to_array_destructured, snapshot_each_value, snapshot_object } from '../shared/utils.js'; export { invalid_default_snippet, diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 10f8597520d9..655d71483346 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -116,3 +116,72 @@ export function to_array(value, n) { return array; } + +/** + * Convert an iterable to an array, immediately destructuring array elements + * at the specified indices. This ensures that when a generator yields the same + * array object multiple times (mutating it), we capture the values at iteration + * time, matching for...of behavior. + * + * Returns an array where each element is a new array containing the destructured + * values, so that extract_paths can process them correctly. + * @template T + * @param {ArrayLike | Iterable | null | undefined} collection + * @param {number[]} destructure_indices - Array indices to extract from each element + * @returns {Array} + */ +export function to_array_destructured(collection, destructure_indices) { + if (collection == null) { + return []; + } + + const result = []; + + // Helper to destructure a single element + const destructure_element = (element) => { + const destructured = []; + for (let j = 0; j < destructure_indices.length; j++) { + destructured.push(element?.[destructure_indices[j]]); + } + return destructured; + }; + + // If already an array, destructure each element immediately + if (is_array(collection)) { + for (let i = 0; i < collection.length; i++) { + result.push(destructure_element(collection[i])); + } + return result; + } + + // For iterables, destructure during iteration + for (const element of collection) { + result.push(destructure_element(element)); + } + + return result; +} + +/** + * Snapshot items produced by an iterator so that destructured values reflect + * what was yielded before the iterator mutates the value again. + * Used for object destructuring where we need to shallow copy the object. + * @template T + * @param {ArrayLike | Iterable | null | undefined} collection + * @param {(value: T) => T} mapper + * @returns {Array} + */ +export function snapshot_each_value(collection, mapper) { + if (collection == null) { + return []; + } + + return is_array(collection) ? collection : array_from(collection, mapper); +} + +/** + * @param {any} value + */ +export function snapshot_object(value) { + return value == null || typeof value !== 'object' ? value : { ...value }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/_config.js b/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/_config.js new file mode 100644 index 000000000000..31f0f8ff9841 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

0

+

1

+

2

+

3

+

4

+ ` +}); + diff --git a/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/main.svelte new file mode 100644 index 000000000000..75a795bfeb7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-destructure-generator/main.svelte @@ -0,0 +1,17 @@ + + + + +{#each gen() as [item]} +

{item}

+{/each} +