Repository: sveltejs/svelte (JavaScript)
Date: 2019-02-07 16:36:52+00:00
Task type: Bug Fix

Issue:
Error with static slot scope values
A slot with a static scope value (`<slot value="Hi" />`) tries to get a variable named `Hi`, throwing `Hi is not defined`.

REPL: https://v3.svelte.technology/repl?version=3.0.0-beta.1&gist=7d9d362f30f917013bf8d91e9364b28d


Issue discussion:
It looks like the problem is here:

https://github.com/sveltejs/svelte/blob/c2e6d1bf0db20272a4ce53ffbf62c96e4e468ce3/src/utils/get_slot_data.ts#L14

But I'm not sure what we should have there instead. `stringify_attribute` turns things like `foo{bar}` into `foo${escape(bar)}`, so perhaps we should be enclosing its value in backquotes. But I don't think we want to be calling `escape()` on these values.

Solution patch:
diff --git a/src/compile/render-dom/wrappers/Slot.ts b/src/compile/render-dom/wrappers/Slot.ts
--- a/src/compile/render-dom/wrappers/Slot.ts
+++ b/src/compile/render-dom/wrappers/Slot.ts
@@ -63,7 +63,7 @@ export default class SlotWrapper extends Wrapper {
 			get_slot_changes = renderer.component.getUniqueName(`get_${slot_name}_slot_changes`);
 			get_slot_context = renderer.component.getUniqueName(`get_${slot_name}_slot_context`);
 
-			const context_props = get_slot_data(attributes);
+			const context_props = get_slot_data(attributes, false);
 			const changes_props = [];
 
 			const dependencies = new Set();
diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts
--- a/src/compile/render-ssr/handlers/Element.ts
+++ b/src/compile/render-ssr/handlers/Element.ts
@@ -78,7 +78,7 @@ export default function(node, renderer, options) {
 				args.push(snip(attribute.expression));
 			} else {
 				if (attribute.name === 'value' && node.name === 'textarea') {
-					textareaContents = stringify_attribute(attribute);
+					textareaContents = stringify_attribute(attribute, true);
 				} else if (attribute.isTrue) {
 					args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
 				} else if (
@@ -89,7 +89,7 @@ export default function(node, renderer, options) {
 					// a boolean attribute with one non-Text chunk
 					args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
 				} else {
-					args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringify_attribute(attribute)}\` }`);
+					args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringify_attribute(attribute, true)}\` }`);
 				}
 			}
 		});
@@ -100,7 +100,7 @@ export default function(node, renderer, options) {
 			if (attribute.type !== 'Attribute') return;
 
 			if (attribute.name === 'value' && node.name === 'textarea') {
-				textareaContents = stringify_attribute(attribute);
+				textareaContents = stringify_attribute(attribute, true);
 			} else if (attribute.isTrue) {
 				openingTag += ` ${attribute.name}`;
 			} else if (
@@ -112,14 +112,14 @@ export default function(node, renderer, options) {
 				openingTag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
 			} else if (attribute.name === 'class' && classExpr) {
 				addClassAttribute = false;
-				openingTag += ` class="\${[\`${stringify_attribute(attribute)}\`, ${classExpr}].join(' ').trim() }"`;
+				openingTag += ` class="\${[\`${stringify_attribute(attribute, true)}\`, ${classExpr}].join(' ').trim() }"`;
 			} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
 				const { name } = attribute;
 				const snippet = snip(attribute.chunks[0]);
 
 				openingTag += '${(v => v == null ? "" : ` ' + name + '="${@escape(' + snippet + ')}"`)(' + snippet + ')}';
 			} else {
-				openingTag += ` ${attribute.name}="${stringify_attribute(attribute)}"`;
+				openingTag += ` ${attribute.name}="${stringify_attribute(attribute, true)}"`;
 			}
 		});
 	}
diff --git a/src/compile/render-ssr/handlers/Slot.ts b/src/compile/render-ssr/handlers/Slot.ts
--- a/src/compile/render-ssr/handlers/Slot.ts
+++ b/src/compile/render-ssr/handlers/Slot.ts
@@ -7,7 +7,7 @@ export default function(node, renderer, options) {
 	const slot_name = name && name.chunks[0].data || 'default';
 	const prop = quotePropIfNecessary(slot_name);
 
-	const slot_data = get_slot_data(node.attributes);
+	const slot_data = get_slot_data(node.attributes, true);
 
 	const arg = slot_data.length > 0 ? `{ ${slot_data.join(', ')} }` : '';
 
diff --git a/src/utils/get_slot_data.ts b/src/utils/get_slot_data.ts
--- a/src/utils/get_slot_data.ts
+++ b/src/utils/get_slot_data.ts
@@ -1,7 +1,7 @@
 import { snip } from './snip';
 import { stringify_attribute } from './stringify_attribute';
 
-export default function(attributes) {
+export default function get_slot_data(attributes, is_ssr: boolean) {
 	return attributes
 		.filter(attribute => attribute.name !== 'name')
 		.map(attribute => {
@@ -11,7 +11,7 @@ export default function(attributes) {
 					? '""'
 					: attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text'
 						? snip(attribute.chunks[0])
-						: stringify_attribute(attribute);
+						: '`' + stringify_attribute(attribute, is_ssr) + '`';
 
 			return `${attribute.name}: ${value}`;
 		});
diff --git a/src/utils/stringify_attribute.ts b/src/utils/stringify_attribute.ts
--- a/src/utils/stringify_attribute.ts
+++ b/src/utils/stringify_attribute.ts
@@ -3,14 +3,16 @@ import Node from '../compile/nodes/shared/Node';
 import { escapeTemplate, escape } from './stringify';
 import { snip } from './snip';
 
-export function stringify_attribute(attribute: Attribute) {
+export function stringify_attribute(attribute: Attribute, is_ssr: boolean) {
 	return attribute.chunks
 		.map((chunk: Node) => {
 			if (chunk.type === 'Text') {
 				return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
 			}
 
-			return '${@escape(' + snip(chunk) + ')}';
+			return is_ssr
+				? '${@escape(' + snip(chunk) + ')}'
+				: '${' + snip(chunk) + '}';
 		})
 		.join('');
 }
\ No newline at end of file


Test patch:
diff --git a/test/runtime/samples/component-slot-let-static/Nested.html b/test/runtime/samples/component-slot-let-static/Nested.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-static/Nested.html
@@ -0,0 +1 @@
+<slot value="Hi" />
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-let-static/_config.js b/test/runtime/samples/component-slot-let-static/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-static/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: `<p>Hi</p>`
+};
diff --git a/test/runtime/samples/component-slot-let-static/main.html b/test/runtime/samples/component-slot-let-static/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-let-static/main.html
@@ -0,0 +1,7 @@
+<script>
+	import Nested from './Nested.html';
+</script>
+
+<Nested let:value>
+	<p>{value}</p>
+</Nested>
\ No newline at end of file