use external generator for SSR

pull/1718/head
Rich Harris 7 years ago
parent a4d412fb53
commit 39a159dc22

2729
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -96,5 +96,8 @@
], ],
"sourceMap": true, "sourceMap": true,
"instrument": true "instrument": true
},
"dependencies": {
"@sveltejs/generate-ssr": "github:sveltejs/generate-ssr"
} }
} }

@ -6,7 +6,6 @@ import ThenBlock from './ThenBlock';
import CatchBlock from './CatchBlock'; import CatchBlock from './CatchBlock';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import { SsrTarget } from '../ssr';
export default class AwaitBlock extends Node { export default class AwaitBlock extends Node {
expression: Expression; expression: Expression;
@ -194,23 +193,4 @@ export default class AwaitBlock extends Node {
}); });
}); });
} }
ssr() {
const target: SsrTarget = <SsrTarget>this.compiler.target;
const { snippet } = this.expression;
target.append('${(function(__value) { if(@isPromise(__value)) return `');
this.pending.children.forEach((child: Node) => {
child.ssr();
});
target.append('`; return function(ctx) { return `');
this.then.children.forEach((child: Node) => {
child.ssr();
});
target.append(`\`;}(Object.assign({}, ctx, { ${this.value}: __value }));}(${snippet})) }`);
}
} }

@ -8,11 +8,4 @@ export default class Comment extends Node {
super(compiler, parent, scope, info); super(compiler, parent, scope, info);
this.data = info.data; this.data = info.data;
} }
ssr() {
// Allow option to preserve comments, otherwise ignore
if (this.compiler.options.preserveComments) {
this.compiler.target.append(`<!--${this.data}-->`);
}
}
} }

@ -4,7 +4,6 @@ import CodeBuilder from '../../utils/CodeBuilder';
import getTailSnippet from '../../utils/getTailSnippet'; import getTailSnippet from '../../utils/getTailSnippet';
import getObject from '../../utils/getObject'; import getObject from '../../utils/getObject';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary'; import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import { escape, escapeTemplate, stringify } from '../../utils/stringify';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Attribute from './Attribute'; import Attribute from './Attribute';
@ -12,7 +11,6 @@ import mapChildren from './shared/mapChildren';
import Binding from './Binding'; import Binding from './Binding';
import EventHandler from './EventHandler'; import EventHandler from './EventHandler';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
import { AppendTarget } from '../../interfaces';
import addToSet from '../../utils/addToSet'; import addToSet from '../../utils/addToSet';
export default class Component extends Node { export default class Component extends Node {
@ -513,131 +511,6 @@ export default class Component extends Node {
remount(name: string) { remount(name: string) {
return `${this.var}._mount(${name}._slotted.default, null);`; return `${this.var}._mount(${name}._slotted.default, null);`;
} }
ssr() {
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 `${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 = this.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
this.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.snippet;
} else {
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${this.attributes
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
const expression = (
this.name === 'svelte:self'
? this.compiler.name
: this.name === 'svelte:component'
? `((${this.expression.snippet}) || @missingComponent)`
: `%components-${this.name}`
);
this.bindings.forEach(binding => {
const conditions = [];
let node = this;
while (node = node.parent) {
if (node.type === 'IfBlock') {
// TODO handle contextual bindings...
conditions.push(`(${node.expression.snippet})`);
}
}
conditions.push(
`!('${binding.name}' in ctx)`,
`${expression}.data`
);
const { name } = getObject(binding.value.node);
this.compiler.target.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}, '${this.name}')._render(__result, ${props}`;
const options = [];
options.push(`store: options.store`);
if (this.children.length) {
const appendTarget: AppendTarget = {
slots: { default: '' },
slotStack: ['default']
};
this.compiler.target.appendTargets.push(appendTarget);
this.children.forEach((child: Node) => {
child.ssr();
});
const slotted = Object.keys(appendTarget.slots)
.map(name => `${quoteNameIfNecessary(name)}: () => \`${appendTarget.slots[name]}\``)
.join(', ');
options.push(`slotted: { ${slotted} }`);
this.compiler.target.appendTargets.pop();
}
if (options.length) {
open += `, { ${options.join(', ')} }`;
}
this.compiler.target.append(open);
this.compiler.target.append(')}');
}
} }
function isComputed(node: Node) { function isComputed(node: Node) {

@ -68,22 +68,4 @@ export default class DebugTag extends Node {
`); `);
} }
} }
ssr() {
if (!this.compiler.options.dev) return;
const filename = this.compiler.file || null;
const { line, column } = this.compiler.locate(this.start + 1);
const obj = this.expressions.length === 0
? `ctx`
: `{ ${this.expressions
.map(e => e.node.name)
.map(name => `${name}: ctx.${name}`)
.join(', ')} }`;
const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`;
this.compiler.target.append(str);
}
} }

@ -493,35 +493,4 @@ export default class EachBlock extends Node {
// TODO consider keyed blocks // TODO consider keyed blocks
return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`; return `for (var #i = 0; #i < ${this.iterations}.length; #i += 1) ${this.iterations}[#i].m(${name}._slotted.default, null);`;
} }
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
const props = this.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
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.target.append(open);
this.children.forEach((child: Node) => {
child.ssr();
});
const close = `\`)`;
compiler.target.append(close);
if (this.else) {
compiler.target.append(` : \``);
this.else.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append(`\``);
}
compiler.target.append('}');
}
} }

