diff --git a/.changeset/fluffy-eggs-do.md b/.changeset/fluffy-eggs-do.md index b36927376a..95520ab8f4 100644 --- a/.changeset/fluffy-eggs-do.md +++ b/.changeset/fluffy-eggs-do.md @@ -2,4 +2,4 @@ 'svelte': patch --- -fix: throw runtime error when template returns different html +fix: throw runtime warning 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 6f05d339d1..0beb3cb9a9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -80,12 +80,6 @@ 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/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 284e9a7c3e..1d934c5a8a 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -140,6 +140,12 @@ This warning is thrown when Svelte encounters an error while hydrating the HTML During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing. +### 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_raw_snippet_render ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index d4d3d7a8b9..ab4d1519c1 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -52,10 +52,6 @@ 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/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 943cf6f01f..a5b48b1599 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -118,6 +118,10 @@ This warning is thrown when Svelte encounters an error while hydrating the HTML During development, this error is often preceeded by a `console.error` detailing the offending HTML, which needs fixing. +## 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_raw_snippet_render > The `render` function passed to `createRawSnippet` should return HTML for a single element diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index c002ea608d..b3fc5a9c72 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, false); + var node = create_fragment_from_html(html); 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 b0e3a20481..7e3b1ebdde 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -1,14 +1,13 @@ import { DEV } from 'esm-env'; -import * as e from '../errors.js'; +import * as w from '../warnings.js'; /** * @param {string} html - * @param {boolean} [check_structure] */ -export function create_fragment_from_html(html, check_structure = true) { +export function create_fragment_from_html(html) { var elem = document.createElement('template'); elem.innerHTML = html; - if (DEV && check_structure) { + if (DEV) { 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 @@ -21,7 +20,7 @@ export function create_fragment_from_html(html, check_structure = true) { // 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); + w.invalid_html_structure(remove_attributes_and_text_input, remove_attributes_and_text_output); } } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index de2df62c92..fd9d20713d 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -4,6 +4,7 @@ import { create_text, get_first_child, is_firefox } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; +import { DEV } from 'esm-env'; /** * @param {TemplateNode} start @@ -36,6 +37,18 @@ export function template(content, flags) { */ var has_start = !content.startsWith(''); + function create_node() { + node = create_fragment_from_html(has_start ? content : '' + content); + } + + let eagerly_created = false; + + if (DEV) { + eagerly_created = true; + // in dev we eagerly create the node to provide warnings in case of mismatches + create_node(); + } + return () => { if (hydrating) { assign_nodes(hydrate_node, null); @@ -43,8 +56,11 @@ export function template(content, flags) { } if (node === undefined) { - node = create_fragment_from_html(has_start ? content : '' + content); + create_node(); if (!is_fragment) node = /** @type {Node} */ (get_first_child(node)); + } else if (eagerly_created && !is_fragment) { + eagerly_created = false; + node = /** @type {Node} */ (get_first_child(node)); } var clone = /** @type {TemplateNode} */ ( diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 2cbef310cd..682816e1d6 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -198,23 +198,6 @@ 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/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index 250c6eca2f..0d31a8da51 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -94,6 +94,19 @@ export function hydration_mismatch(location) { } } +/** + * 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 + */ +export function invalid_html_structure(html_input, html_output) { + if (DEV) { + console.warn(`%c[svelte] invalid_html_structure\n%cThis 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`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/invalid_html_structure`); + } +} + /** * The `render` function passed to `createRawSnippet` should return HTML for a single element */ diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index fc748ce6b2..0dba6f4533 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -37,6 +37,7 @@ export interface RuntimeTest = Record void; after_test?: () => void; test?: (args: { @@ -174,6 +175,8 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run return compileOptions; } +let import_logs = new Map(); + async function run_test_variant( cwd: string, config: RuntimeTest, @@ -276,6 +279,13 @@ async function run_test_variant( let mod = await import(`${cwd}/_output/client/main.svelte.js`); + if (config.needs_import_logs && !import_logs.has(`${cwd}/_output/client/main.svelte.js`)) { + import_logs.set(`${cwd}/_output/client/main.svelte.js`, { + logs: [...logs], + warnings: [...warnings] + }); + } + const target = window.document.querySelector('main') as HTMLElement; let snapshot = undefined; @@ -336,6 +346,13 @@ async function run_test_variant( } } else { logs.length = warnings.length = 0; + if (config.needs_import_logs) { + const { logs: import_logs_logs, warnings: import_logs_warnings } = import_logs.get( + `${cwd}/_output/client/main.svelte.js` + ); + logs.push(...import_logs_logs); + warnings.push(...import_logs_warnings); + } config.before_test?.(); 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 index fd6f4c03c4..288bf6adfe 100644 --- a/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/invalid-html-structure/_config.js @@ -1,7 +1,18 @@ import { test } from '../../test'; export default test({ - mode: ['client', 'hydrate'], + mode: ['hydrate', 'client'], recover: true, - runtime_error: 'invalid_html_structure' + needs_import_logs: true, + test({ warnings, assert, variant }) { + const expected_warnings = [ + 'This html structure `

` would be corrected like this `

` by the browser making this component impossible to hydrate properly' + ]; + if (variant === 'hydrate') { + expected_warnings.push( + 'Hydration failed because the initial UI does not match what was rendered on the server' + ); + } + assert.deepEqual(warnings, expected_warnings); + } });