Repository: sveltejs/svelte (JavaScript)
Date: 2017-08-31 15:11:41+00:00
Task type: Bug Fix

Issue:
SSR bug with nested components
Looks like this arose with the `<slot>` changes. Compiling

```html
<Outer>
	<Inner>Foo</Inner>
</Outer>
<script>
	export default { components: { Outer, Inner } }
</script>
```

in SSR mode produces

<details>
<summary>click to expand</summary>

```javascript
'use strict';

var template = (function () {
	return { components: { Outer, Inner } }
}());

var SvelteComponent = {};

SvelteComponent.filename = "SvelteComponent.html";

SvelteComponent.data = function () {
	return {};
};

SvelteComponent.render = function ( state, options ) {
	state = state || {};

	return `
${template.components.Inner.render({}, { slotted: { default: () => `Foo` } })}
${template.components.Outer.render({}, { slotted: { default: () => `Foo`, null: () => `` } })}
`.trim();
};

SvelteComponent.renderCss = function () {
	var components = [];

	var seen = {};

	function addComponent ( component ) {
		var result = component.renderCss();
		result.components.forEach( x => {
			if ( seen[ x.filename ] ) return;
			seen[ x.filename ] = true;
			components.push( x );
		});
	}

	addComponent( template.components.Outer );
	addComponent( template.components.Inner );

	return {
		css: components.map( x => x.css ).join( '\n' ),
		map: null,
		components
	};
};

var escaped = {
	'"': '&quot;',
	"'": '&#39;',
	'&': '&amp;',
	'<': '&lt;',
	'>': '&gt;'
};

function __escape ( html ) {
	return String( html ).replace( /["'&<>]/g, match => escaped[ match ] );
}

module.exports = SvelteComponent;
```
</details>

`<Inner>` is rendered _before_ `<Outer>`, instead of being passed as a slot. This only happens when `<Inner>` has its own child when used inside `<Outer>` (i.e., just `<Outer><Inner/></Outer>` doesn't seem to experience this). Only an issue with SSR so far as I can tell.


Issue discussion:

Solution patch:
diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts
--- a/src/generators/server-side-rendering/index.ts
+++ b/src/generators/server-side-rendering/index.ts
@@ -6,13 +6,13 @@ import preprocess from './preprocess';
 import visit from './visit';
 import { removeNode, removeObjectKey } from '../../utils/removeNode';
 import { Parsed, Node, CompileOptions } from '../../interfaces';
+import { AppendTarget } from './interfaces';
 import { stringify } from '../../utils/stringify';
 
 export class SsrGenerator extends Generator {
 	bindings: string[];
 	renderCode: string;
-	appendTargets: Record<string, string> | null;
-	appendTarget: string | null;
+	appendTargets: AppendTarget[];
 
 	constructor(
 		parsed: Parsed,
@@ -24,7 +24,7 @@ export class SsrGenerator extends Generator {
 		super(parsed, source, name, stylesheet, options);
 		this.bindings = [];
 		this.renderCode = '';
-		this.appendTargets = null;
+		this.appendTargets = [];
 
 		// in an SSR context, we don't need to include events, methods, oncreate or ondestroy
 		const { templateProperties, defaultExport } = this;
@@ -60,24 +60,14 @@ export class SsrGenerator extends Generator {
 	}
 
 	append(code: string) {
-		if (this.appendTarget) {
-			this.appendTargets[this.appendTarget] += code;
+		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;
 		}
 	}
-
-	removeAppendTarget() {
-		this.appendTarget = this.appendTargets = null;
-	}
-
-	setAppendTarget(name: string) {
-		if (!this.appendTargets[name]) {
-			this.appendTargets[name] = '';
-		}
-
-		this.appendTarget = name;
-	}
 }
 
 export default function ssr(
diff --git a/src/generators/server-side-rendering/interfaces.ts b/src/generators/server-side-rendering/interfaces.ts
new file mode 100644
--- /dev/null
+++ b/src/generators/server-side-rendering/interfaces.ts
@@ -0,0 +1,15 @@
+import { SsrGenerator } from './index';
+import Block from './Block';
+import { Node } from '../../interfaces';
+
+export type Visitor = (
+	generator: SsrGenerator,
+	block: Block,
+	node: Node,
+	componentStack: Node[]
+) => void;
+
+export interface AppendTarget {
+	slots: Record<string, string>;
+	slotStack: string[]
+}
\ No newline at end of file
diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts
--- a/src/generators/server-side-rendering/visitors/Component.ts
+++ b/src/generators/server-side-rendering/visitors/Component.ts
@@ -2,6 +2,7 @@ import flattenReference from '../../../utils/flattenReference';
 import visit from '../visit';
 import { SsrGenerator } from '../index';
 import Block from '../Block';
+import { AppendTarget } from '../interfaces';
 import { Node } from '../../../interfaces';
 import getObject from '../../../utils/getObject';
 import getTailSnippet from '../../../utils/getTailSnippet';
@@ -80,19 +81,24 @@ export default function visitComponent(
 	let open = `\${${expression}.render({${props}}`;
 
 	if (node.children.length) {
-		generator.appendTargets = {};
-		generator.setAppendTarget('default');
+		const appendTarget: AppendTarget = {
+			slots: { default: '' },
+			slotStack: ['default']
+		};
+
+		generator.appendTargets.push(appendTarget);
 
 		node.children.forEach((child: Node) => {
 			visit(generator, block, child);
 		});
 
-		const slotted = Object.keys(generator.appendTargets)
-			.map(name => `${name}: () => \`${generator.appendTargets[name]}\``)
+		const slotted = Object.keys(appendTarget.slots)
+			.map(name => `${name}: () => \`${appendTarget.slots[name]}\``)
 			.join(', ');
 
 		open += `, { slotted: { ${slotted} } }`;
-		generator.setAppendTarget(null);
+
+		generator.appendTargets.pop();
 	}
 
 	generator.append(open);
diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts
--- a/src/generators/server-side-rendering/visitors/Element.ts
+++ b/src/generators/server-side-rendering/visitors/Element.ts
@@ -49,7 +49,10 @@ export default function visitElement(
 
 	const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot');
 	if (slot) {
-		generator.setAppendTarget(slot.value[0].data);
+		const slotName = slot.value[0].data;
+		const appendTarget = generator.appendTargets[generator.appendTargets.length - 1];
+		appendTarget.slotStack.push(slotName);
+		appendTarget.slots[slotName] = '';
 	}
 
 	node.attributes.forEach((attribute: Node) => {


Test patch:
diff --git a/test/runtime/samples/component-slot-nested-component/Inner.html b/test/runtime/samples/component-slot-nested-component/Inner.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-nested-component/Inner.html
@@ -0,0 +1,3 @@
+<div class='inner'>
+	<slot></slot>
+</div>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-nested-component/Outer.html b/test/runtime/samples/component-slot-nested-component/Outer.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-nested-component/Outer.html
@@ -0,0 +1,3 @@
+<div class='outer'>
+	<slot></slot>
+</div>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-nested-component/_config.js b/test/runtime/samples/component-slot-nested-component/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-nested-component/_config.js
@@ -0,0 +1,6 @@
+export default {
+	html: `
+		<div class='outer'>
+			<div class='inner'>foo</div>
+		</div>`
+};
diff --git a/test/runtime/samples/component-slot-nested-component/main.html b/test/runtime/samples/component-slot-nested-component/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-nested-component/main.html
@@ -0,0 +1,12 @@
+<Outer>
+	<Inner>foo</Inner>
+</Outer>
+
+<script>
+	import Outer from './Outer.html';
+	import Inner from './Inner.html';
+
+	export default {
+		components: { Outer, Inner }
+	};
+</script>
\ No newline at end of file