Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 81a34aa

Browse files
Rich-Harrisdummdidumm
andauthoredMay 28, 2025··
fix: handle derived destructured iterators (#16015)
* revert #15813 * failing test * tweak * tweak * WIP * WIP * WIP * WIP * WIP * working * tweak * changeset * tweak description * Update packages/svelte/src/compiler/utils/ast.js Co-authored-by: Simon H <[email protected]> --------- Co-authored-by: Simon H <[email protected]>
1 parent 2e27c5d commit 81a34aa

File tree

16 files changed

+348
-238
lines changed

16 files changed

+348
-238
lines changed
 

‎.changeset/purple-dryers-mate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle derived destructured iterators

‎packages/svelte/src/compiler/migrate/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,15 +603,15 @@ const instance_script = {
603603
);
604604
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
605605
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
606-
// const tmp = state.scope.generate('tmp');
607-
// const paths = extract_paths(declarator.id);
606+
// const tmp = b.id(state.scope.generate('tmp'));
607+
// const paths = extract_paths(declarator.id, tmp);
608608
// state.props_pre.push(
609-
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
609+
// b.declaration('const', tmp, visit(declarator.init!) as Expression)
610610
// );
611611
// for (const path of paths) {
612612
// const name = (path.node as Identifier).name;
613613
// const binding = state.scope.get(name)!;
614-
// const value = path.expression!(b.id(tmp));
614+
// const value = path.expression;
615615
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
616616
// state.props.push({
617617
// local: name,

‎packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
77
import * as w from '../../../warnings.js';
88
import { extract_paths } from '../../../utils/ast.js';
99
import { equal } from '../../../utils/assert.js';
10+
import * as b from '#compiler/builders';
1011

1112
/**
1213
* @param {VariableDeclarator} node
@@ -18,7 +19,7 @@ export function VariableDeclarator(node, context) {
1819
if (context.state.analysis.runes) {
1920
const init = node.init;
2021
const rune = get_rune(init, context.state.scope);
21-
const paths = extract_paths(node.id);
22+
const { paths } = extract_paths(node.id, b.id('dummy'));
2223

2324
for (const path of paths) {
2425
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,21 @@ export function EachBlock(node, context) {
234234
} else if (node.context) {
235235
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
236236

237-
for (const path of extract_paths(node.context)) {
237+
const { inserts, paths } = extract_paths(node.context, unwrapped);
238+
239+
for (const { id, value } of inserts) {
240+
id.name = context.state.scope.generate('$$array');
241+
child_state.transform[id.name] = { read: get_value };
242+
243+
const expression = /** @type {Expression} */ (context.visit(b.thunk(value), child_state));
244+
declarations.push(b.var(id, b.call('$.derived', expression)));
245+
}
246+
247+
for (const path of paths) {
238248
const name = /** @type {Identifier} */ (path.node).name;
239249
const needs_derived = path.has_default_value; // to ensure that default value is only called once
240250

241-
const fn = b.thunk(
242-
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
243-
);
251+
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
244252

245253
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
246254

@@ -249,7 +257,7 @@ export function EachBlock(node, context) {
249257
child_state.transform[name] = {
250258
read,
251259
assign: (_, value) => {
252-
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
260+
const left = /** @type {Pattern} */ (path.update_expression);
253261
return b.sequence([b.assignment('=', left, value), ...sequence]);
254262
},
255263
mutate: (_, mutation) => {

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,21 @@ export function SnippetBlock(node, context) {
4343
let arg_alias = `$$arg${i}`;
4444
args.push(b.id(arg_alias));
4545

46-
const paths = extract_paths(argument);
46+
const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));
47+
48+
for (const { id, value } of inserts) {
49+
id.name = context.state.scope.generate('$$array');
50+
transform[id.name] = { read: get_value };
51+
52+
declarations.push(
53+
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
54+
);
55+
}
4756

4857
for (const path of paths) {
4958
const name = /** @type {Identifier} */ (path.node).name;
5059
const needs_derived = path.has_default_value; // to ensure that default value is only called once
51-
const fn = b.thunk(
52-
/** @type {Expression} */ (context.visit(path.expression?.(b.maybe_call(b.id(arg_alias)))))
53-
);
60+
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression)));
5461

5562
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
5663

‎packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js

Lines changed: 90 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
/** @import { Binding } from '#compiler' */
33
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
44
import { dev } from '../../../../state.js';
5-
import { build_pattern, extract_paths } from '../../../../utils/ast.js';
5+
import { extract_paths } from '../../../../utils/ast.js';
66
import * as b from '#compiler/builders';
77
import * as assert from '../../../../utils/assert.js';
88
import { get_rune } from '../../../scope.js';
99
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
1010
import { is_hoisted_function } from '../../utils.js';
11+
import { get_value } from './shared/declarations.js';
1112

1213
/**
1314
* @param {VariableDeclaration} node
@@ -116,7 +117,7 @@ export function VariableDeclaration(node, context) {
116117
}
117118

118119
const args = /** @type {CallExpression} */ (init).arguments;
119-
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
120+
const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
120121

121122
if (rune === '$state' || rune === '$state.raw') {
122123
/**
@@ -137,24 +138,34 @@ export function VariableDeclaration(node, context) {
137138
};
138139

139140
if (declarator.id.type === 'Identifier') {
141+
const expression = /** @type {Expression} */ (context.visit(value));
142+
140143
declarations.push(
141-
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
144+
b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
142145
);
143146
} else {
144-
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
147+
const tmp = b.id(context.state.scope.generate('tmp'));
148+
const { inserts, paths } = extract_paths(declarator.id, tmp);
149+
145150
declarations.push(
146-
b.declarator(pattern, value),
147-
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
148-
([original, replacement]) => {
149-
const binding = context.state.scope.get(original.name);
150-
return b.declarator(
151-
original,
152-
binding?.kind === 'state' || binding?.kind === 'raw_state'
153-
? create_state_declarator(binding.node, replacement)
154-
: replacement
155-
);
156-
}
157-
)
151+
b.declarator(tmp, value),
152+
...inserts.map(({ id, value }) => {
153+
id.name = context.state.scope.generate('$$array');
154+
context.state.transform[id.name] = { read: get_value };
155+
156+
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
157+
return b.declarator(id, b.call('$.derived', expression));
158+
}),
159+
...paths.map((path) => {
160+
const value = /** @type {Expression} */ (context.visit(path.expression));
161+
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
162+
return b.declarator(
163+
path.node,
164+
binding?.kind === 'state' || binding?.kind === 'raw_state'
165+
? create_state_declarator(binding.node, value)
166+
: value
167+
);
168+
})
158169
);
159170
}
160171

