From ef8bd6adeb238f2d8ccc8c04547e9e16cb932c25 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 12 Dec 2024 12:16:51 -0500 Subject: [PATCH] fix: better handle hydration of script/style elements (alternative) (#14683) * alternative approach to #14624 * changeset * fix * lint --- .changeset/rotten-yaks-nail.md | 5 +++++ .../src/internal/client/dom/blocks/svelte-element.js | 6 ++++++ packages/svelte/src/internal/server/index.js | 9 +++------ packages/svelte/src/utils.js | 8 ++++++++ .../svelte/tests/hydration/samples/script/_config.js | 11 +++++++++++ .../tests/hydration/samples/script/_expected.html | 1 + .../svelte/tests/hydration/samples/script/main.svelte | 1 + 7 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 .changeset/rotten-yaks-nail.md create mode 100644 packages/svelte/tests/hydration/samples/script/_config.js create mode 100644 packages/svelte/tests/hydration/samples/script/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/script/main.svelte diff --git a/.changeset/rotten-yaks-nail.md b/.changeset/rotten-yaks-nail.md new file mode 100644 index 0000000000..bbe9b777ae --- /dev/null +++ b/.changeset/rotten-yaks-nail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better handle hydration of script/style elements diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 823b9a4362..35d2f223ae 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -21,6 +21,7 @@ import { component_context, active_effect } from '../../runtime.js'; import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { assign_nodes } from '../template.js'; +import { is_raw_text_element } from '../../../../utils.js'; /** * @param {Comment | Element} node @@ -116,6 +117,11 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio assign_nodes(element, element); if (render_fn) { + if (hydrating && is_raw_text_element(next_tag)) { + // prevent hydration glitches + element.append(document.createComment('')); + } + // If hydrating, use the existing ssr comment as the anchor so that the // inner open and close methods can pick up the existing nodes correctly var child_anchor = /** @type {TemplateNode} */ ( diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index b944c602b8..b8371b7e00 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -16,7 +16,7 @@ import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; -import { is_boolean_attribute, is_void } from '../../utils.js'; +import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 @@ -24,9 +24,6 @@ import { reset_elements } from './dev.js'; const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -/** List of elements that require raw contents and should not have SSR comments put in them */ -const RAW_TEXT_ELEMENTS = ['textarea', 'script', 'style', 'title']; - /** * @param {Payload} to_copy * @returns {Payload} @@ -64,13 +61,13 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) payload.out += ''; if (tag) { - payload.out += `<${tag} `; + payload.out += `<${tag}`; attributes_fn(); payload.out += `>`; if (!is_void(tag)) { children_fn(); - if (!RAW_TEXT_ELEMENTS.includes(tag)) { + if (!is_raw_text_element(tag)) { payload.out += EMPTY_COMMENT; } payload.out += ``; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 75171c1786..9324408007 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -441,3 +441,11 @@ const RUNES = /** @type {const} */ ([ export function is_rune(name) { return RUNES.includes(/** @type {RUNES[number]} */ (name)); } + +/** List of elements that require raw contents and should not have SSR comments put in them */ +const RAW_TEXT_ELEMENTS = /** @type {const} */ (['textarea', 'script', 'style', 'title']); + +/** @param {string} name */ +export function is_raw_text_element(name) { + return RAW_TEXT_ELEMENTS.includes(/** @type {RAW_TEXT_ELEMENTS[number]} */ (name)); +} diff --git a/packages/svelte/tests/hydration/samples/script/_config.js b/packages/svelte/tests/hydration/samples/script/_config.js new file mode 100644 index 0000000000..4723e4e454 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + snapshot(target) { + const script = target.querySelector('script'); + + return { + script + }; + } +}); diff --git a/packages/svelte/tests/hydration/samples/script/_expected.html b/packages/svelte/tests/hydration/samples/script/_expected.html new file mode 100644 index 0000000000..b3a4d92219 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/_expected.html @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/hydration/samples/script/main.svelte b/packages/svelte/tests/hydration/samples/script/main.svelte new file mode 100644 index 0000000000..3904d47f73 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/script/main.svelte @@ -0,0 +1 @@ +{"{}"}