From 183225cafe1f66d136b98d2a76619dab7b390382 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 28 Apr 2018 17:50:35 -0400 Subject: [PATCH] move SSR logic into nodes --- src/generators/nodes/Attribute.ts | 14 ++- src/generators/nodes/AwaitBlock.ts | 20 ++++ src/generators/nodes/Comment.ts | 7 ++ src/generators/nodes/Component.ts | 102 ++++++++++++++++ src/generators/nodes/EachBlock.ts | 33 +++++ src/generators/nodes/Element.ts | 83 +++++++++++++ src/generators/nodes/Head.ts | 10 ++ src/generators/nodes/IfBlock.ts | 24 ++++ src/generators/nodes/MustacheTag.ts | 10 ++ src/generators/nodes/RawMustacheTag.ts | 4 + src/generators/nodes/Slot.ts | 13 ++ src/generators/nodes/Text.ts | 15 ++- src/generators/nodes/Title.ts | 10 ++ src/generators/nodes/Window.ts | 4 + src/generators/server-side-rendering/index.ts | 2 +- src/generators/server-side-rendering/visit.ts | 13 -- .../visitors/AwaitBlock.ts | 28 ----- .../server-side-rendering/visitors/Comment.ts | 14 --- .../visitors/Component.ts | 113 ------------------ .../visitors/EachBlock.ts | 41 ------- .../server-side-rendering/visitors/Element.ts | 103 ---------------- .../server-side-rendering/visitors/Head.ts | 19 --- .../server-side-rendering/visitors/IfBlock.ts | 32 ----- .../visitors/MustacheTag.ts | 19 --- .../visitors/RawMustacheTag.ts | 13 -- .../server-side-rendering/visitors/Slot.ts | 21 ---- .../server-side-rendering/visitors/Text.ts | 21 ---- .../server-side-rendering/visitors/Title.ts | 19 --- .../server-side-rendering/visitors/Window.ts | 3 - .../server-side-rendering/visitors/index.ts | 29 ----- .../shared/stringifyAttributeValue.ts | 15 --- 31 files changed, 348 insertions(+), 506 deletions(-) delete mode 100644 src/generators/server-side-rendering/visit.ts delete mode 100644 src/generators/server-side-rendering/visitors/AwaitBlock.ts delete mode 100644 src/generators/server-side-rendering/visitors/Comment.ts delete mode 100644 src/generators/server-side-rendering/visitors/Component.ts delete mode 100644 src/generators/server-side-rendering/visitors/EachBlock.ts delete mode 100644 src/generators/server-side-rendering/visitors/Element.ts delete mode 100644 src/generators/server-side-rendering/visitors/Head.ts delete mode 100644 src/generators/server-side-rendering/visitors/IfBlock.ts delete mode 100644 src/generators/server-side-rendering/visitors/MustacheTag.ts delete mode 100644 src/generators/server-side-rendering/visitors/RawMustacheTag.ts delete mode 100644 src/generators/server-side-rendering/visitors/Slot.ts delete mode 100644 src/generators/server-side-rendering/visitors/Text.ts delete mode 100644 src/generators/server-side-rendering/visitors/Title.ts delete mode 100644 src/generators/server-side-rendering/visitors/Window.ts delete mode 100644 src/generators/server-side-rendering/visitors/index.ts delete mode 100644 src/generators/server-side-rendering/visitors/shared/stringifyAttributeValue.ts diff --git a/src/generators/nodes/Attribute.ts b/src/generators/nodes/Attribute.ts index 5ddbaa4f85..daecfd17f6 100644 --- a/src/generators/nodes/Attribute.ts +++ b/src/generators/nodes/Attribute.ts @@ -1,5 +1,5 @@ import deindent from '../../utils/deindent'; -import { stringify } from '../../utils/stringify'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import fixAttributeCasing from '../../utils/fixAttributeCasing'; import addToSet from '../../utils/addToSet'; import { DomGenerator } from '../dom/index'; @@ -332,6 +332,18 @@ export default class Attribute extends Node { ); }); } + + stringifyForSsr() { + return this.chunks + .map((chunk: Node) => { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data).replace(/"/g, '"')); + } + + return '${__escape(' + chunk.snippet + ')}'; + }) + .join(''); + } } // source: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes diff --git a/src/generators/nodes/AwaitBlock.ts b/src/generators/nodes/AwaitBlock.ts index 0fe616a049..ebb05d45d7 100644 --- a/src/generators/nodes/AwaitBlock.ts +++ b/src/generators/nodes/AwaitBlock.ts @@ -219,4 +219,24 @@ export default class AwaitBlock extends Node { }); }); } + + ssr(compiler, block) { + const { snippet } = this.expression; + + const childBlock = block.child({}); + + compiler.append('${(function(__value) { if(__isPromise(__value)) return `'); + + this.pending.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + + compiler.append('`; return function(ctx) { return `'); + + this.then.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + + compiler.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`); + } } diff --git a/src/generators/nodes/Comment.ts b/src/generators/nodes/Comment.ts index 31289c1110..4dfd493602 100644 --- a/src/generators/nodes/Comment.ts +++ b/src/generators/nodes/Comment.ts @@ -8,4 +8,11 @@ export default class Comment extends Node { super(compiler, parent, scope, info); this.data = info.data; } + + ssr(compiler) { + // Allow option to preserve comments, otherwise ignore + if (compiler.options.preserveComments) { + compiler.append(``); + } + } } \ No newline at end of file diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index c4a401f897..2832efc735 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -6,6 +6,7 @@ import CodeBuilder from '../../utils/CodeBuilder'; import getTailSnippet from '../../utils/getTailSnippet'; import getObject from '../../utils/getObject'; import quoteIfNecessary from '../../utils/quoteIfNecessary'; +import { escape, escapeTemplate, stringify } from '../../utils/stringify'; import Node from './shared/Node'; import Block from '../dom/Block'; import Attribute from './Attribute'; @@ -14,6 +15,7 @@ import mapChildren from './shared/mapChildren'; import Binding from './Binding'; import EventHandler from './EventHandler'; import Expression from './shared/Expression'; +import { AppendTarget } from '../server-side-rendering/interfaces'; export default class Component extends Node { type: 'Component'; @@ -480,6 +482,106 @@ export default class Component extends Node { remount(name: string) { return `${this.var}._mount(${name}._slotted.default, null);`; } + + ssr(compiler, block) { + function stringifyAttribute(chunk: Node) { + if (chunk.type === 'Text') { + return escapeTemplate(escape(chunk.data)); + } + + return '${__escape( ' + chunk.snippet + ')}'; + } + + const bindingProps = this.bindings.map(binding => { + const { name } = getObject(binding.value.node); + const tail = binding.value.node.type === 'MemberExpression' + ? getTailSnippet(binding.value.node) + : ''; + + return `${binding.name}: ctx.${name}${tail}`; + }); + + function getAttributeValue(attribute) { + if (attribute.isTrue) return `true`; + if (attribute.chunks.length === 0) return `''`; + + if (attribute.chunks.length === 1) { + const chunk = attribute.chunks[0]; + if (chunk.type === 'Text') { + return stringify(chunk.data); + } + + return chunk.snippet; + } + + return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`'; + } + + const usesSpread = this.attributes.find(attr => attr.isSpread); + + const props = usesSpread + ? `Object.assign(${ + this.attributes + .map(attribute => { + if (attribute.isSpread) { + return attribute.expression.snippet; + } else { + return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`; + } + }) + .concat(bindingProps.map(p => `{ ${p} }`)) + .join(', ') + })` + : `{ ${this.attributes + .map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`) + .concat(bindingProps) + .join(', ')} }`; + + const isDynamicComponent = this.name === 'svelte:component'; + + const expression = ( + this.name === 'svelte:self' ? compiler.name : + isDynamicComponent ? `((${this.expression.snippet}) || __missingComponent)` : + `%components-${this.name}` + ); + + this.bindings.forEach(binding => { + block.addBinding(binding, expression); + }); + + let open = `\${${expression}._render(__result, ${props}`; + + const options = []; + options.push(`store: options.store`); + + if (this.children.length) { + const appendTarget: AppendTarget = { + slots: { default: '' }, + slotStack: ['default'] + }; + + compiler.appendTargets.push(appendTarget); + + this.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + + const slotted = Object.keys(appendTarget.slots) + .map(name => `${name}: () => \`${appendTarget.slots[name]}\``) + .join(', '); + + options.push(`slotted: { ${slotted} }`); + + compiler.appendTargets.pop(); + } + + if (options.length) { + open += `, { ${options.join(', ')} }`; + } + + compiler.append(open); + compiler.append(')}'); + } } function isComputed(node: Node) { diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index 394b82369b..81dcdce0ad 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -472,4 +472,37 @@ export default class EachBlock extends Node { // TODO consider keyed blocks return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`; } + + ssr(compiler, block) { + const { snippet } = this.expression; + + const props = [`${this.context}: item`] + .concat(this.destructuredContexts.map((name, i) => `${name}: item[${i}]`)); + + const getContext = this.index + ? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${this.index}: i })` + : `item => Object.assign({}, ctx, { ${props.join(', ')} })`; + + const open = `\${ ${this.else ? `${snippet}.length ? ` : ''}__each(${snippet}, ${getContext}, ctx => \``; + compiler.append(open); + + const childBlock = block.child({}); + + this.children.forEach((child: Node) => { + child.ssr(compiler, childBlock); + }); + + const close = `\`)`; + compiler.append(close); + + if (this.else) { + compiler.append(` : \``); + this.else.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + compiler.append(`\``); + } + + compiler.append('}'); + } } diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 79ab6c2572..5c34939387 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -17,6 +17,9 @@ import Text from './Text'; import * as namespaces from '../../utils/namespaces'; import mapChildren from './shared/mapChildren'; +// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7 +const booleanAttributes = new Set('async autocomplete autofocus autoplay border challenge checked compact contenteditable controls default defer disabled formnovalidate frameborder hidden indeterminate ismap loop multiple muted nohref noresize noshade novalidate nowrap open readonly required reversed scoped scrolling seamless selected sortable spellcheck translate'.split(' ')); + export default class Element extends Node { type: 'Element'; name: string; @@ -827,6 +830,86 @@ export default class Element extends Node { ); } } + + ssr(compiler, block) { + let openingTag = `<${this.name}`; + let textareaContents; // awkward special case + + const slot = this.getStaticAttributeValue('slot'); + if (slot && this.hasAncestor('Component')) { + const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot'); + const slotName = slot.chunks[0].data; + const appendTarget = compiler.appendTargets[compiler.appendTargets.length - 1]; + appendTarget.slotStack.push(slotName); + appendTarget.slots[slotName] = ''; + } + + if (this.attributes.find(attr => attr.isSpread)) { + // TODO dry this out + const args = []; + this.attributes.forEach(attribute => { + if (attribute.isSpread) { + args.push(attribute.expression.snippet); + } else { + if (attribute.name === 'value' && this.name === 'textarea') { + textareaContents = attribute.stringifyForSsr(); + } else if (attribute.isTrue) { + args.push(`{ ${quoteIfNecessary(attribute.name)}: true }`); + } else if ( + booleanAttributes.has(attribute.name) && + attribute.chunks.length === 1 && + attribute.chunks[0].type !== 'Text' + ) { + // a boolean attribute with one non-Text chunk + args.push(`{ ${quoteIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`); + } else { + args.push(`{ ${quoteIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`); + } + } + }); + + openingTag += "${__spread([" + args.join(', ') + "])}"; + } else { + this.attributes.forEach((attribute: Node) => { + if (attribute.type !== 'Attribute') return; + + if (attribute.name === 'value' && this.name === 'textarea') { + textareaContents = attribute.stringifyForSsr(); + } else if (attribute.isTrue) { + openingTag += ` ${attribute.name}`; + } else if ( + booleanAttributes.has(attribute.name) && + attribute.chunks.length === 1 && + attribute.chunks[0].type !== 'Text' + ) { + // a boolean attribute with one non-Text chunk + openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }'; + } else { + openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`; + } + }); + } + + if (this._cssRefAttribute) { + openingTag += ` svelte-ref-${this._cssRefAttribute}`; + } + + openingTag += '>'; + + compiler.append(openingTag); + + if (this.name === 'textarea' && textareaContents !== undefined) { + compiler.append(textareaContents); + } else { + this.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + } + + if (!isVoidElementName(this.name)) { + compiler.append(``); + } + } } function getRenderStatement( diff --git a/src/generators/nodes/Head.ts b/src/generators/nodes/Head.ts index 903d44f0d2..e9d0ea041f 100644 --- a/src/generators/nodes/Head.ts +++ b/src/generators/nodes/Head.ts @@ -35,4 +35,14 @@ export default class Head extends Node { child.build(block, 'document.head', null); }); } + + ssr(compiler, block) { + compiler.append('${(__result.head += `'); + + this.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + + compiler.append('`, "")}'); + } } diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts index 55dd64bb40..40a1ec46b6 100644 --- a/src/generators/nodes/IfBlock.ts +++ b/src/generators/nodes/IfBlock.ts @@ -476,6 +476,30 @@ export default class IfBlock extends Node { return branches; } + ssr(compiler, block) { + const { snippet } = this.expression; + + compiler.append('${ ' + snippet + ' ? `'); + + const childBlock = block.child({ + conditions: block.conditions.concat(snippet), + }); + + this.children.forEach((child: Node) => { + child.ssr(compiler, childBlock); + }); + + compiler.append('` : `'); + + if (this.else) { + this.else.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + } + + compiler.append('` }'); + } + visitChildren(block: Block, node: Node) { node.children.forEach((child: Node) => { child.build(node.block, null, 'nodes'); diff --git a/src/generators/nodes/MustacheTag.ts b/src/generators/nodes/MustacheTag.ts index 7c5adafef0..cd66e4aa30 100644 --- a/src/generators/nodes/MustacheTag.ts +++ b/src/generators/nodes/MustacheTag.ts @@ -24,4 +24,14 @@ export default class MustacheTag extends Tag { remount(name: string) { return `@appendNode(${this.var}, ${name}._slotted.default);`; } + + ssr(compiler) { + compiler.append( + this.parent && + this.parent.type === 'Element' && + this.parent.name === 'style' + ? '${' + this.expression.snippet + '}' + : '${__escape(' + this.expression.snippet + ')}' + ); + } } \ No newline at end of file diff --git a/src/generators/nodes/RawMustacheTag.ts b/src/generators/nodes/RawMustacheTag.ts index 7c22548272..bee92f0fe4 100644 --- a/src/generators/nodes/RawMustacheTag.ts +++ b/src/generators/nodes/RawMustacheTag.ts @@ -87,4 +87,8 @@ export default class RawMustacheTag extends Tag { remount(name: string) { return `@appendNode(${this.var}, ${name}._slotted.default);`; } + + ssr(compiler) { + compiler.append('${' + this.expression.snippet + '}'); + } } \ No newline at end of file diff --git a/src/generators/nodes/Slot.ts b/src/generators/nodes/Slot.ts index 1a1b5f6851..c8b58c9b4d 100644 --- a/src/generators/nodes/Slot.ts +++ b/src/generators/nodes/Slot.ts @@ -150,4 +150,17 @@ export default class Slot extends Element { return null; } + + ssr(compiler, block) { + const name = this.attributes.find(attribute => attribute.name === 'name'); + const slotName = name && name.chunks[0].data || 'default'; + + compiler.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : \``); + + this.children.forEach((child: Node) => { + child.ssr(compiler, block); + }); + + compiler.append(`\`}`); + } } \ No newline at end of file diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts index a7ad47d8f9..8a8c533366 100644 --- a/src/generators/nodes/Text.ts +++ b/src/generators/nodes/Text.ts @@ -1,4 +1,4 @@ -import { stringify } from '../../utils/stringify'; +import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify'; import Node from './shared/Node'; import Block from '../dom/Block'; @@ -67,4 +67,17 @@ export default class Text extends Node { remount(name: string) { return `@appendNode(${this.var}, ${name}._slotted.default);`; } + + ssr(compiler) { + let text = this.data; + if ( + !this.parent || + this.parent.type !== 'Element' || + (this.parent.name !== 'script' && this.parent.name !== 'style') + ) { + // unless this Text node is inside a