Repository: sveltejs/svelte (JavaScript)
Date: 2019-02-14 22:49:52+00:00
Task type: Bug Fix

Issue:
Internal variables showing up in svelte:options bind:props
v3 alpha27

When using `<svelte:options bind:props` in a component where there is slot default content, the props object contains `$$slot_default` and `$$scope`. This causes an error when using the spread operator on html elements eg `<h1 {...props}` 
https://v3.svelte.technology/repl?version=3.0.0-alpha27&gist=b34f72aee305df48c06618219306d1db

the `$` prefixed prop elements are not there if there is no default slot content




Issue discussion:

Solution patch:
diff --git a/src/compile/Component.ts b/src/compile/Component.ts
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -69,6 +69,7 @@ export default class Component {
 	reactive_declaration_nodes: Set<Node> = new Set();
 	has_reactive_assignments = false;
 	injected_reactive_declaration_vars: Set<string> = new Set();
+	helpers: Set<string> = new Set();
 
 	indirectDependencies: Map<string, Set<string>> = new Map();
 
@@ -114,10 +115,6 @@ export default class Component {
 		this.componentOptions = process_component_options(this, this.ast.html.children);
 		this.namespace = namespaces[this.componentOptions.namespace] || this.componentOptions.namespace;
 
-		if (this.componentOptions.props) {
-			this.has_reactive_assignments = true;
-		}
-
 		if (compileOptions.customElement === true && !this.componentOptions.tag) {
 			throw new Error(`No tag name specified`); // TODO better error
 		}
@@ -131,6 +128,13 @@ export default class Component {
 		this.walk_module_js();
 		this.walk_instance_js_pre_template();
 
+		if (this.componentOptions.props) {
+			this.has_reactive_assignments = true;
+
+			const variable = this.var_lookup.get(this.componentOptions.props_object);
+			variable.reassigned = true;
+		}
+
 		this.name = this.getUniqueName(name);
 		this.fragment = new Fragment(this, ast.html);
 
@@ -196,14 +200,12 @@ export default class Component {
 
 		const banner = `/* ${this.file ? `${this.file} ` : ``}generated by Svelte v${"__VERSION__"} */`;
 
-		const helpers = new Set();
-
 		// TODO use same regex for both
 		result = result.replace(compileOptions.generate === 'ssr' ? /(@+|#+)(\w*(?:-\w*)?)/g : /(@+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
 			if (sigil === '@') {
 				if (internal_exports.has(name)) {
 					if (compileOptions.dev && internal_exports.has(`${name}Dev`)) name = `${name}Dev`;
-					helpers.add(name);
+					this.helpers.add(name);
 				}
 
 				return this.alias(name);
@@ -212,7 +214,7 @@ export default class Component {
 			return sigil.slice(1) + name;
 		});
 
-		const importedHelpers = Array.from(helpers)
+		const importedHelpers = Array.from(this.helpers)
 			.sort()
 			.map(name => {
 				const alias = this.alias(name);
@@ -761,9 +763,14 @@ export default class Component {
 										});
 									}
 
+									// can't use the @ trick here, because we're
+									// manipulating the underlying magic string
+									component.helpers.add('exclude_internal_props');
+									const exclude_internal_props = component.alias('exclude_internal_props');
+
 									const suffix = code.original[declarator.end] === ';'
-										? ` = $$props`
-										: ` = $$props;`
+										? ` = ${exclude_internal_props}($$props)`
+										: ` = ${exclude_internal_props}($$props);`
 
 									if (declarator.id.end === declarator.end) {
 										code.appendLeft(declarator.end, suffix);
diff --git a/src/internal/utils.js b/src/internal/utils.js
--- a/src/internal/utils.js
+++ b/src/internal/utils.js
@@ -60,3 +60,8 @@ export function get_slot_context(definition, ctx, fn) {
 		: ctx.$$scope.ctx;
 }
 
+export function exclude_internal_props(props) {
+	const result = {};
+	for (const k in props) if (k[0] !== '$') result[k] = props[k];
+	return result;
+}
\ No newline at end of file


Test patch:
diff --git a/test/runtime/samples/props-excludes-external/RenderProps.svelte b/test/runtime/samples/props-excludes-external/RenderProps.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/props-excludes-external/RenderProps.svelte
@@ -0,0 +1,7 @@
+<svelte:options bind:props/>
+
+<script>
+	let props;
+</script>
+
+<p>{JSON.stringify(props)}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/props-excludes-external/_config.js b/test/runtime/samples/props-excludes-external/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/props-excludes-external/_config.js
@@ -0,0 +1,17 @@
+export default {
+	props: {
+		x: 1
+	},
+
+	html: `
+		<p>{"x":1}</p>
+	`,
+
+	test({ assert, component, target }) {
+		component.x = 2;
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>{"x":2}</p>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/props-excludes-external/main.svelte b/test/runtime/samples/props-excludes-external/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/props-excludes-external/main.svelte
@@ -0,0 +1,9 @@
+<script>
+	import RenderProps from './RenderProps.svelte';
+
+	export let x;
+</script>
+
+<RenderProps {x}>
+	<p>some (unused) slotted content, to create an internal prop</p>
+</RenderProps>
\ No newline at end of file