From cc0143c904ec48dcce1eac2600b5a88ca5df0d17 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 02:39:43 +0100 Subject: [PATCH 1/3] fix: handle `` rendered asynchronously (#17052) * fix: handle `` rendered asynchronously * fix tests --- .changeset/khaki-emus-rest.md | 5 ++++ .../3-transform/client/visitors/SvelteHead.js | 3 ++ .../3-transform/server/visitors/SvelteHead.js | 11 ++++++- .../internal/client/dom/blocks/svelte-head.js | 29 +++++++------------ packages/svelte/src/internal/client/render.js | 10 +------ packages/svelte/src/internal/server/index.js | 7 +++-- packages/svelte/tests/hydration/test.ts | 6 +++- .../runtime-runes/samples/async-head/A.svelte | 7 +++++ .../runtime-runes/samples/async-head/B.svelte | 8 +++++ .../samples/async-head/_config.js | 23 +++++++++++++++ .../samples/async-head/main.svelte | 11 +++++++ 11 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 .changeset/khaki-emus-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/main.svelte diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md new file mode 100644 index 0000000000..5364ff60df --- /dev/null +++ b/.changeset/khaki-emus-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle `` rendered asynchronously diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js index 0701c37c48..3a45389dd7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -13,6 +15,7 @@ export function SvelteHead(node, context) { b.stmt( b.call( '$.head', + b.literal(hash(filename)), b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js index a519057cb6..177ec62416 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -11,6 +13,13 @@ export function SvelteHead(node, context) { const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); context.state.template.push( - b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block))) + b.stmt( + b.call( + '$.head', + b.literal(hash(filename)), + b.id('$$renderer'), + b.arrow([b.id('$$renderer')], block) + ) + ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 66d3371836..13926ccc4b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; -import { HYDRATION_START } from '../../../../constants.js'; - -/** - * @type {Node | undefined} - */ -let head_anchor; - -export function reset_head_anchor() { - head_anchor = undefined; -} /** + * @param {string} hash * @param {(anchor: Node) => void} render_fn * @returns {void} */ -export function head(render_fn) { +export function head(hash, render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. let previous_hydrate_node = null; @@ -30,15 +21,13 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_node = hydrate_node; - // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. - if (head_anchor === undefined) { - head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); - } + var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); + // There might be multiple head blocks in our app, and they could have been + // rendered in an arbitrary order — find one corresponding to this component while ( head_anchor !== null && - (head_anchor.nodeType !== COMMENT_NODE || - /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) + (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash) ) { head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); } @@ -48,7 +37,10 @@ export function head(render_fn) { if (head_anchor === null) { set_hydrating(false); } else { - head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor))); + var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor.remove(); // in case this component is repeated + + set_hydrate_node(start); } } @@ -61,7 +53,6 @@ export function head(render_fn) { } finally { if (was_hydrating) { set_hydrating(true); - head_anchor = hydrate_node; // so that next head block starts from the correct node set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b1165a6e7a..416627a157 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -12,20 +12,13 @@ import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants import { active_effect } from './runtime.js'; import { push, pop, component_context } from './context.js'; import { component_root } from './reactivity/effects.js'; -import { - hydrate_next, - hydrate_node, - hydrating, - set_hydrate_node, - set_hydrating -} from './dom/hydration.js'; +import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; import { all_registered_events, handle_event_propagation, root_event_handles } from './dom/elements/events.js'; -import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; @@ -152,7 +145,6 @@ export function hydrate(component, options) { } finally { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); - reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 74a90a8600..c0dbdbda14 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -64,15 +64,16 @@ export function render(component, options = {}) { } /** + * @param {string} hash * @param {Renderer} renderer * @param {(renderer: Renderer) => Promise | void} fn * @returns {void} */ -export function head(renderer, fn) { +export function head(hash, renderer, fn) { renderer.head((renderer) => { - renderer.push(BLOCK_OPEN); + renderer.push(``); renderer.child(fn); - renderer.push(BLOCK_CLOSE); + renderer.push(EMPTY_COMMENT); }); } diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d072..ba13d2c611 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -132,7 +132,11 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); const normalize = (string: string) => - string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); + string + .trim() + .replaceAll('\r\n', '\n') + .replaceAll('/>', '>') + .replace(//g, ''); const expected = read(`${cwd}/_expected.html`) ?? rendered.html; assert.equal(normalize(target.innerHTML), normalize(expected)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte new file mode 100644 index 0000000000..d821bb6fa0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte new file mode 100644 index 0000000000..d725d5f03b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js new file mode 100644 index 0000000000..6fdf41b434 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, window }) { + await tick(); + + const head = window.document.head; + + // we don't care about the order, but we want to ensure that the + // elements didn't clobber each other + for (let n of ['1', '2', '3']) { + const a = head.querySelector(`meta[name="a-${n}"]`); + assert.equal(a?.getAttribute('content'), n); + + const b1 = head.querySelector(`meta[name="b-${n}-1"]`); + assert.equal(b1?.getAttribute('content'), `${n}-1`); + + const b2 = head.querySelector(`meta[name="b-${n}-2"]`); + assert.equal(b2?.getAttribute('content'), `${n}-2`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte new file mode 100644 index 0000000000..7f23489373 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + + + + + + + From d2f453f8b099ee46eb5835fc2af2952bda0e2fe6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:40:13 +0100 Subject: [PATCH 2/3] fix: don't restore batch in `#await` (#17051) #16977 had one slight regression which might contribute to #16990: The batch from earlier was restored, but that doesn't make sense in this situations since this has nothing to do with our new async logic of batches suspending until pending work is done. As a result you could end up with a batch being created, and then the restore then instead reverting to an earlier batch that was already done, which means a ghost-batch ends up in the set of batches, subsequently triggering time traveling when it shouldn't. This may help with #16990 No test because basically impossible to do so --- .changeset/shaky-jars-cut.md | 5 +++++ packages/svelte/src/internal/client/dom/blocks/await.js | 8 ++++++-- packages/svelte/src/internal/client/reactivity/async.js | 5 ++--- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md new file mode 100644 index 0000000000..b74b00fa1c --- /dev/null +++ b/.changeset/shaky-jars-cut.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't restore batch in `#await` diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index bac01e4c33..87d64df23e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -12,7 +12,7 @@ import { import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { is_runes } from '../../context.js'; -import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; import { capture, unset_context } from '../../reactivity/async.js'; @@ -69,7 +69,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { if (destroyed) return; resolved = true; - restore(); + // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic + // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. + restore(false); + // Make sure we have a batch, since the branch manager expects one to exist + Batch.ensure(); if (hydrating) { // `restore()` could set `hydrating` to `true`, which we very much diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fb836df989..bdd7eed940 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -33,7 +33,6 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; -import { create_text } from '../dom/operations.js'; /** * @@ -102,11 +101,11 @@ export function capture() { var previous_dev_stack = dev_stack; } - return function restore() { + return function restore(activate_batch = true) { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); - previous_batch?.activate(); + if (activate_batch) previous_batch?.activate(); if (was_hydrating) { set_hydrating(true); From 1b2f7b068e01f0407f4013a292ec9f3f3381233e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:43:59 +0100 Subject: [PATCH 3/3] Version Packages (#17053) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/khaki-emus-rest.md | 5 ----- .changeset/shaky-jars-cut.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/khaki-emus-rest.md delete mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md deleted file mode 100644 index 5364ff60df..0000000000 --- a/.changeset/khaki-emus-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle `` rendered asynchronously diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md deleted file mode 100644 index b74b00fa1c..0000000000 --- a/.changeset/shaky-jars-cut.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't restore batch in `#await` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1d58806694..8dabe54b33 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.42.3 + +### Patch Changes + +- fix: handle `` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052)) + +- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051)) + ## 5.42.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9fb7189d07..1ee6c50121 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.42.2", + "version": "5.42.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 605e1d9cdc..999aacc998 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.42.2'; +export const VERSION = '5.42.3'; export const PUBLIC_VERSION = '5';