hydration-error-validation

hydration-error
Dominic Gannaway 4 months ago
parent 6625c1e080
commit 058eb9f254

@ -23,6 +23,8 @@ export interface ClientTransformState extends TransformState {
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
}
export interface TemplateElement { tag: string, children: TemplateElement[] }
export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions;
@ -45,6 +47,9 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly after_update: Statement[];
/** The HTML template string */
readonly template: string[];
/** The HTML element tree */
readonly template_elements: TemplateElement[];
current_template_element: null | TemplateElement;
readonly metadata: {
namespace: Namespace;
bound_contenteditable: boolean;

@ -1125,6 +1125,8 @@ function create_block(parent, name, nodes, context) {
update_effects: [],
after_update: [],
template: [],
template_elements: [],
current_template_element: null,
metadata: {
context: {
template_needs_import_node: false,
@ -1166,7 +1168,8 @@ function create_block(parent, name, nodes, context) {
'$.open',
b.id('$$anchor'),
b.literal(!state.metadata.context.template_needs_import_node),
template_name
template_name,
context.state.options.dev ? b.literal(JSON.stringify(state.template_elements)) : undefined
)
),
...state.init
@ -1224,7 +1227,8 @@ function create_block(parent, name, nodes, context) {
'$.open_frag',
b.id('$$anchor'),
b.literal(!state.metadata.context.template_needs_import_node),
template_name
template_name,
context.state.options.dev ? b.literal(JSON.stringify(state.template_elements)) : undefined
)
)
);
@ -1948,6 +1952,18 @@ export const template_visitors = {
...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
};
const template_element = {
tag: node.name,
children: []
};
const current_template_element = context.state.current_template_element;
if (current_template_element === null) {
context.state.template_elements.push(template_element);
} else {
current_template_element.children.push(template_element);
}
context.state.current_template_element = template_element;
context.state.template.push(`<${node.name}`);
@ -2157,6 +2173,8 @@ export const template_visitors = {
if (!VoidElements.includes(node.name)) {
context.state.template.push(`</${node.name}>`);
}
context.state.current_template_element = current_template_element;
},
SvelteElement(node, context) {
context.state.template.push(`<!>`);

@ -90,7 +90,7 @@ export function labeled(name, body) {
/**
* @param {string | import('estree').Expression} callee
* @param {...(import('estree').Expression | import('estree').SpreadElement)} args
* @param {...(import('estree').Expression | import('estree').SpreadElement | undefined)} args
* @returns {import('estree').CallExpression}
*/
export function call(callee, ...args) {
@ -102,7 +102,7 @@ export function call(callee, ...args) {
return {
type: 'CallExpression',
callee,
arguments: args,
arguments: /** @type {Array<import('estree').Expression | import('estree').SpreadElement>} **/ (args),
optional: false
};
}

@ -167,14 +167,57 @@ export function svg_replace(node) {
return first_child;
}
/**
* @param {import('./types.js').TemplateElement[]} json
* @param {Node[]} fragment
*/
function validate_trees(json, fragment) {
const length = Math.max(json.length, fragment.length);
let offset = 0;
for (let i = 0; i < length; i++) {
const json_node = json[i];
/** @type {Node | null} */
let dom_node = fragment[i + offset];
debugger
while (dom_node && dom_node.nodeType !== 1) {
dom_node = dom_node.nextSibling;
offset++;
}
if (json_node == null && dom_node == null) {
return;
}
if (!json_node || !dom_node || dom_node.nodeName.toLowerCase() !== json_node.tag) {
throw new Error(
'Svelte encountered a DOM template that maybe been altered by the browser or a browser extension. ' +
'This can occur when the browser encounters invalid markup in your HTML, which results in the browser trying to ' +
`fix the HTML.\n\nSvelte expected HTML elements:\n\n ${json.map((n) => `<${n.tag}>`).join(', ')}\n\n` +
`However, the browser rendered HTML elements:\n\n ${fragment
.filter((n) => n.nodeType === 1)
.map((n) => `<${n.nodeName.toLowerCase()}>`)
.join(', ')}\n`
);
}
const json_children = json_node.children;
const dom_children = Array.from(dom_node.childNodes);
validate_trees(json_children, dom_children);
}
}
/**
* @param {boolean} is_fragment
* @param {boolean} use_clone_node
* @param {null | Text | Comment | Element} anchor
* @param {() => Node} [template_element_fn]
* @param {string} [template_element_string]
* @returns {Element | DocumentFragment | Node[]}
*/
function open_template(is_fragment, use_clone_node, anchor, template_element_fn) {
function open_template(
is_fragment,
use_clone_node,
anchor,
template_element_fn,
template_element_string
) {
if (hydrating) {
if (anchor !== null) {
hydrate_block_anchor(anchor, false);
@ -183,34 +226,45 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn)
// so we need to is_fragment flag to properly handle hydrated content accordingly.
const fragment = current_hydration_fragment;
if (fragment !== null) {
if (DEV && template_element_string) {
const json = JSON.parse(template_element_string);
validate_trees(json, fragment);
}
return is_fragment ? fragment : /** @type {Element} */ (fragment[0]);
}
}
return use_clone_node
const cloned = use_clone_node
? clone_node(/** @type {() => Element} */ (template_element_fn)(), true)
: document.importNode(/** @type {() => Element} */ (template_element_fn)(), true);
if (DEV && template_element_string) {
const json = JSON.parse(template_element_string);
validate_trees(json, cloned.nodeType === 11 ? Array.from(cloned.childNodes) : [cloned]);
}
return cloned;
}
/**
* @param {null | Text | Comment | Element} anchor
* @param {boolean} use_clone_node
* @param {() => Node} [template_element_fn]
* @param {string} [template_element_string]
* @returns {Element | DocumentFragment | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function open(anchor, use_clone_node, template_element_fn) {
return open_template(false, use_clone_node, anchor, template_element_fn);
export function open(anchor, use_clone_node, template_element_fn, template_element_string) {
return open_template(false, use_clone_node, anchor, template_element_fn, template_element_string);
}
/**
* @param {null | Text | Comment | Element} anchor
* @param {boolean} use_clone_node
* @param {() => Node} [template_element_fn]
* @param {string} [template_element_string]
* @returns {Element | DocumentFragment | Node[]}
*/
/*#__NO_SIDE_EFFECTS__*/
export function open_frag(anchor, use_clone_node, template_element_fn) {
return open_template(true, use_clone_node, anchor, template_element_fn);
export function open_frag(anchor, use_clone_node, template_element_fn, template_element_string) {
return open_template(true, use_clone_node, anchor, template_element_fn, template_element_string);
}
const space_template = template(' ', false);

@ -349,6 +349,8 @@ export type TransitionFn<P> = (
options: { direction?: 'in' | 'out' | 'both' }
) => TransitionPayload;
export type TemplateElement = { tag: string, children: TemplateElement[] }
export type AnimateFn<P> = (
element: Element,
rects: { from: DOMRect; to: DOMRect },

Loading…
Cancel
Save