diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md new file mode 100644 index 0000000000..bddad21bff --- /dev/null +++ b/.changeset/legal-mangos-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: change title only after any pending work has completed diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2a2cd28698..bcf17fe45e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.43.2 + +### Patch Changes + +- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077)) + ## 5.43.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a7e0c618bf..f7a1cca616 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.43.1", + "version": "5.43.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 98d7880b25..edd8835e00 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -1,16 +1,19 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_template_chunk } from './shared/utils.js'; +import { build_template_chunk, Memoizer } from './shared/utils.js'; /** * @param {AST.TitleElement} node * @param {ComponentContext} context */ export function TitleElement(node, context) { + const memoizer = new Memoizer(); const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context + context, + context.state, + (value, metadata) => memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); @@ -26,9 +29,21 @@ export function TitleElement(node, context) { ) ); + // Always in an $effect so it only changes the title once async work is done if (has_state) { - context.state.update.push(statement); + context.state.after_update.push( + b.stmt( + b.call( + '$.template_effect', + b.arrow(memoizer.apply(), b.block([statement])), + memoizer.sync_values(), + memoizer.async_values(), + memoizer.blockers(), + b.true + ) + ) + ); } else { - context.state.init.push(statement); + context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement]))))); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 3588f2843a..c7f843af48 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) { is_element && // In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled // TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally) - !(node.body.metadata.has_await || node.metadata.expression.has_await) + !(node.body.metadata.has_await || node.metadata.expression.is_async()) ) { node.metadata.is_controlled = true; } else { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9d61b6bbf9..27c90d7708 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,33 +14,25 @@ import { MAYBE_DIRTY, DERIVED, BOUNDARY_EFFECT, - EAGER_EFFECT + EAGER_EFFECT, + HEAD_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, get, - increment_write_version, is_dirty, is_updating_effect, set_is_updating_effect, set_signal_status, - tick, update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { - flush_eager_effects, - eager_effects, - old_values, - set_eager_effects, - source, - update -} from './sources.js'; +import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; /** @@ -800,7 +792,12 @@ export function schedule_effect(signal) { // if the effect is being scheduled because a parent (each/await/etc) block // updated an internal source, bail out or we'll cause a second flush - if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) { + if ( + is_flushing && + effect === active_effect && + (flags & BLOCK_EFFECT) !== 0 && + (flags & HEAD_EFFECT) === 0 + ) { return; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 4a9fce7286..8c4b84438c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -366,10 +366,11 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Array>} blockers + * @param {boolean} defer */ -export function template_effect(fn, sync = [], async = [], blockers = []) { +export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true); }); } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4c0a8f05c8..0a28702778 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.43.1'; +export const VERSION = '5.43.2'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte new file mode 100644 index 0000000000..089ba43607 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte @@ -0,0 +1,15 @@ + + + + title + + +

{await push()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte new file mode 100644 index 0000000000..b2a8656276 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte @@ -0,0 +1,13 @@ + + + + {await push()} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js new file mode 100644 index 0000000000..b89dce62d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if}