Repository: sveltejs/svelte (JavaScript)
Date: 2019-02-18 04:08:12+00:00
Task type: Bug Fix

Issue:
Update store bindings when stores change
[This works...](https://v3.svelte.technology/repl?version=3.0.0-alpha21&gist=10aa8a10f0d48c466271b36a59506929)

```html
<script>
  import { writable } from 'svelte/store';
  let foo = writable(0);
</script>

<h1>{$foo}</h1>
<button on:click="{() => foo.update(n => n + 1)}">+1</button>
```

...but this will break:

```diff
<script>
  import { writable } from 'svelte/store';
  let foo = writable(0);
</script>

<h1>{$foo}</h1>
<button on:click="{() => foo.update(n => n + 1)}">+1</button>
+<button on:click="{() => foo = writable(0)}">reset</button>
```

Svelte doesn't know what to do when a store variable is reassigned. Ideally it would immediately tear down the previous subscription and create a new one.


Issue discussion:

Solution patch:
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
   ],
   "scripts": {
     "test": "mocha --opts mocha.opts",
+    "test:unit": "mocha --require sucrase/register --recursive ./**/__test__.ts",
     "quicktest": "mocha --opts mocha.opts",
     "precoverage": "c8 mocha --opts mocha.coverage.opts",
     "coverage": "c8 report --reporter=text-lcov > coverage.lcov && c8 report --reporter=html",
diff --git a/src/compile/Component.ts b/src/compile/Component.ts
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -183,7 +183,11 @@ export default class Component {
 				writable: true
 			});
 
