Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Regression in 1.47.0 with empty default slots
[This REPL](https://svelte.technology/repl?version=1.47.0&gist=9eef46e2ee66b098bfca26d29ab47017) fails with `Cannot read property 'default' of undefined`

but works in `<= v1.46.0`

The following ways of adding components in `v1.47.0` work:
```
<Person :data :foo :bar />
```
```
<Person :data :foo :bar ></Person>
```

The following ways fail:
```
<Person :data :foo :bar >
</Person
```
```
<Person 
  :data 
  :foo 
  :bar
>
</Person>
```



Discussion:
It looks like there are a couple things going on here.

- Trying to send a default slot into a component that isn't set up to use it (by containing `<slot/>` somewhere in its definition) will result in that runtime error. This is also present in 1.46.0
- Whitespace-only content is treated as no content for the default slot in 1.46.0 but is treated as something to try to stick into the default slot in 1.46.1.

I'm not sure which of these is the best to address. Fixing the second one would restore 1.46.0 behavior. Is fixing the first one more desirable? Or might that be considered hiding an error, and we actually want an exception to be thrown in that case?
IMO if you send slot content into a component that does not have a `<slot />` it should **not** throw an exception, maybe a dev warning

Resolving patch:
diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts
--- a/src/generators/nodes/Element.ts
+++ b/src/generators/nodes/Element.ts
@@ -29,7 +29,7 @@ export default class Element extends Node {
 			this.cannotUseInnerHTML();
 		}
 
-		const parentElement = this.parent && this.parent.findNearest('Element');
+		const parentElement = this.parent && this.parent.findNearest(/^Element/);
 		this.namespace = this.name === 'svg' ?
 			namespaces.svg :
 			parentElement ? parentElement.namespace : this.generator.namespace;
@@ -133,7 +133,7 @@ export default class Element extends Node {
 			this.cannotUseInnerHTML();
 			this.slotted = true;
 			// TODO validate slots — no nesting, no dynamic names...
-			const component = this.findNearest('Component');
+			const component = this.findNearest(/^Component/);
 			component._slots.add(slot);
 		}
 
@@ -171,7 +171,7 @@ export default class Element extends Node {
 
 		const slot = this.attributes.find((attribute: Node) => attribute.name === 'slot');
 		const initialMountNode = this.slotted ?
-			`${this.findNearest('Component').var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
+			`${this.findNearest(/^Component/).var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
 			parentNode;
 
 		block.addVariable(name);
diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts
--- a/src/generators/nodes/Text.ts
+++ b/src/generators/nodes/Text.ts
@@ -17,15 +17,26 @@ const elementsWithoutText = new Set([
 	'video',
 ]);
 
+function shouldSkip(node: Text) {
+	if (/\S/.test(node.data)) return false;
+
+	const parentElement = node.findNearest(/(?:Element|Component)/);
+	if (!parentElement) return false;
+
+	if (parentElement.type === 'Component') return parentElement.children.length === 1 && node === parentElement.children[0];
+
+	return parentElement.namespace || elementsWithoutText.has(parentElement.name);
+}
+
 export default class Text extends Node {
 	type: 'Text';
 	data: string;
 	shouldSkip: boolean;
 
 	init(block: Block) {
-		const parentElement = this.findNearest('Element');
+		const parentElement = this.findNearest(/(?:Element|Component)/);
 
-		if (!/\S/.test(this.data) && parentElement && (parentElement.namespace || elementsWithoutText.has(parentElement.name))) {
+		if (shouldSkip(this)) {
 			this.shouldSkip = true;
 			return;
 		}
diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts
--- a/src/generators/nodes/shared/Node.ts
+++ b/src/generators/nodes/shared/Node.ts
@@ -133,9 +133,9 @@ export default class Node {
 			false;
 	}
 
-	findNearest(type: string) {
-		if (this.type === type) return this;
-		if (this.parent) return this.parent.findNearest(type);
+	findNearest(selector: RegExp) {
+		if (selector.test(this.type)) return this;
+		if (this.parent) return this.parent.findNearest(selector);
 	}
 
 	getOrCreateAnchor(block: Block, parentNode: string) {


Test patch:
diff --git a/test/runtime/samples/component-slot-empty/Nested.html b/test/runtime/samples/component-slot-empty/Nested.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-empty/Nested.html
@@ -0,0 +1 @@
+<p>no slot here</p>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-empty/_config.js b/test/runtime/samples/component-slot-empty/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-empty/_config.js
@@ -0,0 +1,3 @@
+export default {
+	html: '<p>no slot here</p>'
+};
diff --git a/test/runtime/samples/component-slot-empty/main.html b/test/runtime/samples/component-slot-empty/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-empty/main.html
@@ -0,0 +1,12 @@
+<Nested>
+</Nested>
+
+<script>
+	import Nested from './Nested.html';
+
+	export default {
+		components: {
+			Nested
+		}
+	};
+</script>