Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-emus-rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: handle `<svelte:head>` rendered asynchronously
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)))
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
)
)
);
}
29 changes: 10 additions & 19 deletions packages/svelte/src/internal/client/dom/blocks/svelte-head.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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));
}
Expand All @@ -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);
}
}

Expand All @@ -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));
}
}
Expand Down
10 changes: 1 addition & 9 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -152,7 +145,6 @@ export function hydrate(component, options) {
} finally {
set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node);
reset_head_anchor();
}
}

Expand Down
7 changes: 4 additions & 3 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,16 @@ export function render(component, options = {}) {
}

/**
* @param {string} hash
* @param {Renderer} renderer
* @param {(renderer: Renderer) => Promise<void> | void} fn
* @returns {void}
*/
export function head(renderer, fn) {
export function head(hash, renderer, fn) {
renderer.head((renderer) => {
renderer.push(BLOCK_OPEN);
renderer.push(`<!--${hash}-->`);
renderer.child(fn);
renderer.push(BLOCK_CLOSE);
renderer.push(EMPTY_COMMENT);
});
}

Expand Down
6 changes: 5 additions & 1 deletion packages/svelte/tests/hydration/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@
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));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
let { name, content } = $props();
</script>

<svelte:head>
<meta name={name} content={content} />
</svelte:head>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
let { name, content } = $props();
</script>

<svelte:head>
<meta name="{name}-1" content="{content}-1" />
<meta name="{name}-2" content="{content}-2" />
</svelte:head>
23 changes: 23 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/async-head/_config.js
Original file line number Diff line number Diff line change
@@ -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`);
}
}
});
11 changes: 11 additions & 0 deletions packages/svelte/tests/runtime-runes/samples/async-head/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import A from './A.svelte';
import B from './B.svelte';
</script>

<A name="a-1" content={await 1} />
<A name="a-2" content={await 2} />
<B name="b-1" content={1} />
<A name="a-3" content={await 3} />
<B name="b-2" content={2} />
<B name="b-3" content={3} />
Loading