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