@@ -163,44 +174,41 @@ export function VariableDeclaration(node, context) {
163174

164175
if (rune === '$derived' || rune === '$derived.by') {
165176
if (declarator.id.type === 'Identifier') {
166-
declarations.push(
167-
b.declarator(
168-
declarator.id,
169-
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
170-
)
171-
);
177+
let expression = /** @type {Expression} */ (context.visit(value));
178+
if (rune === '$derived') expression = b.thunk(expression);
179+
180+
declarations.push(b.declarator(declarator.id, b.call('$.derived', expression)));
172181
} else {
173-
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
174182
const init = /** @type {CallExpression} */ (declarator.init);
175183

176-
/** @type {Identifier} */
177-
let id;
178184
let rhs = value;
179185

180-
if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
181-
id = init.arguments[0];
182-
} else {
183-
id = b.id(context.state.scope.generate('$$d'));
186+
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
187+
const id = b.id(context.state.scope.generate('$$d'));
184188
rhs = b.call('$.get', id);
185189

186-
declarations.push(
187-
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)))
188-
);
190+
let expression = /** @type {Expression} */ (context.visit(value));
191+
if (rune === '$derived') expression = b.thunk(expression);
192+
193+
declarations.push(b.declarator(id, b.call('$.derived', expression)));
189194
}
190195

