diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index b21a1aa24b..9363015b1c 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -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 diff --git a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts index fd43ffcc32..1315f1e144 100644 --- a/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts +++ b/src/compiler/compile/render_dom/wrappers/RawMustacheTag.ts @@ -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) { diff --git a/src/compiler/compile/render_ssr/handlers/Element.ts b/src/compiler/compile/render_ssr/handlers/Element.ts index d10c165198..a0bae4044a 100644 --- a/src/compiler/compile/render_ssr/handlers/Element.ts +++ b/src/compiler/compile/render_ssr/handlers/Element.ts @@ -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) { @@ -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)} }`); } } }); @@ -64,10 +67,11 @@ 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 && @@ -75,17 +79,17 @@ export default function(node: Element, renderer: Renderer, options: RenderOption ) { // 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('"'); } diff --git a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts index c0a7952e65..cd62b95981 100644 --- a/src/compiler/compile/render_ssr/handlers/HtmlTag.ts +++ b/src/compiler/compile/render_ssr/handlers/HtmlTag.ts @@ -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(''); renderer.add_expression(node.expression.node as Expression); + if (options.hydratable) renderer.add_string(''); } diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 40471c2980..1b4c4451bc 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -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; @@ -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); } diff --git a/test/js/samples/each-block-changed-check/expected.js b/test/js/samples/each-block-changed-check/expected.js index 63bc1d8607..0020235d44 100644 --- a/test/js/samples/each-block-changed-check/expected.js +++ b/test/js/samples/each-block-changed-check/expected.js @@ -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) { diff --git a/test/runtime/index.ts b/test/runtime/index.ts index 153ac9dbb8..ff6d1039d3 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -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'); @@ -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 => { @@ -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 = '
') { diff --git a/test/runtime/samples/attribute-boolean-indeterminate/_config.js b/test/runtime/samples/attribute-boolean-indeterminate/_config.js index d16cb5f96b..d6e97ffc0e 100644 --- a/test/runtime/samples/attribute-boolean-indeterminate/_config.js +++ b/test/runtime/samples/attribute-boolean-indeterminate/_config.js @@ -7,9 +7,11 @@ export default { indeterminate: true }, - html: ` - - `, + html: "", + + // somehow ssr will render indeterminate="" + // the hydrated html will still contain that attribute + ssrHtml: "", test({ assert, component, target }) { const input = target.querySelector('input'); diff --git a/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js b/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js index 0439aca06a..9f8ee61c7a 100644 --- a/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js +++ b/test/runtime/samples/attribute-casing-foreign-namespace-compiler-option/_config.js @@ -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' }, diff --git a/test/runtime/samples/attribute-casing-foreign-namespace/_config.js b/test/runtime/samples/attribute-casing-foreign-namespace/_config.js index d7eca6aba9..f74f622524 100644 --- a/test/runtime/samples/attribute-casing-foreign-namespace/_config.js +++ b/test/runtime/samples/attribute-casing-foreign-namespace/_config.js @@ -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; diff --git a/test/runtime/samples/attribute-dynamic-type/_config.js b/test/runtime/samples/attribute-dynamic-type/_config.js index 19aa33d65e..fa00cb6bc3 100644 --- a/test/runtime/samples/attribute-dynamic-type/_config.js +++ b/test/runtime/samples/attribute-dynamic-type/_config.js @@ -1,12 +1,11 @@ export default { - skip_if_ssr: true, - props: { inputType: 'text', inputValue: 42 }, html: '', + ssrHtml: '', test({ assert, component, target }) { const input = target.querySelector('input'); diff --git a/test/runtime/samples/binding-this-each-block-property-2/_config.js b/test/runtime/samples/binding-this-each-block-property-2/_config.js index b67d27050e..49131d6635 100644 --- a/test/runtime/samples/binding-this-each-block-property-2/_config.js +++ b/test/runtime/samples/binding-this-each-block-property-2/_config.js @@ -7,7 +7,7 @@ export default { props: { callback }, - after_test() { + before_test() { calls = []; }, async test({ assert, component, target }) { diff --git a/test/runtime/samples/component-namespaced/_config.js b/test/runtime/samples/component-namespaced/_config.js index 35a5e46d47..7ec4a35c6d 100644 --- a/test/runtime/samples/component-namespaced/_config.js +++ b/test/runtime/samples/component-namespaced/_config.js @@ -1,5 +1,3 @@ -import * as path from 'path'; - export default { props: { a: 1 @@ -9,10 +7,6 @@ export default {foo 1
`, - before_test() { - delete require.cache[path.resolve(__dirname, 'components.js')]; - }, - test({ assert, component, target }) { component.a = 2; assert.htmlEqual(target.innerHTML, ` diff --git a/test/runtime/samples/component-namespaced/components.js b/test/runtime/samples/component-namespaced/components.js deleted file mode 100644 index 7dcfcf157c..0000000000 --- a/test/runtime/samples/component-namespaced/components.js +++ /dev/null @@ -1,3 +0,0 @@ -import Foo from './Foo.svelte'; - -export default { Foo }; diff --git a/test/runtime/samples/component-namespaced/components.svelte b/test/runtime/samples/component-namespaced/components.svelte new file mode 100644 index 0000000000..5b9a6c5167 --- /dev/null +++ b/test/runtime/samples/component-namespaced/components.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/test/runtime/samples/component-namespaced/main.svelte b/test/runtime/samples/component-namespaced/main.svelte index 541b68e47e..25862cf6f2 100644 --- a/test/runtime/samples/component-namespaced/main.svelte +++ b/test/runtime/samples/component-namespaced/main.svelte @@ -1,5 +1,5 @@ diff --git a/test/runtime/samples/deconflict-builtins-2/_config.js b/test/runtime/samples/deconflict-builtins-2/_config.js index e136b0410d..fba811b880 100644 --- a/test/runtime/samples/deconflict-builtins-2/_config.js +++ b/test/runtime/samples/deconflict-builtins-2/_config.js @@ -1,4 +1,4 @@ export default { - html: 'potato
', + before_test() { + count = 0; + }, test({ assert, component, target }) { assert.equal(count, 1); diff --git a/test/runtime/samples/if-block-else-conservative-update/_config.js b/test/runtime/samples/if-block-else-conservative-update/_config.js index da83385dd5..b3db0bca54 100644 --- a/test/runtime/samples/if-block-else-conservative-update/_config.js +++ b/test/runtime/samples/if-block-else-conservative-update/_config.js @@ -17,6 +17,11 @@ export default { html: 'potato
', + before_test() { + count_a = 0; + count_b = 0; + }, + test({ assert, component, target }) { assert.equal(count_a, 1); assert.equal(count_b, 0); diff --git a/test/runtime/samples/lifecycle-render-order-for-children/_config.js b/test/runtime/samples/lifecycle-render-order-for-children/_config.js index d182d5a97e..b85c91d07f 100644 --- a/test/runtime/samples/lifecycle-render-order-for-children/_config.js +++ b/test/runtime/samples/lifecycle-render-order-for-children/_config.js @@ -2,7 +2,9 @@ import order from './order.js'; export default { skip_if_ssr: true, - + before_test() { + order.length = 0; + }, test({ assert, component, target, compileOptions }) { if (compileOptions.hydratable) { assert.deepEqual(order, [ @@ -43,7 +45,5 @@ export default { '0: afterUpdate' ]); } - - order.length = 0; } }; diff --git a/test/runtime/samples/lifecycle-render-order/_config.js b/test/runtime/samples/lifecycle-render-order/_config.js index 5080973cef..2bbab7a838 100644 --- a/test/runtime/samples/lifecycle-render-order/_config.js +++ b/test/runtime/samples/lifecycle-render-order/_config.js @@ -3,6 +3,9 @@ import order from './order.js'; export default { skip_if_ssr: true, + before_test() { + order.length = 0; + }, test({ assert }) { assert.deepEqual(order, [ 'beforeUpdate', @@ -10,7 +13,5 @@ export default { 'onMount', 'afterUpdate' ]); - - order.length = 0; } }; diff --git a/test/runtime/samples/noscript-removal/_config.js b/test/runtime/samples/noscript-removal/_config.js index 709792c14a..0fd03ea223 100644 --- a/test/runtime/samples/noscript-removal/_config.js +++ b/test/runtime/samples/noscript-removal/_config.js @@ -1,9 +1,33 @@ export default { - skip_if_ssr: true, + ssrHtml: ` + - html: ` -count: 0
', + before_test() { + count.set(0); + }, + async test({ assert, component, target }) { await component.increment(); assert.htmlEqual(target.innerHTML, 'count: 1
'); - - count.set(0); } };