@ -1,9 +1,6 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import { stringify, escapeHTML } from '../../utils/stringify'; import { stringify, escapeHTML } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName'; import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import fixAttributeCasing from '../../utils/fixAttributeCasing'; import fixAttributeCasing from '../../utils/fixAttributeCasing';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary'; import { quoteNameIfNecessary, quotePropIfNecessary } from '../../utils/quoteIfNecessary';
import Compiler from '../Compiler'; import Compiler from '../Compiler';
@ -21,47 +18,6 @@ import * as namespaces from '../../utils/namespaces';
import mapChildren from './shared/mapChildren'; import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns'; import { dimensions } from '../../utils/patterns';
// 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'
]);
export default class Element extends Node { export default class Element extends Node {
type: 'Element'; type: 'Element';
name: string; name: string;
@ -969,99 +925,6 @@ export default class Element extends Node {
); );
} }
} }
ssr() {
const { compiler } = this;
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.target.appendTargets[compiler.target.appendTargets.length - 1];
appendTarget.slotStack.push(slotName);
appendTarget.slots[slotName] = '';
}
const classExpr = this.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 (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(`{ ${quoteNameIfNecessary(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(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].snippet} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(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 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 += '>';
compiler.target.append(openingTag);
if (this.name === 'textarea' && textareaContents !== undefined) {
compiler.target.append(textareaContents);
} else {
this.children.forEach((child: Node) => {
child.ssr();
});
}
if (!isVoidElementName(this.name)) {
compiler.target.append(`</${this.name}>`);
}
}
} }
function getRenderStatement( function getRenderStatement(

@ -35,14 +35,4 @@ export default class Head extends Node {
child.build(block, 'document.head', null); child.build(block, 'document.head', null);
}); });
} }
ssr() {
this.compiler.target.append('${(__result.head += `');
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append('`, "")}');
}
} }

@ -1,7 +1,6 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import Node from './shared/Node'; import Node from './shared/Node';
import ElseBlock from './ElseBlock'; import ElseBlock from './ElseBlock';
import Compiler from '../Compiler';
import Block from '../dom/Block'; import Block from '../dom/Block';
import createDebuggingComment from '../../utils/createDebuggingComment'; import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression'; import Expression from './shared/Expression';
@ -481,27 +480,6 @@ export default class IfBlock extends Node {
return branches; return branches;
} }
ssr() {
const { compiler } = this;
const { snippet } = this.expression;
compiler.target.append('${ ' + snippet + ' ? `');
this.children.forEach((child: Node) => {
child.ssr();
});
compiler.target.append('` : `');
if (this.else) {
this.else.children.forEach((child: Node) => {
child.ssr();
});
}
compiler.target.append('` }');
}
visitChildren(block: Block, node: Node) { visitChildren(block: Block, node: Node) {
node.children.forEach((child: Node) => { node.children.forEach((child: Node) => {
child.build(node.block, null, 'nodes'); child.build(node.block, null, 'nodes');

@ -24,14 +24,4 @@ export default class MustacheTag extends Tag {
remount(name: string) { remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`; return `@append(${name}._slotted.default, ${this.var});`;
} }
ssr() {
this.compiler.target.append(
this.parent &&
this.parent.type === 'Element' &&
this.parent.name === 'style'
? '${' + this.expression.snippet + '}'
: '${@escape(' + this.expression.snippet + ')}'
);
}
} }

@ -93,8 +93,4 @@ export default class RawMustacheTag extends Tag {
remount(name: string) { remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`; return `@append(${name}._slotted.default, ${this.var});`;
} }
ssr() {
this.compiler.target.append('${' + this.expression.snippet + '}');
}
} }

@ -1,6 +1,4 @@
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import isValidIdentifier from '../../utils/isValidIdentifier';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node'; import Node from './shared/Node';
import Element from './Element'; import Element from './Element';
import Attribute from './Attribute'; import Attribute from './Attribute';
@ -153,19 +151,4 @@ export default class Slot extends Element {
return null; return null;
} }
ssr() {
const name = this.attributes.find(attribute => attribute.name === 'name');
const slotName = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slotName);
this.compiler.target.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`\`}`);
}
} }