191-
for (let i = 0; i < replacements.size; i++) {
192-
const [original, replacement] = [...replacements][i];
193-
declarations.push(
194-
b.declarator(
195-
original,
196-
b.call(
197-
'$.derived',
198-
b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)]))
199-
)
200-
)
201-
);
196+
const { inserts, paths } = extract_paths(declarator.id, rhs);
197+
198+
for (const { id, value } of inserts) {
199+
id.name = context.state.scope.generate('$$array');
200+
context.state.transform[id.name] = { read: get_value };
201+
202+
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
203+
declarations.push(b.declarator(id, b.call('$.derived', expression)));
204+
}
205+
206+
for (const path of paths) {
207+
const expression = /** @type {Expression} */ (context.visit(path.expression));
208+
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
202209
}
203210
}
211+
204212
continue;
205213
}
206214
}
@@ -229,20 +237,29 @@ export function VariableDeclaration(node, context) {
229237
if (declarator.id.type !== 'Identifier') {
230238
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
231239
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
232-
const tmp = context.state.scope.generate('tmp');
233-
const paths = extract_paths(declarator.id);
240+
const tmp = b.id(context.state.scope.generate('tmp'));
241+
const { inserts, paths } = extract_paths(declarator.id, tmp);
234242

235243
declarations.push(
236244
b.declarator(
237-
b.id(tmp),
245+
tmp,
238246
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
239247
)
240248
);
241249

250+
for (const { id, value } of inserts) {
251+
id.name = context.state.scope.generate('$$array');
252+
context.state.transform[id.name] = { read: get_value };
253+
254+
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
255+
declarations.push(b.declarator(id, b.call('$.derived', expression)));
256+
}
257+
242258
for (const path of paths) {
243259
const name = /** @type {Identifier} */ (path.node).name;
244260
const binding = /** @type {Binding} */ (context.state.scope.get(name));
245-
const value = path.expression?.(b.id(tmp));
261+
const value = /** @type {Expression} */ (context.visit(path.expression));
262+
246263
declarations.push(
247264
b.declarator(
248265
path.node,
@@ -276,7 +293,7 @@ export function VariableDeclaration(node, context) {
276293
declarations.push(
277294
...create_state_declarators(
278295
declarator,
279-
context.state,
296+
context,
280297
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
281298
)
282299
);
@@ -296,32 +313,41 @@ export function VariableDeclaration(node, context) {
296313
/**
297314
* Creates the output for a state declaration in legacy mode.
298315
* @param {VariableDeclarator} declarator
299-
* @param {ComponentClientTransformState} scope
316+
* @param {ComponentContext} context
300317
* @param {Expression} value
301318
*/
302-
function create_state_declarators(declarator, { scope, analysis }, value) {
319+
function create_state_declarators(declarator, context, value) {
303320
if (declarator.id.type === 'Identifier') {
304321
return [
305322
b.declarator(
306323
declarator.id,
307-
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
324+
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
308325
)
309326
];
310327
}
311328

312-
const [pattern, replacements] = build_pattern(declarator.id, scope);
329+
const tmp = b.id(context.state.scope.generate('tmp'));
330+
const { inserts, paths } = extract_paths(declarator.id, tmp);
331+
313332
return [
314-
b.declarator(pattern, value),
315-
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
316-
([original, replacement]) => {
317-
const binding = scope.get(original.name);
318-
return b.declarator(
319-
original,
320-
binding?.kind === 'state'
321-
? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined)
322-
: replacement
323-
);
324-
}
325-
)
333+
b.declarator(tmp, value),
334+
...inserts.map(({ id, value }) => {
335+
id.name = context.state.scope.generate('$$array');
336+
context.state.transform[id.name] = { read: get_value };
337+
338+
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
339+
return b.declarator(id, b.call('$.derived', expression));
340+
}),
341+
...paths.map((path) => {
342+
const value = /** @type {Expression} */ (context.visit(path.expression));
343+
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
344+
345+
return b.declarator(
346+
path.node,
347+
binding?.kind === 'state'
348+
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
349+
: value
350+
);
351+
})
326352
];
327353
}

‎packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/** @import { Context } from '../types.js' */
44
/** @import { ComponentAnalysis } from '../../../types.js' */
55
/** @import { Scope } from '../../../scope.js' */
6-
import { build_pattern, build_fallback, extract_paths } from '../../../../utils/ast.js';
6+
import { build_fallback, extract_paths } from '../../../../utils/ast.js';
77
import * as b from '#compiler/builders';
88
import { get_rune } from '../../../scope.js';
99
import { walk } from 'zimmerframe';
@@ -120,21 +120,29 @@ export function VariableDeclaration(node, context) {
120120
if (declarator.id.type !== 'Identifier') {
121121
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
122122
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
123-
const tmp = context.state.scope.generate('tmp');
124-
const paths = extract_paths(declarator.id);
123+
const tmp = b.id(context.state.scope.generate('tmp'));
124+
const { inserts, paths } = extract_paths(declarator.id, tmp);
125+
125126
declarations.push(
126127
b.declarator(
127-
b.id(tmp),
128+
tmp,
128129
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
129130
)
130131
);
132+
133+
for (const { id, value } of inserts) {
134+
id.name = context.state.scope.generate('$$array');
135+
declarations.push(b.declarator(id, value));
136+
}
137+
131138
for (const path of paths) {
132-
const value = path.expression?.(b.id(tmp));
139+
const value = path.expression;
133140
const name = /** @type {Identifier} */ (path.node).name;
134141
const binding = /** @type {Binding} */ (context.state.scope.get(name));
135142
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
136143
declarations.push(b.declarator(path.node, build_fallback(prop, value)));
137144
}
145+
138146
continue;
139147
}
140148

@@ -188,10 +196,13 @@ function create_state_declarators(declarator, scope, value) {
188196
return [b.declarator(declarator.id, value)];
189197
}
190198

191-
const [pattern, replacements] = build_pattern(declarator.id, scope);
199+
const tmp = b.id(scope.generate('tmp'));
200+
const { paths } = extract_paths(declarator.id, tmp);
192201
return [
193-
b.declarator(pattern, value),
194-
// TODO inject declarator for opts, so we can use it below
195-
...[...replacements].map(([original, replacement]) => b.declarator(original, replacement))
202+
b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below
203+
...paths.map((path) => {
204+
const value = path.expression;
205+
return b.declarator(path.node, value);
206+
})
196207
];
197208
}

‎packages/svelte/src/compiler/phases/3-transform/shared/assignments.js

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Node, Pattern } from 'estree' */
1+
/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern, Statement } from 'estree' */
22
/** @import { Context as ClientContext } from '../client/types.js' */
33
/** @import { Context as ServerContext } from '../server/types.js' */
4-
import { build_pattern, is_expression_async } from '../../../utils/ast.js';
4+
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
55
import * as b from '#compiler/builders';
6+
import { get_value } from '../client/visitors/shared/declarations.js';
67

78
/**
89
* @template {ClientContext | ServerContext} Context
@@ -23,60 +24,64 @@ export function visit_assignment_expression(node, context, build_assignment) {
2324

2425
let changed = false;
2526

26-
const [pattern, replacements] = build_pattern(node.left, context.state.scope);
27-
28-
const assignments = [
29-
b.let(pattern, rhs),
30-
...[...replacements].map(([original, replacement]) => {
31-
let assignment = build_assignment(node.operator, original, replacement, context);
32-
if (assignment !== null) changed = true;
33-
return b.stmt(
34-
assignment ??
35-
b.assignment(
36-
node.operator,
37-
/** @type {Identifier} */ (context.visit(original)),
38-
/** @type {Expression} */ (context.visit(replacement))
39-
)
40-
);
41-
})
42-
];
27+
const { inserts, paths } = extract_paths(node.left, rhs);
28+
29+
for (const { id } of inserts) {
30+
id.name = context.state.scope.generate('$$array');
31+
}
32+
33+
const assignments = paths.map((path) => {
34+
const value = path.expression;
35+
36+
let assignment = build_assignment('=', path.node, value, context);
37+
if (assignment !== null) changed = true;
38+
39+
return (
40+
assignment ??
41+
b.assignment(
42+
'=',
43+
/** @type {Pattern} */ (context.visit(path.node)),
44+
/** @type {Expression} */ (context.visit(value))
45+
)
46+
);
47+
});
4348

