fix: move the error to a warning

invalid-html-structure-error
paoloricciuti 6 months ago
parent a9b26c3ffe
commit 62a6a15b8a

@ -2,4 +2,4 @@
'svelte': patch 'svelte': patch
--- ---
fix: throw runtime error when template returns different html fix: throw runtime warning when template returns different html

@ -80,12 +80,6 @@ Maximum update depth exceeded. This can happen when a reactive block or effect r
Failed to hydrate the application 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 ### invalid_snippet
``` ```

@ -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. 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 ### invalid_raw_snippet_render
``` ```

@ -52,10 +52,6 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
> Failed to hydrate the application > 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 ## invalid_snippet
> Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}` > Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`

@ -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. 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 ## invalid_raw_snippet_render
> The `render` function passed to `createRawSnippet` should return HTML for a single element > The `render` function passed to `createRawSnippet` should return HTML for a single element

@ -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. // 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. // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
/** @type {DocumentFragment | Element} */ /** @type {DocumentFragment | Element} */
var node = create_fragment_from_html(html, false); var node = create_fragment_from_html(html);
if (svg || mathml) { if (svg || mathml) {
node = /** @type {Element} */ (get_first_child(node)); node = /** @type {Element} */ (get_first_child(node));

@ -1,14 +1,13 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import * as e from '../errors.js'; import * as w from '../warnings.js';
/** /**
* @param {string} html * @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'); var elem = document.createElement('template');
elem.innerHTML = html; elem.innerHTML = html;
if (DEV && check_structure) { if (DEV) {
let replace_comments = html.replaceAll('<!>', '<!---->'); let replace_comments = html.replaceAll('<!>', '<!---->');
let remove_attributes_and_text_input = replace_comments let remove_attributes_and_text_input = replace_comments
// we remove every attribute since the template automatically adds ="" after boolean attributes // 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 &amp; (and similar) // we remove the text within the elements because the template change & to &amp; (and similar)
.replace(/>([^<>]*)/g, '>'); .replace(/>([^<>]*)/g, '>');
if (remove_attributes_and_text_input !== remove_attributes_and_text_output) { 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);
} }
} }

@ -4,6 +4,7 @@ import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
import { DEV } from 'esm-env';
/** /**
* @param {TemplateNode} start * @param {TemplateNode} start
@ -36,6 +37,18 @@ export function template(content, flags) {
*/ */
var has_start = !content.startsWith('<!>'); 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 () => { return () => {
if (hydrating) { if (hydrating) {
assign_nodes(hydrate_node, null); assign_nodes(hydrate_node, null);
@ -43,8 +56,11 @@ export function template(content, flags) {
} }
if (node === undefined) { if (node === undefined) {
node = create_fragment_from_html(has_start ? content : '<!>' + content); create_node();
if (!is_fragment) node = /** @type {Node} */ (get_first_child(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} */ ( var clone = /** @type {TemplateNode} */ (

@ -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?.()}` * Could not `{@render}` snippet due to the expression being `null` or `undefined`. Consider using optional chaining `{@render snippet?.()}`
* @returns {never} * @returns {never}

@ -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 * The `render` function passed to `createRawSnippet` should return HTML for a single element
*/ */

@ -37,6 +37,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
props?: Props; props?: Props;
server_props?: Props; server_props?: Props;
id_prefix?: string; id_prefix?: string;
needs_import_logs?: boolean;
before_test?: () => void; before_test?: () => void;
after_test?: () => void; after_test?: () => void;
test?: (args: { test?: (args: {
@ -174,6 +175,8 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
return compileOptions; return compileOptions;
} }
let import_logs = new Map();
async function run_test_variant( async function run_test_variant(
cwd: string, cwd: string,
config: RuntimeTest, config: RuntimeTest,
@ -276,6 +279,13 @@ async function run_test_variant(
let mod = await import(`${cwd}/_output/client/main.svelte.js`); 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; const target = window.document.querySelector('main') as HTMLElement;
let snapshot = undefined; let snapshot = undefined;
@ -336,6 +346,13 @@ async function run_test_variant(
} }
} else { } else {
logs.length = warnings.length = 0; 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?.(); config.before_test?.();

@ -1,7 +1,18 @@
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
mode: ['client', 'hydrate'], mode: ['hydrate', 'client'],
recover: true, recover: true,
runtime_error: 'invalid_html_structure' needs_import_logs: true,
test({ warnings, assert, variant }) {
const expected_warnings = [
'This html structure `<p></p><tr></tr>` would be corrected like this `<p></p>` 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);
}
}); });

Loading…
Cancel
Save