fix: handle `<svelte:head>` rendered asynchronously (#17052)

* fix: handle `<svelte:head>` rendered asynchronously

* fix tests
pull/17053/head
Rich Harris 2 weeks ago committed by GitHub
parent da00abe116
commit cc0143c904
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle `<svelte:head>` rendered asynchronously

@ -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)))
)
)

@ -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)
)
)
);
}

@ -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));
}
}

@ -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();
}
}

@ -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);
});
}

@ -132,7 +132,11 @@ const { test, run } = suite<HydrationTest>(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));

@ -0,0 +1,7 @@
<script>
let { name, content } = $props();
</script>
<svelte:head>
<meta name={name} content={content} />
</svelte:head>

@ -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>

@ -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`);
}
}
});

@ -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…
Cancel
Save