Skip to content

Commit 3f990a9

Browse files
authored
Use SSR rendered as initial html for runtime hydration test (#4444)
1 parent 8dd9c1b commit 3f990a9

File tree

29 files changed

+159
-55
lines changed

29 files changed

+159
-55
lines changed

src/compiler/compile/render_dom/wrappers/Element/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,9 +372,9 @@ export default class ElementWrapper extends Wrapper {
372372
}
373373

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

379379
const name = this.node.namespace
380380
? this.node.name

src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ export default class RawMustacheTagWrapper extends Tag {
5151

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

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

5761
if (needs_anchor) {

src/compiler/compile/render_ssr/handlers/Element.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Element from '../../nodes/Element';
66
import { x } from 'code-red';
77
import Expression from '../../nodes/shared/Expression';
88
import remove_whitespace_children from './utils/remove_whitespace_children';
9+
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
10+
import { namespaces } from '../../../utils/namespaces';
911

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

@@ -41,20 +43,21 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
4143
if (attribute.is_spread) {
4244
args.push(attribute.expression.node);
4345
} else {
46+
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
4447
const name = attribute.name.toLowerCase();
4548
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
4649
node_contents = get_attribute_value(attribute);
4750
} else if (attribute.is_true) {
48-
args.push(x`{ ${attribute.name}: true }`);
51+
args.push(x`{ ${attr_name}: true }`);
4952
} else if (
5053
boolean_attributes.has(name) &&
5154
attribute.chunks.length === 1 &&
5255
attribute.chunks[0].type !== 'Text'
5356
) {
5457
// a boolean attribute with one non-Text chunk
55-
args.push(x`{ ${attribute.name}: ${(attribute.chunks[0] as Expression).node} || null }`);
58+
args.push(x`{ ${attr_name}: ${(attribute.chunks[0] as Expression).node} || null }`);
5659
} else {
57-
args.push(x`{ ${attribute.name}: ${get_attribute_value(attribute)} }`);
60+
args.push(x`{ ${attr_name}: ${get_attribute_value(attribute)} }`);
5861
}
5962
}
6063
});
@@ -64,28 +67,29 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
6467
let add_class_attribute = !!class_expression;
6568
node.attributes.forEach(attribute => {
6669
const name = attribute.name.toLowerCase();
70+
const attr_name = node.namespace === namespaces.foreign ? attribute.name : fix_attribute_casing(attribute.name);
6771
if (name === 'value' && node.name.toLowerCase() === 'textarea') {
6872
node_contents = get_attribute_value(attribute);
6973
} else if (attribute.is_true) {
70-
renderer.add_string(` ${attribute.name}`);
74+
renderer.add_string(` ${attr_name}`);
7175
} else if (
7276
boolean_attributes.has(name) &&
7377
attribute.chunks.length === 1 &&
7478
attribute.chunks[0].type !== 'Text'
7579
) {
7680
// a boolean attribute with one non-Text chunk
7781
renderer.add_string(' ');
78-
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attribute.name}" : ""`);
82+
renderer.add_expression(x`${(attribute.chunks[0] as Expression).node} ? "${attr_name}" : ""`);
7983
} else if (name === 'class' && class_expression) {
8084
add_class_attribute = false;
81-
renderer.add_string(` ${attribute.name}="`);
85+
renderer.add_string(` ${attr_name}="`);
8286
renderer.add_expression(x`[${get_class_attribute_value(attribute)}, ${class_expression}].join(' ').trim()`);
8387
renderer.add_string('"');
8488
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
8589
const snippet = (attribute.chunks[0] as Expression).node;
86-
renderer.add_expression(x`@add_attribute("${attribute.name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
90+
renderer.add_expression(x`@add_attribute("${attr_name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
8791
} else {
88-
renderer.add_string(` ${attribute.name}="`);
92+
renderer.add_string(` ${attr_name}="`);
8993
renderer.add_expression((name === 'class' ? get_class_attribute_value : get_attribute_value)(attribute));
9094
renderer.add_string('"');
9195
}

src/compiler/compile/render_ssr/handlers/HtmlTag.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Renderer, { RenderOptions } from '../Renderer';
22
import RawMustacheTag from '../../nodes/RawMustacheTag';
33
import { Expression } from 'estree';
44

5-
export default function(node: RawMustacheTag, renderer: Renderer, _options: RenderOptions) {
5+
export default function(node: RawMustacheTag, renderer: Renderer, options: RenderOptions) {
6+
if (options.hydratable) renderer.add_string('<!-- HTML_TAG_START -->');
67
renderer.add_expression(node.expression.node as Expression);
8+
if (options.hydratable) renderer.add_string('<!-- HTML_TAG_END -->');
79
}

src/runtime/internal/dom.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,29 @@ export function claim_space(nodes) {
191191
return claim_text(nodes, ' ');
192192
}
193193

194+
function find_comment(nodes, text, start) {
195+
for (let i = start; i < nodes.length; i += 1) {
196+
const node = nodes[i];
197+
if (node.nodeType === 8 /* comment node */ && node.textContent.trim() === text) {
198+
return i;
199+
}
200+
}
201+
return nodes.length;
202+
}
203+
204+
export function claim_html_tag(nodes) {
205+
// find html opening tag
206+
const start_index = find_comment(nodes, 'HTML_TAG_START', 0);
207+
const end_index = find_comment(nodes, 'HTML_TAG_END', start_index);
208+
if (start_index === end_index) {
209+
return new HtmlTag();
210+
}
211+
const html_tag_nodes = nodes.splice(start_index, end_index + 1);
212+
detach(html_tag_nodes[0]);
213+
detach(html_tag_nodes[html_tag_nodes.length - 1]);
214+
return new HtmlTag(html_tag_nodes.slice(1, html_tag_nodes.length - 1));
215+
}
216+
194217
export function set_data(text, data) {
195218
data = '' + data;
196219
if (text.wholeText !== data) text.data = data;
@@ -318,27 +341,37 @@ export function query_selector_all(selector: string, parent: HTMLElement = docum
318341
}
319342

320343
export class HtmlTag {
344+
// parent for creating node
321345
e: HTMLElement;
346+
// html tag nodes
322347
n: ChildNode[];
348+
// hydration claimed nodes
349+
l: ChildNode[] | void;
350+
// target
323351
t: HTMLElement;
352+
// anchor
324353
a: HTMLElement;
325354

326-
constructor(anchor: HTMLElement = null) {
327-
this.a = anchor;
355+
constructor(claimed_nodes?: ChildNode[]) {
328356
this.e = this.n = null;
357+
this.l = claimed_nodes;
329358
}
330359

331360
m(html: string, target: HTMLElement, anchor: HTMLElement = null) {
332361
if (!this.e) {
333362
this.e = element(target.nodeName as keyof HTMLElementTagNameMap);
334363
this.t = target;
335-
this.h(html);
364+
if (this.l) {
365+
this.n = this.l;
366+
} else {
367+
this.h(html);
368+
}
336369
}
337370

338371
this.i(anchor);
339372
}
340373

341-
h(html) {
374+
h(html: string) {
342375
this.e.innerHTML = html;
343376
this.n = Array.from(this.e.childNodes);
344377
}

test/js/samples/each-block-changed-check/expected.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ function create_each_block(ctx) {
5252
t4 = text(t4_value);
5353
t5 = text(" ago:");
5454
t6 = space();
55+
html_tag = new HtmlTag();
5556
attr(span, "class", "meta");
56-
html_tag = new HtmlTag(null);
57+
html_tag.a = null;
5758
attr(div, "class", "comment");
5859
},
5960
m(target, anchor) {

test/runtime/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,21 @@ describe('runtime', () => {
4949

5050
const failed = new Set();
5151

52-
function runTest(dir, hydrate) {
52+
function runTest(dir, hydrate, from_ssr_html) {
5353
if (dir[0] === '.') return;
5454

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

5858
if (hydrate && config.skip_if_hydrate) return;
59+
if (hydrate && from_ssr_html && config.skip_if_hydrate_from_ssr) return;
5960

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

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

149-
if (config.before_test) config.before_test();
150-
151151
// Put things we need on window for testing
152152
window.SvelteComponent = SvelteComponent;
153153

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

156+
if (hydrate && from_ssr_html) {
157+
// ssr into target
158+
compileOptions.generate = 'ssr';
159+
cleanRequireCache();
160+
const SsrSvelteComponent = require(`./samples/${dir}/main.svelte`).default;
161+
const { html } = SsrSvelteComponent.render(config.props);
162+
target.innerHTML = html;
163+
delete compileOptions.generate;
164+
} else {
165+
target.innerHTML = '';
166+
}
167+
168+
if (config.before_test) config.before_test();
169+
156170
const warnings = [];
157171
const warn = console.warn;
158172
console.warn = warning => {
@@ -245,7 +259,8 @@ describe('runtime', () => {
245259

246260
fs.readdirSync(`${__dirname}/samples`).forEach(dir => {
247261
runTest(dir, false);
248-
runTest(dir, true);
262+
runTest(dir, true, false);
263+
runTest(dir, true, true);
249264
});
250265

251266
async function create_component(src = '<div></div>') {

test/runtime/samples/attribute-boolean-indeterminate/_config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ export default {
77
indeterminate: true
88
},
99

10-
html: `
11-
<input type='checkbox'>
12-
`,
10+
html: "<input type='checkbox'>",
11+
12+
// somehow ssr will render indeterminate=""
13+
// the hydrated html will still contain that attribute
14+
ssrHtml: "<input type='checkbox' indeterminate=''>",
1315

1416
test({ assert, component, target }) {
1517
const input = target.querySelector('input');

test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default {
1111
options: {
1212
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
1313
},
14+
skip_if_hydrate_from_ssr: true,
1415
compileOptions: {
1516
namespace: 'foreign'
1617
},

test/runtime/samples/attribute-casing-foreign-namespace/_config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export default {
99
options: {
1010
hydrate: false // Hydration test will fail as case sensitivity is only handled for svg elements.
1111
},
12+
skip_if_hydrate_from_ssr: true,
1213

1314
test({ assert, target }) {
1415
const attr = sel => target.querySelector(sel).attributes[0].name;

0 commit comments

Comments
 (0)