diff --git a/.changeset/rich-elephants-relax.md b/.changeset/rich-elephants-relax.md new file mode 100644 index 0000000000..dbf1c88282 --- /dev/null +++ b/.changeset/rich-elephants-relax.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: better destructuring assignments diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 6636b82a20..4365870bf7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -11,7 +11,13 @@ import { visit_assignment_expression } from '../../shared/assignments.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - return visit_assignment_expression(node, context, build_assignment); + const expression = /** @type {Expression} */ ( + visit_assignment_expression(node, context, build_assignment) ?? context.next() + ); + + return is_ignored(node, 'ownership_invalid_mutation') + ? b.call('$.skip_ownership_validation', b.thunk(expression)) + : expression; } /** @@ -109,19 +115,17 @@ function build_assignment(operator, left, right, context) { return transform.assign(object, value); } - /** @type {Expression} */ - let mutation = b.assignment( - operator, - /** @type {Pattern} */ (context.visit(left)), - /** @type {Expression} */ (context.visit(right)) - ); - // mutation if (transform?.mutate) { - mutation = transform.mutate(object, mutation); + return transform.mutate( + object, + b.assignment( + operator, + /** @type {Pattern} */ (context.visit(left)), + /** @type {Expression} */ (context.visit(right)) + ) + ); } - return is_ignored(left, 'ownership_invalid_mutation') - ? b.call('$.skip_ownership_validation', b.thunk(mutation)) - : mutation; + return null; } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index e92d54c3b7..2988c590fd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -10,7 +10,7 @@ import { visit_assignment_expression } from '../../shared/assignments.js'; * @param {Context} context */ export function AssignmentExpression(node, context) { - return visit_assignment_expression(node, context, build_assignment); + return visit_assignment_expression(node, context, build_assignment) ?? context.next(); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js index ce7595a06e..e8e02b8f58 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -41,7 +41,7 @@ export function visit_assignment_expression(node, context, build_assignment) { if (!changed) { // No change to output -> nothing to transform -> we can keep the original assignment - return context.next(); + return null; } const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement'); @@ -70,8 +70,5 @@ export function visit_assignment_expression(node, context, build_assignment) { throw new Error(`Unexpected assignment type ${node.left.type}`); } - return ( - build_assignment(node.operator, node.left, node.right, context) ?? - /** @type {Expression} */ (context.next()) - ); + return build_assignment(node.operator, node.left, node.right, context); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 75896ccc07..ec16be69f6 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -571,53 +571,6 @@ export function schedule_effect(signal) { current_queued_root_effects.push(effect); } -/** - * @param {Effect} effect - * @param {Effect[]} effects - */ -function process_effect_children(effect, effects) { - var current = effect.first; - - while (current !== null) { - var next = current.next; - process_effect(current, effects); - current = next; - } -} - -/** - * @param {Effect} effect - * @param {Effect[]} effects - */ -function process_effect(effect, effects) { - var flags = effect.f; - // TODO: we probably don't need to check for destroyed as it shouldn't be encountered? - var is_active = (flags & (DESTROYED | INERT)) === 0; - var is_branch = (flags & BRANCH_EFFECT) !== 0; - var is_clean = (flags & CLEAN) !== 0; - - // Skip this branch if it's clean - if (is_active && (!is_branch || !is_clean)) { - if (is_branch) { - set_signal_status(effect, CLEAN); - } - - if ((flags & RENDER_EFFECT) !== 0) { - if (!is_branch && check_dirtiness(effect)) { - update_effect(effect); - } - - process_effect_children(effect, effects); - } else if ((flags & EFFECT) !== 0) { - if (is_branch || is_clean) { - process_effect_children(effect, effects); - } else { - effects.push(effect); - } - } - } -} - /** * * This function both runs render effects and collects user effects in topological order @@ -630,13 +583,70 @@ function process_effect(effect, effects) { * @returns {void} */ function process_effects(effect, collected_effects) { - /** @type {Effect[]} */ + var current_effect = effect.first; var effects = []; - process_effect_children(effect, effects); + + main_loop: while (current_effect !== null) { + var flags = current_effect.f; + // TODO: we probably don't need to check for destroyed as it shouldn't be encountered? + var is_active = (flags & (DESTROYED | INERT)) === 0; + var is_branch = (flags & BRANCH_EFFECT) !== 0; + var is_clean = (flags & CLEAN) !== 0; + var child = current_effect.first; + + // Skip this branch if it's clean + if (is_active && (!is_branch || !is_clean)) { + if (is_branch) { + set_signal_status(current_effect, CLEAN); + } + + if ((flags & RENDER_EFFECT) !== 0) { + if (!is_branch && check_dirtiness(current_effect)) { + update_effect(current_effect); + // Child might have been mutated since running the effect + child = current_effect.first; + } + + if (child !== null) { + current_effect = child; + continue; + } + } else if ((flags & EFFECT) !== 0) { + if (is_branch || is_clean) { + if (child !== null) { + current_effect = child; + continue; + } + } else { + effects.push(current_effect); + } + } + } + var sibling = current_effect.next; + + if (sibling === null) { + let parent = current_effect.parent; + + while (parent !== null) { + if (effect === parent) { + break main_loop; + } + var parent_sibling = parent.next; + if (parent_sibling !== null) { + current_effect = parent_sibling; + continue main_loop; + } + parent = parent.parent; + } + } + + current_effect = sibling; + } + // We might be dealing with many effects here, far more than can be spread into // an array push call (callstack overflow). So let's deal with each effect in a loop. for (var i = 0; i < effects.length; i++) { - var child = effects[i]; + child = effects[i]; collected_effects.push(child); process_effects(child, collected_effects); } diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 9400b52718..a10f4ca1a5 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -3,10 +3,14 @@ import * as $ from "svelte/internal/client"; let a = $.source(1); let b = $.source(2); +let c = 3; +let d = 4; export function update(array) { ( $.set(a, $.proxy(array[0])), $.set(b, $.proxy(array[1])) ); + + [c, d] = array; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js index 846ed48458..2797d4312b 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js @@ -3,7 +3,10 @@ import * as $ from "svelte/internal/server"; let a = 1; let b = 2; +let c = 3; +let d = 4; export function update(array) { [a, b] = array; + [c, d] = array; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js index 9c0da7558a..558cc66cc2 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js @@ -1,6 +1,9 @@ let a = $state(1); let b = $state(2); +let c = 3; +let d = 4; export function update(array) { [a, b] = array; + [c, d] = array; } diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md index 73e966af38..e4fdcc86b3 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/03-deprecations.md @@ -82,6 +82,8 @@ When authoring custom elements, use the new [host rune](/docs/runes#$host) to di >greet ``` +Note that using `$props` and `$host` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly. + ## `` in runes mode In previous versions of Svelte, the component constructor was fixed when the component was rendered. In other words, if you wanted `` to re-render when `X` changed, you would either have to use `` or put the component inside a `{#key X}...{/key}` block. @@ -112,8 +114,6 @@ A derived value may be used in other contexts: + ``` -Note that using `$props` and `$host` will put you in [runes mode](/docs/runes) — be sure to update your props and state accordingly. - ## `immutable` The `immutable` compiler option is deprecated. Use runes mode instead, where all state is immutable (which means that assigning to `object.property` won't cause updates for anything that is observing `object` itself, or a different property of it).