Skip to content

Commit 4e8d1c8

Browse files
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 <[email protected]>
1 parent 2cee6fb commit 4e8d1c8

File tree

12 files changed

+94
-3
lines changed

12 files changed

+94
-3
lines changed

.changeset/few-badgers-guess.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+
feat: warn in dev on `{@html ...}` block hydration mismatch

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
> 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
44
5+
## hydration_html_changed
6+
7+
> The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value
8+
9+
> 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
10+
511
## hydration_mismatch
612

713
> Hydration failed because the initial UI does not match what was rendered on the server

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
2828
import { analyze_css } from './css/css-analyze.js';
2929
import { prune } from './css/css-prune.js';
30-
import { hash } from './utils.js';
30+
import { hash } from '../../../utils.js';
3131
import { warn_unused } from './css/css-warn.js';
3232
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
3333
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,7 @@ const template_visitors = {
11691169
},
11701170
HtmlTag(node, context) {
11711171
const expression = /** @type {import('estree').Expression} */ (context.visit(node.expression));
1172-
context.state.template.push(empty_comment, expression, empty_comment);
1172+
context.state.template.push(b.call('$.html', expression));
11731173
},
11741174
ConstTag(node, { state, visit }) {
11751175
const declaration = node.declaration.declarations[0];

packages/svelte/src/internal/client/dom/blocks/html.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,32 @@ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from '../hydr
55
import { create_fragment_from_html } from '../reconciler.js';
66
import { assign_nodes } from '../template.js';
77
import * as w from '../../warnings.js';
8+
import { hash } from '../../../../utils.js';
9+
import { DEV } from 'esm-env';
10+
import { dev_current_component_function } from '../../runtime.js';
11+
12+
/**
13+
* @param {Element} element
14+
* @param {string | null} server_hash
15+
* @param {string} value
16+
*/
17+
function check_hash(element, server_hash, value) {
18+
if (!server_hash || server_hash === hash(String(value ?? ''))) return;
19+
20+
let location;
21+
22+
// @ts-expect-error
23+
const loc = element.__svelte_meta?.loc;
24+
if (loc) {
25+
location = `near ${loc.file}:${loc.line}:${loc.column}`;
26+
} else if (dev_current_component_function.filename) {
27+
location = `in ${dev_current_component_function.filename}`;
28+
}
29+
30+
w.hydration_html_changed(
31+
location?.replace(/\//g, '/\u200b') // prevent devtools trying to make it a clickable link by inserting a zero-width space
32+
);
33+
}
834

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

3460
effect = branch(() => {
3561
if (hydrating) {
62+
var hash = /** @type {Comment} */ (hydrate_node).data;
3663
var next = hydrate_next();
3764
var last = next;
3865

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

79+
if (DEV) {
80+
check_hash(/** @type {Element} */ (next.parentNode), hash, value);
81+
}
82+
5283
assign_nodes(hydrate_node, last);
5384
anchor = set_hydrate_node(next);
5485
return;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ export function hydration_attribute_changed(attribute, html, value) {
2020
}
2121
}
2222

23+
/**
24+
* 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
25+
* @param {string | undefined | null} [location]
26+
*/
27+
export function hydration_html_changed(location) {
28+
if (DEV) {
29+
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);
30+
} else {
31+
// TODO print a link to the documentation
32+
console.warn("hydration_html_changed");
33+
}
34+
}
35+
2336
/**
2437
* Hydration failed because the initial UI does not match what was rendered on the server. The error occurred near %location%
2538
* @param {string | undefined | null} [location]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { DEV } from 'esm-env';
2+
import { hash } from '../../../utils.js';
3+
4+
/**
5+
* @param {string} value
6+
*/
7+
export function html(value) {
8+
var open = DEV ? `<!--${hash(String(value ?? ''))}-->` : '<!---->';
9+
return `${open}${value}<!---->`;
10+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ export function once(get_value) {
545545
};
546546
}
547547

548+
export { html } from './blocks/html.js';
549+
548550
export { push, pop } from './context.js';
549551

550552
export { push_element, pop_element } from './dev.js';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
server_props: {
5+
browser: false
6+
},
7+
8+
props: {
9+
browser: true
10+
},
11+
12+
compileOptions: {
13+
dev: true
14+
},
15+
16+
errors: [
17+
'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'
18+
]
19+
});

0 commit comments

Comments
 (0)