SSR bindings work. now to fix the other stuff i just broke

pull/1839/head
Rich Harris 7 years ago
parent 03af42fad4
commit 1cd946e2f6

@ -11,7 +11,7 @@ import Stylesheet from './css/Stylesheet';
import { test } from '../config';
import Fragment from './nodes/Fragment';
import * as internal from '../internal/index';
import { Node, Ast, CompileOptions, CustomElementOptions } from '../interfaces';
import { Node, Ast, CompileOptions } from '../interfaces';
import error from '../utils/error';
import getCodeFrame from '../utils/getCodeFrame';
import flattenReference from '../utils/flattenReference';

@ -11,6 +11,7 @@ export default class EachBlock extends Node {
block: Block;
expression: Expression;
context_node: Node;
iterations: string;
index: string;
@ -28,6 +29,7 @@ export default class EachBlock extends Node {
this.expression = new Expression(component, this, scope, info.expression);
this.context = info.context.name || 'each'; // TODO this is used to facilitate binding; currently fails with destructuring
this.context_node = info.context;
this.index = info.index;
this.scope = scope.child();

@ -167,6 +167,7 @@ export default class Expression {
return this.node.type in precedence ? precedence[this.node.type](this.node) : 0;
}
// TODO move this into a render-dom wrapper?
render() {
if (this.rendered) return this.rendered;

@ -1,6 +1,4 @@
import Component from './../../Component';
import Block from '../../render-dom/Block';
import { trimStart, trimEnd } from '../../../utils/trim';
export default class Node {
readonly start: number;

@ -39,15 +39,9 @@ const handlers: Record<string, Handler> = {
type AppendTarget = any; // TODO
export default class Renderer {
bindings: string[];
code: string;
targets: AppendTarget[];
constructor() {
this.bindings = [];
this.code = '';
this.targets = [];
}
has_bindings = false;
code = '';
targets: AppendTarget[] = [];
append(code: string) {
if (this.targets.length) {

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

@ -7,10 +7,9 @@ export default function(node, renderer, options) {
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})}`;

@ -1,13 +1,15 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const snippet = node.expression.render();
const snippet = snip(node.expression);
const props = node.contexts.map(prop => `${prop.key.name}: item${prop.tail}`);
const { start, end } = node.context_node;
const getContext = node.index
? `(item, i) => Object.assign({}, ctx, { ${props.join(', ')}, ${node.index}: i })`
: `item => Object.assign({}, ctx, { ${props.join(', ')} })`;
const ctx = node.index
? `([✂${start}-${end}✂], ${node.index})`
: `([✂${start}-${end}✂])`
const open = `\${ ${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${getContext}, ctx => \``;
const open = `\${${node.else ? `${snippet}.length ? ` : ''}@each(${snippet}, ${ctx} => \``;
renderer.append(open);
renderer.render(node.children, options);

@ -3,6 +3,7 @@ import isVoidElementName from '../../../utils/isVoidElementName';
import Attribute from '../../nodes/Attribute';
import Node from '../../nodes/shared/Node';
import { escape, escapeTemplate } from '../../../utils/stringify';
import { snip } from '../utils';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
@ -60,7 +61,7 @@ export default function(node, renderer, options) {
const classExpr = node.classes.map((classDir: Class) => {
const { expression, name } = classDir;
const snippet = expression ? expression.render() : `ctx${quotePropIfNecessary(name)}`;
const snippet = expression ? snip(expression) : `ctx${quotePropIfNecessary(name)}`;
return `${snippet} ? "${name}" : ""`;
}).join(', ');
@ -71,7 +72,7 @@ export default function(node, renderer, options) {
const args = [];
node.attributes.forEach(attribute => {
if (attribute.isSpread) {
args.push(attribute.expression.render());
args.push(snip(attribute.expression));
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttribute(attribute);
@ -83,7 +84,7 @@ export default function(node, renderer, options) {
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${attribute.chunks[0].render()} }`);
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringifyAttribute(attribute)}\` }`);
}
@ -105,13 +106,13 @@ export default function(node, renderer, options) {
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
openingTag += '${' + attribute.chunks[0].render() + ' ? " ' + attribute.name + '" : "" }';
openingTag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${[\`${stringifyAttribute(attribute)}\`, ${classExpr}].join(' ').trim() }"`;
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const snippet = attribute.chunks[0].render();
const snippet = snip(attribute.chunks[0]);
openingTag += '${(v => v == null ? "" : ` ' + name + '="${@escape(' + snippet + ')}"`)(' + snippet + ')}';
} else {
@ -126,7 +127,7 @@ export default function(node, renderer, options) {
if (name === 'group') {
// TODO server-render group bindings
} else {
const snippet = expression.render();
const snippet = snip(expression);
openingTag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}';
}
});
@ -157,7 +158,7 @@ function stringifyAttribute(attribute: Attribute) {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + chunk.render() + ')}';
return '${@escape(' + snip(chunk) + ')}';
})
.join('');
}

@ -1,3 +1,5 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
renderer.append('${' + node.expression.render() + '}');
renderer.append('${' + snip(node.expression) + '}');
}

@ -1,5 +1,7 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const snippet = node.expression.render();
const snippet = snip(node.expression);
renderer.append('${ ' + snippet + ' ? `');

@ -3,103 +3,92 @@ import getObject from '../../../utils/getObject';
import { get_tail_snippet } from '../../../utils/get_tail_snippet';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import deindent from '../../../utils/deindent';
import { snip } from '../utils';
import Renderer from '../Renderer';
import stringifyProps from '../../../utils/stringifyProps';
type AppendTarget = any; // TODO
export default function(node, renderer, options) {
function stringifyAttribute(chunk: Node) {
function stringifyAttribute(chunk: Node) {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data));
}
return '${@escape( ' + snip(chunk) + ')}';
}
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 escapeTemplate(escape(chunk.data));
return stringify(chunk.data);
}
return '${@escape( ' + chunk.render() + ')}';
return snip(chunk);
}
const bindingProps = node.bindings.map(binding => {
const { name } = getObject(binding.expression.node);
const tail = binding.expression.node.type === 'MemberExpression'
? get_tail_snippet(binding.expression.node)
: '';
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
return `${quoteNameIfNecessary(binding.name)}: ctx${quotePropIfNecessary(name)}${tail}`;
});
function stringifyObject(props) {
return props.length > 0
? `{ ${props.join(', ')} }`
: `{};`
}
function getAttributeValue(attribute) {
if (attribute.isTrue) return `true`;
if (attribute.chunks.length === 0) return `''`;
export default function(node, renderer: Renderer, options) {
const binding_props = [];
const binding_fns = [];
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
node.bindings.forEach(binding => {
renderer.has_bindings = true;
return chunk.render();
}
// TODO this probably won't work for contextual bindings
const snippet = snip(binding.expression);
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
binding_props.push(`${binding.name}: ${snippet}`);
binding_fns.push(`${binding.name}: $$value => { ${snippet} = $$value; $$settled = false }`);
});
const usesSpread = node.attributes.find(attr => attr.isSpread);
const props = usesSpread
? `Object.assign(${
let props;
if (usesSpread) {
props = `Object.assign(${
node.attributes
.map(attribute => {
if (attribute.isSpread) {
return attribute.expression.render();
return snip(attribute.expression);
} else {
return `{ ${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)} }`;
return `{ ${attribute.name}: ${getAttributeValue(attribute)} }`;
}
})
.concat(bindingProps.map(p => `{ ${p} }`))
.concat(binding_props.map(p => `{ ${p} }`))
.join(', ')
})`
: `{ ${node.attributes
.map(attribute => `${quoteNameIfNecessary(attribute.name)}: ${getAttributeValue(attribute)}`)
.concat(bindingProps)
.join(', ')} }`;
})`;
} else if (node.attributes.length || binding_props.length) {
props = stringifyProps(
node.attributes
.map(attribute => `${attribute.name}: ${getAttributeValue(attribute)}`)
.concat(binding_props)
);
}
const bindings = stringifyProps(binding_fns);
const expression = (
node.name === 'svelte:self'
? node.component.name
: node.name === 'svelte:component'
? `((${node.expression.render()}) || @missingComponent)`
: `ctx.${node.name}`
? `((${snip(node.expression)}) || @missingComponent)`
: 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.render()})`);
}
}
conditions.push(
`!('${binding.name}' in ctx)`,
`${expression}.data`
);
const { name } = getObject(binding.expression.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 = [];
const slot_fns = [];
if (node.children.length) {
const target: AppendTarget = {
@ -111,19 +100,16 @@ export default function(node, renderer, options) {
renderer.render(node.children, options);
const slotted = Object.keys(target.slots)
.map(name => `${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``)
.join(', ');
component_options.push(`slotted: { ${slotted} }`);
Object.keys(target.slots).forEach(name => {
slot_fns.push(
`${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``
);
});
renderer.targets.pop();
}
if (component_options.length) {
open += `, { ${component_options.join(', ')} }`;
}
const slots = stringifyProps(slot_fns);
renderer.append(open);
renderer.append(')}');
renderer.append(`\${@validate_component(${expression}, '${node.name}').$$render($$result, ${props}, ${bindings}, ${slots})}`);
}

@ -3,10 +3,10 @@ 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);
const slot_name = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slot_name);
renderer.append(`\${options && options.slotted && options.slotted${prop} ? options.slotted${prop}() : \``);
renderer.append(`\${$$slots${prop} ? $$slots${prop}() : \``);
renderer.render(node.children, options);

@ -1,5 +1,7 @@
import { snip } from '../utils';
export default function(node, renderer, options) {
const snippet = node.expression.render();
const snippet = snip(node.expression);
renderer.append(
node.parent &&

@ -12,74 +12,68 @@ export default function ssr(
const { name } = component;
// create main render() function
// create $$render function
renderer.render(trim(component.fragment.children), Object.assign({
locate: component.locate
}, options));
// TODO concatenate CSS maps
const css = options.customElement ?
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);
// TODO concatenate CSS maps
return (deindent`
${component.javascript && deindent`
function #define($$props) {
${component.javascript}
return { ${component.declarations.join(', ')} };
}`}
var ${name} = {};
${name}.render = function(props = {}, options = {}) {
var components = new Set();
let setup;
function addComponent(component) {
components.add(component);
}
if (component.javascript) {
setup = component.javascript;
} else if (component.props.length > 0) {
const props = component.props.map(prop => {
return prop.as === prop.name
? prop.as
: `${prop.as}: ${prop.name}`
});
var result = { head: '', addComponent };
var html = ${name}.$$render(result, props, options);
setup = `let { ${props.join(', ')} } = $$props;`
}
var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n');
// TODO only do this for props with a default value
const parent_bindings = component.javascript
? component.props.map(prop => {
return `if ($$props.${prop.as} === void 0 && $$bindings.${prop.as} && ${prop.name} !== void 0) $$bindings.${prop.as}(${prop.name});`;
})
: [];
return {
html,
head: result.head,
css: { code: cssCode, map: null },
toString() {
return html;
}
};
}
const main = renderer.has_bindings
? deindent`
let $$settled;
let $$rendered;
${name}.$$render = function($$result, ${component.javascript ? 'props' : 'ctx'}, options) {
${component.javascript && `const ctx = #define(props);`}
do {
$$settled = true;
$$result.addComponent(${name});
$$rendered = \`${renderer.code}\`;
} while (!$$settled);
${renderer.bindings.length &&
deindent`
var settled = false;
var tmp;
return $$rendered;
`
: `return \`${renderer.code}\`;`;
while (!settled) {
settled = true;
return (deindent`
${css.code && deindent`
const #css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};`}
${renderer.bindings.join('\n\n')}
}
`}
const ${name} = @create_ssr_component(($$result, $$props, $$bindings, $$slots) => {
${setup}
return \`${renderer.code}\`;
};
${parent_bindings}
${name}.css = {
code: ${css.code ? stringify(css.code) : `''`},
map: ${css.map ? stringify(css.map.toString()) : 'null'}
};
${css.code && `$$result.css.add(#css);`}
var warned = false;
${main}
});
`).trim();
}

@ -0,0 +1,3 @@
export function snip(expression) {
return `[✂${expression.node.start}-${expression.node.end}✂]`;
}

@ -1,3 +1,6 @@
import { set_current_component } from './lifecycle.js';
import { run_all } from './utils.js';
export const invalidAttributeNameCharacter = /[\s'">\/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u;
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter
@ -35,10 +38,10 @@ export function escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
export function each(items, assign, fn) {
export function each(items, fn) {
let str = '';
for (let i = 0; i < items.length; i += 1) {
str += fn(assign(items[i], i));
str += fn(items[i], i);
}
return str;
}
@ -47,7 +50,7 @@ export const missingComponent = {
$$render: () => ''
};
export function validateSsrComponent(component, name) {
export function validate_component(component, name) {
if (!component || !component.$$render) {
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`);
@ -60,4 +63,37 @@ export function debug(file, line, column, values) {
console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`);
console.log(values);
return '';
}
export function create_ssr_component($$render) {
return {
render: (props = {}, options = {}) => {
// TODO do we need on_ready, since on_mount,
// before_render and after_render don't run?
const $$ = {
on_mount: [],
on_destroy: [],
before_render: [],
after_render: []
};
set_current_component({ $$ });
const result = { head: '', css: new Set() };
const html = $$render(result, props, {}, options);
run_all($$.on_destroy);
return {
html,
css: {
code: Array.from(result.css).map(css => css.code).join('\n'),
map: null // TODO
},
head: result.head
};
},
$$render
};
}

@ -18,7 +18,8 @@ function normalize(str) {
const cwd = process.cwd();
describe('cli', function() {
// TODO figure out what to do with the CLI
describe.skip('cli', function() {
this.timeout(10000);
afterEach(() => {

@ -36,7 +36,7 @@ function create(code) {
return module.exports.default;
}
describe.only('css', () => {
describe('css', () => {
fs.readdirSync('test/css/samples').forEach(dir => {
if (dir[0] === '.') return;

@ -21,7 +21,7 @@ function tryToReadFile(file) {
const sveltePath = process.cwd();
describe("ssr", () => {
describe.only("ssr", () => {
before(() => {
require("../../register")({
extensions: ['.svelte', '.html'],
@ -64,9 +64,6 @@ describe("ssr", () => {
const rendered = Component.render(props);
const { html, css, head } = rendered;
// rendered.toString() === rendered.html
assert.equal(rendered, html);
fs.writeFileSync(`${dir}/_actual.html`, html);
if (css.code) fs.writeFileSync(`${dir}/_actual.css`, css.code);

@ -1,8 +1,8 @@
<script>
import Foo from './Foo.html';
export let y;
export let x;
export let y;
</script>
{y}<Foo bind:y={x}/>{y}
{y}<Foo bind:x={y}/>{y}

Loading…
Cancel
Save