@ -1,4 +1,4 @@
import { escape, escapeHTML, escapeTemplate, stringify } from '../../utils/stringify'; import { stringify } from '../../utils/stringify';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
@ -63,17 +63,4 @@ export default class Text extends Node {
remount(name: string) { remount(name: string) {
return `@append(${name}._slotted.default, ${this.var});`; return `@append(${name}._slotted.default, ${this.var});`;
} }
ssr() {
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 <script> or <style> element, escape &,<,>
text = escapeHTML(text);
}
this.compiler.target.append(escape(escapeTemplate(text)));
}
} }

@ -101,14 +101,4 @@ export default class Title extends Node {
block.builders.hydrate.addLine(`document.title = ${value};`); block.builders.hydrate.addLine(`document.title = ${value};`);
} }
} }
ssr() {
this.compiler.target.append(`<title>`);
this.children.forEach((child: Node) => {
child.ssr();
});
this.compiler.target.append(`</title>`);
}
} }

@ -1,10 +1,4 @@
import CodeBuilder from '../../utils/CodeBuilder';
import deindent from '../../utils/deindent'; import deindent from '../../utils/deindent';
import { stringify } from '../../utils/stringify';
import flattenReference from '../../utils/flattenReference';
import isVoidElementName from '../../utils/isVoidElementName';
import validCalleeObjects from '../../utils/validCalleeObjects';
import reservedNames from '../../utils/reservedNames';
import Node from './shared/Node'; import Node from './shared/Node';
import Block from '../dom/Block'; import Block from '../dom/Block';
import Binding from './Binding'; import Binding from './Binding';
@ -219,8 +213,4 @@ export default class Window extends Node {
`); `);
} }
} }
ssr() {
// noop
}
} }

@ -1,13 +1,9 @@
import deindent from '../../utils/deindent'; import generate from '@sveltejs/generate-ssr';
import Compiler from '../Compiler'; import Compiler from '../Compiler';
import Stats from '../../Stats'; import Stats from '../../Stats';
import Stylesheet from '../../css/Stylesheet'; import Stylesheet from '../../css/Stylesheet';
import { removeNode, removeObjectKey } from '../../utils/removeNode'; import { Ast, CompileOptions } from '../../interfaces';
import getName from '../../utils/getName';
import globalWhitelist from '../../utils/globalWhitelist';
import { Ast, Node, CompileOptions } from '../../interfaces';
import { AppendTarget } from '../../interfaces'; import { AppendTarget } from '../../interfaces';
import { stringify } from '../../utils/stringify';
export class SsrTarget { export class SsrTarget {
bindings: string[]; bindings: string[];
@ -38,117 +34,9 @@ export default function ssr(
options: CompileOptions, options: CompileOptions,
stats: Stats stats: Stats
) { ) {
const format = options.format || 'cjs'; const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false);
const target = new SsrTarget(); return generate(compiler);
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
const { computations, name, templateProperties } = compiler;
// create main render() function
trim(compiler.fragment.children).forEach((node: Node) => {
node.ssr();
});
const css = compiler.customElement ?
{ code: null, map: null } :
compiler.stylesheet.render(options.filename, true);
// generate initial state object
const expectedProperties = Array.from(compiler.expectedProperties);
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
const initialState = [];
if (globals.length > 0) {
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
}
if (storeProps.length > 0) {
const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`
initialState.push(`options.store.${initialize}`);
}
if (templateProperties.data) {
initialState.push(`%data()`);
} else if (globals.length === 0 && storeProps.length === 0) {
initialState.push('{}');
}
initialState.push('ctx');
const helpers = new Set();
// TODO concatenate CSS maps
const result = deindent`
${compiler.javascript}
var ${name} = {};
${options.filename && `${name}.filename = ${stringify(options.filename)}`};
${name}.data = function() {
return ${templateProperties.data ? `%data()` : `{}`};
};
${name}.render = function(state, options = {}) {
var components = new Set();
function addComponent(component) {
components.add(component);
}
var result = { head: '', addComponent };
var html = ${name}._render(result, state, options);
var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n');
return {
html,
head: result.head,
css: { code: cssCode, map: null },
toString() {
return html;
}
};
}
${name}._render = function(__result, ctx, options) {
${templateProperties.store && `options.store = %store();`}
__result.addComponent(${name});
ctx = Object.assign(${initialState.join(', ')});
${computations.map(
({ key }) => `ctx.${key} = %computed-${key}(ctx);`
)}
${target.bindings.length &&
deindent`
var settled = false;
var tmp;
while (!settled) {
settled = true;
${target.bindings.join('\n\n')}
}
`}
return \`${target.renderCode}\`;
};
${name}.css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};
var warned = false;
${templateProperties.preload && `${name}.preload = %preload;`}
`;
return compiler.generate(result, options, { name, format });
} }
function trim(nodes) { function trim(nodes) {

Loading…
Cancel
Save