Skip to content
5 changes: 5 additions & 0 deletions .changeset/purple-dryers-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: handle derived destructured iterators
8 changes: 4 additions & 4 deletions packages/svelte/src/compiler/migrate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,15 +603,15 @@ const instance_script = {
);
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
// const tmp = state.scope.generate('tmp');
// const paths = extract_paths(declarator.id);
// const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
// b.declaration('const', tmp, visit(declarator.init!) as Expression)
// );
// for (const path of paths) {
// const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp));
// const value = path.expression;
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({
// local: name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
import * as b from '#compiler/builders';

/**
* @param {VariableDeclarator} node
Expand All @@ -18,7 +19,7 @@ export function VariableDeclarator(node, context) {
if (context.state.analysis.runes) {
const init = node.init;
const rune = get_rune(init, context.state.scope);
const paths = extract_paths(node.id);
const { paths } = extract_paths(node.id, b.id('dummy'));

for (const path of paths) {
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,21 @@ export function EachBlock(node, context) {
} else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;

for (const path of extract_paths(node.context)) {
const { inserts, paths } = extract_paths(node.context, unwrapped);

for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
child_state.transform[id.name] = { read: get_value };

const expression = /** @type {Expression} */ (context.visit(b.thunk(value), child_state));
declarations.push(b.var(id, b.call('$.derived', expression)));
}

for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const needs_derived = path.has_default_value; // to ensure that default value is only called once

const fn = b.thunk(
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
);
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));

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

Expand All @@ -249,7 +257,7 @@ export function EachBlock(node, context) {
child_state.transform[name] = {
read,
assign: (_, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
const left = /** @type {Pattern} */ (path.update_expression);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,21 @@ export function SnippetBlock(node, context) {
let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias));

const paths = extract_paths(argument);
const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));

for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
transform[id.name] = { read: get_value };

declarations.push(
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
);
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
/** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { build_pattern, extract_paths } from '../../../../utils/ast.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';

/**
* @param {VariableDeclaration} node
Expand Down Expand Up @@ -116,7 +117,7 @@ export function VariableDeclaration(node, context) {
}

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

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

if (declarator.id.type === 'Identifier') {
const expression = /** @type {Expression} */ (context.visit(value));

declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
);
} else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);

declarations.push(
b.declarator(pattern, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
([original, replacement]) => {
const binding = context.state.scope.get(original.name);
return b.declarator(
original,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, replacement)
: replacement
);
}
)
b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };

const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}

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

if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);

declarations.push(b.declarator(declarator.id, b.call('$.derived', expression)));
} else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const init = /** @type {CallExpression} */ (declarator.init);

/** @type {Identifier} */
let id;
let rhs = value;

if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id);

declarations.push(
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)))
);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);

declarations.push(b.declarator(id, b.call('$.derived', expression)));
}

for (let i = 0; i < replacements.size; i++) {
const [original, replacement] = [...replacements][i];
declarations.push(
b.declarator(
original,
b.call(
'$.derived',
b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)]))
)
)
);
const { inserts, paths } = extract_paths(declarator.id, rhs);

for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };

const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}

for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
}
}

continue;
}
}
Expand Down Expand Up @@ -229,20 +237,29 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);

declarations.push(
b.declarator(
b.id(tmp),
tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);

for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };

const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}

for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const value = path.expression?.(b.id(tmp));
const value = /** @type {Expression} */ (context.visit(path.expression));

declarations.push(
b.declarator(
path.node,
Expand Down Expand Up @@ -276,7 +293,7 @@ export function VariableDeclaration(node, context) {
declarations.push(
...create_state_declarators(
declarator,
context.state,
context,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
)
);
Expand All @@ -296,32 +313,41 @@ export function VariableDeclaration(node, context) {
/**
* Creates the output for a state declaration in legacy mode.
* @param {VariableDeclarator} declarator
* @param {ComponentClientTransformState} scope
* @param {ComponentContext} context
* @param {Expression} value
*/
function create_state_declarators(declarator, { scope, analysis }, value) {
function create_state_declarators(declarator, context, value) {
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
}

const [pattern, replacements] = build_pattern(declarator.id, scope);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);

return [
b.declarator(pattern, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
([original, replacement]) => {
const binding = scope.get(original.name);
return b.declarator(
original,
binding?.kind === 'state'
? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined)
: replacement
);
}
)
b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };

const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);

return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value
);
})
];
}
Loading