Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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));
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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));
}
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
69 changes: 69 additions & 0 deletions packages/svelte/src/internal/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<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
* @returns {Array<T>}
*/
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 };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test } from '../../test';

export default test({
html: `
<p>0</p>
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
`
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<svelte:options runes />

<script>
function* gen() {
const arr = [0];

for (let i = 0; i < 5; i += 1) {
arr[0] = i;
yield arr;
}
}
</script>

{#each gen() as [item]}
<p>{item}</p>
{/each}

Loading