diff --git a/.changeset/fluffy-eggs-do.md b/.changeset/fluffy-eggs-do.md new file mode 100644 index 0000000000..b36927376a --- /dev/null +++ b/.changeset/fluffy-eggs-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw runtime error when template returns different html diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 0beb3cb9a9..6f05d339d1 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -80,6 +80,12 @@ Maximum update depth exceeded. This can happen when a reactive block or effect r Failed to hydrate the application ``` +### invalid_html_structure + +``` +This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly +``` + ### invalid_snippet ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ab4d1519c1..d4d3d7a8b9 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -52,6 +52,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Failed to hydrate the application +## invalid_html_structure + +> This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly + ## invalid_snippet > Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index b3fc5a9c72..c002ea608d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -99,7 +99,7 @@ export function html(node, get_value, svg, mathml, skip_warning) { // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. /** @type {DocumentFragment | Element} */ - var node = create_fragment_from_html(html); + var node = create_fragment_from_html(html, false); if (svg || mathml) { node = /** @type {Element} */ (get_first_child(node)); diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 9897e08d53..b0e3a20481 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -1,6 +1,29 @@ -/** @param {string} html */ -export function create_fragment_from_html(html) { +import { DEV } from 'esm-env'; +import * as e from '../errors.js'; + +/** + * @param {string} html + * @param {boolean} [check_structure] + */ +export function create_fragment_from_html(html, check_structure = true) { var elem = document.createElement('template'); elem.innerHTML = html; + if (DEV && check_structure) { + let replace_comments = html.replaceAll('', ''); + let remove_attributes_and_text_input = replace_comments + // we remove every attribute since the template automatically adds ="" after boolean attributes + .replace(/<([a-z0-9]+)(\s+[^>]+?)?>/g, '<$1>') + // we remove the text within the elements because the template change & to & (and similar) + .replace(/>([^<>]*)/g, '>'); + let remove_attributes_and_text_output = elem.innerHTML + // we remove every attribute since the template automatically adds ="" after boolean attributes + .replace(/<([a-z0-9]+)(\s+[^>]+?)?>/g, '<$1>') + // we remove the text within the elements because the template change & to & (and similar) + .replace(/>([^<>]*)/g, '>'); + if (remove_attributes_and_text_input !== remove_attributes_and_text_output) { + e.invalid_html_structure(remove_attributes_and_text_input, remove_attributes_and_text_output); + } + } + return elem.content; } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 682816e1d6..2cbef310cd 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -198,6 +198,23 @@ export function hydration_failed() { } } +/** + * This html structure `%html_input%` would be corrected like this `%html_output%` by the browser making this component impossible to hydrate properly + * @param {string} html_input + * @param {string} html_output + * @returns {never} + */ +export function invalid_html_structure(html_input, html_output) { + if (DEV) { + const error = new Error(`invalid_html_structure\nThis html structure \`${html_input}\` would be corrected like this \`${html_output}\` by the browser making this component impossible to hydrate properly\nhttps://svelte.dev/e/invalid_html_structure`); + + error.name = 'Svelte error'; + throw error; + } else { + throw new Error(`https://svelte.dev/e/invalid_html_structure`); + } +} + /** * Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` * @returns {never} diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js new file mode 100644 index 0000000000..fd6f4c03c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + recover: true, + runtime_error: 'invalid_html_structure' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte new file mode 100644 index 0000000000..ce131ae22a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/main.svelte @@ -0,0 +1,3 @@ + +

+ \ No newline at end of file