-			this.add_reference(name.slice(1));
+			const subscribable_name = name.slice(1);
+			this.add_reference(subscribable_name);
+
+			const variable = this.var_lookup.get(subscribable_name);
+			variable.subscribable = true;
 		} else if (!this.ast.instance) {
 			this.add_var({
 				name,
@@ -213,6 +217,11 @@ export default class Component {
 		return this.aliases.get(name);
 	}
 
+	helper(name: string) {
+		this.helpers.add(name);
+		return this.alias(name);
+	}
+
 	generate(result: string) {
 		const { compileOptions, name } = this;
 		const { format = 'esm' } = compileOptions;
@@ -641,6 +650,9 @@ export default class Component {
 				});
 
 				this.add_reference(name.slice(1));
+
+				const variable = this.var_lookup.get(name.slice(1));
+				variable.subscribable = true;
 			} else {
 				this.add_var({
 					name,
@@ -738,7 +750,15 @@ export default class Component {
 		});
 	}
 
-	rewrite_props() {
+	invalidate(name, value = name) {
+		const variable = this.var_lookup.get(name);
+		if (variable && (variable.subscribable && variable.reassigned)) {
+			return `$$subscribe_${name}(), $$invalidate('${name}', ${value})`;
+		}
+		return `$$invalidate('${name}', ${value})`;
+	}
+
+	rewrite_props(get_insert: (variable: Var) => string) {
 		const component = this;
 		const { code, instance_scope, instance_scope_map: map, componentOptions } = this;
 		let scope = instance_scope;
@@ -759,72 +779,97 @@ export default class Component {
 
 				if (node.type === 'VariableDeclaration') {
 					if (node.kind === 'var' || scope === instance_scope) {
-						let has_exports = false;
-						let has_only_exports = true;
+						node.declarations.forEach((declarator, i) => {
+							const next = node.declarations[i + 1];
 
-						node.declarations.forEach(declarator => {
-							extractNames(declarator.id).forEach(name => {
-								const variable = component.var_lookup.get(name);
+							if (declarator.id.type !== 'Identifier') {
+								const inserts = [];
 
-								if (name === componentOptions.props_object) {
-									if (variable.export_name) {
-										component.error(declarator, {
-											code: 'exported-options-props',
-											message: `Cannot export props binding`
-										});
-									}
+								extractNames(declarator.id).forEach(name => {
+									const variable = component.var_lookup.get(name);
 
-									if (declarator.id.type !== 'Identifier') {
+									if (variable.export_name || name === componentOptions.props_object) {
 										component.error(declarator, {
-											code: 'todo',
-											message: `props binding in destructured declaration is not yet supported`
+											code: 'destructured-prop',
+											message: `Cannot declare props in destructured declaration`
 										});
 									}
 
-									// 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] === ';'
-										? ` = ${exclude_internal_props}($$props)`
-										: ` = ${exclude_internal_props}($$props);`
+									if (variable.subscribable) {
+										inserts.push(get_insert(variable));
+									}
+								});
 
-									if (declarator.id.end === declarator.end) {
-										code.appendLeft(declarator.end, suffix);
+								if (inserts.length > 0) {
+									if (next) {
+										code.overwrite(declarator.end, next.start, `; ${inserts.join('; ')}; ${node.kind} `);
 									} else {
-										code.overwrite(declarator.id.end, declarator.end, suffix);
+										code.appendLeft(declarator.end, `; ${inserts.join('; ')}`);
 									}
 								}
 
+								return;
+							}
+
+							const { name } = declarator.id;
+							const variable = component.var_lookup.get(name);
+
+							if (name === componentOptions.props_object) {
 								if (variable.export_name) {
-									has_exports = true;
+									component.error(declarator, {
+										code: 'exported-options-props',
+										message: `Cannot export props binding`
+									});
+								}
+
+								// can't use the @ trick here, because we're
+								// manipulating the underlying magic string
+								const exclude_internal_props = component.helper('exclude_internal_props');
+
+								const suffix = code.original[declarator.end] === ';'
+									? ` = ${exclude_internal_props}($$props)`
+									: ` = ${exclude_internal_props}($$props);`
+
+								if (declarator.id.end === declarator.end) {
+									code.appendLeft(declarator.end, suffix);
 								} else {
-									has_only_exports = false;
+									code.overwrite(declarator.id.end, declarator.end, suffix);
 								}
-							});
-						});
+							}
 
-						if (has_only_exports) {
-							if (current_group && current_group[current_group.length - 1].kind !== node.kind) {
+							if (variable.export_name) {
+								if (variable.subscribable) {
+									coalesced_declarations.push({
+										kind: node.kind,
+										declarators: [declarator],
+										insert: get_insert(variable)
+									});
+								} else {
+									if (current_group && current_group.kind !== node.kind) {
+										current_group = null;
+									}
+
+									if (!current_group) {
+										current_group = { kind: node.kind, declarators: [], insert: null };
+										coalesced_declarations.push(current_group);
+									}
+
+									current_group.declarators.push(declarator);
+								}
+							} else {
 								current_group = null;
-							}
 
-							// rewrite as a group, later
-							if (!current_group) {
-								current_group = [];
-								coalesced_declarations.push(current_group);
-							}
+								if (variable.subscribable) {
+									let insert = get_insert(variable);
 
-							current_group.push(node);
-						} else {
-							if (has_exports) {
-								// rewrite in place
-								throw new Error('TODO rewrite prop declaration in place');
+									if (next) {
+										code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `);
+									} else {
+										code.appendLeft(declarator.end, `; ${insert}`);
+									}
+								}
 							}
-
-							current_group = null;
-						}
+						});
 					}
 				} else {
 					if (node.type !== 'ExportNamedDeclaration') {
@@ -845,31 +890,25 @@ export default class Component {
 
 			let combining = false;
 
-			group.forEach(node => {
-				node.declarations.forEach(declarator => {
-					const { id, init } = declarator;
+			group.declarators.forEach(declarator => {
+				const { id } = declarator;
 
-					if (id.type === 'Identifier') {
-						const value = init
-							? this.code.slice(id.start, init.end)
-							: this.code.slice(id.start, id.end);
-
-						if (combining) {
-							code.overwrite(c, id.start, ', ');
-						} else {
-							code.appendLeft(id.start, '{ ');
-							combining = true;
-						}
-					} else {
-						throw new Error('TODO destructured declarations');
-					}
+				if (combining) {
+					code.overwrite(c, id.start, ', ');
+				} else {
+					code.appendLeft(id.start, '{ ');
+					combining = true;
+				}
 
-					c = declarator.end;
-				});
+				c = declarator.end;
 			});
 
 			if (combining) {
-				const suffix = code.original[c] === ';' ? ` } = $$props` : ` } = $$props;`;
+				const insert = group.insert
+					? `; ${group.insert}`
+					: '';
+
+				const suffix = code.original[c] === ';' ? ` } = $$props${insert}` : ` } = $$props${insert};`;
 				code.appendLeft(c, suffix);
 			}
 		});
diff --git a/src/compile/nodes/shared/Expression.ts b/src/compile/nodes/shared/Expression.ts
--- a/src/compile/nodes/shared/Expression.ts
+++ b/src/compile/nodes/shared/Expression.ts
@@ -290,7 +290,7 @@ export default class Expression {
 
 							if (dirty.length) component.has_reactive_assignments = true;
 
-							code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; '));
+							code.overwrite(node.start, node.end, dirty.map(n => component.invalidate(n)).join('; '));
 						} else {
 							names.forEach(name => {
 								if (scope.declarations.has(name)) return;
@@ -356,7 +356,7 @@ export default class Expression {
 					let body = code.slice(node.body.start, node.body.end).trim();
 					if (node.body.type !== 'BlockStatement') {
 						if (pending_assignments.size > 0) {
-							const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ');
+							const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
 							pending_assignments = new Set();
 
 							component.has_reactive_assignments = true;
@@ -431,7 +431,7 @@ export default class Expression {
 
 						const insert = (
 							(has_semi ? ' ' : '; ') +
-							Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ')
+							Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ')
 						);
 
 						if (/^(Break|Continue|Return)Statement/.test(node.type)) {
diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts
--- a/src/compile/render-dom/index.ts
+++ b/src/compile/render-dom/index.ts
@@ -79,12 +79,13 @@ export default function dom(
 				${component.componentOptions.props && deindent`
 				if (!${component.componentOptions.props}) ${component.componentOptions.props} = {};
 				@assign(${component.componentOptions.props}, $$props);
-				$$invalidate('${component.componentOptions.props_object}', ${component.componentOptions.props_object});
+				${component.invalidate(component.componentOptions.props_object)};
 				`}
 				${writable_props.map(prop =>
-				`if ('${prop.export_name}' in $$props) $$invalidate('${prop.name}', ${prop.name} = $$props.${prop.export_name});`)}
+				`if ('${prop.export_name}' in $$props) ${component.invalidate(prop.name, `${prop.name} = $$props.${prop.export_name}`)};`
+				)}
 				${renderer.slots.size > 0 &&
-				`if ('$$scope' in $$props) $$invalidate('$$scope', $$scope = $$props.$$scope);`}
+				`if ('$$scope' in $$props) ${component.invalidate('$$scope', `$$scope = $$props.$$scope`)};`}
 			}
 		`
 		: null;
@@ -175,7 +176,7 @@ export default function dom(
 
 						if (dirty.length) component.has_reactive_assignments = true;
 
-						code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; '));
+						code.overwrite(node.start, node.end, dirty.map(n => component.invalidate(n)).join('; '));
 					} else {
 						names.forEach(name => {
 							const owner = scope.findOwner(name);
@@ -204,7 +205,7 @@ export default function dom(
 
 				if (pending_assignments.size > 0) {
 					if (node.type === 'ArrowFunctionExpression') {
-						const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join(';');
+						const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
 						pending_assignments = new Set();
 
 						code.prependRight(node.body.start, `{ const $$result = `);
@@ -214,7 +215,7 @@ export default function dom(
 					}
 
 					else if (/Statement/.test(node.type)) {
-						const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join('; ');
+						const insert = Array.from(pending_assignments).map(name => component.invalidate(name)).join('; ');
 
 						if (/^(Break|Continue|Return)Statement/.test(node.type)) {
 							if (node.argument) {
@@ -240,7 +241,25 @@ export default function dom(
 			throw new Error(`TODO this should not happen!`);
 		}
 
-		component.rewrite_props();
+		component.rewrite_props(({ name, reassigned }) => {
+			const value = `$${name}`;
+
+			const callback = `$value => { ${value} = $$value; $$invalidate('${value}', ${value}) }`;
+
+			if (reassigned) {
+				return `$$subscribe_${name}()`;
+			}
+
+			const subscribe = component.helper('subscribe');
+
+			let insert = `${subscribe}($$self, ${name}, $${callback})`;
+			if (component.compileOptions.dev) {
+				const validate_store = component.helper('validate_store');
+				insert = `${validate_store}(${name}, '${name}'); ${insert}`;
+			}
+
+			return insert;
+		});
 	}
 
 	const args = ['$$self'];
@@ -301,22 +320,45 @@ export default function dom(
 		addToSet(all_reactive_dependencies, d.dependencies);
 	});
 
-	const user_code = component.javascript || (
-		!component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props)
-			? [
-				component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`,
-				filtered_props.length > 0 && `let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`
-			].filter(Boolean).join('\n')
-			: null
-	);
+	let user_code;
+
+	if (component.javascript) {
+		user_code = component.javascript;
+	} else {
+		if (!component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props)) {
+			const statements = [];
+
+			if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`);
+			if (filtered_props.length > 0) statements.push(`let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`);
 
-	const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores
+			reactive_stores.forEach(({ name }) => {
+				if (component.compileOptions.dev) {
+					statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`);
+				}
+
+				statements.push(`@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });`);
+			});
+
+			user_code = statements.join('\n');
+		}
+	}
+
+	const reactive_store_subscriptions = reactive_stores
+		.filter(store => {
+			const variable = component.var_lookup.get(store.name.slice(1));
+			return variable.hoistable;
+		})
 		.map(({ name }) => deindent`
-			let ${name};
 			${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}
-			$$self.$$.on_destroy.push(${name.slice(1)}.subscribe($$value => { ${name} = $$value; $$invalidate('${name}', ${name}); }));
-		`)
-		.join('\n\n');
+			@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });
+		`);
+
+	const resubscribable_reactive_store_unsubscribers = reactive_stores
+		.filter(store => {
+			const variable = component.var_lookup.get(store.name.slice(1));
+			return variable.reassigned;
+		})
+		.map(({ name }) => `$$self.$$.on_destroy.push(() => $$unsubscribe_${name.slice(1)}());`);
 
 	if (has_definition) {
 		const reactive_declarations = component.reactive_declarations.map(d => {
@@ -343,8 +385,26 @@ export default function dom(
 			return variable.injected;
 		});
 
+		const reactive_store_declarations = reactive_stores.map(variable => {
+			const $name = variable.name;
+			const name = $name.slice(1);
+
+			const store = component.var_lookup.get(name);
+			if (store.reassigned) {
+				return `${$name}, $$unsubscribe_${name} = @noop, $$subscribe_${name} = () => { $$unsubscribe_${name}(); $$unsubscribe_${name} = ${name}.subscribe($$value => { ${$name} = $$value; $$invalidate('${$name}', ${$name}); }) }`
+			}
+
+			return $name;
+		});
+
 		builder.addBlock(deindent`
 			function ${definition}(${args.join(', ')}) {
+				${reactive_store_declarations.length > 0 && `let ${reactive_store_declarations.join(', ')};`}
+
+				${reactive_store_subscriptions}
+
+				${resubscribable_reactive_store_unsubscribers}
+
 				${user_code}
 
 				${renderer.slots.size && `let { ${[...renderer.slots].map(name => `$$slot_${sanitize(name)}`).join(', ')}, $$scope } = $$props;`}
@@ -353,8 +413,6 @@ export default function dom(
 
 				${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')}
 
-				${reactive_store_subscriptions}
-
 				${set && `$$self.$set = ${set};`}
 
 				${reactive_declarations.length > 0 && deindent`
diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts
--- a/src/compile/render-dom/wrappers/Element/index.ts
+++ b/src/compile/render-dom/wrappers/Element/index.ts
@@ -465,7 +465,7 @@ export default class ElementWrapper extends Wrapper {
 			this.renderer.component.partly_hoisted.push(deindent`
 				function ${handler}(${contextual_dependencies.size > 0 ? `{ ${Array.from(contextual_dependencies).join(', ')} }` : ``}) {
 					${group.bindings.map(b => b.handler.mutation)}
-					${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `$$invalidate('${dep}', ${dep});`)}
+					${Array.from(dependencies).filter(dep => dep[0] !== '$').map(dep => `${this.renderer.component.invalidate(dep)};`)}
 				}
 			`);
 
@@ -532,7 +532,7 @@ export default class ElementWrapper extends Wrapper {
 			renderer.component.partly_hoisted.push(deindent`
 				function ${name}(${['$$node', 'check'].concat(args).join(', ')}) {
 					${handler.snippet ? `if ($$node || (!$$node && ${handler.snippet} === check)) ` : ''}${handler.mutation}
-					$$invalidate('${object}', ${object});
+					${renderer.component.invalidate(object)};
 				}
 			`);
 
diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts
--- a/src/compile/render-dom/wrappers/InlineComponent/index.ts
+++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts
@@ -266,7 +266,7 @@ export default class InlineComponentWrapper extends Wrapper {
 				component.partly_hoisted.push(deindent`
 					function ${fn}($$component) {
 						${lhs} = $$component;
-						${object && `$$invalidate('${object}', ${object});`}
+						${object && component.invalidate(object)}
 					}
 				`);
 
@@ -341,7 +341,7 @@ export default class InlineComponentWrapper extends Wrapper {
 			const body = deindent`
 				function ${name}(${args.join(', ')}) {
 					${lhs} = value;
-					return $$invalidate('${dependencies[0]}', ${dependencies[0]});
+					return ${component.invalidate(dependencies[0])}
 				}
 			`;
 
diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts
--- a/src/compile/render-ssr/index.ts
+++ b/src/compile/render-ssr/index.ts
@@ -3,6 +3,8 @@ import Component from '../Component';
 import { CompileOptions } from '../../interfaces';
 import { stringify } from '../../utils/stringify';
 import Renderer from './Renderer';
+import { walk } from 'estree-walker';
+import { extractNames } from '../../utils/annotateWithScopes';
 
 export default function ssr(
 	component: Component,
@@ -22,29 +24,53 @@ export default function ssr(
 		{ code: null, map: null } :
 		component.stylesheet.render(options.filename, true);
 
-	let user_code;
+	const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
+	const reactive_store_values = reactive_stores
+		.map(({ name }) => {
+			const assignment = `${name} = @get_store_value(${name.slice(1)});`;
+
+			return component.compileOptions.dev
+				? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}`
+				: assignment;
+		});
 
 	// TODO remove this, just use component.vars everywhere
 	const props = component.vars.filter(variable => !variable.module && variable.export_name && variable.export_name !== component.componentOptions.props_object);
 
+	let user_code;
+
 	if (component.javascript) {
-		component.rewrite_props();
+		component.rewrite_props(({ name }) => {
+			const value = `$${name}`;
+
+			const get_store_value = component.helper('get_store_value');
+
+			let insert = `${value} = ${get_store_value}(${name})`;
+			if (component.compileOptions.dev) {
+				const validate_store = component.helper('validate_store');
+				insert = `${validate_store}(${name}, '${name}'); ${insert}`;
+			}
+
+			return insert;
+		});
+
 		user_code = component.javascript;
 	} else if (!component.ast.instance && !component.ast.module && (props.length > 0 || component.componentOptions.props)) {
-		user_code = [
-			component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`,
-			props.length > 0 && `let { ${props.map(prop => prop.export_name).join(', ')} } = $$props;`
-		].filter(Boolean).join('\n');
-	}
+		const statements = [];
 
-	const reactive_stores = component.vars.filter(variable => variable.name[0] === '$');
-	const reactive_store_values = reactive_stores.map(({ name }) => {
-		const assignment = `const ${name} = @get_store_value(${name.slice(1)});`;
+		if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`);
+		if (props.length > 0) statements.push(`let { ${props.map(x => x.name).join(', ')} } = $$props;`);
 
-		return component.compileOptions.dev
-			? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}`
-			: assignment;
-	});
+		reactive_stores.forEach(({ name }) => {
+			if (component.compileOptions.dev) {
+				statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`);
+			}
+
+			statements.push(`${name} = @get_store_value(${name.slice(1)});`);
+		});
+
+		user_code = statements.join('\n');
+	}
 
 	// TODO only do this for props with a default value
 	const parent_bindings = component.javascript
@@ -83,6 +109,7 @@ export default function ssr(
 			return \`${renderer.code}\`;`;
 
 	const blocks = [
+		reactive_stores.length > 0 && `let ${reactive_stores.map(store => store.name).join(', ')};`,
 		user_code,
 		parent_bindings.join('\n'),
 		css.code && `$$result.css.add(#css);`,
diff --git a/src/interfaces.ts b/src/interfaces.ts
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -89,4 +89,5 @@ export interface Var {
 	internal?: boolean; // event handlers, bindings
 	initialised?: boolean;
 	hoistable?: boolean;
+	subscribable?: boolean;
 }
\ No newline at end of file
diff --git a/src/internal/utils.js b/src/internal/utils.js
--- a/src/internal/utils.js
+++ b/src/internal/utils.js
@@ -47,6 +47,10 @@ export function validate_store(store, name) {
 	}
 }
 
+export function subscribe(component, store, callback) {
+	component.$$.on_destroy.push(store.subscribe(callback));
+}
+
 export function create_slot(definition, ctx, fn) {
 	if (definition) {
 		const slot_ctx = get_slot_context(definition, ctx, fn);
diff --git a/src/utils/deindent.ts b/src/utils/deindent.ts
--- a/src/utils/deindent.ts
+++ b/src/utils/deindent.ts
@@ -39,7 +39,7 @@ export default function deindent(
 		current_indentation = get_current_indentation(result);
 	}
 
-	return result.trim().replace(/\t+$/gm, '');
+	return result.trim().replace(/\t+$/gm, '').replace(/{\n\n/gm, '{\n');
 }
 
 function get_current_indentation(str: string) {


Test patch:
diff --git a/src/utils/__test__.ts b/src/utils/__test__.ts
--- a/src/utils/__test__.ts
+++ b/src/utils/__test__.ts
@@ -34,6 +34,45 @@ describe('deindent', () => {
 
 		assert.equal(deindented, `before\n\tline one\n\tline two\nafter`);
 	});
+
+	it('removes newlines before an empty expression', () => {
+		const deindented = deindent`
+			{
+				some text
+
+				${null}
+			}`;
+
+		assert.equal(deindented, `{\n\tsome text\n}`);
+	});
+
+	it('removes newlines after an empty expression', () => {
+		const deindented = deindent`
+			{
+				${null}
+
+				some text
+			}`;
+
+		assert.equal(deindented, `{\n\tsome text\n}`);
+	});
+
+	it('removes newlines around empty expressions', () => {
+		const deindented = deindent`
+			{
+				${null}
+
+				some text
+
+				${null}
+
+				some text
+
+				${null}
+			}`;
+
+		assert.equal(deindented, `{\n\tsome text\n\n\tsome text\n}`);
+	});
 });
 
 describe('CodeBuilder', () => {
diff --git a/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js
@@ -0,0 +1,9 @@
+export default {
+	html: `
+		<p>42</p>
+	`,
+
+	async test({ assert, component }) {
+		assert.equal(component.initial_foo, 42);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	import { writable } from 'svelte/store';
+	const foo = writable(42), bar = 'something else';
+	export let initial_foo = $foo;
+</script>
+
+<p>{initial_foo}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-immediate/_config.js b/test/runtime/samples/store-auto-subscribe-immediate/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-immediate/_config.js
@@ -0,0 +1,9 @@
+export default {
+	html: `
+		<p>42</p>
+	`,
+
+	async test({ assert, component }) {
+		assert.equal(component.initial_foo, 42);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-auto-subscribe-immediate/main.svelte b/test/runtime/samples/store-auto-subscribe-immediate/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-auto-subscribe-immediate/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	import { writable } from 'svelte/store';
+	let foo = writable(42);
+	export let initial_foo = $foo;
+</script>
+
+<p>{initial_foo}</p>
\ No newline at end of file
diff --git a/test/runtime/samples/store-resubscribe/_config.js b/test/runtime/samples/store-resubscribe/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe/_config.js
@@ -0,0 +1,36 @@
+export default {
+	html: `
+		<h1>0</h1>
+		<button>+1</button>
+		<button>reset</button>
+	`,
+
+	async test({ assert, component, target, window }) {
+		const buttons = target.querySelectorAll('button');
+		const click = new window.MouseEvent('click');
+
+		await buttons[0].dispatchEvent(click);
+
+		assert.htmlEqual(target.innerHTML, `
+			<h1>1</h1>
+			<button>+1</button>
+			<button>reset</button>
+		`);
+
+		await buttons[1].dispatchEvent(click);
+
+		assert.htmlEqual(target.innerHTML, `
+			<h1>0</h1>
+			<button>+1</button>
+			<button>reset</button>
+		`);
+
+		await buttons[0].dispatchEvent(click);
+
+		assert.htmlEqual(target.innerHTML, `
+			<h1>1</h1>
+			<button>+1</button>
+			<button>reset</button>
+		`);
+	}
+};
\ No newline at end of file
diff --git a/test/runtime/samples/store-resubscribe/main.svelte b/test/runtime/samples/store-resubscribe/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/store-resubscribe/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import { writable } from 'svelte/store';
+	let foo = writable(0);
+</script>
+
+<h1>{$foo}</h1>
+<button on:click="{() => foo.update(n => n + 1)}">+1</button>
+<button on:click="{() => foo = writable(0)}">reset</button>
\ No newline at end of file