diff --git a/.changeset/easy-paths-take.md b/.changeset/easy-paths-take.md new file mode 100644 index 0000000000..1378322abe --- /dev/null +++ b/.changeset/easy-paths-take.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take into account static blocks when determining transition locality diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md new file mode 100644 index 0000000000..f2b1ba6f25 --- /dev/null +++ b/.changeset/huge-poets-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: coordinate mount of snippets with await expressions diff --git a/.changeset/odd-plants-lead.md b/.changeset/odd-plants-lead.md new file mode 100644 index 0000000000..1df2236c2a --- /dev/null +++ b/.changeset/odd-plants-lead.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better optimization of await expressions diff --git a/.changeset/slimy-turtles-yell.md b/.changeset/slimy-turtles-yell.md new file mode 100644 index 0000000000..e3f3a66264 --- /dev/null +++ b/.changeset/slimy-turtles-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush pending changes after rendering `failed` snippet diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 52be997374..b4c704c34d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -6,7 +6,7 @@ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers } from '../../utils/ast.js'; +import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index f5132c1cf8..92653ed73c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -12,7 +12,7 @@ import { import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; -import { has_await } from '../../../../../utils/ast.js'; +import { has_await_expression } from '../../../../../utils/ast.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ export const block_open = b.literal(BLOCK_OPEN); @@ -315,7 +315,7 @@ export class PromiseOptimiser { const promises = b.array( this.expressions.map((expression) => { - return expression.type === 'AwaitExpression' && !has_await(expression.argument) + return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument) ? expression.argument : b.call(b.thunk(expression, true)); }) diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 541921befb..bd92dda5d9 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -611,16 +611,20 @@ export function build_assignment_value(operator, left, right) { } /** - * @param {ESTree.Expression} expression + * @param {ESTree.Node} node */ -export function has_await(expression) { +export function has_await_expression(node) { let has_await = false; - walk(expression, null, { + walk(node, null, { AwaitExpression(_node, context) { has_await = true; context.stop(); - } + }, + // don't traverse into these + FunctionDeclaration() {}, + FunctionExpression() {}, + ArrowFunctionExpression() {} }); return has_await; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 99306ce4d9..f21b0dc8b4 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -2,7 +2,7 @@ import { walk } from 'zimmerframe'; import { regex_is_valid_identifier } from '../phases/patterns.js'; import { sanitize_template_string } from './sanitize_template_string.js'; -import { has_await } from './ast.js'; +import { has_await_expression } from './ast.js'; /** * @param {Array} elements @@ -451,7 +451,7 @@ export function thunk(expression, async = false) { export function unthunk(expression) { // optimize `async () => await x()`, but not `async () => await x(await y)` if (expression.async && expression.body.type === 'AwaitExpression') { - if (!has_await(expression.body.argument)) { + if (!has_await_expression(expression.body.argument)) { return unthunk(arrow(expression.params, expression.body.argument)); } } diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 0fb4a44389..24dc9e4fb8 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -14,7 +14,10 @@ export const DESTROYED = 1 << 14; // Flags exclusive to effects export const EFFECT_RAN = 1 << 15; -/** 'Transparent' effects do not create a transition boundary */ +/** + * 'Transparent' effects do not create a transition boundary. + * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned + */ export const EFFECT_TRANSPARENT = 1 << 16; export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3da9204571..febbc00898 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,7 +30,6 @@ import { skip_nodes, set_hydrate_node } from '../hydration.js'; -import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; @@ -39,6 +38,7 @@ import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; +import { create_text } from '../operations.js'; /** * @typedef {{ @@ -93,6 +93,9 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; + /** @type {TemplateNode | null} */ + #pending_anchor = null; + #local_pending_count = 0; #pending_count = 0; @@ -156,8 +159,10 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { + var anchor = this.#get_anchor(); + try { - this.#main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(anchor)); } catch (error) { this.error(error); } @@ -168,6 +173,10 @@ export class Boundary { this.#pending = false; } } + + return () => { + this.#pending_anchor?.remove(); + }; }, flags); if (hydrating) { @@ -195,9 +204,11 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { + var anchor = this.#get_anchor(); + this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => this.#children(anchor)); }); if (this.#pending_count > 0) { @@ -212,6 +223,19 @@ export class Boundary { }); } + #get_anchor() { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + + return anchor; + } + /** * Returns `true` if the effect exists inside a boundary whose pending snippet is shown * @returns {boolean} @@ -253,6 +277,7 @@ export class Boundary { if (this.#main_effect !== null) { this.#offscreen_fragment = document.createDocumentFragment(); + this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); move_effect(this.#main_effect, this.#offscreen_fragment); } @@ -402,6 +427,7 @@ export class Boundary { if (failed) { queue_micro_task(() => { this.#failed_effect = this.#run(() => { + Batch.ensure(); this.#is_creating_fallback = true; try { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index bd262258dc..4235e9cb24 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -149,6 +149,9 @@ function create_effect(type, fn, sync, push = true) { (e.f & EFFECT_PRESERVED) === 0 ) { e = e.first; + if ((type & BLOCK_EFFECT) !== 0 && (type & EFFECT_TRANSPARENT) !== 0 && e !== null) { + e.f |= EFFECT_TRANSPARENT; + } } if (e !== null) { @@ -604,7 +607,12 @@ export function pause_children(effect, transitions, local) { while (child !== null) { var sibling = child.next; - var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0; + var transparent = + (child.f & EFFECT_TRANSPARENT) !== 0 || + // If this is a branch effect without a block effect parent, + // it means the parent block effect was pruned. In that case, + // transparency information was transferred to the branch effect. + ((child.f & BRANCH_EFFECT) !== 0 && (effect.f & BLOCK_EFFECT) !== 0); // TODO we don't need to call pause_children recursively with a linked list in place // it's slightly more involved though as we have to account for `transparent` changing // through the tree. diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte new file mode 100644 index 0000000000..7085219a5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte @@ -0,0 +1,7 @@ + + +

message: {message}

+{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js new file mode 100644 index 0000000000..b6ca2ae3d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

loading...

`); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

message: hello from child

+

hello from parent

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte new file mode 100644 index 0000000000..3ad2c9572a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte @@ -0,0 +1,21 @@ + + + + + + +

{await push('hello from parent')}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js new file mode 100644 index 0000000000..7a6a66eb66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + btn?.click(); + await tick(); + + assert.deepEqual(logs, ['attachment']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte new file mode 100644 index 0000000000..c1fe20d931 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte @@ -0,0 +1,20 @@ + + + + {fail ? error() : 'all good'} + + + {#snippet failed()} +
oops!
+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js new file mode 100644 index 0000000000..900d6daff8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` + +
Should not transition out
+ ` + ); + + btn?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte new file mode 100644 index 0000000000..84f6ee77af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/transition-if-nested-static/main.svelte @@ -0,0 +1,18 @@ + + + + + +{#if showText} + {#if show} +
+ Should not transition out +
+ {/if} +{/if}