From 4dfad737861c66aedf27fd3a8d3b74935dcb6e63 Mon Sep 17 00:00:00 2001 From: 7nik Date: Thu, 26 Jun 2025 19:51:57 +0300 Subject: [PATCH 1/4] fix: remount at any hydration error --- .changeset/hip-eagles-yawn.md | 5 ++++ packages/svelte/src/internal/client/render.js | 23 ++++++++++--------- .../whitespace-at-block-start/Nested.svelte | 1 + .../whitespace-at-block-start/_config.js | 21 +++++++++++++++++ .../whitespace-at-block-start/_expected.html | 1 + .../whitespace-at-block-start/_override.html | 2 ++ .../whitespace-at-block-start/main.svelte | 7 ++++++ .../samples/keyed-each-dev-unique/_config.js | 1 + .../inspect-state-unsafe-mutation/_config.js | 1 + .../samples/props-bound-fallback/_config.js | 1 + 10 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 .changeset/hip-eagles-yawn.md create mode 100644 packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte create mode 100644 packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js create mode 100644 packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html create mode 100644 packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte diff --git a/.changeset/hip-eagles-yawn.md b/.changeset/hip-eagles-yawn.md new file mode 100644 index 000000000000..c987e7c7f2eb --- /dev/null +++ b/.changeset/hip-eagles-yawn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remount at any hydration error diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index ff6844453dcc..dfb40f179661 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -136,20 +136,21 @@ export function hydrate(component, options) { return /** @type {Exports} */ (instance); } catch (error) { - if (error === HYDRATION_ERROR) { - if (options.recover === false) { - e.hydration_failed(); - } - - // If an error occured above, the operations might not yet have been initialised. - init_operations(); - clear_text_content(target); + if (error !== HYDRATION_ERROR) { + // eslint-disable-next-line no-console + console.error('Failed to hydrate: ', error); + } - set_hydrating(false); - return mount(component, options); + if (options.recover === false) { + e.hydration_failed(); } - throw error; + // If an error occured above, the operations might not yet have been initialised. + init_operations(); + clear_text_content(target); + + set_hydrating(false); + return mount(component, options); } finally { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte new file mode 100644 index 000000000000..70bf63ad9de2 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/Nested.svelte @@ -0,0 +1 @@ +

nested

\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js new file mode 100644 index 000000000000..e01861a8231b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../test'; + +/** @type {string[]} */ +let logs = []; +/** @type {typeof console['error']} */ +let console_error; + +export default test({ + before_test() { + console_error = console.error; + console.error = (...args) => logs.push(args.join('')); + }, + after_test() { + console.error = console_error; + }, + test({ deepEqual }) { + deepEqual(logs, [ + "Failed to hydrate: HierarchyRequestError: Node can't be inserted in a #text parent." + ]); + } +}); diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html new file mode 100644 index 000000000000..46f8e8a7ace4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_expected.html @@ -0,0 +1 @@ +

nested

\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html new file mode 100644 index 000000000000..90ca4ef4b8c9 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_override.html @@ -0,0 +1,2 @@ + +

nested

\ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte new file mode 100644 index 000000000000..f1f962b95848 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/main.svelte @@ -0,0 +1,7 @@ + + +
+ +
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js b/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js index 6e64245d861e..97ccaebac252 100644 --- a/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js @@ -1,6 +1,7 @@ import { test } from '../../test'; export default test({ + mode: ['server', 'client'], compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js index dcf3a8bc3d3f..27b9cc7fecab 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js @@ -2,6 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + mode: ['client'], compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js index d04d8884ca37..26cb426127c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js @@ -5,6 +5,7 @@ import { test } from '../../test'; // uses a prop it does not write to but has a fallback value export default test({ accessors: false, // so that prop actually becomes $.prop and not $.prop_source + mode: ['server', 'client'], html: `0`, test({ assert, target }) { From bda8955687acd3bfd11b7835c43738346a6f2e6f Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 27 Jun 2025 11:40:18 +0300 Subject: [PATCH 2/4] re-throw svelte error immediately --- packages/svelte/src/internal/client/render.js | 4 ++++ .../runtime-legacy/samples/keyed-each-dev-unique/_config.js | 1 - .../samples/inspect-state-unsafe-mutation/_config.js | 1 - .../runtime-runes/samples/props-bound-fallback/_config.js | 1 - 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index dfb40f179661..7df1d419e067 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -136,6 +136,10 @@ export function hydrate(component, options) { return /** @type {Exports} */ (instance); } catch (error) { + // re-throw Svelte errors - they are certainly not related to hydration + if (error instanceof Error && error.message.includes('https://svelte.dev/e/')) { + throw error; + } if (error !== HYDRATION_ERROR) { // eslint-disable-next-line no-console console.error('Failed to hydrate: ', error); diff --git a/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js b/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js index 97ccaebac252..6e64245d861e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/keyed-each-dev-unique/_config.js @@ -1,7 +1,6 @@ import { test } from '../../test'; export default test({ - mode: ['server', 'client'], compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js index 27b9cc7fecab..dcf3a8bc3d3f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-state-unsafe-mutation/_config.js @@ -2,7 +2,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - mode: ['client'], compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js index 26cb426127c7..d04d8884ca37 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/_config.js @@ -5,7 +5,6 @@ import { test } from '../../test'; // uses a prop it does not write to but has a fallback value export default test({ accessors: false, // so that prop actually becomes $.prop and not $.prop_source - mode: ['server', 'client'], html: `0`, test({ assert, target }) { From 0300fbca904e97fcb8abb56795487cd86ff24fe7 Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 27 Jun 2025 11:49:16 +0300 Subject: [PATCH 3/4] log error as warn --- packages/svelte/src/internal/client/render.js | 2 +- .../whitespace-at-block-start/_config.js | 21 ++++--------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 7df1d419e067..f09625d9510c 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -142,7 +142,7 @@ export function hydrate(component, options) { } if (error !== HYDRATION_ERROR) { // eslint-disable-next-line no-console - console.error('Failed to hydrate: ', error); + console.warn('Failed to hydrate: ', error); } if (options.recover === false) { diff --git a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js index e01861a8231b..457eeb2201d2 100644 --- a/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js +++ b/packages/svelte/tests/hydration/samples/whitespace-at-block-start/_config.js @@ -1,21 +1,8 @@ import { test } from '../../test'; -/** @type {string[]} */ -let logs = []; -/** @type {typeof console['error']} */ -let console_error; - export default test({ - before_test() { - console_error = console.error; - console.error = (...args) => logs.push(args.join('')); - }, - after_test() { - console.error = console_error; - }, - test({ deepEqual }) { - deepEqual(logs, [ - "Failed to hydrate: HierarchyRequestError: Node can't be inserted in a #text parent." - ]); - } + errors: [ + 'Failed to hydrate: ', + new DOMException("Node can't be inserted in a #text parent.", 'HierarchyRequestError') + ] }); From ab587bff3516643e4c734c4f9abbdb6e942dda6c Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 27 Jun 2025 12:33:06 +0300 Subject: [PATCH 4/4] fix? --- packages/svelte/src/internal/client/render.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index f09625d9510c..3a489f9f927b 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -137,7 +137,10 @@ export function hydrate(component, options) { return /** @type {Exports} */ (instance); } catch (error) { // re-throw Svelte errors - they are certainly not related to hydration - if (error instanceof Error && error.message.includes('https://svelte.dev/e/')) { + if ( + error instanceof Error && + error.message.split('\n').some((line) => line.startsWith('https://svelte.dev/e/')) + ) { throw error; } if (error !== HYDRATION_ERROR) {