From 42a7a0ecd8bad5b4ed0d099c311697aede22e5fe Mon Sep 17 00:00:00 2001
From: Simon H <5968653+dummdidumm@users.noreply.github.com>
Date: Thu, 11 Jul 2024 03:07:17 +0200
Subject: [PATCH] chore: document `@html` and `` hydration change
(#12373)
* chore: document `@html` and `` hydration change
Also add a test for it
closes #12333
* add a test
* Update sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
Co-authored-by: Rich Harris
* lint
* update example and wording
* update test
* since it turns out we already had a test, we can delete the new one
* fix test
---------
Co-authored-by: Rich Harris
---
.../src/internal/client/dom/blocks/html.js | 4 ++-
packages/svelte/tests/helpers.js | 14 +++++++--
.../samples/html-tag-hydration-2/_config.js | 19 ------------
.../samples/html-tag-hydration-2/main.svelte | 5 ----
.../samples/img-src-mismatch/_config.js | 17 +++++++++++
.../samples/img-src-mismatch/main.svelte | 5 ++++
.../samples/raw-mismatch-static/_config.js | 15 ++++++++++
.../raw-mismatch-static/main.client.svelte | 1 +
.../raw-mismatch-static/main.server.svelte | 1 +
.../hydration/samples/raw-mismatch/_config.js | 23 +++++++++++----
.../samples/raw-mismatch/main.svelte | 6 +++-
.../03-appendix/02-breaking-changes.md | 29 +++++++++++++++++++
12 files changed, 105 insertions(+), 34 deletions(-)
delete mode 100644 packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js
delete mode 100644 packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte
create mode 100644 packages/svelte/tests/hydration/samples/img-src-mismatch/_config.js
create mode 100644 packages/svelte/tests/hydration/samples/img-src-mismatch/main.svelte
create mode 100644 packages/svelte/tests/hydration/samples/raw-mismatch-static/_config.js
create mode 100644 packages/svelte/tests/hydration/samples/raw-mismatch-static/main.client.svelte
create mode 100644 packages/svelte/tests/hydration/samples/raw-mismatch-static/main.server.svelte
diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js
index f0a4d2bc79..055d94a77a 100644
--- a/packages/svelte/src/internal/client/dom/blocks/html.js
+++ b/packages/svelte/src/internal/client/dom/blocks/html.js
@@ -23,7 +23,7 @@ function check_hash(element, server_hash, value) {
const loc = element.__svelte_meta?.loc;
if (loc) {
location = `near ${loc.file}:${loc.line}:${loc.column}`;
- } else if (dev_current_component_function.filename) {
+ } else if (dev_current_component_function?.filename) {
location = `in ${dev_current_component_function.filename}`;
}
@@ -59,6 +59,8 @@ export function html(node, get_value, svg, mathml) {
effect = branch(() => {
if (hydrating) {
+ // We're deliberately not trying to repair mismatches between server and client,
+ // as it's costly and error-prone (and it's an edge case to have a mismatch anyway)
var hash = /** @type {Comment} */ (hydrate_node).data;
var next = hydrate_next();
var last = next;
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index f8dbf4c866..002ebf2e38 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -69,7 +69,7 @@ export async function compile_directory(
fs.rmSync(output_dir, { recursive: true, force: true });
- for (const file of glob('**', { cwd, filesOnly: true })) {
+ for (let file of glob('**', { cwd, filesOnly: true })) {
if (file.startsWith('_')) continue;
let text = fs.readFileSync(`${cwd}/${file}`, 'utf-8').replace(/\r\n/g, '\n');
@@ -101,7 +101,17 @@ export async function compile_directory(
write(out, result);
}
- } else if (file.endsWith('.svelte')) {
+ } else if (
+ file.endsWith('.svelte') &&
+ // Make it possible to compile separate versions for client and server to simulate
+ // cases where `{browser ? 'foo' : 'bar'}` is turning into `{'foo'}` on the server
+ // and `{bar}` on the client, assuming we have sophisticated enough treeshaking
+ // in the future to make this a thing.
+ (!file.endsWith('.server.svelte') || generate === 'server') &&
+ (!file.endsWith('.client.svelte') || generate === 'client')
+ ) {
+ file = file.replace(/\.client\.svelte$/, '.svelte').replace(/\.server\.svelte$/, '.svelte');
+
if (preprocessor?.preprocess) {
const preprocessed = await preprocess(
text,
diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js
deleted file mode 100644
index 7713578002..0000000000
--- a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/_config.js
+++ /dev/null
@@ -1,19 +0,0 @@
-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'
- ]
-});
diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte b/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte
deleted file mode 100644
index 6794452c67..0000000000
--- a/packages/svelte/tests/hydration/samples/html-tag-hydration-2/main.svelte
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-{@html browser ? 'browser' : 'server'}
diff --git a/packages/svelte/tests/hydration/samples/img-src-mismatch/_config.js b/packages/svelte/tests/hydration/samples/img-src-mismatch/_config.js
new file mode 100644
index 0000000000..864bd4fac7
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/img-src-mismatch/_config.js
@@ -0,0 +1,17 @@
+import { test } from '../../test';
+
+export default test({
+ server_props: {
+ src: 'server.jpg'
+ },
+ props: {
+ src: 'client.jpg'
+ },
+ test(assert, target) {
+ // We deliberately don't slow down hydration just for supporting this edge case mismatch.
+ assert.htmlEqual(target.innerHTML, '');
+ },
+ errors: [
+ 'The `src` attribute on `...` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value'
+ ]
+});
diff --git a/packages/svelte/tests/hydration/samples/img-src-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/img-src-mismatch/main.svelte
new file mode 100644
index 0000000000..dc25ec0d73
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/img-src-mismatch/main.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch-static/_config.js b/packages/svelte/tests/hydration/samples/raw-mismatch-static/_config.js
new file mode 100644
index 0000000000..e807deb199
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/raw-mismatch-static/_config.js
@@ -0,0 +1,15 @@
+import { test } from '../../test';
+
+export default test({
+ test(assert, target) {
+ // This test case guards against a potential future bug where we could optimize html tags away for static content:
+ // Even if the {@html } block seems static, it should be preserved as such, because it could be dynamic originally
+ // (like {@html browser ? 'foo' : 'bar'} which is then different between client and server.
+ // Also see https://github.com/sveltejs/svelte/issues/8683 where this happened for Svelte 4.
+ assert.htmlEqual(target.innerHTML, 'Server');
+ },
+
+ errors: [
+ 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'
+ ]
+});
diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.client.svelte b/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.client.svelte
new file mode 100644
index 0000000000..c04ecb327a
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.client.svelte
@@ -0,0 +1 @@
+{@html 'Client
has more nodes so if we would walk this because we think it is static we would get an error'}
diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.server.svelte b/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.server.svelte
new file mode 100644
index 0000000000..e7167a0969
--- /dev/null
+++ b/packages/svelte/tests/hydration/samples/raw-mismatch-static/main.server.svelte
@@ -0,0 +1 @@
+{@html 'Server'}
diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch/_config.js b/packages/svelte/tests/hydration/samples/raw-mismatch/_config.js
index b6f54c3757..2942ee80f7 100644
--- a/packages/svelte/tests/hydration/samples/raw-mismatch/_config.js
+++ b/packages/svelte/tests/hydration/samples/raw-mismatch/_config.js
@@ -1,10 +1,21 @@
import { test } from '../../test';
export default test({
- // Even if the {@html } block seems static, it should be preserved as such, because it could be dynamic originally
- // (like {@html browser ? 'foo' : 'bar'} which is then different between client and server.
- // Question is whether that's actually something someone would do in practise, and why, so it's probably better to not
- // slow down hydration just for supporting this edge case - so far we've said no. If someone really needs this we could
- // add something like {@html dynamic ...}
- skip: true
+ server_props: {
+ html: 'Server'
+ },
+
+ props: {
+ html: 'Client'
+ },
+
+ test(assert, target) {
+ // We deliberately don't slow down hydration just for supporting this edge case mismatch.
+ // If someone really needs this and workarounds are insufficient we could add something like {@html dynamic ...}
+ assert.htmlEqual(target.innerHTML, 'Server');
+ },
+
+ errors: [
+ 'The value of an `{@html ...}` block changed between server and client renders. The client value will be ignored in favour of the server value'
+ ]
});
diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/raw-mismatch/main.svelte
index d052b2a9c1..36dab58554 100644
--- a/packages/svelte/tests/hydration/samples/raw-mismatch/main.svelte
+++ b/packages/svelte/tests/hydration/samples/raw-mismatch/main.svelte
@@ -1 +1,5 @@
-{@html 'foo
'}
+
+
+{@html html}
diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
index 921b409dca..e2aa97c7d4 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md
@@ -287,3 +287,32 @@ Note that whereas Svelte 4 would treat `` (for exam
### `mount` plays transitions by default
The `mount` function used to render a component tree plays transitions by default unless the `intro` option is set to `false`. This is different from legacy class components which, when manually instantiated, didn't play transitions by default.
+
+### `` and `{@html ...}` hydration mismatches are not repaired
+
+In Svelte 4, if the value of a `src` attribute or `{@html ...}` tag differ between server and client (a.k.a. a hydration mismatch), the mismatch is repaired. This is very costly: setting a `src` attribute (even if it evaluates to the same thing) causes images and iframes to be reloaded, and reinserting a large blob of HTML is slow.
+
+Since these mismatches are extremely rare, Svelte 5 assumes that the values are unchanged, but in development will warn you if they are not. To force an update you can do something like this:
+
+```svelte
+
+
+{@html markup}
+
+```