4449
if (!changed) {
4550
// No change to output -> nothing to transform -> we can keep the original assignment
4651
return null;
4752
}
4853

4954
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
50-
const block = b.block(assignments);
5155

52-
if (!is_standalone) {
53-
// this is part of an expression, we need the sequence to end with the value
54-
block.body.push(b.return(rhs));
55-
}
56+
if (inserts.length > 0 || should_cache) {
57+
/** @type {Statement[]} */
58+
const statements = [
59+
...inserts.map(({ id, value }) => b.var(id, value)),
60+
...assignments.map(b.stmt)
61+
];
5662

57-
if (is_standalone && !should_cache) {
58-
return block;
63+
if (!is_standalone) {
64+
// this is part of an expression, we need the sequence to end with the value
65+
statements.push(b.return(rhs));
66+
}
67+
68+
const iife = b.arrow([rhs], b.block(statements));
69+
70+
const iife_is_async =
71+
is_expression_async(value) ||
72+
assignments.some((assignment) => is_expression_async(assignment));
73+
74+
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
5975
}
6076

61-
const iife = b.arrow(should_cache ? [rhs] : [], block);
62-
63-
const iife_is_async =
64-
is_expression_async(value) ||
65-
assignments.some(
66-
(assignment) =>
67-
(assignment.type === 'ExpressionStatement' &&
68-
is_expression_async(assignment.expression)) ||
69-
(assignment.type === 'VariableDeclaration' &&
70-
assignment.declarations.some(
71-
(declaration) =>
72-
is_expression_async(declaration.id) ||
73-
(declaration.init && is_expression_async(declaration.init))
74-
))
75-
);
77+
const sequence = b.sequence(assignments);
78+
79+
if (!is_standalone) {
80+
// this is part of an expression, we need the sequence to end with the value
81+
sequence.expressions.push(rhs);
82+
}
7683

77-
return iife_is_async
78-
? b.await(b.call(b.async(iife), should_cache ? value : undefined))
79-
: b.call(iife, should_cache ? value : undefined);
84+
return sequence;
8085
}
8186

