feat: runtime dev warn for mismatched `@html` (#12396)

* feat: runtime dev warn for mismatched `@html`

* fix: limit the length of the client value shown in the error

* put logic inside a helper

* remove $.hash, no longer needed

* fix

* tweak

* update changeset

* fix

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12397/head
Paolo Ricciuti 1 year ago committed by GitHub
parent 2cee6fb141
commit 4e8d1c8c52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: warn in dev on `{@html ...}` block hydration mismatch

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

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

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

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

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

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

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

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

@ -0,0 +1,5 @@
<script lang="ts">
let { browser } = $props();
</script>
{@html browser ? 'browser' : 'server'}

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

Loading…
Cancel
Save