Skip to content

Commit

Permalink
Use SSR rendered as initial html for runtime hydration test (#4444)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanhauhau authored Jun 19, 2021
1 parent 8dd9c1b commit 3f990a9
Show file tree
Hide file tree
Showing 29 changed files with 159 additions and 55 deletions.
6 changes: 3 additions & 3 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,9 @@ export default class ElementWrapper extends Wrapper {
}

get_claim_statement(nodes: Identifier) {
const attributes = this.node.attributes
.filter((attr) => attr.type === 'Attribute')
.map((attr) => p`${attr.name}: true`);
const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);

const name = this.node.namespace
? this.node.name
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ export default class RawMustacheTagWrapper extends Tag {

const update_anchor = needs_anchor ? html_anchor : this.next ? this.next.var : 'null';

block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${update_anchor});`);
block.chunks.create.push(b`${html_tag} = new @HtmlTag();`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`${html_tag} = @claim_html_tag(${_parent_nodes});`);
}
block.chunks.hydrate.push(b`${html_tag}.a = ${update_anchor};`);
block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`);

if (needs_anchor) {
Expand Down
20 changes: 12 additions & 8 deletions src/compiler/compile/render_ssr/handlers/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Element from '../../nodes/Element';
import { x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
import { namespaces } from '../../../utils/namespaces';

export default function(node: Element, renderer: Renderer, options: RenderOptions) {

Expand Down Expand Up @@ -41,20 +43,21 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (attribute.is_spread) {
args.push(attribute.expression.node);
} else {
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
const name = attribute.name.toLowerCase();
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
args.push(x`{ ${attribute.name}: true }`);
args.push(x`{ ${attr_name}: true }`);
} else if (
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} || null }`);
args.push(x`{ ${attr_name}: ${(attribute.chunks[0] as Expression).node} || null }`);
} else {
args.push(x`{ ${attribute.name}: ${get_attribute_value(attribute)} }`);
args.push(x`{ ${attr_name}: ${get_attribute_value(attribute)} }`);
}
}
});
Expand All @@ -64,28 +67,29 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
let add_class_attribute = !!class_expression;
node.attributes.forEach(attribute => {
const name = attribute.name.toLowerCase();
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
node_contents = get_attribute_value(attribute);
} else if (attribute.is_true) {
renderer.add_string(` ${attribute.name}`);
renderer.add_string(` ${attr_name}`);
} else if (
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
renderer.add_string(' ');
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`);
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attr_name}" : ""`);
} else if (name === 'class' && class_expression) {
add_class_attribute = false;
renderer.add_string(` ${attribute.name}="`);
renderer.add_string(` ${attr_name}="`);
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string('"');
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(x`@add_attribute("${attribute.name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
} else {
renderer.add_string(` ${attribute.name}="`);
renderer.add_string(` ${attr_name}="`);
renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute));
renderer.add_string('"');
}
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/compile/render_ssr/handlers/HtmlTag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import Renderer, { RenderOptions } from '../Renderer';
import RawMustacheTag from '../../nodes/RawMustacheTag';
import { Expression } from 'estree';

export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) {
export default function(node: RawMustacheTag, renderer: Renderer, options: RenderOptions) {
if (options.hydratable) renderer.add_string('<!-- HTML_TAG_START -->');
renderer.add_expression(node.expression.node as Expression);
if (options.hydratable) renderer.add_string('<!-- HTML_TAG_END -->');
}
41 changes: 37 additions & 4 deletions src/runtime/internal/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,29 @@ export function claim_space(nodes) {
return claim_text(nodes, ' ');
}

function find_comment(nodes, text, start) {
for (let i = start; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) {
return i;
}
}
return nodes.length;
}

export function claim_html_tag(nodes) {
// find html opening tag
const start_index = find_comment(nodes, 'HTML_TAG_START', 0);
const end_index = find_comment(nodes, 'HTML_TAG_END', start_index);
if (start_index === end_index) {
return new HtmlTag();
}
const html_tag_nodes = nodes.splice(start_index, end_index + 1);
detach(html_tag_nodes[0]);
detach(html_tag_nodes[html_tag_nodes.length - 1]);
return new HtmlTag(html_tag_nodes.slice(1, html_tag_nodes.length - 1));
}

