Repository: sveltejs/svelte (JavaScript)
Date: 2019-01-26 18:27:20+00:00
Task type: Bug Fix

Issue:
Inline mutations aren't tracked
[REPL](https://v3.svelte.technology/repl?version=3.0.0-alpha19&gist=6bf337a0d221413293a0640494692ddf). If a mutation happens inside the template, instead of in the `<script>`, it doesn't mark the value as mutable so it gets erroneously hoisted. Working on a fix


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
@@ -160,9 +160,12 @@ export default class Component {
 			: this.name;
 
 		this.walk_module_js();
-		this.walk_instance_js();
+		this.walk_instance_js_pre_template();
 
 		this.fragment = new Fragment(this, ast.html);
+
+		this.walk_instance_js_post_template();
+
 		if (!options.customElement) this.stylesheet.reify();
 
 		this.stylesheet.warnOnUnusedSelectors(stats);
@@ -510,7 +513,7 @@ export default class Component {
 		this.module_javascript = this.extract_javascript(script);
 	}
 
-	walk_instance_js() {
+	walk_instance_js_pre_template() {
 		const script = this.instance_script;
 		if (!script) return;
 
@@ -545,6 +548,12 @@ export default class Component {
 
 		this.track_mutations();
 		this.extract_imports_and_exports(script.content, this.imports, this.props);
+	}
+
+	walk_instance_js_post_template() {
+		const script = this.instance_script;
+		if (!script) return;
+		
 		this.hoist_instance_declarations();
 		this.extract_reactive_declarations();
 		this.extract_reactive_store_references();
@@ -756,12 +765,13 @@ export default class Component {
 		// hoistable functions. TODO others?
 
 		const { hoistable_names, hoistable_nodes, imported_declarations, instance_scope: scope } = this;
+		const template_scope = this.fragment.scope;
 
 		const top_level_function_declarations = new Map();
 
 		this.instance_script.content.body.forEach(node => {
 			if (node.type === 'VariableDeclaration') {
-				if (node.declarations.every(d => d.init && d.init.type === 'Literal' && !this.mutable_props.has(d.id.name))) {
+				if (node.declarations.every(d => d.init && d.init.type === 'Literal' && !this.mutable_props.has(d.id.name) && !template_scope.containsMutable([d.id.name]))) {
 					node.declarations.forEach(d => {
 						hoistable_names.add(d.id.name);
 					});


Test patch:
diff --git a/test/js/samples/instrumentation-template-if-no-block/expected.js b/test/js/samples/instrumentation-template-if-no-block/expected.js
--- a/test/js/samples/instrumentation-template-if-no-block/expected.js
+++ b/test/js/samples/instrumentation-template-if-no-block/expected.js
@@ -11,7 +11,7 @@ function create_fragment($$, ctx) {
 			text1 = createText("\n\n");
 			p = createElement("p");
 			text2 = createText("x: ");
-			text3 = createText(x);
+			text3 = createText(ctx.x);
 			dispose = addListener(button, "click", ctx.click_handler);
 		},
 
@@ -25,7 +25,7 @@ function create_fragment($$, ctx) {
 
 		p(changed, ctx) {
 			if (changed.x) {
-				setData(text3, x);
+				setData(text3, ctx.x);
 			}
 		},
 
@@ -44,15 +44,14 @@ function create_fragment($$, ctx) {
 	};
 }
 
-let x = 0;
-
 function instance($$self, $$props, $$invalidate) {
+	let x = 0;
 
 	function click_handler() {
 		if (true) { x += 1; $$invalidate('x', x); }
 	}
 
-	return { click_handler };
+	return { x, click_handler };
 }
 
 class SvelteComponent extends SvelteComponent_1 {
diff --git a/test/runtime/samples/component-template-inline-mutation/Widget.html b/test/runtime/samples/component-template-inline-mutation/Widget.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-template-inline-mutation/Widget.html
@@ -0,0 +1,5 @@
+<script>
+  let count = 0;
+</script>
+
+<button on:click="{() => count += 1}">{count}</button>
\ No newline at end of file
diff --git a/test/runtime/samples/component-template-inline-mutation/_config.js b/test/runtime/samples/component-template-inline-mutation/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-template-inline-mutation/_config.js
@@ -0,0 +1,14 @@
+export default {
+	async test({ assert, target }) {
+		const btns = target.querySelectorAll('button');
+		const event = new window.MouseEvent('click');
+
+		await btns[0].dispatchEvent(event);
+		await btns[0].dispatchEvent(event);
+		await btns[1].dispatchEvent(event);
+		await btns[1].dispatchEvent(event);
+		await btns[1].dispatchEvent(event);
+
+		assert.equal(btns[1].innerHTML, '3');
+	}
+};
diff --git a/test/runtime/samples/component-template-inline-mutation/main.html b/test/runtime/samples/component-template-inline-mutation/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-template-inline-mutation/main.html
@@ -0,0 +1,6 @@
+<script>
+	import Widget from './Widget.html';
+</script>
+
+<Widget />
+<Widget />
\ No newline at end of file