move render logic to render-ssr

pull/1746/head
Rich Harris 7 years ago
parent be84c96eba
commit 7ef5f4f54f

@ -19,7 +19,7 @@ import Fragment from './nodes/Fragment';
import shared from './shared';
import { DomTarget } from './dom';
import { SsrTarget } from './ssr';
import { Node, GenerateOptions, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import { Node, ShorthandImport, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame';
import checkForComputedKeys from './validate/js/utils/checkForComputedKeys';
@ -319,7 +319,11 @@ export default class Component {
return this.aliases.get(name);
}
generate(result: string, options: CompileOptions, { banner = '', name, format }: GenerateOptions ) {
generate(result: string, options: CompileOptions, {
banner = '',
name,
format
}) {
const pattern = /\[✂(\d+)-(\d+)$/;
const helpers = new Set();

@ -2,12 +2,12 @@ import MagicString from 'magic-string';
import { walk } from 'estree-walker';
import { getLocator } from 'locate-character';
import Selector from './Selector';
import getCodeFrame from '../utils/getCodeFrame';
import hash from '../utils/hash';
import removeCSSPrefix from '../utils/removeCSSPrefix';
import Element from '../compile/nodes/Element';
import { Node, Ast, Warning } from '../interfaces';
import Component from '../compile/Component';
import getCodeFrame from '../../utils/getCodeFrame';
import hash from '../../utils/hash';
import removeCSSPrefix from '../../utils/removeCSSPrefix';
import Element from '../nodes/Element';
import { Node, Ast, Warning } from '../../interfaces';
import Component from '../Component';
const isKeyframesNode = (node: Node) => removeCSSPrefix(node.name) === 'keyframes'

@ -1,8 +1,8 @@
import { assign } from '../shared';
import Stats from '../Stats';
import parse from '../parse/index';
import generate, { DomTarget } from './dom/index';
import generateSSR, { SsrTarget } from './ssr/index';
import renderDOM, { DomTarget } from './render-dom/index';
import renderSSR from './render-ssr/index';
import { CompileOptions, Warning, Ast } from '../interfaces';
import Component from './Component';
@ -77,7 +77,7 @@ export default function compile(source: string, options: CompileOptions) {
stats,
// TODO make component generator-agnostic, to allow e.g. WebGL generator
options.generate === 'ssr' ? new SsrTarget() : new DomTarget()
options.generate === 'ssr' ? null : new DomTarget()
);
stats.stop('create component');
@ -85,9 +85,11 @@ export default function compile(source: string, options: CompileOptions) {
return { ast, stats: stats.render(null), js: null, css: null };
}
const compiler = options.generate === 'ssr' ? generateSSR : generate;
if (options.generate === 'ssr') {
return renderSSR(component, options);
}
return compiler(component, options);
return renderDOM(component, options);
} catch (err) {
options.onerror(err);
return;

@ -6,7 +6,7 @@ import Component from '../Component';
import Node from './shared/Node';
import Element from './Element';
import Text from './Text';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
export interface StyleProp {

@ -1,6 +1,6 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import PendingBlock from './PendingBlock';
import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock';

@ -4,7 +4,7 @@ import getObject from '../../utils/getObject';
import getTailSnippet from '../../utils/getTailSnippet';
import flattenReference from '../../utils/flattenReference';
import Component from '../Component';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
import { dimensions } from '../../utils/patterns';

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class CatchBlock extends Node {

@ -1,6 +1,6 @@
import Node from './shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Expression from './shared/Expression';
import deindent from '../../utils/deindent';
import addToSet from '../../utils/addToSet';

@ -1,7 +1,7 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';

@ -8,7 +8,7 @@ import fixAttributeCasing from '../../utils/fixAttributeCasing';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import Component from '../Component';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class ElseBlock extends Node {

@ -1,7 +1,7 @@
import Node from './shared/Node';
import Component from '../Component';
import mapChildren from './shared/mapChildren';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import TemplateScope from './shared/TemplateScope';
export default class Fragment extends Node {

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class Head extends Node {

@ -2,7 +2,7 @@ import deindent from '../../utils/deindent';
import Node from './shared/Node';
import ElseBlock from './ElseBlock';
import Component from '../Component';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';

@ -6,7 +6,7 @@ import getObject from '../../utils/getObject';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Attribute from './Attribute';
import mapChildren from './shared/mapChildren';
import Binding from './Binding';

@ -1,6 +1,6 @@
import Node from './shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
export default class MustacheTag extends Tag {
build(

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class PendingBlock extends Node {

@ -1,7 +1,7 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Tag from './shared/Tag';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
export default class RawMustacheTag extends Tag {
build(

@ -4,7 +4,7 @@ import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node';
import Element from './Element';
import Attribute from './Attribute';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import { quotePropIfNecessary } from '../../utils/quoteIfNecessary';
function sanitize(name) {

@ -1,6 +1,6 @@
import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
// Whitespace inside one of these elements will not result in
// a whitespace node being created in any circumstances. (This

@ -1,5 +1,5 @@
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class ThenBlock extends Node {

@ -1,6 +1,6 @@
import { stringify } from '../../utils/stringify';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import mapChildren from './shared/mapChildren';
export default class Title extends Node {

@ -1,6 +1,6 @@
import deindent from '../../utils/deindent';
import Node from './shared/Node';
import Block from '../dom/Block';
import Block from '../render-dom/Block';
import Binding from './Binding';
import EventHandler from './EventHandler';
import flattenReference from '../../utils/flattenReference';

@ -1,5 +1,5 @@
import Component from './../../Component';
import Block from '../../dom/Block';
import Block from '../../render-dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {

@ -1,6 +1,6 @@
import Node from './Node';
import Expression from './Expression';
import Block from '../../dom/Block';
import Block from '../../render-dom/Block';
export default class Tag extends Node {
expression: Expression;

@ -0,0 +1,70 @@
import AwaitBlock from './handlers/AwaitBlock';
import Comment from './handlers/Comment';
import DebugTag from './handlers/DebugTag';
import EachBlock from './handlers/EachBlock';
import Element from './handlers/Element';
import Head from './handlers/Head';
import HtmlTag from './handlers/HtmlTag';
import IfBlock from './handlers/IfBlock';
import InlineComponent from './handlers/InlineComponent';
import Slot from './handlers/Slot';
import Tag from './handlers/Tag';
import Text from './handlers/Text';
import Title from './handlers/Title';
type Handler = (node: any, target: any, options: any) => void;
function noop(){}
const handlers: Record<string, Handler> = {
AwaitBlock,
Comment,
DebugTag,
EachBlock,
Element,
Head,
IfBlock,
InlineComponent,
MustacheTag: Tag, // TODO MustacheTag is an anachronism
RawMustacheTag: HtmlTag,
Slot,
Text,
Title,
Window: noop
};
type AppendTarget = any; // TODO
export default class Renderer {
bindings: string[];
code: string;
targets: AppendTarget[];
constructor() {
this.bindings = [];
this.code = '';
this.targets = [];
}
append(code: string) {
if (this.targets.length) {
const target = this.targets[this.targets.length - 1];
const slotName = target.slotStack[target.slotStack.length - 1];
target.slots[slotName] += code;
} else {
this.code += code;
}
}
render(nodes, options) {
nodes.forEach(node => {
const handler = handlers[node.type];
if (!handler) {
throw new Error(`No handler for '${node.type}' nodes`);
}
handler(node, this, options);
});
}
}

@ -0,0 +1,16 @@
import Renderer from '../Renderer';
import { CompileOptions } from '../../../interfaces';
export default function(node, renderer: Renderer, options: CompileOptions) {
const { snippet } = node.expression;
renderer.append('${(function(__value) { if(@isPromise(__value)) return `');
renderer.render(node.pending.children, options);
renderer.append('`; return function(ctx) { return `');
renderer.render(node.then.children, options);
renderer.append(`\`;}(Object.assign({}, ctx, { ${node.value}: __value }));}(${snippet})) }`);
}

@ -0,0 +1,8 @@
import Renderer from '../Renderer';
import { CompileOptions } from '../../../interfaces';
export default function(node, target: Renderer, options: CompileOptions) {
if (options.preserveComments) {
target.append(`<!--${node.data}-->`);
}
}

@ -0,0 +1,19 @@
import { stringify } from '../../../utils/stringify';
export default function(node, target, options) {
if (!options.dev) return;
const filename = options.file || null;
const { line, column } = options.locate(node.start + 1);
const obj = node.expressions.length === 0
? `ctx`
: `{ ${node.expressions
.map(e => e.node.name)
.map(name => `${name}: ctx.${name}`)
.join(', ')} }`;
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
target.append(str);
}

@ -0,0 +1,25 @@
export default function(node, renderer, options) {
const { snippet } = node.expression;
const props = node.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const getContext = node.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${node.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
renderer.append(open);
renderer.render(node.children, options);
const close = `\`)`;
renderer.append(close);
if (node.else) {
renderer.append(` : \``);
renderer.render(node.else.children, options);
renderer.append(`\``);
}
renderer.append('}');
}

@ -0,0 +1,132 @@
import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../utils/quoteIfNecessary';
import isVoidElementName from '../../../utils/isVoidElementName';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = 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'
]);
export default function(node, renderer, options) {
let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
const slot = node.getStaticAttributeValue('slot');
if (slot && node.hasAncestor('InlineComponent')) {
const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
const slotName = slot.chunks[0].data;
const target = renderer.targets[renderer.targets.length - 1];
target.slotStack.push(slotName);
target.slots[slotName] = '';
}
const classExpr = node.classes.map((classDir: Class) => {
const { expression, name } = classDir;
const snippet = expression ? expression.snippet : `ctx${quotePropIfNecessary(name)}`;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
let addClassAttribute = classExpr ? true : false;
if (node.attributes.find(attr => attr.isSpread)) {
// TODO dry this out
const args = [];
node.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.snippet);
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
} else if (
boolean_attributes.has(attribute.name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${attribute.stringifyForSsr()}\` }`);
}
}
});
openingTag += "${@spread([" + args.join(', ') + "])}";
} else {
node.attributes.forEach((attribute: Node) => {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = attribute.stringifyForSsr();
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
boolean_attributes.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 if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ').trim() }"`;
} else {
openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`;
}
});
}
if (addClassAttribute) {
openingTag += ` class="\${ [${classExpr}].join(' ').trim() }"`;
}
openingTag += '>';
renderer.append(openingTag);
if (node.name === 'textarea' && textareaContents !== undefined) {
renderer.append(textareaContents);
} else {
renderer.render(node.children, options);
}
if (!isVoidElementName(node.name)) {
renderer.append(`</${node.name}>`);
}
}

@ -0,0 +1,7 @@
export default function(node, renderer, options) {
renderer.append('${(__result.head += `');
renderer.render(node.children, options);
renderer.append('`, "")}');
}

@ -0,0 +1,3 @@
export default function(node, target, options) {
target.append('${' + node.expression.snippet + '}');
}

@ -0,0 +1,15 @@
export default function(node, renderer, options) {
const { snippet } = node.expression;
renderer.append('${ ' + snippet + ' ? `');
renderer.render(node.children, options);
renderer.append('` : `');
if (node.else) {
renderer.render(node.else.children, options);
}
renderer.append('` }');
}

@ -0,0 +1,130 @@
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import deindent from '../../../utils/deindent';
type AppendTarget = any; // TODO
export default function(node, renderer, options) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
return '${@escape( ' + chunk.snippet + ')}';
}
const bindingProps = node.bindings.map(binding => {
const { name } = getObject(binding.value.node);
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(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 = node.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
node.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.snippet;
} else {
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${node.attributes
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const expression = (
node.name === 'svelte:self'
? node.component.name
: node.name === 'svelte:component'
? `((${node.expression.snippet}) || @missingComponent)`
: `%components-${node.name}`
);
node.bindings.forEach(binding => {
const conditions = [];
let parent = node;
while (parent = parent.parent) {
if (parent.type === 'IfBlock') {
// TODO handle contextual bindings...
conditions.push(`(${parent.expression.snippet})`);
}
}
conditions.push(
`!('${binding.name}' in ctx)`,
`${expression}.data`
);
const { name } = getObject(binding.value.node);
renderer.bindings.push(deindent`
if (${conditions.reverse().join('&&')}) {
tmp = ${expression}.data();
if ('${name}' in tmp) {
ctx${quotePropIfNecessary(binding.name)} = tmp.${name};
settled = false;
}
}
`);
});
let open = `\${@validateSsrComponent(${expression}, '${node.name}')._render(__result, ${props}`;
const component_options = [];
component_options.push(`store: options.store`);
if (node.children.length) {
const target: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
renderer.targets.push(target);
renderer.render(node.children, options);
const slotted = Object.keys(target.slots)
.map(name => `${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``)
.join(', ');
component_options.push(`slotted: { ${slotted} }`);
renderer.targets.pop();
}
if (component_options.length) {
open += `, { ${component_options.join(', ')} }`;
}
renderer.append(open);
renderer.append(')}');
}

@ -0,0 +1,14 @@
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
export default function(node, renderer, options) {
const name = node.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slotName);
renderer.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
renderer.render(node.children, options);
renderer.append(`\`}`);
}

@ -0,0 +1,9 @@
export default function(node, target, options) {
target.append(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + node.expression.snippet + '}'
: '${@escape(' + node.expression.snippet + ')}'
);
}

@ -0,0 +1,14 @@
import { escapeHTML, escapeTemplate, escape } from '../../../utils/stringify';
export default function(node, target, options) {
let text = node.data;
if (
!node.parent ||
node.parent.type !== 'Element' ||
(node.parent.name !== 'script' && node.parent.name !== 'style')
) {
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
target.append(escape(escapeTemplate(text)));
}

@ -0,0 +1,7 @@
export default function(node, renderer, options) {
renderer.append(`<title>`);
renderer.render(node.children, options);
renderer.append(`</title>`);
}

@ -1,45 +1,25 @@
import deindent from '../../utils/deindent';
import Component from '../Component';
import globalWhitelist from '../../utils/globalWhitelist';
import { Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces';
import { CompileOptions } from '../../interfaces';
import { stringify } from '../../utils/stringify';
import CodeBuilder from '../../utils/CodeBuilder';
export class SsrTarget {
bindings: string[];
renderCode: string;
appendTargets: AppendTarget[];
constructor() {
this.bindings = [];
this.renderCode = '';
this.appendTargets = [];
}
append(code: string) {
if (this.appendTargets.length) {
const appendTarget = this.appendTargets[this.appendTargets.length - 1];
const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1];
appendTarget.slots[slotName] += code;
} else {
this.renderCode += code;
}
}
}
import Renderer from './Renderer';
export default function ssr(
component: Component,
options: CompileOptions
) {
const renderer = new Renderer();
const format = options.format || 'cjs';
const { computations, name, templateProperties } = component;
// create main render() function
trim(component.fragment.children).forEach((node: Node) => {
node.ssr();
});
renderer.render(trim(component.fragment.children), Object.assign({
locate: component.locate
}, options));
const css = component.customElement ?
{ code: null, map: null } :
@ -139,7 +119,7 @@ export default function ssr(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${component.target.bindings.length &&
${renderer.bindings.length &&
deindent`
var settled = false;
var tmp;
@ -147,11 +127,11 @@ export default function ssr(
while (!settled) {
settled = true;
${component.target.bindings.join('\n\n')}
${renderer.bindings.join('\n\n')}
}
`}
return \`${component.target.renderCode}\`;
return \`${renderer.code}\`;
};
${name}.css = {

@ -68,13 +68,6 @@ export interface CompileOptions {
nestedTransitions?: boolean;
}
export interface GenerateOptions {
name: string;
format: ModuleFormat;
banner?: string;
sharedPath?: string;
}
export interface ShorthandImport {
name: string;
source: string;

@ -49,7 +49,7 @@ export const missingComponent = {
export function validateSsrComponent(component, name) {
if (!component || !component._render) {
if (name === 'svelte:component') name += 'this={...}';
if (name === 'svelte:component') name += ' this={...}';
throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules`);
}

Loading…
Cancel
Save