export function set_data(text, data) {
data = '' + data;
if (text.wholeText !== data) text.data = data;
Expand Down Expand Up @@ -318,27 +341,37 @@ export function query_selector_all(selector: string, parent: HTMLElement = docum
}

export class HtmlTag {
// parent for creating node
e: HTMLElement;
// html tag nodes
n: ChildNode[];
// hydration claimed nodes
l: ChildNode[] | void;
// target
t: HTMLElement;
// anchor
a: HTMLElement;

constructor(anchor: HTMLElement = null) {
this.a = anchor;
constructor(claimed_nodes?: ChildNode[]) {
this.e = this.n = null;
this.l = claimed_nodes;
}

m(html: string, target: HTMLElement, anchor: HTMLElement = null) {
if (!this.e) {
this.e = element(target.nodeName as keyof HTMLElementTagNameMap);
this.t = target;
this.h(html);
if (this.l) {
this.n = this.l;
} else {
this.h(html);
}
}

this.i(anchor);
}

h(html) {
h(html: string) {
this.e.innerHTML = html;
this.n = Array.from(this.e.childNodes);
}
Expand Down
3 changes: 2 additions & 1 deletion test/js/samples/each-block-changed-check/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ function create_each_block(ctx) {
t4 = text(t4_value);
t5 = text(" ago:");
t6 = space();
html_tag = new HtmlTag();
attr(span, "class", "meta");
html_tag = new HtmlTag(null);
html_tag.a = null;
attr(div, "class", "comment");
},
m(target, anchor) {
Expand Down
25 changes: 20 additions & 5 deletions test/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,21 @@ describe('runtime', () => {

const failed = new Set();

function runTest(dir, hydrate) {
function runTest(dir, hydrate, from_ssr_html) {
if (dir[0] === '.') return;

const config = loadConfig(`${__dirname}/samples/${dir}/_config.js`);
const solo = config.solo || /\.solo/.test(dir);

if (hydrate && config.skip_if_hydrate) return;
if (hydrate && from_ssr_html && config.skip_if_hydrate_from_ssr) return;

if (solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}

(config.skip ? it.skip : solo ? it.only : it)(`${dir} ${hydrate ? '(with hydration)' : ''}`, () => {
const testName = `${dir} ${hydrate ? `(with hydration${from_ssr_html ? ' from ssr rendered html' : ''})` : ''}`;
(config.skip ? it.skip : solo ? it.only : it)(testName, () => {
if (failed.has(dir)) {
// this makes debugging easier, by only printing compiled output once
throw new Error('skipping test, already failed');
Expand Down Expand Up @@ -146,13 +148,25 @@ describe('runtime', () => {
throw err;
}

if (config.before_test) config.before_test();

// Put things we need on window for testing
window.SvelteComponent = SvelteComponent;

const target = window.document.querySelector('main');

if (hydrate && from_ssr_html) {
// ssr into target
compileOptions.generate = 'ssr';
cleanRequireCache();
const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default;
const { html } = SsrSvelteComponent.render(config.props);
target.innerHTML = html;
delete compileOptions.generate;
} else {
target.innerHTML = '';
}

if (config.before_test) config.before_test();

const warnings = [];
const warn = console.warn;
console.warn = warning => {
Expand Down Expand Up @@ -245,7 +259,8 @@ describe('runtime', () => {

fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
runTest(dir, false);
runTest(dir, true);
runTest(dir, true, false);
runTest(dir, true, true);
});

async function create_component(src = '<div></div>') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ export default {
indeterminate: true
},

html: `
<input type='checkbox'>
`,
html: "<input type='checkbox'>",

// somehow ssr will render indeterminate=""
// the hydrated html will still contain that attribute
ssrHtml: "<input type='checkbox' indeterminate=''>",

test({ assert, component, target }) {
const input = target.querySelector('input');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
skip_if_hydrate_from_ssr: true,
compileOptions: {
namespace: 'foreign'
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default {
options: {
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
},
skip_if_hydrate_from_ssr: true,

test({ assert, target }) {
const attr = sel => target.querySelector(sel).attributes[0].name;
Expand Down
3 changes: 1 addition & 2 deletions test/runtime/samples/attribute-dynamic-type/_config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
export default {
skip_if_ssr: true,

props: {
inputType: 'text',
inputValue: 42
},

html: '<input type="text">',
ssrHtml: '<input type="text" value="42">',

test({ assert, component, target }) {
const input = target.querySelector('input');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default {
props: {
callback
},
after_test() {
before_test() {
calls = [];
},
async test({ assert, component, target }) {
Expand Down
6 changes: 0 additions & 6 deletions test/runtime/samples/component-namespaced/_config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as path from 'path';

export default {
props: {
a: 1
Expand All @@ -9,10 +7,6 @@ export default {
<p>foo 1</p>
`,

before_test() {
delete require.cache[path.resolve(__dirname, 'components.js')];
},

test({ assert, component, target }) {
component.a = 2;
assert.htmlEqual(target.innerHTML, `
Expand Down
3 changes: 0 additions & 3 deletions test/runtime/samples/component-namespaced/components.js

This file was deleted.

5 changes: 5 additions & 0 deletions test/runtime/samples/component-namespaced/components.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script context="module">
import Foo from './Foo.svelte';
export const Components = { Foo };
</script>
2 changes: 1 addition & 1 deletion test/runtime/samples/component-namespaced/main.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import Components from './components.js';
import { Components } from './components.svelte';
export let a;
</script>
Expand Down
2 changes: 1 addition & 1 deletion test/runtime/samples/deconflict-builtins-2/_config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
html: '<text>hello world</text>',
html: '<svg><text>hello world</text></svg>',
preserveIdentifiers: true
};
5 changes: 3 additions & 2 deletions test/runtime/samples/deconflict-builtins-2/main.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script>
let foo = 'hello world'
</script>

<text>{foo}</text>
<svg>
<text>{foo}</text>
</svg>
4 changes: 4 additions & 0 deletions test/runtime/samples/each-block-keyed-dyanmic-key/_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default {
}
},

before_test() {
count = 0;
},

html: `
<div>foo</div>
<div>foo</div>
Expand Down
3 changes: 3 additions & 0 deletions test/runtime/samples/if-block-conservative-update/_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export default {

html: '<p>potato</p>',

before_test() {
count = 0;
},
test({ assert, component, target }) {
assert.equal(count, 1);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export default {

html: '<p>potato</p>',

before_test() {
count_a = 0;
count_b = 0;
},

test({ assert, component, target }) {
assert.equal(count_a, 1);
assert.equal(count_b, 0);
Expand Down
Loading

0 comments on commit 3f990a9

Please sign in to comment.