feat: improve hydration, claim static html elements using innerHTML instead of deopt to claiming every nodes (#7426)

Related: #7341, #7226

For purely static HTML, instead of walking the node tree and claiming every node/text etc, hydration now uses the same innerHTML optimization technique for hydration compared to normal create. It uses a new data-svelte-h attribute which is added upon server side rendering containing a hash (computed at build time), and then comparing that hash in the client to ensure it's the same node. If the hash is the same, the whole child content is expected to be the same. If the hash is different, the whole child content is replaced with innerHTML.

---------

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/8530/head
Tan Li Hau 2 years ago committed by GitHub
parent 6f8cdf3b0f
commit df2f656557
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings';
import compiler_errors from './compiler_errors'; import compiler_errors from './compiler_errors';
import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore';
import check_enable_sourcemap from './utils/check_enable_sourcemap'; import check_enable_sourcemap from './utils/check_enable_sourcemap';
import Tag from './nodes/shared/Tag';
interface ComponentOptions { interface ComponentOptions {
namespace?: string; namespace?: string;
@ -110,6 +111,8 @@ export default class Component {
slots: Map<string, Slot> = new Map(); slots: Map<string, Slot> = new Map();
slot_outlets: Set<string> = new Set(); slot_outlets: Set<string> = new Set();
tags: Tag[] = [];
constructor( constructor(
ast: Ast, ast: Ast,
source: string, source: string,
@ -761,6 +764,7 @@ export default class Component {
this.hoist_instance_declarations(); this.hoist_instance_declarations();
this.extract_reactive_declarations(); this.extract_reactive_declarations();
this.check_if_tags_content_dynamic();
} }
post_template_walk() { post_template_walk() {
@ -1479,6 +1483,12 @@ export default class Component {
unsorted_reactive_declarations.forEach(add_declaration); unsorted_reactive_declarations.forEach(add_declaration);
} }
check_if_tags_content_dynamic() {
this.tags.forEach(tag => {
tag.check_if_content_dynamic();
});
}
warn_if_undefined(name: string, node, template_scope: TemplateScope) { warn_if_undefined(name: string, node, template_scope: TemplateScope) {
if (name[0] === '$') { if (name[0] === '$') {
if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) { if (name === '$' || name[1] === '$' && !is_reserved_keyword(name)) {

@ -59,6 +59,11 @@ export default class Attribute extends Node {
return expression; return expression;
}); });
} }
if (this.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
}
} }
get_dependencies() { get_dependencies() {

@ -27,6 +27,8 @@ export default class AwaitBlock extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);

@ -33,6 +33,8 @@ export default class EachBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring

@ -15,6 +15,7 @@ import { is_name_contenteditable, get_contenteditable_attr, has_contenteditable_
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns'; import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character, regex_box_size } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list'; import list from '../../utils/list';
import hash from '../utils/hash';
import Let from './Let'; import Let from './Let';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
@ -503,6 +504,25 @@ export default class Element extends Node {
this.optimise(); this.optimise();
component.apply_stylesheet(this); component.apply_stylesheet(this);
if (this.parent) {
if (this.actions.length > 0 ||
this.animation ||
this.bindings.length > 0 ||
this.classes.length > 0 ||
this.intro || this.outro ||
this.handlers.length > 0 ||
this.styles.length > 0 ||
this.name === 'option' ||
this.is_dynamic_element ||
this.tag_expr.dynamic_dependencies().length ||
this.is_dynamic_element ||
component.compile_options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
} }
validate() { validate() {
@ -1262,6 +1282,20 @@ export default class Element extends Node {
} }
}); });
} }
get can_use_textcontent() {
return this.is_static_content && this.children.every(node => node.type === 'Text' || node.type === 'MustacheTag');
}
get can_optimise_to_html_string() {
const can_use_textcontent = this.can_use_textcontent;
const is_template_with_text_content = this.name === 'template' && can_use_textcontent;
return !is_template_with_text_content && !this.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.children.length > 0;
}
hash() {
return `svelte-${hash(this.component.source.slice(this.start, this.end))}`;
}
} }
const regex_starts_with_vowel = /^[aeiou]/; const regex_starts_with_vowel = /^[aeiou]/;

@ -15,6 +15,8 @@ export default class Head extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
if (info.attributes.length) { if (info.attributes.length) {
component.error(info.attributes[0], compiler_errors.invalid_attribute_head); component.error(info.attributes[0], compiler_errors.invalid_attribute_head);
return; return;

@ -18,6 +18,8 @@ export default class IfBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.scope = scope.child(); this.scope = scope.child();
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, this.scope, info.expression); this.expression = new Expression(component, this, this.scope, info.expression);
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this)); ([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));

@ -28,6 +28,9 @@ export default class InlineComponent extends Node {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
if (info.name !== 'svelte:component' && info.name !== 'svelte:self') { if (info.name !== 'svelte:component' && info.name !== 'svelte:self') {
const name = info.name.split('.')[0]; // accommodate namespaces const name = info.name.split('.')[0]; // accommodate namespaces
component.warn_if_undefined(name, info, scope); component.warn_if_undefined(name, info, scope);

@ -13,6 +13,8 @@ export default class KeyBlock extends AbstractBlock {
constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Node, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);

@ -2,4 +2,9 @@ import Tag from './shared/Tag';
export default class RawMustacheTag extends Tag { export default class RawMustacheTag extends Tag {
type: 'RawMustacheTag'; type: 'RawMustacheTag';
constructor(component, parent, scope, info) {
super(component, parent, scope, info);
this.cannot_use_innerhtml();
this.not_static_content();
}
} }

@ -60,5 +60,8 @@ export default class Slot extends Element {
} }
component.slots.set(this.slot_name, this); component.slots.set(this.slot_name, this);
this.cannot_use_innerhtml();
this.not_static_content();
} }
} }

@ -18,6 +18,7 @@ const elements_without_text = new Set([
]); ]);
const regex_ends_with_svg = /svg$/; const regex_ends_with_svg = /svg$/;
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class Text extends Node { export default class Text extends Node {
type: 'Text'; type: 'Text';
@ -63,4 +64,11 @@ export default class Text extends Node {
return false; return false;
} }
use_space(): boolean {
if (this.component.compile_options.preserveWhitespace) return false;
if (regex_non_whitespace_characters.test(this.data)) return false;
return !this.within_pre();
}
} }

@ -197,6 +197,15 @@ export default class Expression {
}); });
} }
dynamic_contextual_dependencies() {
return Array.from(this.contextual_dependencies).filter(name => {
return Array.from(this.template_scope.dependencies_for_name.get(name)).some(variable_name => {
const variable = this.component.var_lookup.get(variable_name);
return is_dynamic(variable);
});
});
}
// TODO move this into a render-dom wrapper? // TODO move this into a render-dom wrapper?
manipulate(block?: Block, ctx?: string | void) { manipulate(block?: Block, ctx?: string | void) {
// TODO ideally we wouldn't end up calling this method // TODO ideally we wouldn't end up calling this method

@ -15,6 +15,7 @@ export default class Node {
next?: INode; next?: INode;
can_use_innerhtml: boolean; can_use_innerhtml: boolean;
is_static_content: boolean;
var: string; var: string;
attributes: Attribute[]; attributes: Attribute[];
@ -33,6 +34,9 @@ export default class Node {
value: parent value: parent
} }
}); });
this.can_use_innerhtml = true;
this.is_static_content = true;
} }
cannot_use_innerhtml() { cannot_use_innerhtml() {
@ -42,6 +46,11 @@ export default class Node {
} }
} }
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
find_nearest(selector: RegExp) { find_nearest(selector: RegExp) {
if (selector.test(this.type)) return this; if (selector.test(this.type)) return this;
if (this.parent) return this.parent.find_nearest(selector); if (this.parent) return this.parent.find_nearest(selector);

@ -8,6 +8,9 @@ export default class Tag extends Node {
constructor(component, parent, scope, info) { constructor(component, parent, scope, info) {
super(component, parent, scope, info); super(component, parent, scope, info);
component.tags.push(this);
this.cannot_use_innerhtml();
this.expression = new Expression(component, this, scope, info.expression); this.expression = new Expression(component, this, scope, info.expression);
this.should_cache = ( this.should_cache = (
@ -15,4 +18,12 @@ export default class Tag extends Node {
(this.expression.dependencies.size && scope.names.has(info.expression.name)) (this.expression.dependencies.size && scope.names.has(info.expression.name))
); );
} }
is_dependencies_static() {
return this.expression.dynamic_contextual_dependencies().length === 0 && this.expression.dynamic_dependencies().length === 0;
}
check_if_content_dynamic() {
if (!this.is_dependencies_static()) {
this.not_static_content();
}
}
} }

@ -143,9 +143,6 @@ export default class AwaitBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
block.add_dependencies(this.node.expression.dependencies); block.add_dependencies(this.node.expression.dependencies);
let is_dynamic = false; let is_dynamic = false;

@ -38,4 +38,10 @@ export default class CommentWrapper extends Wrapper {
parent_node parent_node
); );
} }
text() {
if (!this.renderer.options.preserveComments) return '';
return `<!--${this.node.data}-->`;
}
} }

@ -80,8 +80,6 @@ export default class EachBlockWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
const { dependencies } = node.expression; const { dependencies } = node.expression;
block.add_dependencies(dependencies); block.add_dependencies(dependencies);

@ -36,9 +36,6 @@ export class BaseAttributeWrapper {
this.parent = parent; this.parent = parent;
if (node.dependencies.size > 0) { if (node.dependencies.size > 0) {
parent.cannot_use_innerhtml();
parent.not_static_content();
block.add_dependencies(node.dependencies); block.add_dependencies(node.dependencies);
} }
} }

