diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md new file mode 100644 index 000000000000..5364ff60df3f --- /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 0701c37c4854..3a45389dd73d 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 a519057cb691..177ec62416dd 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 66d337183637..13926ccc4b83 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 b1165a6e7aee..416627a1572a 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 74a90a8600f5..c0dbdbda14f6 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 70d5c5d0724f..ba13d2c61143 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 000000000000..d821bb6fa0e9 --- /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 000000000000..d725d5f03b59 --- /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 000000000000..6fdf41b4340c --- /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 000000000000..7f2348937394 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + + + + + + +