basic SSR working

pull/3539/head
Richard Harris 6 years ago
parent e3c98cf279
commit a2e96dbaba

@ -837,7 +837,7 @@ export default class Component {
}
rewrite_props(get_insert: (variable: Var) => Node[]) {
// TODO
if (!this.ast.instance) return;
const component = this;
const { instance_scope, instance_scope_map: map } = this;

@ -13,6 +13,7 @@ import Text from './handlers/Text';
import Title from './handlers/Title';
import { AppendTarget, CompileOptions } from '../../interfaces';
import { INode } from '../nodes/interfaces';
import { Expression } from 'estree';
type Handler = (node: any, renderer: Renderer, options: CompileOptions) => void;
@ -43,17 +44,45 @@ export interface RenderOptions extends CompileOptions{
export default class Renderer {
has_bindings = false;
code = '';
state = {
quasi: {
type: 'TemplateElement',
value: { raw: '' }
}
};
literal = {
type: 'TemplateLiteral',
expressions: [],
quasis: []
};
targets: AppendTarget[] = [];
append(code: string) {
if (this.targets.length) {
const target = this.targets[this.targets.length - 1];
const slot_name = target.slot_stack[target.slot_stack.length - 1];
target.slots[slot_name] += code;
} else {
this.code += code;
throw new Error('no more append');
// if (this.targets.length) {
// const target = this.targets[this.targets.length - 1];
// const slot_name = target.slot_stack[target.slot_stack.length - 1];
// target.slots[slot_name] += code;
// } else {
// this.code += code;
// }
}
add_string(str: string) {
this.state.quasi.value.raw += str;
}
add_expression(node: Expression) {
this.literal.quasis.push(this.state.quasi);
this.literal.expressions.push(node);
this.state.quasi = {
type: 'TemplateElement',
value: { raw: '' }
};
}
render(nodes: INode[], options: RenderOptions) {

@ -1,12 +1,12 @@
import { is_void, quote_prop_if_necessary, quote_name_if_necessary } from '../../../utils/names';
import Attribute from '../../nodes/Attribute';
import Class from '../../nodes/Class';
import { snip } from '../../utils/snip';
import { stringify_attribute, stringify_class_attribute } from '../../utils/stringify_attribute';
import { get_slot_scope } from './shared/get_slot_scope';
import Renderer, { RenderOptions } from '../Renderer';
import Element from '../../nodes/Element';
import Text from '../../nodes/Text';
import { x } from 'code-red';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
@ -52,7 +52,7 @@ const boolean_attributes = new Set([
export default function(node: Element, renderer: Renderer, options: RenderOptions & {
slot_scopes: Map<any, any>;
}) {
let opening_tag = `<${node.name}`;
renderer.add_string(`<${node.name}`);
// awkward special case
let node_contents;
@ -96,29 +96,29 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
const args = [];
node.attributes.forEach(attribute => {
if (attribute.is_spread) {
args.push(snip(attribute.expression));
args.push(attribute.expression.node);
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
node_contents = stringify_attribute(attribute, true);
} else if (attribute.is_true) {
args.push(`{ ${quote_name_if_necessary(attribute.name)}: true }`);
args.push(x`{ ${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(`{ ${quote_name_if_necessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
args.push(x`{ ${attribute.name}: ${attribute.chunks[0].node} }`);
} else if (attribute.name === 'class' && class_expression) {
// Add class expression
args.push(`{ ${quote_name_if_necessary(attribute.name)}: [\`${stringify_class_attribute(attribute)}\`, \`\${${class_expression}}\`].join(' ').trim() }`);
args.push(x`{ ${attribute.name}: [${stringify_class_attribute(attribute)}, ${class_expression}}].join(' ').trim() }`);
} else {
args.push(`{ ${quote_name_if_necessary(attribute.name)}: \`${attribute.name === 'class' ? stringify_class_attribute(attribute) : stringify_attribute(attribute, true)}\` }`);
args.push(x`{ ${attribute.name}: ${attribute.name === 'class' ? stringify_class_attribute(attribute) : stringify_attribute(attribute, true)} }`);
}
}
});
opening_tag += "${@spread([" + args.join(', ') + "])}";
renderer.add_expression(x`@spread([${args}])`);
} else {
node.attributes.forEach((attribute: Attribute) => {
if (attribute.type !== 'Attribute') return;
@ -126,23 +126,32 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (attribute.name === 'value' && node.name === 'textarea') {
node_contents = stringify_attribute(attribute, true);
} else if (attribute.is_true) {
opening_tag += ` ${attribute.name}`;
renderer.add_string(` ${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
opening_tag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
throw new Error('here');
renderer.add_expression(x`${attribute.chunks[0]} ? "${attribute.name}" : ""`);
} else if (attribute.name === 'class' && class_expression) {
add_class_attribute = false;
opening_tag += ` class="\${[\`${stringify_class_attribute(attribute)}\`, ${class_expression}].join(' ').trim() }"`;
renderer.add_string(` class="`);
renderer.add_expression(x`[${stringify_class_attribute(attribute)}, ${class_expression}].join(' ').trim()`);
renderer.add_string(`"`);
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const snippet = snip(attribute.chunks[0]);
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ', ' + (boolean_attributes.has(name) ? 1 : 0) + ')}';
const snippet = attribute.chunks[0].node;
console.log(snippet);
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, ${boolean_attributes.has(name) ? 1 : 0})`);
} else {
opening_tag += ` ${attribute.name}="${attribute.name === 'class' ? stringify_class_attribute(attribute) : stringify_attribute(attribute, true)}"`;
renderer.add_string(` ${attribute.name}="`);
attribute.chunks.forEach(chunk => {
if (chunk.type === 'Text') renderer.add_string(chunk.data);
else renderer.add_expression(x`@escape(${chunk.node})`);
});
renderer.add_string(`"`);
}
});
}
@ -157,38 +166,36 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (name === 'group') {
// TODO server-render group bindings
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) {
node_contents = snip(expression);
value = name === 'textContent' ? '@escape($$value)' : '$$value';
node_contents = expression.node;
value = name === 'textContent' ? x`@escape($$value)` : x`$$value`;
} else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = snip(expression);
node_contents = '${(' + snippet + ') || ""}';
const snippet = expression.node;
node_contents = x`${snippet} || ""`;
} else {
const snippet = snip(expression);
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ', 1)}';
const snippet = expression.node;
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, 1)`);
}
});
if (add_class_attribute) {
opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
}
opening_tag += '>';
// if (add_class_attribute) {
// opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
// }
renderer.append(opening_tag);
renderer.add_string('>');
if (node_contents !== undefined) {
if (contenteditable) {
renderer.append('${($$value => $$value === void 0 ? `');
renderer.render(node.children, options);
renderer.append('` : ' + value + ')(' + node_contents + ')}');
} else {
renderer.append(node_contents);
}
// if (contenteditable) {
// renderer.append('${($$value => $$value === void 0 ? `');
// renderer.render(node.children, options);
// renderer.append('` : ' + value + ')(' + node_contents + ')}');
// } else {
// renderer.append(node_contents);
// }
} else {
renderer.render(node.children, options);
}
if (!is_void(node.name)) {
renderer.append(`</${node.name}>`);
renderer.add_string(`</${node.name}>`);
}
}

@ -1,6 +1,5 @@
import { escape, escape_template, stringify } from '../../utils/stringify';
import { escape, escape_template, stringify, string_literal } from '../../utils/stringify';
import { quote_name_if_necessary } from '../../../utils/names';
import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
import { get_slot_scope } from './shared/get_slot_scope';
import { AppendTarget } from '../../../interfaces';
@ -18,19 +17,15 @@ function stringify_attribute(chunk: INode) {
}
function get_attribute_value(attribute) {
if (attribute.is_true) return `true`;
if (attribute.chunks.length === 0) return `''`;
if (attribute.is_true) return x`true`;
if (attribute.chunks.length === 0) return x`''`;
if (attribute.chunks.length === 1) {
const chunk = attribute.chunks[0];
if (chunk.type === 'Text') {
return stringify(chunk.data);
}
return snip(chunk);
}
return '`' + attribute.chunks.map(stringify_attribute).join('') + '`';
return attribute.chunks
.map(chunk => {
if (chunk.type === 'Text') return string_literal(chunk.data);
return chunk.node;
})
.reduce((lhs, rhs) => x`${lhs} + ${rhs}`);
}
export default function(node: InlineComponent, renderer: Renderer, options: RenderOptions) {
@ -41,7 +36,7 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
renderer.has_bindings = true;
// TODO this probably won't work for contextual bindings
const snippet = snip(binding.expression);
const snippet = binding.expression.node;
binding_props.push(p`${binding.name}: ${snippet}`);
binding_fns.push(p`${binding.name}: $$value => { ${snippet} = $$value; $$settled = false }`);
@ -66,7 +61,7 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
})`;
} else {
props = x`{
${node.attributes.map(attribute => p`${attribute.name}: ${get_attribute_value(attribute)}`)}
${node.attributes.map(attribute => p`${attribute.name}: ${get_attribute_value(attribute)}`)},
${binding_props}
}`;
}
@ -115,5 +110,5 @@ export default function(node: InlineComponent, renderer: Renderer, options: Rend
${slot_fns}
}`;
renderer.append(`\${@validate_component(${expression}, '${node.name}').$$render($$result, ${props}, ${bindings}, ${slots})}`);
renderer.add_expression(x`@validate_component(${expression}, "${node.name}").$$render($$result, ${props}, ${bindings}, ${slots})`);
}

@ -1,13 +1,15 @@
import { snip } from '../../utils/snip';
import Renderer, { RenderOptions } from '../Renderer';
import { x } from 'code-red';
export default function(node, renderer: Renderer, _options: RenderOptions) {
const snippet = snip(node.expression);
const snippet = node.expression.node;
renderer.append(
renderer.add_expression(
node.parent &&
node.parent.type === 'Element' &&
node.parent.name === 'style'
? '${' + snippet + '}'
: '${@escape(' + snippet + ')}'
? snippet
: x`@escape(${snippet})`
);
}

@ -1,4 +1,4 @@
import { escape_html, escape_template, escape } from '../../utils/stringify';
import { escape_html } from '../../utils/stringify';
import Renderer, { RenderOptions } from '../Renderer';
import Text from '../../nodes/Text';
import Element from '../../nodes/Element';
@ -13,5 +13,6 @@ export default function(node: Text, renderer: Renderer, _options: RenderOptions)
// unless this Text node is inside a <script> or <style> element, escape &,<,>
text = escape_html(text);
}
renderer.append(escape(escape_template(text)));
renderer.add_string(text);
}

@ -19,6 +19,9 @@ export default function ssr(
locate: component.locate
}, options));
// TODO put this inside the Renderer class
renderer.literal.quasis.push(renderer.state.quasi);
// TODO concatenate CSS maps
const css = options.customElement ?
{ code: null, map: null } :
@ -60,7 +63,7 @@ export default function ssr(
// TODO only do this for props with a default value
const parent_bindings = instance_javascript
? props.map(prop => {
return `if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
return b`if ($$props.${prop.export_name} === void 0 && $$bindings.${prop.export_name} && ${prop.name} !== void 0) $$bindings.${prop.export_name}(${prop.name});`;
})
: [];
@ -105,7 +108,7 @@ export default function ssr(
${reactive_declarations}
$$rendered = \`${renderer.code}\`;
$$rendered = ${renderer.literal};
} while (!$$settled);
return $$rendered;
@ -115,23 +118,22 @@ export default function ssr(
${reactive_declarations}
return \`${renderer.code}\`;`;
return ${renderer.literal};`;
const blocks = [
reactive_stores.length > 0 && `let ${reactive_stores
.map(({ name }) => {
...reactive_stores.map(({ name }) => {
const store_name = name.slice(1);
const store = component.var_lookup.get(store_name);
if (store && store.hoistable) {
const get_store_value = component.helper('get_store_value');
return `${name} = ${get_store_value}(${store_name})`;
return b`let ${name} = ${get_store_value}(${store_name});`;
}
return name;
})
.join(', ')};`,
return b`let ${name};`;
}),
instance_javascript,
parent_bindings.join('\n'),
css.code && `$$result.css.add(#css);`,
...parent_bindings,
css.code && b`$$result.css.add(#css);`,
main
].filter(Boolean);
@ -147,7 +149,7 @@ export default function ssr(
${component.fully_hoisted}
const ${name} = @create_ssr_component(($$result, $$props, $$bindings, $$slots) => {
${blocks.join('\n\n')}
${blocks}
});
`;
}

@ -182,6 +182,7 @@ export function showOutput(cwd, options = {}, compile = svelte.compile) {
glob('**/*.svelte', { cwd }).forEach(file => {
if (file[0] === '_') return;
try {
const { js } = compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
Object.assign(options, {
@ -192,6 +193,9 @@ export function showOutput(cwd, options = {}, compile = svelte.compile) {
console.log( // eslint-disable-line no-console
`\n>> ${colors.cyan().bold(file)}\n${addLineNumbers(js.code)}\n<< ${colors.cyan().bold(file)}`
);
} catch (err) {
console.log(`failed to generate output: ${err.message}`);
}
});
}

@ -26,7 +26,7 @@ process.on('unhandledRejection', err => {
unhandled_rejection = err;
});
describe.only("runtime", () => {
describe("runtime", () => {
before(() => {
svelte = loadSvelte(false);
svelte$ = loadSvelte(true);

@ -20,7 +20,7 @@ function tryToReadFile(file) {
const sveltePath = process.cwd().split('\\').join('/');
describe("ssr", () => {
describe.only("ssr", () => {
before(() => {
require("../../register")({
extensions: ['.svelte', '.html'],
@ -75,6 +75,7 @@ describe("ssr", () => {
if (show) showOutput(dir, { generate: 'ssr' });
} catch (err) {
showOutput(dir, { generate: 'ssr' });
err.stack += `\n\ncmd-click: ${path.relative(process.cwd(), dir)}/main.svelte`;
throw err;
}
});

Loading…
Cancel
Save