pull/15524/merge
Paolo Ricciuti 4 months ago committed by GitHub
commit 380c85e551
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: throw runtime warning when template returns different html

@ -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
```

@ -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

@ -1,6 +1,28 @@
/** @param {string} html */
import { DEV } from 'esm-env';
import * as w from '../warnings.js';
/**
* @param {string} html
*/
export function create_fragment_from_html(html) {
var elem = document.createElement('template');
elem.innerHTML = html;
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
.replace(/<([a-z0-9]+)(\s+[^>]+?)?>/g, '<$1>')
// we remove the text within the elements because the template change & to &amp; (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 &amp; (and similar)
.replace(/>([^<>]*)/g, '>');
if (remove_attributes_and_text_input !== remove_attributes_and_text_output) {
w.invalid_html_structure(remove_attributes_and_text_input, remove_attributes_and_text_output);
}
}
return elem.content;
}

@ -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} */ (

@ -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
*/

@ -37,6 +37,7 @@ export interface RuntimeTest<Props extends Record<string, any> = Record<string,
props?: Props;
server_props?: Props;
id_prefix?: string;
needs_import_logs?: boolean;
before_test?: () => 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?.();

@ -0,0 +1,18 @@
import { test } from '../../test';
export default test({
mode: ['hydrate', 'client'],
recover: true,
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