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/few-badgers-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: warn in dev on `{@html ...}` block hydration mismatch
6 changes: 6 additions & 0 deletions packages/svelte/messages/client-warnings/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value

## hydration_html_changed

> The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value

> The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value

## hydration_mismatch

> Hydration failed because the initial UI does not match what was rendered on the server
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
import { hash } from '../../../utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1169,7 +1169,7 @@ const template_visitors = {
},
HtmlTag(node, context) {
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
context.state.template.push(empty_comment, expression, empty_comment);
context.state.template.push(b.call('$.html', expression));
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
Expand Down
31 changes: 31 additions & 0 deletions packages/svelte/src/internal/client/dom/blocks/html.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydr
import { create_fragment_from_html } from '../reconciler.js';
import { assign_nodes } from '../template.js';
import * as w from '../../warnings.js';
import { hash } from '../../../../utils.js';
import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../runtime.js';

/**
* @param {Element} element
* @param {string | null} server_hash
* @param {string} value
*/
function check_hash(element, server_hash, value) {
if (!server_hash || server_hash === hash(String(value ?? ''))) return;

let location;

// @ts-expect-error
const loc = element.__svelte_meta?.loc;
if (loc) {
location = `near ${loc.file}:${loc.line}:${loc.column}`;
} else if (dev_current_component_function.filename) {
location = `in ${dev_current_component_function.filename}`;
}

w.hydration_html_changed(
location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
);
}

/**
* @param {Element | Text | Comment} node
Expand Down Expand Up @@ -33,6 +59,7 @@ export function html(node, get_value, svg, mathml) {

effect = branch(() => {
if (hydrating) {
var hash = /** @type {Comment} */ (hydrate_node).data;
var next = hydrate_next();
var last = next;

Expand All @@ -49,6 +76,10 @@ export function html(node, get_value, svg, mathml) {
throw HYDRATION_ERROR;
}

if (DEV) {
check_hash(/** @type {Element} */ (next.parentNode), hash, value);
}

assign_nodes(hydrate_node, last);
anchor = set_hydrate_node(next);
return;
Expand Down
13 changes: 13 additions & 0 deletions packages/svelte/src/internal/client/warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ export function hydration_attribute_changed(attribute, html, value) {
}
}

/**
* The value of an `{@html ...}` block %location% changed between server and client renders. The client value will be ignored in favour of the server value
* @param {string | undefined | null} [location]
*/
export function hydration_html_changed(location) {
if (DEV) {
console.warn(`%c[svelte] hydration_html_changed\n%c${location ? `The value of an \`{@html ...}\` block ${location} changed between server and client renders. The client value will be ignored in favour of the server value` : "The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value"}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("hydration_html_changed");
}
}

/**
* Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
* @param {string | undefined | null} [location]
Expand Down
10 changes: 10 additions & 0 deletions packages/svelte/src/internal/server/blocks/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { DEV } from 'esm-env';
import { hash } from '../../../utils.js';

/**
* @param {string} value
*/
export function html(value) {
var open = DEV ? `<!--${hash(String(value ?? ''))}-->` : '<!---->';
return `${open}${value}<!---->`;
}
2 changes: 2 additions & 0 deletions packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@ export function once(get_value) {
};
}

export { html } from './blocks/html.js';

export { push, pop } from './context.js';

export { push_element, pop_element } from './dev.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test } from '../../test';

export default test({
server_props: {
browser: false
},

props: {
browser: true
},

compileOptions: {
dev: true
},

errors: [
'The value of an `{@html ...}` block in packages/​svelte/​tests/​hydration/​samples/​html-tag-hydration-2/​main.svelte changed between server and client renders. The client value will be ignored in favour of the server value'
]
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script lang="ts">
let { browser } = $props();
</script>

{@html browser ? 'browser' : 'server'}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default test({
if (variant === 'dom') {
assert.ok(!span.previousSibling);
} else {
assert.ok(span.previousSibling?.textContent === ''); // ssr commment node
assert.equal(span.previousSibling?.textContent, '1tbe2lq'); // hash of the value
}

component.raw = '<span>bar</span>';
Expand Down