Skip to content

Commit cc0143c

Browse files
authored
fix: handle <svelte:head> rendered asynchronously (#17052)
* fix: handle `<svelte:head>` rendered asynchronously * fix tests
1 parent da00abe commit cc0143c

File tree

11 files changed

+87
-33
lines changed

11 files changed

+87
-33
lines changed

.changeset/khaki-emus-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle `<svelte:head>` rendered asynchronously

packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types' */
44
import * as b from '#compiler/builders';
5+
import { hash } from '../../../../../utils.js';
6+
import { filename } from '../../../../state.js';
57

68
/**
79
* @param {AST.SvelteHead} node
@@ -13,6 +15,7 @@ export function SvelteHead(node, context) {
1315
b.stmt(
1416
b.call(
1517
'$.head',
18+
b.literal(hash(filename)),
1619
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
1720
)
1821
)

packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
/** @import { AST } from '#compiler' */
33
/** @import { ComponentContext } from '../types.js' */
44
import * as b from '#compiler/builders';
5+
import { hash } from '../../../../../utils.js';
6+
import { filename } from '../../../../state.js';
57

68
/**
79
* @param {AST.SvelteHead} node
@@ -11,6 +13,13 @@ export function SvelteHead(node, context) {
1113
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
1214

1315
context.state.template.push(
14-
b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block)))
16+
b.stmt(
17+
b.call(
18+
'$.head',
19+
b.literal(hash(filename)),
20+
b.id('$$renderer'),
21+
b.arrow([b.id('$$renderer')], block)
22+
)
23+
)
1524
);
1625
}

packages/svelte/src/internal/client/dom/blocks/svelte-head.js

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd
33
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
44
import { block } from '../../reactivity/effects.js';
55
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
6-
import { HYDRATION_START } from '../../../../constants.js';
7-
8-
/**
9-
* @type {Node | undefined}
10-
*/
11-
let head_anchor;
12-
13-
export function reset_head_anchor() {
14-
head_anchor = undefined;
15-
}
166

177
/**
8+
* @param {string} hash
189
* @param {(anchor: Node) => void} render_fn
1910
* @returns {void}
2011
*/
21-
export function head(render_fn) {
12+
export function head(hash, render_fn) {
2213
// The head function may be called after the first hydration pass and ssr comment nodes may still be present,
2314
// therefore we need to skip that when we detect that we're not in hydration mode.
2415
let previous_hydrate_node = null;
@@ -30,15 +21,13 @@ export function head(render_fn) {
3021
if (hydrating) {
3122
previous_hydrate_node = hydrate_node;
3223

33-
// There might be multiple head blocks in our app, so we need to account for each one needing independent hydration.
34-
if (head_anchor === undefined) {
35-
head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
36-
}
24+
var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
3725

26+
// There might be multiple head blocks in our app, and they could have been
27+
// rendered in an arbitrary order — find one corresponding to this component
3828
while (
3929
head_anchor !== null &&
40-
(head_anchor.nodeType !== COMMENT_NODE ||
41-
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
30+
(head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
4231
) {
4332
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
4433
}
@@ -48,7 +37,10 @@ export function head(render_fn) {
4837
if (head_anchor === null) {
4938
set_hydrating(false);
5039
} else {
51-
head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor)));
40+
var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
41+
head_anchor.remove(); // in case this component is repeated
42+
43+
set_hydrate_node(start);
5244
}
5345
}
5446

@@ -61,7 +53,6 @@ export function head(render_fn) {
6153
} finally {
6254
if (was_hydrating) {
6355
set_hydrating(true);
64-
head_anchor = hydrate_node; // so that next head block starts from the correct node
6556
set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node));
6657
}
6758
}

packages/svelte/src/internal/client/render.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,13 @@ import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants
1212
import { active_effect } from './runtime.js';
1313
import { push, pop, component_context } from './context.js';
1414
import { component_root } from './reactivity/effects.js';
15-
import {
16-
hydrate_next,
17-
hydrate_node,
18-
hydrating,
19-
set_hydrate_node,
20-
set_hydrating
21-
} from './dom/hydration.js';
15+
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js';
2216
import { array_from } from '../shared/utils.js';
2317
import {
2418
all_registered_events,
2519
handle_event_propagation,
2620
root_event_handles
2721
} from './dom/elements/events.js';
28-
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
2922
import * as w from './warnings.js';
3023
import * as e from './errors.js';
3124
import { assign_nodes } from './dom/template.js';
@@ -152,7 +145,6 @@ export function hydrate(component, options) {
152145
} finally {
153146
set_hydrating(was_hydrating);
154147
set_hydrate_node(previous_hydrate_node);
155-
reset_head_anchor();
156148
}
157149
}
158150

packages/svelte/src/internal/server/index.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,16 @@ export function render(component, options = {}) {
6464
}
6565

6666
/**
67+
* @param {string} hash
6768
* @param {Renderer} renderer
6869
* @param {(renderer: Renderer) => Promise<void> | void} fn
6970
* @returns {void}
7071
*/
71-
export function head(renderer, fn) {
72+
export function head(hash, renderer, fn) {
7273
renderer.head((renderer) => {
73-
renderer.push(BLOCK_OPEN);
74+
renderer.push(`<!--${hash}-->`);
7475
renderer.child(fn);
75-
renderer.push(BLOCK_CLOSE);
76+
renderer.push(EMPTY_COMMENT);
7677
});
7778
}
7879

packages/svelte/tests/hydration/test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
132132
flushSync();
133133

134134
const normalize = (string: string) =>
135-
string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>');
135+
string
136+
.trim()
137+
.replaceAll('\r\n', '\n')
138+
.replaceAll('/>', '>')
139+
.replace(/<!--.+?-->/g, '');
136140

137141
const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
138142
assert.equal(normalize(target.innerHTML), normalize(expected));
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
let { name, content } = $props();
3+
</script>
4+
5+
<svelte:head>
6+
<meta name={name} content={content} />
7+
</svelte:head>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
let { name, content } = $props();
3+
</script>
4+
5+
<svelte:head>
6+
<meta name="{name}-1" content="{content}-1" />
7+
<meta name="{name}-2" content="{content}-2" />
8+
</svelte:head>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, window }) {
6+
await tick();
7+
8+
const head = window.document.head;
9+
10+
// we don't care about the order, but we want to ensure that the
11+
// elements didn't clobber each other
12+
for (let n of ['1', '2', '3']) {
13+
const a = head.querySelector(`meta[name="a-${n}"]`);
14+
assert.equal(a?.getAttribute('content'), n);
15+
16+
const b1 = head.querySelector(`meta[name="b-${n}-1"]`);
17+
assert.equal(b1?.getAttribute('content'), `${n}-1`);
18+
19+
const b2 = head.querySelector(`meta[name="b-${n}-2"]`);
20+
assert.equal(b2?.getAttribute('content'), `${n}-2`);
21+
}
22+
}
23+
});

0 commit comments

Comments
 (0)