fix: destructure array patterns immediately in #each blocks to match for...of behavior

Fixes #17227

When using array destructuring in #each blocks (e.g., {#each gen() as [item]}),
if the generator mutates the yielded array object, all items would end up with
the same final value because destructuring happened after the collection was
converted to an array, rather than immediately during iteration.

This fix ensures that array destructuring happens immediately during iteration,
capturing values at the time they are yielded, matching the behavior of a
standard for...of loop with destructuring.

- Add to_array_destructured utility that destructures during iteration
- Apply immediate destructuring for array patterns in both client and server EachBlock visitors
- Keep object destructuring using snapshotting (shallow copy)
- Add test case to prevent regression
pull/17232/head
AyushCoder9 2 weeks ago
parent 0529a5090f
commit 8348132c9c

@ -41,12 +41,25 @@ export function EachBlock(node, context) {
const destructured_pattern = get_destructured_pattern(node.context);
if (destructured_pattern) {
const mapper =
destructured_pattern.type === 'ArrayPattern'
? create_array_snapshot_mapper(destructured_pattern)
: create_object_snapshot_mapper();
collection = b.call('$.snapshot_each_value', collection, mapper);
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) {
@ -390,19 +403,6 @@ function get_destructured_pattern(pattern) {
return null;
}
/**
* @param {import('estree').ArrayPattern} pattern
*/
function create_array_snapshot_mapper(pattern) {
const value = b.id('$$value');
const has_rest = pattern.elements.some((element) => element?.type === 'RestElement');
return b.arrow(
[value],
b.call('$.snapshot_array', value, b.literal(pattern.elements.length), has_rest ? b.true : b.false)
);
}
function create_object_snapshot_mapper() {
const value = b.id('$$value');
return b.arrow([value], b.call('$.snapshot_object', value));

@ -16,12 +16,24 @@ export function EachBlock(node, context) {
const destructured_pattern = get_destructured_pattern(node.context);
if (destructured_pattern) {
const mapper =
destructured_pattern.type === 'ArrayPattern'
? create_array_snapshot_mapper(destructured_pattern)
: create_object_snapshot_mapper();
collection = b.call('$.snapshot_each_value', collection, mapper);
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);
@ -102,19 +114,6 @@ function get_destructured_pattern(pattern) {
return null;
}
/**
* @param {import('estree').ArrayPattern} pattern
*/
function create_array_snapshot_mapper(pattern) {
const value = b.id('$$value');
const has_rest = pattern.elements.some((element) => element?.type === 'RestElement');
return b.arrow(
[value],
b.call('$.snapshot_array', value, b.literal(pattern.elements.length), has_rest ? b.true : b.false)
);
}
function create_object_snapshot_mapper() {
const value = b.id('$$value');
return b.arrow([value], b.call('$.snapshot_object', value));

@ -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, snapshot_array, snapshot_each_value, snapshot_object } 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,

@ -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, snapshot_array, snapshot_each_value, snapshot_object } from '../shared/utils.js';
export { fallback, to_array, to_array_destructured, snapshot_each_value, snapshot_object } from '../shared/utils.js';
export {
invalid_default_snippet,

@ -117,9 +117,55 @@ 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<T> | Iterable<T> | null | undefined} collection
* @param {number[]} destructure_indices - Array indices to extract from each element
* @returns {Array<any[]>}
*/
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<T> | Iterable<T> | null | undefined} collection
* @param {(value: T) => T} mapper
@ -133,17 +179,6 @@ export function snapshot_each_value(collection, mapper) {
return is_array(collection) ? collection : array_from(collection, mapper);
}
/**
* @param {any} value
* @param {number} length
* @param {boolean} has_rest
* @returns {any[]}
*/
export function snapshot_array(value, length, has_rest) {
const array = to_array(value, has_rest ? undefined : length);
return array.slice();
}
/**
* @param {any} value
*/

Loading…
Cancel
Save