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} + +```