diff --git a/.changeset/nasty-yaks-peel.md b/.changeset/nasty-yaks-peel.md new file mode 100644 index 0000000000..267e4458a5 --- /dev/null +++ b/.changeset/nasty-yaks-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support async/await in destructuring assignments diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index ca652801a7..2ecec2eeb3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -115,6 +115,103 @@ export function serialize_get_binding(node, state) { return node; } +/** + * @param {import('estree').Expression | import('estree').Pattern} expression + * @returns {boolean} + */ +function is_expression_async(expression) { + switch (expression.type) { + case 'AwaitExpression': { + return true; + } + case 'ArrayPattern': { + return expression.elements.some((element) => element && is_expression_async(element)); + } + case 'ArrayExpression': { + return expression.elements.some((element) => { + if (!element) { + return false; + } else if (element.type === 'SpreadElement') { + return is_expression_async(element.argument); + } else { + return is_expression_async(element); + } + }); + } + case 'AssignmentPattern': + case 'AssignmentExpression': + case 'BinaryExpression': + case 'LogicalExpression': { + return is_expression_async(expression.left) || is_expression_async(expression.right); + } + case 'CallExpression': + case 'NewExpression': { + return ( + (expression.callee.type !== 'Super' && is_expression_async(expression.callee)) || + expression.arguments.some((element) => { + if (element.type === 'SpreadElement') { + return is_expression_async(element.argument); + } else { + return is_expression_async(element); + } + }) + ); + } + case 'ChainExpression': { + return is_expression_async(expression.expression); + } + case 'ConditionalExpression': { + return ( + is_expression_async(expression.test) || + is_expression_async(expression.alternate) || + is_expression_async(expression.consequent) + ); + } + case 'ImportExpression': { + return is_expression_async(expression.source); + } + case 'MemberExpression': { + return ( + (expression.object.type !== 'Super' && is_expression_async(expression.object)) || + (expression.property.type !== 'PrivateIdentifier' && + is_expression_async(expression.property)) + ); + } + case 'ObjectPattern': + case 'ObjectExpression': { + return expression.properties.some((property) => { + if (property.type === 'SpreadElement') { + return is_expression_async(property.argument); + } else if (property.type === 'Property') { + return ( + (property.key.type !== 'PrivateIdentifier' && is_expression_async(property.key)) || + is_expression_async(property.value) + ); + } + }); + } + case 'RestElement': { + return is_expression_async(expression.argument); + } + case 'SequenceExpression': + case 'TemplateLiteral': { + return expression.expressions.some((subexpression) => is_expression_async(subexpression)); + } + case 'TaggedTemplateExpression': { + return is_expression_async(expression.tag) || is_expression_async(expression.quasi); + } + case 'UnaryExpression': + case 'UpdateExpression': { + return is_expression_async(expression.argument); + } + case 'YieldExpression': { + return expression.argument ? is_expression_async(expression.argument) : false; + } + default: + return false; + } +} + /** * @template {import('./types').ClientTransformState} State * @param {import('estree').AssignmentExpression} node @@ -153,17 +250,28 @@ export function serialize_set_binding(node, context, fallback) { return fallback(); } - return b.call( - b.thunk( - b.block([ - b.const(tmp_id, /** @type {import('estree').Expression} */ (visit(node.right))), - b.stmt(b.sequence(assignments)), - // return because it could be used in a nested expression where the value is needed. - // example: { foo: ({ bar } = { bar: 1 })} - b.return(b.id(tmp_id)) - ]) - ) + const rhs_expression = /** @type {import('estree').Expression} */ (visit(node.right)); + + const iife_is_async = + is_expression_async(rhs_expression) || + assignments.some((assignment) => is_expression_async(assignment)); + + const iife = b.arrow( + [], + b.block([ + b.const(tmp_id, rhs_expression), + b.stmt(b.sequence(assignments)), + // return because it could be used in a nested expression where the value is needed. + // example: { foo: ({ bar } = { bar: 1 })} + b.return(b.id(tmp_id)) + ]) ); + + if (iife_is_async) { + return b.await(b.call(b.async(iife))); + } else { + return b.call(iife); + } } if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') { diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index f0907f293f..1595f79879 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -44,6 +44,23 @@ export function assignment(operator, left, right) { return { type: 'AssignmentExpression', operator, left, right }; } +/** + * @template T + * @param {T & import('estree').BaseFunction} func + * @returns {T & import('estree').BaseFunction} + */ +export function async(func) { + return { ...func, async: true }; +} + +/** + * @param {import('estree').Expression} argument + * @returns {import('estree').AwaitExpression} + */ +export function await_builder(argument) { + return { type: 'AwaitExpression', argument }; +} + /** * @param {import('estree').BinaryOperator} operator * @param {import('estree').Expression} left @@ -573,6 +590,7 @@ export function throw_error(str) { } export { + await_builder as await, new_builder as new, let_builder as let, const_builder as const, diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/_config.js b/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/_config.js new file mode 100644 index 0000000000..e7d3c80cfd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/_config.js @@ -0,0 +1,75 @@ +import { test } from '../../test'; + +export default test({ + html: ` + +

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+

0

+ `, + + async test({ assert, target, window }) { + const btn = target.querySelector('button'); + const clickEvent = new window.Event('click', { bubbles: true }); + await btn?.dispatchEvent(clickEvent); + for (let i = 1; i <= 42; i += 1) { + await Promise.resolve(); + } + + assert.htmlEqual( + target.innerHTML, + ` + +

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/main.svelte b/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/main.svelte new file mode 100644 index 0000000000..2d2a6538d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/destructure-async-assignments/main.svelte @@ -0,0 +1,89 @@ + + + +

{a}

+

{b}

+

{c}

+

{d}

+

{e}

+

{f}

+

{g}

+

{h}

+

{i}

+

{j}

+

{k}

+

{l}

+

{m}

+

{n}

+

{o}

+

{p}

+

{q}

+

{r}

+

{s}

+

{t}

+

{u}

+

{v}

+

{w}

+

{x}

+

{y}

+

{z}

\ No newline at end of file