@ -29,6 +29,7 @@ import is_dynamic from '../shared/is_dynamic';
import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable'; import { is_name_contenteditable, has_contenteditable_attr } from '../../../utils/contenteditable';
import create_debugging_comment from '../shared/create_debugging_comment'; import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array'; import { push_array } from '../../../../utils/push_array';
import CommentWrapper from '../Comment';
interface BindingGroup { interface BindingGroup {
events: string[]; events: string[];
@ -288,24 +289,6 @@ export default class ElementWrapper extends Wrapper {
} }
}); });
if (this.parent) {
if (node.actions.length > 0 ||
node.animation ||
node.bindings.length > 0 ||
node.classes.length > 0 ||
node.intro || node.outro ||
node.handlers.length > 0 ||
node.styles.length > 0 ||
this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
node.is_dynamic_element ||
renderer.options.dev
) {
this.parent.cannot_use_innerhtml(); // need to use add_location
this.parent.not_static_content();
}
}
this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling); this.fragment = new FragmentWrapper(renderer, block, node.children, this, strip_whitespace, next_sibling);
this.element_data_name = block.get_unique_name(`${this.var.name}_data`); this.element_data_name = block.get_unique_name(`${this.var.name}_data`);
@ -445,6 +428,7 @@ export default class ElementWrapper extends Wrapper {
render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this; const { renderer } = this;
const hydratable = renderer.options.hydratable;
if (this.node.name === 'noscript') return; if (this.node.name === 'noscript') return;
@ -458,13 +442,15 @@ export default class ElementWrapper extends Wrapper {
b`${node} = ${render_statement};` b`${node} = ${render_statement};`
); );
if (renderer.options.hydratable) { const { can_use_textcontent, can_optimise_to_html_string } = this.node;
if (hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(block, parent_nodes)}; ${node} = ${this.get_claim_statement(block, parent_nodes, can_optimise_to_html_string)};
`); `);
if (!this.void && this.node.children.length > 0) { if (!can_optimise_to_html_string && !this.void && this.node.children.length > 0) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
var ${nodes} = ${children}; var ${nodes} = ${children};
`); `);
@ -502,15 +488,19 @@ export default class ElementWrapper extends Wrapper {
// insert static children with textContent or innerHTML // insert static children with textContent or innerHTML
// skip textcontent for <template>. append nodes to TemplateElement.content instead // skip textcontent for <template>. append nodes to TemplateElement.content instead
const can_use_textcontent = this.can_use_textcontent(); if (can_optimise_to_html_string) {
const is_template = this.node.name === 'template';
const is_template_with_text_content = is_template && can_use_textcontent;
if (!is_template_with_text_content && !this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') { if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.chunks.create.push( let text: Node = string_literal((this.fragment.nodes[0] as TextWrapper).data);
b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};` if (hydratable) {
); const variable = block.get_unique_name('textContent');
block.add_variable(variable, text);
text = variable;
}
block.chunks.create.push(b`${node}.textContent = ${text};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.textContent = ${text};`);
}
} else { } else {
const state = { const state = {
quasi: { quasi: {
@ -519,25 +509,33 @@ export default class ElementWrapper extends Wrapper {
} }
}; };
const literal = { let literal: Node = {
type: 'TemplateLiteral', type: 'TemplateLiteral',
expressions: [], expressions: [],
quasis: [] quasis: []
}; };
const can_use_raw_text = !this.can_use_innerhtml && can_use_textcontent; const can_use_raw_text = !this.node.can_use_innerhtml && can_use_textcontent;
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text); to_html((this.fragment.nodes as unknown as Array<ElementWrapper | CommentWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi as any);
block.chunks.create.push( if (hydratable) {
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};` const variable = block.get_unique_name('textContent');
); block.add_variable(variable, literal);
literal = variable;
}
const property = this.node.can_use_innerhtml ? 'innerHTML' : 'textContent';
block.chunks.create.push(b`${node}.${property} = ${literal};`);
if (hydratable) {
block.chunks.claim.push(b`if (@get_svelte_dataset(${node}) !== "${this.node.hash()}") ${node}.${property} = ${literal};`);
}
} }
} else { } else {
this.fragment.nodes.forEach((child: Wrapper) => { this.fragment.nodes.forEach((child: Wrapper) => {
child.render( child.render(
block, block,
is_template ? x`${node}.content` : node, this.node.name === 'template' ? x`${node}.content` : node,
nodes, nodes,
{ element_data_name: this.element_data_name } { element_data_name: this.element_data_name }
); );
@ -566,7 +564,7 @@ export default class ElementWrapper extends Wrapper {
this.add_styles(block); this.add_styles(block);
this.add_manual_style_scoping(block); this.add_manual_style_scoping(block);
if (nodes && this.renderer.options.hydratable && !this.void) { if (nodes && hydratable && !this.void && !can_optimise_to_html_string) {
block.chunks.claim.push( block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`
); );
@ -582,10 +580,6 @@ export default class ElementWrapper extends Wrapper {
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies()); block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
} }
can_use_textcontent() {
return this.is_static_content && this.fragment.nodes.every(node => node.node.type === 'Text' || node.node.type === 'MustacheTag');
}
get_render_statement(block: Block) { get_render_statement(block: Block) {
const { name, namespace, tag_expr } = this.node; const { name, namespace, tag_expr } = this.node;
const reference = tag_expr.manipulate(block); const reference = tag_expr.manipulate(block);
@ -606,7 +600,7 @@ export default class ElementWrapper extends Wrapper {
return x`@element(${reference})`; return x`@element(${reference})`;
} }
get_claim_statement(block: Block, nodes: Identifier) { get_claim_statement(block: Block, nodes: Identifier, to_optimise_hydration: boolean) {
const attributes = this.attributes const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`); .map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
@ -624,6 +618,10 @@ export default class ElementWrapper extends Wrapper {
reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`; reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`;
} }
if (to_optimise_hydration) {
attributes.push(p`["data-svelte-h"]: true`);
}
if (this.node.namespace === namespaces.svg) { if (this.node.namespace === namespaces.svg) {
return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`; return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} else { } else {
@ -1288,13 +1286,19 @@ export default class ElementWrapper extends Wrapper {
const regex_backticks = /`/g; const regex_backticks = /`/g;
const regex_dollar_signs = /\$/g; const regex_dollar_signs = /\$/g;
function to_html(wrappers: Array<ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) { function to_html(wrappers: Array<CommentWrapper | ElementWrapper | TextWrapper | MustacheTagWrapper | RawMustacheTagWrapper>, block: Block, literal: any, state: any, can_use_raw_text?: boolean) {
wrappers.forEach(wrapper => { wrappers.forEach(wrapper => {
if (wrapper instanceof TextWrapper) { if (wrapper instanceof CommentWrapper) {
state.quasi.value.raw += wrapper.text();
} else if (wrapper instanceof TextWrapper) {
// Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML // Don't add the <pre>/<textarea> newline logic here because pre/textarea.innerHTML
// would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't // would keep the leading newline, too, only someParent.innerHTML = '..<pre/textarea>..' won't
if ((wrapper as TextWrapper).use_space()) state.quasi.value.raw += ' '; if (wrapper.use_space()) {
// use space instead of the text content
state.quasi.value.raw += ' ';
return;
}
const parent = wrapper.node.parent as Element; const parent = wrapper.node.parent as Element;

@ -20,8 +20,6 @@ export default class HeadWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.can_use_innerhtml = false;
this.fragment = new FragmentWrapper( this.fragment = new FragmentWrapper(
renderer, renderer,
block, block,

@ -105,9 +105,6 @@ export default class IfBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.branches = []; this.branches = [];
const blocks: Block[] = []; const blocks: Block[] = [];

@ -44,9 +44,6 @@ export default class InlineComponentWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.expression) { if (this.node.expression) {
block.add_dependencies(this.node.expression.dependencies); block.add_dependencies(this.node.expression.dependencies);
} }

@ -24,9 +24,6 @@ export default class KeyBlockWrapper extends Wrapper {
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
this.dependencies = node.expression.dynamic_dependencies(); this.dependencies = node.expression.dynamic_dependencies();
if (this.dependencies.length) { if (this.dependencies.length) {

@ -20,8 +20,6 @@ export default class RawMustacheTagWrapper extends Tag {
node: MustacheTag | RawMustacheTag node: MustacheTag | RawMustacheTag
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
} }
render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, _parent_nodes: Identifier) {

@ -30,8 +30,6 @@ export default class SlotWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
this.not_static_content();
if (this.node.children.length) { if (this.node.children.length) {
this.fallback = block.child({ this.fallback = block.child({

@ -5,11 +5,9 @@ import Wrapper from './shared/Wrapper';
import { x } from 'code-red'; import { x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
const regex_non_whitespace_characters = /[\S\u00A0]/;
export default class TextWrapper extends Wrapper { export default class TextWrapper extends Wrapper {
node: Text; node: Text;
data: string; _data: string;
skip: boolean; skip: boolean;
var: Identifier; var: Identifier;
@ -23,15 +21,22 @@ export default class TextWrapper extends Wrapper {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.skip = this.node.should_skip(); this.skip = this.node.should_skip();
this.data = data; this._data = data;
this.var = (this.skip ? null : x`t`) as unknown as Identifier; this.var = (this.skip ? null : x`t`) as unknown as Identifier;
} }
use_space() { use_space() {
if (this.renderer.component.component_options.preserveWhitespace) return false; return this.node.use_space();
if (regex_non_whitespace_characters.test(this.data)) return false; }
return !this.node.within_pre(); set data(value: string) {
// when updating `this.data` during optimisation
// propagate the changes over to the underlying node
// so that the node.use_space reflects on the latest `data` value
this.node.data = this._data = value;
}
get data() {
return this._data;
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {

@ -12,18 +12,9 @@ export default class Tag extends Wrapper {
constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) { constructor(renderer: Renderer, block: Block, parent: Wrapper, node: MustacheTag | RawMustacheTag) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
this.cannot_use_innerhtml();
if (!this.is_dependencies_static()) {
this.not_static_content();
}
block.add_dependencies(node.expression.dependencies); block.add_dependencies(node.expression.dependencies);
} }
is_dependencies_static() {
return this.node.expression.contextual_dependencies.size === 0 && this.node.expression.dynamic_dependencies().length === 0;
}
rename_this_method( rename_this_method(
block: Block, block: Block,
update: ((value: Node) => (Node | Node[])) update: ((value: Node) => (Node | Node[]))

@ -13,8 +13,6 @@ export default class Wrapper {
next: Wrapper | null; next: Wrapper | null;
var: Identifier; var: Identifier;
can_use_innerhtml: boolean;
is_static_content: boolean;
constructor( constructor(
renderer: Renderer, renderer: Renderer,
@ -35,22 +33,9 @@ export default class Wrapper {
} }
}); });
this.can_use_innerhtml = !renderer.options.hydratable;
this.is_static_content = !renderer.options.hydratable;
block.wrappers.push(this); block.wrappers.push(this);
} }
cannot_use_innerhtml() {
this.can_use_innerhtml = false;
if (this.parent) this.parent.cannot_use_innerhtml();
}
not_static_content() {
this.is_static_content = false;
if (this.parent) this.parent.not_static_content();
}
get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) { get_or_create_anchor(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
// TODO use this in EachBlock and IfBlock — tricky because // TODO use this in EachBlock and IfBlock — tricky because
// children need to be created first // children need to be created first

@ -48,6 +48,7 @@ const handlers: Record<string, Handler> = {
export interface RenderOptions extends CompileOptions{ export interface RenderOptions extends CompileOptions{
locate: (c: number) => { line: number; column: number }; locate: (c: number) => { line: number; column: number };
head_id?: string; head_id?: string;
has_added_svelte_hash?: boolean;
} }
export default class Renderer { export default class Renderer {

@ -158,6 +158,13 @@ export default function (node: Element, renderer: Renderer, options: RenderOptio
} }
}); });
if (options.hydratable) {
if (node.can_optimise_to_html_string && !options.has_added_svelte_hash) {
renderer.add_string(` data-svelte-h="${node.hash()}"`);
options = { ...options, has_added_svelte_hash: true };
}
}
renderer.add_string('>'); renderer.add_string('>');
if (node_contents !== undefined) { if (node_contents !== undefined) {

@ -5,7 +5,9 @@ import Element from '../../nodes/Element';
export default function(node: Text, renderer: Renderer, _options: RenderOptions) { export default function(node: Text, renderer: Renderer, _options: RenderOptions) {
let text = node.data; let text = node.data;
if ( if (node.use_space()) {
text = ' ';
} else if (
!node.parent || !node.parent ||
node.parent.type !== 'Element' || node.parent.type !== 'Element' ||
((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style') ((node.parent as Element).name !== 'script' && (node.parent as Element).name !== 'style')

@ -39,6 +39,7 @@ export default function remove_whitespace_children(children: INode[], next?: INo
continue; continue;
} }
child.data = data;
nodes.unshift(child); nodes.unshift(child);
link(last_child, last_child = child); link(last_child, last_child = child);
} else { } else {

@ -1,4 +1,5 @@
export function string_literal(data: string) { import { Literal } from 'estree';
export function string_literal(data: string): Literal {
return { return {
type: 'Literal', type: 'Literal',
value: data value: data

@ -362,6 +362,10 @@ export function xlink_attr(node, attribute, value) {
node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value); node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);
} }
export function get_svelte_dataset(node: HTMLElement) {
return node.dataset.svelteH;
}
export function get_binding_group_value(group, __value, checked) { export function get_binding_group_value(group, __value, checked) {
const value = new Set(); const value = new Set();
for (let i = 0; i < group.length; i += 1) { for (let i = 0; i < group.length; i += 1) {

@ -4,7 +4,7 @@ import glob from 'tiny-glob/sync';
import * as path from 'path'; import * as path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
import * as colors from 'kleur'; import * as colors from 'kleur';
export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual, expected, message?) => void, htmlEqualWithComments: (actual, expected, message?) => void }; export const assert = (assert$1 as unknown) as typeof assert$1 & { htmlEqual: (actual: string, expected: string, message?: string) => void, htmlEqualWithOptions: (actual: string, expected: string, options: { preserveComments?: boolean, withoutNormalizeHtml?: boolean }, message?: string) => void };
// for coverage purposes, we need to test source files, // for coverage purposes, we need to test source files,
// but for sanity purposes, we need to test dist files // but for sanity purposes, we need to test dist files
@ -140,11 +140,12 @@ function cleanChildren(node) {
} }
} }
export function normalizeHtml(window, html, preserveComments = false) { export function normalizeHtml(window, html, { removeDataSvelte = false, preserveComments = false }: { removeDataSvelte?: boolean, preserveComments?: boolean }) {
try { try {
const node = window.document.createElement('div'); const node = window.document.createElement('div');
node.innerHTML = html node.innerHTML = html
.replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '') .replace(/(<!--.*?-->)/g, preserveComments ? '$1' : '')
.replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1')
.replace(/>[ \t\n\r\f]+</g, '><') .replace(/>[ \t\n\r\f]+</g, '><')
.trim(); .trim();
cleanChildren(node); cleanChildren(node);
@ -154,22 +155,30 @@ export function normalizeHtml(window, html, preserveComments = false) {
} }
} }
export function setupHtmlEqual() { export function normalizeNewline(html: string) {
return html.replace(/\r\n/g, '\n');
}
export function setupHtmlEqual(options: { removeDataSvelte?: boolean } = {}) {
const window = env(); const window = env();
// eslint-disable-next-line no-import-assign // eslint-disable-next-line no-import-assign
assert.htmlEqual = (actual, expected, message) => { assert.htmlEqual = (actual, expected, message) => {
assert.deepEqual( assert.deepEqual(
normalizeHtml(window, actual), normalizeHtml(window, actual, options),
normalizeHtml(window, expected), normalizeHtml(window, expected, options),
message message
); );
}; };
// eslint-disable-next-line no-import-assign // eslint-disable-next-line no-import-assign
assert.htmlEqualWithComments = (actual, expected, message) => { assert.htmlEqualWithOptions = (actual: string, expected: string, { preserveComments, withoutNormalizeHtml }: { preserveComments?: boolean, withoutNormalizeHtml?: boolean }, message?: string) => {
assert.deepEqual( assert.deepEqual(
normalizeHtml(window, actual, true), withoutNormalizeHtml
normalizeHtml(window, expected, true), ? normalizeNewline(actual).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, actual, { ...options, preserveComments }),
withoutNormalizeHtml
? normalizeNewline(expected).replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1')
: normalizeHtml(window, expected, { ...options, preserveComments }),
message message
); );
}; };

@ -106,6 +106,13 @@ describe('hydration', () => {
} }
} }
if (config.snapshot) {
const snapshot_after = config.snapshot(target);
for (const s in snapshot_after) {
assert.equal(snapshot_after[s], snapshot[s], `Expected snapshot key "${s}" to have same value/reference`);
}
}
if (config.test) { if (config.test) {
config.test(assert, target, snapshot, component, window); config.test(assert, target, snapshot, component, window);
} else { } else {

@ -1 +1 @@
<h1>Hello world!</h1> <h1 data-svelte-h="svelte-1vv3a6r">Hello world!</h1>

@ -1 +1 @@
<h1>Hello world!</h1> <h1 data-svelte-h="svelte-1vv3a6r">Hello world!</h1>

@ -6,12 +6,5 @@ export default {
h1, h1,
text: h1.childNodes[0] text: h1.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
} }
}; };

@ -10,13 +10,8 @@ export default {
}; };
}, },
async test(assert, target, snapshot, component, window) { async test(assert, target, _, component, window) {
const input = target.querySelector('input'); const input = target.querySelector('input');
const p = target.querySelector('p');
assert.equal(input, snapshot.input);
assert.equal(p, snapshot.p);
input.value = 'everybody'; input.value = 'everybody';
await input.dispatchEvent(new window.Event('input')); await input.dispatchEvent(new window.Event('input'));

@ -1 +1,3 @@
<div><!-- test1 --><!-- test2 --></div> <div><!-- test1 --><!-- test2 --></div>
p
<div><!-- test1 --><!-- test2 --></div>

@ -1 +1,3 @@
<div><!-- test1 --></div> <div><!-- test1 --></div>
p
<div><!-- test1 --><!-- test2 --></div>

@ -3,18 +3,8 @@ export default {
preserveComments:true preserveComments:true
}, },
snapshot(target) { snapshot(target) {
const div = target.querySelector('div');
return { return {
div, div: target.querySelectorAll('div')[1]
comment: div.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.childNodes[0], snapshot.comment);
assert.equal(div.childNodes[1].nodeType, 8);
} }
}; };

@ -1 +1,3 @@
<div><!-- test1 --><!-- test2 --></div> <div><!-- test1 --><!-- test2 --></div>
{"p"}
<div><!-- test1 --><!-- test2 --></div>

@ -0,0 +1,8 @@
<div data-svelte-h="xxx">hello</div>
<div data-svelte-h="xxx"><div>bye</div></div>
<div data-svelte-h="xxx">
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div data-svelte-h="xxx">hello</div>
<div data-svelte-h="xxx"><div data-svelte-h="yyy">bye</div></div>
<div data-svelte-h="xxx">
<div data-svelte-h="yyy">aaa</div>
<div data-svelte-h="zzz">bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
<div>hello</div>
<div><div>bye</div></div>
<div>
<div>aaa</div>
<div>bbb</div>
</div>

@ -0,0 +1,8 @@
export default {
snapshot(target) {
return {
main: target.querySelector('main'),
p: target.querySelector('p')
};
}
};

@ -1,3 +1,3 @@
<div> <div>
<p>nested</p> <p data-svelte-h="svelte-1x3hbnh">nested</p>
</div> </div>

@ -1,3 +1,3 @@
<div> <div>
<p>nested</p> <p data-svelte-h="svelte-1x3hbnh">nested</p>
</div> </div>

@ -8,14 +8,5 @@ export default {
p, p,
text: p.childNodes[0] text: p.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const p = target.querySelector('p');
assert.equal(div, snapshot.div);
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
} }
}; };

@ -1 +1 @@
<p>nested</p> <p data-svelte-h="svelte-1x3hbnh">nested</p>

@ -1 +1 @@
<p>nested</p> <p data-svelte-h="svelte-1x3hbnh">nested</p>

@ -6,12 +6,5 @@ export default {
p, p,
text: p.childNodes[0] text: p.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
} }
}; };

@ -10,12 +10,5 @@ export default {
h1, h1,
text: h1.childNodes[0] text: h1.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
} }
}; };

@ -9,13 +9,5 @@ export default {
nullText, nullText,
undefinedText undefinedText
}; };
},
test(assert, target, snapshot) {
const nullText = target.querySelectorAll('p')[0].textContent;
const undefinedText = target.querySelectorAll('p')[1].textContent;
assert.equal(nullText, snapshot.nullText);
assert.equal(undefinedText, snapshot.undefinedText);
} }
}; };

@ -10,12 +10,5 @@ export default {
h1, h1,
text: h1.childNodes[0] text: h1.childNodes[0]
}; };
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
} }
}; };

@ -15,17 +15,9 @@ export default {
return { return {
ul, ul,
lis lis0: lis[0],
lis1: lis[1],
lis2: lis[2]
}; };
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
} }
}; };

@ -13,17 +13,9 @@ export default {
return { return {
ul, ul,
lis lis0: lis[0],
lis1: lis[1],
lis2: lis[2]
}; };
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
} }
}; };

@ -9,11 +9,5 @@ export default {
return { return {
div div
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
} }
}; };

@ -9,11 +9,5 @@ export default {
return { return {
div div
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
} }
}; };

@ -9,11 +9,5 @@ export default {
return { return {
div div
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
} }
}; };

@ -5,11 +5,5 @@ export default {
return { return {
div div
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
} }
}; };

@ -7,13 +7,5 @@ export default {
span: p.querySelector('span'), span: p.querySelector('span'),
code: p.querySelector('code') code: p.querySelector('code')
}; };
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.querySelector('span'), snapshot.span);
assert.equal(p.querySelector('code'), snapshot.code);
} }
}; };

@ -1,3 +1,3 @@
<div> <div data-svelte-h="svelte-1aqf5aj">
<p>nested</p> <p>nested</p>
</div> </div>

@ -1,3 +1,3 @@
<div> <div data-svelte-h="svelte-1aqf5aj">
<p>nested</p> <p>nested</p>
</div> </div>

@ -6,12 +6,5 @@ export default {
div, div,
p: div.querySelector('p') p: div.querySelector('p')
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.querySelector('p'), snapshot.p);
} }
}; };

@ -7,10 +7,8 @@ export default {
}; };
}, },
test(assert, target, snapshot, component) { test(assert, target, _, component) {
const h1 = target.querySelector('h1'); const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(component.h1, h1); assert.equal(component.h1, h1);
} }
}; };

@ -11,10 +11,8 @@ export default {
}; };
}, },
async test(assert, target, snapshot, component, window) { async test(assert, target, _, component, window) {
const button = target.querySelector('button'); const button = target.querySelector('button');
assert.equal(button, snapshot.button);
await button.dispatchEvent(new window.MouseEvent('click')); await button.dispatchEvent(new window.MouseEvent('click'));
assert.ok(component.clicked); assert.ok(component.clicked);

@ -7,13 +7,5 @@ export default {
text: p.childNodes[0], text: p.childNodes[0],
span: p.querySelector('span') span: p.querySelector('span')
}; };
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
assert.equal(p.querySelector('span'), snapshot.span);
} }
}; };

@ -13,14 +13,5 @@ export default {
p0: ps[0], p0: ps[0],
p1: ps[1] p1: ps[1]
}; };
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
assert.equal(div, snapshot.div);
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[1], snapshot.p1);
} }
}; };

@ -9,11 +9,5 @@ export default {
return { return {
p p
}; };
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
} }
}; };

@ -12,10 +12,7 @@ export default {
}; };
}, },
test(assert, target, snapshot, component) { test(assert, target, _, component) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
component.foo = false; component.foo = false;
component.bar = true; component.bar = true;

@ -9,11 +9,5 @@ export default {
return { return {
p p
}; };
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
} }
}; };

@ -14,14 +14,5 @@ export default {
p1: ps[1], p1: ps[1],
text1: ps[1].firstChild text1: ps[1].firstChild
}; };
},
test(assert, target, snapshot) {
const ps = target.querySelectorAll('p');
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[0].firstChild, snapshot.text0);
assert.equal(ps[1], snapshot.p1);
assert.equal(ps[1].firstChild, snapshot.text1);
} }
}; };

@ -9,17 +9,7 @@ function foo() {
} }
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => { const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return ` return ` <div class="class1 class2" style="color:red;">-</div> <div${add_attribute("class", const1, 0)}>-</div> <div${add_attribute("class", const1, 0)}>-</div> <div class="${"class1 " + escape('class2', true)}">-</div> <div class="${"class1 " + escape(const2, true)}">-</div> <div class="${"class1 " + escape(const2, true)}"${add_attribute("style", foo(), 0)}>-</div>`;
<div class="class1 class2" style="color:red;">-</div>
<div${add_attribute("class", const1, 0)}>-</div>
<div${add_attribute("class", const1, 0)}>-</div>
<div class="${"class1 " + escape('class2', true)}">-</div>
<div class="${"class1 " + escape(const2, true)}">-</div>
<div class="${"class1 " + escape(const2, true)}"${add_attribute("style", foo(), 0)}>-</div>`;
}); });
export default Component; export default Component;

@ -8,11 +8,8 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo); if ($$props.foo === void 0 && $$bindings.foo && foo !== void 0) $$bindings.foo(foo);
return `${each(things, thing => { return `${each(things, thing => {
return `<span>${escape(thing.name)}</span> return `<span>${escape(thing.name)}</span> ${debug(null, 7, 2, { foo })}`;
${debug(null, 7, 2, { foo })}`; })} <p>foo: ${escape(foo)}</p>`;
})}
<p>foo: ${escape(foo)}</p>`;
}); });
export default Component; export default Component;

@ -27,7 +27,6 @@ function get_each_context(ctx, list, i) {
function create_each_block(ctx) { function create_each_block(ctx) {
let div; let div;
let strong; let strong;
let t0;
let t1; let t1;
let span; let span;
let t2_value = /*comment*/ ctx[4].author + ""; let t2_value = /*comment*/ ctx[4].author + "";
@ -44,7 +43,7 @@ function create_each_block(ctx) {
c() { c() {
div = element("div"); div = element("div");
strong = element("strong"); strong = element("strong");
t0 = text(/*i*/ ctx[6]); strong.textContent = `${/*i*/ ctx[6]}`;
t1 = space(); t1 = space();
span = element("span"); span = element("span");
t2 = text(t2_value); t2 = text(t2_value);
@ -60,7 +59,6 @@ function create_each_block(ctx) {
m(target, anchor) { m(target, anchor) {
insert(target, div, anchor); insert(target, div, anchor);
append(div, strong); append(div, strong);
append(strong, t0);
append(div, t1); append(div, t1);
append(div, span); append(div, span);
append(span, t2); append(span, t2);

@ -2,9 +2,7 @@
import { create_ssr_component } from "svelte/internal"; import { create_ssr_component } from "svelte/internal";
const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => { const Component = create_ssr_component(($$result, $$props, $$bindings, slots) => {
return `<div>content</div> return `<div>content</div> <!-- comment --> <div>more content</div>`;
<!-- comment -->
<div>more content</div>`;
}); });
export default Component; export default Component;

@ -45,7 +45,7 @@ describe('runtime', () => {
return module._compile(code, filename); return module._compile(code, filename);
}; };
return setupHtmlEqual(); return setupHtmlEqual({ removeDataSvelte: true });
}); });
after(() => process.removeListener('unhandledRejection', unhandledRejection_handler)); after(() => process.removeListener('unhandledRejection', unhandledRejection_handler));
@ -154,15 +154,23 @@ describe('runtime', () => {
window.SvelteComponent = SvelteComponent; window.SvelteComponent = SvelteComponent;
const target = window.document.querySelector('main'); const target = window.document.querySelector('main');
let snapshot = undefined;
if (hydrate && from_ssr_html) { if (hydrate && from_ssr_html) {
// ssr into target // ssr into target
compileOptions.generate = 'ssr'; compileOptions.generate = 'ssr';
cleanRequireCache(); cleanRequireCache();
if (config.before_test) config.before_test();
const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default; const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default;
const { html } = SsrSvelteComponent.render(config.props); const { html } = SsrSvelteComponent.render(config.props);
target.innerHTML = html; target.innerHTML = html;
if (config.snapshot) {
snapshot = config.snapshot(target);
}
delete compileOptions.generate; delete compileOptions.generate;
if (config.after_test) config.after_test();
} else { } else {
target.innerHTML = ''; target.innerHTML = '';
} }
@ -199,7 +207,9 @@ describe('runtime', () => {
} }
if (config.html) { if (config.html) {
assert.htmlEqual(target.innerHTML, config.html); assert.htmlEqualWithOptions(target.innerHTML, config.html, {
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} }
if (config.test) { if (config.test) {
@ -208,6 +218,7 @@ describe('runtime', () => {
component, component,
mod, mod,
target, target,
snapshot,
window, window,
raf, raf,
compileOptions compileOptions

@ -5,7 +5,9 @@ export default {
<h1>tag is h1.</h1> <h1>tag is h1.</h1>
`, `,
props: { props: {
logs pushLogs(log) {
logs.push(log);
}
}, },
after_test() { after_test() {
logs = []; logs = [];

@ -1,12 +1,12 @@
<script> <script>
export let logs = []; export let pushLogs;
export let tag = "h1"; export let tag = "h1";
export let opt = "opt1"; export let opt = "opt1";
function foo(node, {tag, opt}) { function foo(node, {tag, opt}) {
logs.push(`create: ${tag},${opt}`); pushLogs(`create: ${tag},${opt}`);
return { return {
update: ({tag, opt}) => logs.push(`update: ${tag},${opt}`), update: ({tag, opt}) => pushLogs(`update: ${tag},${opt}`),
destroy: () => logs.push('destroy'), destroy: () => pushLogs('destroy'),
}; };
} }
</script> </script>

@ -8,9 +8,9 @@ export default {
async test({ assert, target }) { async test({ assert, target }) {
const firstSpanList = target.children[0]; const firstSpanList = target.children[0];
assert.equal(firstSpanList.innerHTML, expected); assert.htmlEqualWithOptions(firstSpanList.innerHTML, expected, { withoutNormalizeHtml: true });
const secondSpanList = target.children[1]; const secondSpanList = target.children[1];
assert.equal(secondSpanList.innerHTML, expected); assert.htmlEqualWithOptions(secondSpanList.innerHTML, expected, { withoutNormalizeHtml: true });
} }
}; };

@ -1,19 +1,14 @@
export default { export default {
test({ assert, target }) { withoutNormalizeHtml: true,
// Test for <pre> tag html: get_html(false),
const elementPre = target.querySelector('#pre'); ssrHtml: get_html(true)
// Test for non <pre> tag };
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
// Test for <pre> tag with leading newline
const elementPreWithLeadingNewline = target.querySelector('#pre-with-leading-newline');
const elementPreWithoutLeadingNewline = target.querySelector('#pre-without-leading-newline');
const elementPreWithMultipleLeadingNewline = target.querySelector('#pre-with-multiple-leading-newlines');
assert.equal( function get_html(ssr) {
elementPre.innerHTML, // ssr rendered HTML has an extra newline prefixed within `<pre>` tag,
` A // if the <pre> tag starts with `\n`
// because when browser parses the SSR rendered HTML, it will ignore the 1st '\n' character
return `<pre id="pre"> A
B B
<span> <span>
C C
@ -21,20 +16,12 @@ export default {
</span> </span>
E E
F F
` </pre> <div id="div">A
);
assert.equal(
elementDiv.innerHTML,
`A
B B
<span>C <span>C
D</span> D</span>
E E
F` F</div> <div id="div-with-pre"><pre> A
);
assert.equal(
elementDivWithPre.innerHTML,
`<pre> A
B B
<span> <span>
C C
@ -42,14 +29,9 @@ export default {
</span> </span>
E E
F F
</pre>` </pre></div> <div id="pre-with-leading-newline"><pre>leading newline</pre> <pre> leading newline and spaces</pre> <pre>${ssr ? '\n' : ''}
); leading newlines</pre></div> <div id="pre-without-leading-newline"><pre>without spaces</pre> <pre> with spaces </pre> <pre>
assert.equal(elementPreWithLeadingNewline.children[0].innerHTML, 'leading newline'); newline after leading space</pre></div> <pre id="pre-with-multiple-leading-newlines">${ssr ? '\n' : ''}
assert.equal(elementPreWithLeadingNewline.children[1].innerHTML, ' leading newline and spaces');
assert.equal(elementPreWithLeadingNewline.children[2].innerHTML, '\nleading newlines'); multiple leading newlines</pre>`;
assert.equal(elementPreWithoutLeadingNewline.children[0].innerHTML, 'without spaces'); }
assert.equal(elementPreWithoutLeadingNewline.children[1].innerHTML, ' with spaces ');
assert.equal(elementPreWithoutLeadingNewline.children[2].innerHTML, ' \nnewline after leading space');
assert.equal(elementPreWithMultipleLeadingNewline.innerHTML, '\n\nmultiple leading newlines');
}
};

@ -2,17 +2,8 @@ export default {
compileOptions: { compileOptions: {
preserveWhitespace: true preserveWhitespace: true
}, },
test({ assert, target }) {
// Test for <pre> tag
const elementPre = target.querySelector('#pre');
// Test for non <pre> tag
const elementDiv = target.querySelector('#div');
// Test for <pre> tag in non <pre> tag
const elementDivWithPre = target.querySelector('#div-with-pre');
assert.equal( html: `<pre id="pre"> A
elementPre.innerHTML,
` A
B B
<span> <span>
C C
@ -20,11 +11,9 @@ export default {
</span> </span>
E E
F F
` </pre>
);
assert.equal( <div id="div">
elementDiv.innerHTML,
`
A A
B B
<span> <span>
@ -33,11 +22,9 @@ export default {
</span> </span>
E E
F F
` </div>
);
assert.equal( <div id="div-with-pre">
elementDivWithPre.innerHTML,
`
<pre> A <pre> A
B B
<span> <span>
@ -47,7 +34,5 @@ export default {
E E
F F
</pre> </pre>
` </div>`
);
}
}; };

@ -11,7 +11,7 @@ export default {
const p = target.querySelector('p'); const p = target.querySelector('p');
component.raw = '<p>does not change</p>'; component.raw = '<p>does not change</p>';
assert.equal(target.innerHTML, '<div><p>does not change</p></div>'); assert.htmlEqualWithOptions(target.innerHTML, '<div><p>does not change</p></div>', { withoutNormalizeHtml: true });
assert.strictEqual(target.querySelector('p'), p); assert.strictEqual(target.querySelector('p'), p);
} }
}; };

@ -1,4 +1,21 @@
export default { export default {
withoutNormalizeHtml: true,
// Unable to test `html` with `<textarea>` content
// as the textarea#value will not show within `innerHtml`
ssrHtml: `<textarea id="textarea"> A
B
</textarea> <div id="div-with-textarea"><textarea> A
B
</textarea></div> <div id="textarea-with-leading-newline"><textarea>leading newline</textarea> <textarea> leading newline and spaces</textarea> <textarea>
leading newlines</textarea></div> <div id="textarea-without-leading-newline"><textarea>without spaces</textarea> <textarea> with spaces </textarea> <textarea>
newline after leading space</textarea></div> <textarea id="textarea-with-multiple-leading-newlines">
multiple leading newlines</textarea> <div id="div-with-textarea-with-multiple-leading-newlines"><textarea>
multiple leading newlines</textarea></div>`,
test({ assert, target }) { test({ assert, target }) {
// Test for <textarea> tag // Test for <textarea> tag
const elementTextarea = target.querySelector('#textarea'); const elementTextarea = target.querySelector('#textarea');

@ -83,13 +83,7 @@ describe('ssr', () => {
if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code); if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code);
try { try {
if (config.withoutNormalizeHtml) { assert.htmlEqualWithOptions(html, expectedHtml, { preserveComments: compileOptions.preserveComments, withoutNormalizeHtml: config.withoutNormalizeHtml });
assert.strictEqual(html.trim().replace(/\r\n/g, '\n'), expectedHtml.trim().replace(/\r\n/g, '\n'));
} else {
(compileOptions.preserveComments
? assert.htmlEqualWithComments
: assert.htmlEqual)(html, expectedHtml);
}
} catch (error) { } catch (error) {
if (shouldUpdateExpected()) { if (shouldUpdateExpected()) {
fs.writeFileSync(`${dir}/_expected.html`, html); fs.writeFileSync(`${dir}/_expected.html`, html);
@ -117,11 +111,10 @@ describe('ssr', () => {
fs.writeFileSync(`${dir}/_actual-head.html`, head); fs.writeFileSync(`${dir}/_actual-head.html`, head);
try { try {
(compileOptions.hydratable assert.htmlEqualWithOptions(
? assert.htmlEqualWithComments
: assert.htmlEqual)(
head, head,
fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8') fs.readFileSync(`${dir}/_expected-head.html`, 'utf-8'),
{ preserveComments: compileOptions.hydratable }
); );
} catch (error) { } catch (error) {
if (shouldUpdateExpected()) { if (shouldUpdateExpected()) {
@ -214,9 +207,15 @@ describe('ssr', () => {
}); });
if (config.ssrHtml) { if (config.ssrHtml) {
assert.htmlEqual(html, config.ssrHtml); assert.htmlEqualWithOptions(html, config.ssrHtml, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} else if (config.html) { } else if (config.html) {
assert.htmlEqual(html, config.html); assert.htmlEqualWithOptions(html, config.html, {
preserveComments: compileOptions.preserveComments,
withoutNormalizeHtml: config.withoutNormalizeHtml
});
} }
if (config.test_ssr) { if (config.test_ssr) {

@ -1,3 +1 @@
<div data-svelte-h="svelte-156ixv6">Just a dummy page.</div>
<div>Just a dummy page.</div>

@ -1,2 +0,0 @@
[{main.svelte,_expected.html}]
trim_trailing_whitespace = unset

@ -1,3 +0,0 @@
export default {
withoutNormalizeHtml: true
};

@ -1,39 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>A
B
<span>C
D
</span>
E
F
</div>
<div><pre> A
B
<span>
C
D
</span>
E
F
</pre></div>
<div id="pre-with-leading-newline"><pre>leading newline</pre>
<pre> leading newline and spaces</pre>
<pre>
leading newlines</pre></div>
<div id="pre-without-leading-newline"><pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre></div>

@ -1,51 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>
<div id="pre-with-leading-newline">
<pre>
leading newline</pre>
<pre>
leading newline and spaces</pre>
<pre>
leading newlines</pre>
</div>
<div id="pre-without-leading-newline">
<pre>without spaces</pre>
<pre> with spaces </pre>
<pre>
newline after leading space</pre>
</div>

@ -1,6 +0,0 @@
export default {
withoutNormalizeHtml: true,
compileOptions: {
preserveWhitespace: true
}
};

@ -1,32 +0,0 @@
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre> A
B
<span>
C
D
</span>
E
F
</pre>
</div>

@ -1,34 +0,0 @@
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
<div>
A
B
<span>
C
D
</span>
E
F
</div>
<div>
<pre>
A
B
<span>
C
D
</span>
E
F
</pre>
</div>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save