8287
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {

‎packages/svelte/src/compiler/phases/scope.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -630,10 +630,9 @@ export class Scope {
630630

631631
/**
632632
* @param {string} preferred_name
633-
* @param {(name: string, counter: number) => string} [generator]
634633
* @returns {string}
635634
*/
636-
generate(preferred_name, generator = (name, counter) => `${name}_${counter}`) {
635+
generate(preferred_name) {
637636
if (this.#porous) {
638637
return /** @type {Scope} */ (this.parent).generate(preferred_name);
639638
}
@@ -648,7 +647,7 @@ export class Scope {
648647
this.root.conflicts.has(name) ||
649648
is_reserved(name)
650649
) {
651-
name = generator(preferred_name, n++);
650+
name = `${preferred_name}_${n++}`;
652651
}
653652

654653
this.references.set(name, []);

‎packages/svelte/src/compiler/utils/ast.js

Lines changed: 78 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/** @import { AST, Scope } from '#compiler' */
1+
/** @import { AST } from '#compiler' */
22
/** @import * as ESTree from 'estree' */
33
import { walk } from 'zimmerframe';
44
import * as b from '#compiler/builders';
5-
import is_reference from 'is-reference';
65

76
/**
87
* Gets the left-most identifier of a member expression or identifier.
@@ -129,49 +128,6 @@ export function unwrap_pattern(pattern, nodes = []) {
129128
return nodes;
130129
}
131130

132-
/**
133-
* @param {ESTree.Pattern} id
134-
* @param {Scope} scope
135-
* @returns {[ESTree.Pattern, Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>]}
136-
*/
137-
export function build_pattern(id, scope) {
138-
/** @type {Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>} */
139-
const map = new Map();
140-
141-
/** @type {Map<string, string>} */
142-
const names = new Map();
143-
144-
let counter = 0;
145-
146-
for (const node of unwrap_pattern(id)) {
147-
const name = scope.generate(`$$${++counter}`, (_, counter) => `$$${counter}`);
148-
149-
map.set(node, b.id(name));
150-
151-
if (node.type === 'Identifier') {
152-
names.set(node.name, name);
153-
}
154-
}
155-
156-
const pattern = walk(id, null, {
157-
Identifier(node, context) {
158-
if (is_reference(node, /** @type {ESTree.Pattern} */ (context.path.at(-1)))) {
159-
const name = names.get(node.name);
160-
if (name) return b.id(name);
161-
}
162-
},
163-
164-
MemberExpression(node, context) {
165-
const n = map.get(node);
166-
if (n) return n;
167-
168-
context.next();
169-
}
170-
});
171-
172-
return [pattern, map];
173-
}
174-
175131
/**
176132
* Extracts all identifiers from a pattern.
177133
* @param {ESTree.Pattern} pattern
@@ -271,40 +227,50 @@ export function extract_identifiers_from_destructuring(node, nodes = []) {
271227
* @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
272228
* @property {boolean} is_rest `true` if this is a `...rest` destructuring
273229
* @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..`
274-
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} expression Returns an expression which walks the path starting at the given expression.
230+
* @property {ESTree.Expression} expression The value of the current path
275231
* This will be a call expression if a rest element or default is involved — e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` — since we can't represent `baz` or `rest` purely as a path
276232
* Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
277-
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} update_expression Like `expression` but without default values.
233+
* @property {ESTree.Expression} update_expression Like `expression` but without default values.
278234
*/
279235

280236
/**
281237
* Extracts all destructured assignments from a pattern.
238+
* For each `id` in the returned `inserts`, make sure to adjust the `name`.
282239
* @param {ESTree.Node} param
283-
* @returns {DestructuredAssignment[]}
240+
* @param {ESTree.Expression} initial
241+
* @returns {{ inserts: Array<{ id: ESTree.Identifier, value: ESTree.Expression }>, paths: DestructuredAssignment[] }}
284242
*/
285-
export function extract_paths(param) {
286-
return _extract_paths(
287-
[],
288-
param,
289-
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
290-
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
291-
false
292-
);
243+
export function extract_paths(param, initial) {
244+
/**
245+
* When dealing with array destructuring patterns (`let [a, b, c] = $derived(blah())`)
246+
* we need an intermediate declaration that creates an array, since `blah()` could
247+
* return a non-array-like iterator
248+
* @type {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>}
249+
*/
250+
const inserts = [];
251+
252+
/** @type {DestructuredAssignment[]} */
253+
const paths = [];
254+
255+
_extract_paths(paths, inserts, param, initial, initial, false);
256+
257+
return { inserts, paths };
293258
}
294259

295260
/**
296-
* @param {DestructuredAssignment[]} assignments
261+
* @param {DestructuredAssignment[]} paths
262+
* @param {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>} inserts
297263
* @param {ESTree.Node} param
298-
* @param {DestructuredAssignment['expression']} expression
299-
* @param {DestructuredAssignment['update_expression']} update_expression
264+
* @param {ESTree.Expression} expression
265+
* @param {ESTree.Expression} update_expression
300266
* @param {boolean} has_default_value
301267
* @returns {DestructuredAssignment[]}
302268
*/
303-
function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) {
269+
function _extract_paths(paths, inserts, param, expression, update_expression, has_default_value) {
304270
switch (param.type) {
305271
case 'Identifier':
306272
case 'MemberExpression':
307-
assignments.push({
273+
paths.push({
308274
node: param,
309275
is_rest: false,
310276
has_default_value,
@@ -316,28 +282,25 @@ function _extract_paths(assignments = [], param, expression, update_expression,
316282
case 'ObjectPattern':
317283
for (const prop of param.properties) {
318284
if (prop.type === 'RestElement') {
319-
/** @type {DestructuredAssignment['expression']} */
320-
const rest_expression = (object) => {
321-
/** @type {ESTree.Expression[]} */
322-
const props = [];
323-
324-
for (const p of param.properties) {
325-
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
326-
if (p.key.type === 'Identifier' && !p.computed) {
327-
props.push(b.literal(p.key.name));
328-
} else if (p.key.type === 'Literal') {
329-
props.push(b.literal(String(p.key.value)));
330-
} else {
331-
props.push(b.call('String', p.key));
332-
}
285+
/** @type {ESTree.Expression[]} */
286+
const props = [];
287+
288+
for (const p of param.properties) {
289+
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
290+
if (p.key.type === 'Identifier' && !p.computed) {
291+
props.push(b.literal(p.key.name));
292+
} else if (p.key.type === 'Literal') {
293+
props.push(b.literal(String(p.key.value)));
294+
} else {
295+
props.push(b.call('String', p.key));
333296
}
334297
}
298+
}
335299

336-
return b.call('$.exclude_from_object', expression(object), b.array(props));
337-
};
300+
const rest_expression = b.call('$.exclude_from_object', expression, b.array(props));
338301

339302
if (prop.argument.type === 'Identifier') {
340-
assignments.push({
303+
paths.push({
341304
node: prop.argument,
342305
is_rest: true,
343306
has_default_value,
@@ -346,19 +309,24 @@ function _extract_paths(assignments = [], param, expression, update_expression,
346309
});
347310
} else {
348311
_extract_paths(
349-
assignments,
312+
paths,
313+
inserts,
350314
prop.argument,
351315
rest_expression,
352316
rest_expression,
353317
has_default_value
354318
);
355319
}
356320
} else {
357-
/** @type {DestructuredAssignment['expression']} */
358-
const object_expression = (object) =>
359-
b.member(expression(object), prop.key, prop.computed || prop.key.type !== 'Identifier');
321+
const object_expression = b.member(
322+
expression,
323+
prop.key,
324+
prop.computed || prop.key.type !== 'Identifier'
325+
);
326+
360327
_extract_paths(
361-
assignments,
328+
paths,
329+
inserts,
362330
prop.value,
363331
object_expression,
364332
object_expression,
@@ -369,16 +337,27 @@ function _extract_paths(assignments = [], param, expression, update_expression,
369337

370338
break;
371339

372-
case 'ArrayPattern':
340+
case 'ArrayPattern': {
341+
// we create an intermediate declaration to convert iterables to arrays if necessary.
342+
// the consumer is responsible for setting the name of the identifier
343+
const id = b.id('#');
344+
345+
const value = b.call(
346+
'$.to_array',
347+
expression,
348+
param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length)
349+
);
350+
351+
inserts.push({ id, value });
352+
373353
for (let i = 0; i < param.elements.length; i += 1) {
374354
const element = param.elements[i];
375355
if (element) {
376356
if (element.type === 'RestElement') {
377-
/** @type {DestructuredAssignment['expression']} */
378-
const rest_expression = (object) =>
379-
b.call(b.member(expression(object), 'slice'), b.literal(i));
357+
const rest_expression = b.call(b.member(id, 'slice'), b.literal(i));
358+
380359
if (element.argument.type === 'Identifier') {
381-
assignments.push({
360+
paths.push({
382361
node: element.argument,
383362
is_rest: true,
384363
has_default_value,
@@ -387,18 +366,20 @@ function _extract_paths(assignments = [], param, expression, update_expression,
387366
});
388367
} else {
389368
_extract_paths(
390-
assignments,
369+
paths,
370+
inserts,
391371
element.argument,
392372
rest_expression,
393373
rest_expression,
394374
has_default_value
395375
);
396376
}
397377
} else {
398-
/** @type {DestructuredAssignment['expression']} */
399-
const array_expression = (object) => b.member(expression(object), b.literal(i), true);
378+
const array_expression = b.member(id, b.literal(i), true);
379+
400380
_extract_paths(
401-
assignments,
381+
paths,
382+
inserts,
402383
element,
403384
array_expression,
404385
array_expression,
@@ -409,28 +390,28 @@ function _extract_paths(assignments = [], param, expression, update_expression,
409390
}
410391

411392
break;
393+
}
412394

413395
case 'AssignmentPattern': {
414-
/** @type {DestructuredAssignment['expression']} */
415-
const fallback_expression = (object) => build_fallback(expression(object), param.right);
396+
const fallback_expression = build_fallback(expression, param.right);
416397

417398
if (param.left.type === 'Identifier') {
418-
assignments.push({
399+
paths.push({
419400
node: param.left,
420401
is_rest: false,
421402
has_default_value: true,
422403
expression: fallback_expression,
423404
update_expression
424405
});
425406
} else {
426-
_extract_paths(assignments, param.left, fallback_expression, update_expression, true);
407+
_extract_paths(paths, inserts, param.left, fallback_expression, update_expression, true);
427408
}
428409

429410
break;
430411
}
431412
}
432413

433-
return assignments;
414+
return paths;
434415
}
435416

436417
/**

‎packages/svelte/src/internal/client/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export {
154154
} from './dom/operations.js';
155155
export { attr, clsx } from '../shared/attributes.js';
156156
export { snapshot } from '../shared/clone.js';
157-
export { noop, fallback } from '../shared/utils.js';
157+
export { noop, fallback, to_array } from '../shared/utils.js';
158158
export {
159159
invalid_default_snippet,
160160
validate_dynamic_element_tag,

‎packages/svelte/src/internal/server/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,7 @@ export { assign_payload, copy_payload } from './payload.js';
504504

505505
export { snapshot } from '../shared/clone.js';
506506

507-
export { fallback } from '../shared/utils.js';
507+
export { fallback, to_array } from '../shared/utils.js';
508508

509509
export {
510510
invalid_default_snippet,

‎packages/svelte/src/internal/shared/utils.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,38 @@ export function fallback(value, fallback, lazy = false) {
8181
: /** @type {V} */ (fallback)
8282
: value;
8383
}
84+
85+
/**
86+
* When encountering a situation like `let [a, b, c] = $derived(blah())`,
87+
* we need to stash an intermediate value that `a`, `b`, and `c` derive
88+
* from, in case it's an iterable
89+
* @template T
90+
* @param {ArrayLike<T> | Iterable<T>} value
91+
* @param {number} [n]
92+
* @returns {Array<T>}
93+
*/
94+
export function to_array(value, n) {
95+
// return arrays unchanged
96+
if (Array.isArray(value)) {
97+
return value;
98+
}
99+
100+
// if value is not iterable, or `n` is unspecified (indicates a rest
101+
// element, which means we're not concerned about unbounded iterables)
102+
// convert to an array with `Array.from`
103+
if (n === undefined || !(Symbol.iterator in value)) {
104+
return Array.from(value);
105+
}
106+
107+
// otherwise, populate an array with `n` values
108+
109+
/** @type {T[]} */
110+
const array = [];
111+
112+
for (const element of value) {
113+
array.push(element);
114+
if (array.length === n) break;
115+
}
116+
117+
return array;
118+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { flushSync } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
html: `<button>increment</button><p>a: 1</p><p>b: 2</p><p>c: 3</p>`,
6+
7+
test({ assert, target }) {
8+
const button = target.querySelector('button');
9+
10+
flushSync(() => button?.click());
11+
assert.htmlEqual(
12+
target.innerHTML,
13+
`<button>increment</button><p>a: 2</p><p>b: 3</p><p>c: 4</p>`
14+
);
15+
}
16+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script>
2+
let offset = $state(1);
3+
4+
function* count(offset) {
5+
let i = offset;
6+
while (true) yield i++;
7+
}
8+
9+
let [a, b, c] = $derived(count(offset));
10+
</script>
11+
12+
<button onclick={() => offset += 1}>increment</button>
13+
14+
<p>a: {a}</p>
15+
<p>b: {b}</p>
16+
<p>c: {c}</p>

‎packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ let c = 3;
77
let d = 4;
88

99
export function update(array) {
10-
{
11-
let [$$1, $$2] = array;
10+
((array) => {
11+
var $$array = $.to_array(array, 2);
1212

13-
$.set(a, $$1, true);
14-
$.set(b, $$2, true);
15-
};
13+
$.set(a, $$array[0], true);
14+
$.set(b, $$array[1], true);
15+
})(array);
1616

1717
[c, d] = array;
1818
}

0 commit comments

Comments
 (0)
Please sign in to comment.