Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
`<svelte:meta immutable/>` and `options.immutable`
Having `<svelte:meta immutable/>` set does not seem to find its way into into the generated code. `safe_not_equal` is still what's used to compare values. Also, `<svelte:meta immutable={false}/>` is a compiler error, which imo should be what the syntax actually is, not `<svelte:meta immutable='false'/>`.

Having `options.immutable` set when compiling _does_ cause (the non-safe) `not_equal` to be used.

Which of these should have precedence? (Is there a reason to have this as a compiler option, and not just as a component `svelte:meta` flag?) Whichever has precedence, we ought to have a way in the REPL to have the `immutable` compiler option set to 'not specified'.


Discussion:
. | `immutable:` not specified | `immutable: false` | `immutable: true`
-- | -- | -- | --
`<svelte:meta>` not specified | false | false | true
`<svelte:meta immutable={false}>` | false | false | false
`<svelte:meta immutable={true}>` | true | true | true
Ok, after discussion in Discord, I think we've reached a decision — `<svelte:meta>` always overrides `options.immutable`. In the absence of both, mutability is assumed.

Resolving patch:
diff --git a/src/compile/Component.ts b/src/compile/Component.ts
--- a/src/compile/Component.ts
+++ b/src/compile/Component.ts
@@ -1030,20 +1030,29 @@ export default class Component {
 }
 
 function process_meta(component, nodes) {
-	const meta: Meta = {};
+	const meta: Meta = {
+		immutable: component.options.immutable || false
+	};
+
 	const node = nodes.find(node => node.name === 'svelte:meta');
 
-	function get_value(attribute, message) {
-		const { name, value } = attribute;
+	function get_value(attribute, code, message) {
+		const { value } = attribute;
+		const chunk = value[0];
 
-		if (value.length > 1 || (value[0] && value[0].type !== 'Text')) {
-			component.error(attribute, {
-				code: `invalid-${name}-attribute`,
-				message
-			});
+		if (!chunk) return true;
+
+		if (value.length > 1) {
+			component.error(attribute, { code, message });
+		}
+
+		if (chunk.type === 'Text') return chunk.data;
+
+		if (chunk.expression.type !== 'Literal') {
+			component.error(attribute, { code, message });
 		}
 
-		return value[0] ? value[0].data : true;
+		return chunk.expression.value;
 	}
 
 	if (node) {
@@ -1052,8 +1061,12 @@ function process_meta(component, nodes) {
 				const { name } = attribute;
 
 				switch (name) {
-					case 'tag':
-						const tag = get_value(attribute, `'tag' must be a string literal`);
+					case 'tag': {
+						const code = 'invalid-tag-attribute';
+						const message = `'tag' must be a string literal`;
+						const tag = get_value(attribute, code, message);
+
+						if (typeof tag !== 'string') component.error(attribute, { code, message });
 
 						if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
 							component.error(attribute, {
@@ -1064,9 +1077,14 @@ function process_meta(component, nodes) {
 
 						meta.tag = tag;
 						break;
+					}
+
+					case 'namespace': {
+						const code = 'invalid-namespace-attribute';
+						const message = `The 'namespace' attribute must be a string literal representing a valid namespace`;
+						const ns = get_value(attribute, code, message);
 
-					case 'namespace':
-						const ns = get_value(attribute, `The 'namespace' attribute must be a string literal representing a valid namespace`);
+						if (typeof ns !== 'string') component.error(attribute, { code, message });
 
 						if (validNamespaces.indexOf(ns) === -1) {
 							const match = fuzzymatch(ns, validNamespaces);
@@ -1085,9 +1103,16 @@ function process_meta(component, nodes) {
 
 						meta.namespace = ns;
 						break;
+					}
 
 					case 'immutable':
-						meta.immutable = get_value(attribute, `immutable attribute must be true or false`) !== 'false';
+						const code = `invalid-immutable-value`;
+						const message = `immutable attribute must be true or false`
+						const value = get_value(attribute, code, message);
+
+						if (typeof value !== 'boolean') component.error(attribute, { code, message });
+
+						meta.immutable = value;
 						break;
 
 					default:
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
@@ -87,7 +87,7 @@ export default function dom(
 
 	const body = [];
 
-	const not_equal = component.options.immutable ? `@not_equal` : `@safe_not_equal`;
+	const not_equal = component.meta.immutable ? `@not_equal` : `@safe_not_equal`;
 	let dev_props_check;
 
 	component.props.forEach(x => {


Test patch:
diff --git a/test/runtime/samples/immutable-option/_config.js b/test/runtime/samples/immutable-option/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/immutable-option/_config.js
@@ -0,0 +1,10 @@
+export default {
+	immutable: true,
+
+	html: `<div><h3>Called 1 times.</h3></div>`,
+
+	test({ assert, component, target }) {
+		component.foo = component.foo;
+		assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`);
+	}
+};
diff --git a/test/runtime/samples/immutable-root/main.html b/test/runtime/samples/immutable-option/main.html
similarity index 78%
rename from test/runtime/samples/immutable-root/main.html
rename to test/runtime/samples/immutable-option/main.html
--- a/test/runtime/samples/immutable-root/main.html
+++ b/test/runtime/samples/immutable-option/main.html
@@ -1,6 +1,8 @@
 <script>
 	export let count = 0;
 	export let foo = { bar: 'baz' };
+
+	$: if (foo) count += 1;
 </script>
 
 <div>
diff --git a/test/runtime/samples/immutable-root/_config.js b/test/runtime/samples/immutable-root/_config.js
deleted file mode 100644
--- a/test/runtime/samples/immutable-root/_config.js
+++ /dev/null
@@ -1,17 +0,0 @@
-export default {
-	immutable: true,
-	html: `<div><h3>Called 0 times.</h3></div>`,
-
-	test({ assert, component, target, window }) {
-		component.$on('state', ({ changed }) => {
-			if (changed.foo) {
-				component.count = component.count + 1;
-			}
-		});
-
-		assert.htmlEqual(target.innerHTML, `<div><h3>Called 0 times.</h3></div>`);
-
-		component.foo = component.foo;
-		assert.htmlEqual(target.innerHTML, `<div><h3>Called 0 times.</h3></div>`);
-	}
-};
diff --git a/test/runtime/samples/immutable-svelte-meta-false/_config.js b/test/runtime/samples/immutable-svelte-meta-false/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/immutable-svelte-meta-false/_config.js
@@ -0,0 +1,10 @@
+export default {
+	immutable: true,
+
+	html: `<div><h3>Called 1 times.</h3></div>`,
+
+	test({ assert, component, target }) {
+		component.foo = component.foo;
+		assert.htmlEqual(target.innerHTML, `<div><h3>Called 2 times.</h3></div>`);
+	}
+};
diff --git a/test/runtime/samples/immutable-svelte-meta-false/main.html b/test/runtime/samples/immutable-svelte-meta-false/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/immutable-svelte-meta-false/main.html
@@ -0,0 +1,12 @@
+<svelte:meta immutable={false}/>
+
+<script>
+	export let count = 0;
+	export let foo = { bar: 'baz' };
+
+	$: if (foo) count += 1;
+</script>
+
+<div>
+	<h3>Called {count} times.</h3>
+</div>
\ No newline at end of file
diff --git a/test/runtime/samples/immutable-svelte-meta/_config.js b/test/runtime/samples/immutable-svelte-meta/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/immutable-svelte-meta/_config.js
@@ -0,0 +1,8 @@
+export default {
+	html: `<div><h3>Called 1 times.</h3></div>`,
+
+	test({ assert, component, target }) {
+		component.foo = component.foo;
+		assert.htmlEqual(target.innerHTML, `<div><h3>Called 1 times.</h3></div>`);
+	}
+};
diff --git a/test/runtime/samples/immutable-svelte-meta/main.html b/test/runtime/samples/immutable-svelte-meta/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/immutable-svelte-meta/main.html
@@ -0,0 +1,12 @@
+<svelte:meta immutable/>
+
+<script>
+	export let count = 0;
+	export let foo = { bar: 'baz' };
+
+	$: if (foo) count += 1;
+</script>
+
+<div>
+	<h3>Called {count} times.</h3>
+</div>
\ No newline at end of file