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 { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/** /**
* @param {AST.SvelteHead} node * @param {AST.SvelteHead} node
@ -13,6 +15,7 @@ export function SvelteHead(node, context) {
b.stmt( b.stmt(
b.call( b.call(
'$.head', '$.head',
b.literal(hash(filename)),
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))) b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
) )
) )

@ -2,6 +2,8 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */ /** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { hash } from '../../../../../utils.js';
import { filename } from '../../../../state.js';
/** /**
* @param {AST.SvelteHead} node * @param {AST.SvelteHead} node
@ -11,6 +13,13 @@ export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push( 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 { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js'; import { block } from '../../reactivity/effects.js';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; 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 * @param {(anchor: Node) => void} render_fn
* @returns {void} * @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, // 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. // therefore we need to skip that when we detect that we're not in hydration mode.
let previous_hydrate_node = null; let previous_hydrate_node = null;
@ -30,15 +21,13 @@ export function head(render_fn) {
if (hydrating) { if (hydrating) {
previous_hydrate_node = hydrate_node; 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. var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head));
if (head_anchor === undefined) {
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 ( while (
head_anchor !== null && head_anchor !== null &&
(head_anchor.nodeType !== COMMENT_NODE || (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash)
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) { ) {
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
} }
@ -48,7 +37,10 @@ export function head(render_fn) {
if (head_anchor === null) { if (head_anchor === null) {
set_hydrating(false); set_hydrating(false);
} else { } 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 { } finally {
if (was_hydrating) { if (was_hydrating) {
set_hydrating(true); 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)); 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 { active_effect } from './runtime.js';
import { push, pop, component_context } from './context.js'; import { push, pop, component_context } from './context.js';
import { component_root } from './reactivity/effects.js'; import { component_root } from './reactivity/effects.js';
import { import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js';
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating
} from './dom/hydration.js';
import { array_from } from '../shared/utils.js'; import { array_from } from '../shared/utils.js';
import { import {
all_registered_events, all_registered_events,
handle_event_propagation, handle_event_propagation,
root_event_handles root_event_handles
} from './dom/elements/events.js'; } from './dom/elements/events.js';
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { assign_nodes } from './dom/template.js'; import { assign_nodes } from './dom/template.js';
@ -152,7 +145,6 @@ export function hydrate(component, options) {
} finally { } finally {
set_hydrating(was_hydrating); set_hydrating(was_hydrating);
set_hydrate_node(previous_hydrate_node); 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
* @param {(renderer: Renderer) => Promise<void> | void} fn * @param {(renderer: Renderer) => Promise<void> | void} fn
* @returns {void} * @returns {void}
*/ */
export function head(renderer, fn) { export function head(hash, renderer, fn) {
renderer.head((renderer) => { renderer.head((renderer) => {
renderer.push(BLOCK_OPEN); renderer.push(`<!--${hash}-->`);
renderer.child(fn); renderer.child(fn);
renderer.push(BLOCK_CLOSE); renderer.push(EMPTY_COMMENT);
}); });
} }

@ -132,7 +132,11 @@ const { test, run } = suite<HydrationTest>(async (config, cwd) => {
flushSync(); flushSync();
const normalize = (string: string) => 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; const expected = read(`${cwd}/_expected.html`) ?? rendered.html;
assert.equal(normalize(target.innerHTML), normalize(expected)); 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