Repository: sveltejs/svelte (JavaScript)
Date: 2021-07-27 05:48:07+00:00
Task type: Feature

Issue:
Object destructuring when prop is exported
<!--
If you'd like to propose an implementation for a large new feature or change then please create an RFC:
https://github.com/sveltejs/rfcs
-->


**Describe the solution you'd like**
```js
const THING = {propName: 'lol'};
export let {propName} = THING;
```

**How important is this feature to you?**
1/5, nice to have but not required, just my linting blares errors over it.



Issue discussion:
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Solution patch:
diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts
--- a/src/compiler/compile/Component.ts
+++ b/src/compiler/compile/Component.ts
@@ -24,10 +24,10 @@ import TemplateScope from './nodes/shared/TemplateScope';
 import fuzzymatch from '../utils/fuzzymatch';
 import get_object from './utils/get_object';
 import Slot from './nodes/Slot';
-import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, Program, ExpressionStatement, AssignmentExpression, Literal, ExportDefaultDeclaration, ExportAllDeclaration } from 'estree';
+import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration } from 'estree';
 import add_to_set from './utils/add_to_set';
 import check_graph_for_cycles from './utils/check_graph_for_cycles';
-import { print, x, b } from 'code-red';
+import { print, b } from 'code-red';
 import { is_reserved_keyword } from './utils/reserved_keywords';
 import { apply_preprocessor_sourcemap } from '../utils/mapped_code';
 import Element from './nodes/Element';
@@ -943,7 +943,7 @@ export default class Component {
 		let scope = instance_scope;
 
 		walk(this.ast.instance.content, {
-			enter(node: Node, parent, key, index) {
+			enter(node: Node) {
 				if (/Function/.test(node.type)) {
 					return this.skip();
 				}
@@ -952,75 +952,130 @@ export default class Component {
 					scope = map.get(node);
 				}
 
+				if (node.type === 'ExportNamedDeclaration' && node.declaration) {
+					return this.replace(node.declaration);
+				}
+
 				if (node.type === 'VariableDeclaration') {
+					// NOTE: `var` does not follow block scoping
 					if (node.kind === 'var' || scope === instance_scope) {
-						node.declarations.forEach(declarator => {
-							if (declarator.id.type !== 'Identifier') {
-								const inserts = [];
-
-								extract_names(declarator.id).forEach(name => {
-									const variable = component.var_lookup.get(name);
-
-									if (variable.export_name) {
-										// TODO is this still true post-#3539?
-										return component.error(declarator as any, compiler_errors.destructured_prop);
+						const inserts = [];
+						const props = [];
+
+						function add_new_props(exported, local, default_value) {
+							props.push({
+								type: 'Property',
+								method: false,
+								shorthand: false,
+								computed: false,
+								kind: 'init',
+								key: exported,
+								value: default_value
+									? {
+										type: 'AssignmentPattern',
+										left: local,
+										right: default_value
 									}
+									: local
+							});
+						}
 
+						// transform
+						// ```
+						// export let { x, y = 123 } = OBJ, z = 456
+						// ```
+						// into
+						// ```
+						// let { x: x$, y: y$ = 123 } = OBJ;
+						// let { x = x$, y = y$, z = 456 } = $$props;
+						// ```
+						for (let index = 0; index < node.declarations.length; index++) {
+							const declarator = node.declarations[index];
+							if (declarator.id.type !== 'Identifier') {
+								function get_new_name(local) {
+									const variable = component.var_lookup.get(local.name);
 									if (variable.subscribable) {
 										inserts.push(get_insert(variable));
 									}
-								});
 
-								if (inserts.length) {
-									parent[key].splice(index + 1, 0, ...inserts);
+									if (variable.export_name && variable.writable) {
+										const alias_name = component.get_unique_name(local.name);
+										add_new_props({ type: 'Identifier', name: variable.export_name }, local, alias_name);
+										return alias_name;
+									}
+									return local;
 								}
 
-								return;
-							}
-
-							const { name } = declarator.id;
-							const variable = component.var_lookup.get(name);
+								function rename_identifiers(param: Node) {
+									switch (param.type) {
+										case 'ObjectPattern': {
+											const handle_prop = (prop: Property | RestElement) => {
+												if (prop.type === 'RestElement') {
+													rename_identifiers(prop);
+												} else if (prop.value.type === 'Identifier') {
+													prop.value = get_new_name(prop.value);
+												} else {
+													rename_identifiers(prop.value);
+												}
+											};
+
+											param.properties.forEach(handle_prop);
+											break;
+										}
+										case 'ArrayPattern': {
+											const handle_element = (element: Node, index: number, array: Node[]) => {
+												if (element) {
+													if (element.type === 'Identifier') {
+														array[index] = get_new_name(element);
+													} else {
+														rename_identifiers(element);
+													}
+												}
+											};
+
+											param.elements.forEach(handle_element);
+											break;
+										}
+
+										case 'RestElement':
+											param.argument = get_new_name(param.argument);
+											break;
+
+										case 'AssignmentPattern':
+											param.left = get_new_name(param.left);
+											break;
+									}
+								}
 
-							if (variable.export_name && variable.writable) {
-								declarator.id = {
-									type: 'ObjectPattern',
-									properties: [{
-										type: 'Property',
-										method: false,
-										shorthand: false,
-										computed: false,
-										kind: 'init',
-										key: { type: 'Identifier', name: variable.export_name },
-										value: declarator.init
-											? {
-												type: 'AssignmentPattern',
-												left: declarator.id,
-												right: declarator.init
-											}
-											: declarator.id
-									}]
-								};
-
-								declarator.init = x`$$props`;
+								rename_identifiers(declarator.id);
+							} else {
+								const { name } = declarator.id;
+								const variable = component.var_lookup.get(name);
+								const is_props = variable.export_name && variable.writable;
+								if (is_props) {
+									add_new_props({ type: 'Identifier', name: variable.export_name }, declarator.id, declarator.init);
+									node.declarations.splice(index--, 1);
+								}
+								if (variable.subscribable && (is_props || declarator.init)) {
+									inserts.push(get_insert(variable));
+								}
 							}
+						}
 
-							if (variable.subscribable && declarator.init) {
-								const insert = get_insert(variable);
-								parent[key].splice(index + 1, 0, ...insert);
-							}
-						});
+						this.replace(b`
+							${node.declarations.length ? node : null}
+							${ props.length > 0 && b`let { ${ props } } = $$props;`}
+							${inserts}
+						` as any);
+						return this.skip();
 					}
 				}
 			},
 
-			leave(node: Node, parent, _key, index) {
+			leave(node: Node) {
 				if (map.has(node)) {
 					scope = scope.parent;
 				}
-
-				if (node.type === 'ExportNamedDeclaration' && node.declaration) {
-					(parent as Program).body[index] = node.declaration;
-				}
 			}
 		});
 	}
diff --git a/src/compiler/compile/compiler_errors.ts b/src/compiler/compile/compiler_errors.ts
--- a/src/compiler/compile/compiler_errors.ts
+++ b/src/compiler/compile/compiler_errors.ts
@@ -186,10 +186,6 @@ export default {
 		code: 'illegal-global',
 		message: `${name} is an illegal variable name`
 	}),
-	destructured_prop: {
-		code: 'destructured-prop',
-		message: 'Cannot declare props in destructured declaration'
-	},
 	cyclical_reactive_declaration: (cycle: string[]) => ({
 		code: 'cyclical-reactive-declaration',
 		message: `Cyclical dependency detected: ${cycle.join(' → ')}`


Test patch:
diff --git a/test/runtime/samples/destructured-props-1/A.svelte b/test/runtime/samples/destructured-props-1/A.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-1/A.svelte
@@ -0,0 +1,24 @@
+<script>
+  import { writable } from 'svelte/store';
+
+  const THING = { a: 1, b: { c: 2, d: [3, 4, writable(5)] }, e: [6], h: 8 };
+  const default_g = 9;
+
+  export let { a, b: { c, d: [d_one,,d_three], f }, e: [e_one], g = default_g } = THING;
+  export const { a: A, b: { c: C } } = THING;
+</script>
+
+<div>
+a: {a},
+b: {typeof b},
+c: {c},
+d_one: {d_one},
+d_three: {$d_three},
+f: {f},
+g: {g},
+e: {typeof e},
+e_one: {e_one},
+A: {A},
+C: {C}
+</div>
+<div>{JSON.stringify(THING)}</div>
\ No newline at end of file
diff --git a/test/runtime/samples/destructured-props-1/_config.js b/test/runtime/samples/destructured-props-1/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-1/_config.js
@@ -0,0 +1,9 @@
+export default {
+	html: `
+		<div>a: 1, b: undefined, c: 2, d_one: 3, d_three: 5, f: undefined, g: 9, e: undefined, e_one: 6, A: 1, C: 2</div>
+		<div>{"a":1,"b":{"c":2,"d":[3,4,{}]},"e":[6],"h":8}</div>
+		<br>
+		<div>a: a, b: undefined, c: 2, d_one: d_one, d_three: 5, f: f, g: g, e: undefined, e_one: 6, A: 1, C: 2</div>
+		<div>{"a":1,"b":{"c":2,"d":[3,4,{}]},"e":[6],"h":8}</div>
+	`
+};
diff --git a/test/runtime/samples/destructured-props-1/main.svelte b/test/runtime/samples/destructured-props-1/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-1/main.svelte
@@ -0,0 +1,7 @@
+<script>
+	import A from './A.svelte';
+</script>
+
+<A />
+<br />
+<A a="a" d_one="d_one" list_one="list_one" f="f" list_two_b="list_two_b" g="g" A="A" C="C" />
diff --git a/test/runtime/samples/destructured-props-2/A.svelte b/test/runtime/samples/destructured-props-2/A.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-2/A.svelte
@@ -0,0 +1,21 @@
+<script>
+  import { writable } from 'svelte/store';
+
+  let default_b = 5;
+  const LIST = [1, { a: 2 }, [3, writable(4)]];
+  export const [x, { a: list_two_a, b: list_two_b = default_b }, [, y]] = LIST;
+  export let [m, { a: n, b: o = default_b }, [p, q]] = LIST;
+</script>
+
+<div>
+  x: {x},
+  list_two_a: {list_two_a},
+  list_two_b: {list_two_b},
+  y: {$y},
+  m: {m},
+  n: {n},
+  o: {o},
+  p: {p},
+  q: {$q}
+</div>
+<div>{JSON.stringify(LIST)}</div>
\ No newline at end of file
diff --git a/test/runtime/samples/destructured-props-2/_config.js b/test/runtime/samples/destructured-props-2/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-2/_config.js
@@ -0,0 +1,19 @@
+export default {
+	html: `
+		<div>x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: 1, n: 2, o: 5, p: 3, q: 4</div>
+		<div>[1,{"a":2},[3,{}]]</div>
+		<br><div>x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: m, n: n, o: o, p: p, q: q</div>
+		<div>[1,{"a":2},[3,{}]]</div>
+	`,
+
+	async test({ component, assert, target }) {
+		await component.update();
+
+		assert.htmlEqual(target.innerHTML, `
+			<div>x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: 1, n: 2, o: 5, p: 3, q: 4</div>
+			<div>[1,{"a":2},[3,{}]]</div>
+			<br><div>x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: MM, n: NN, o: OO, p: PP, q: QQ</div>
+			<div>[1,{"a":2},[3,{}]]</div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/destructured-props-2/main.svelte b/test/runtime/samples/destructured-props-2/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-2/main.svelte
@@ -0,0 +1,30 @@
+<script>
+	import A from './A.svelte';
+	import { writable } from 'svelte/store';
+
+	let x = 'x',
+		list_two_a = 'list_two_a',
+		list_two_b = 'list_two_b',
+		y = writable('y'),
+		m = 'm',
+		n = 'n',
+		o = 'o',
+		p = 'p',
+		q = writable('q');
+
+	export function update() {
+		x = 'XX';
+		list_two_a = 'LIST_TWO_A';
+		list_two_b = 'LIST_TWO_B';
+		y = writable('YY');
+		m = 'MM';
+		n = 'NN';
+		o = 'OO';
+		p = 'PP';
+		q = writable('QQ');
+	}
+</script>
+
+<A />
+<br />
+<A {x} {list_two_a} {list_two_b} {y} {m} {n} {o} {p} {q} />
diff --git a/test/runtime/samples/destructured-props-3/A.svelte b/test/runtime/samples/destructured-props-3/A.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-3/A.svelte
@@ -0,0 +1,10 @@
+<script>
+  import { writable } from 'svelte/store';
+
+  const { i, j, k } = { i: 9, j: 10, k: writable(11) }, l = 12, m = 13, n = writable(14);
+  let { a, b, c } = { a: 9, b: 10, c: writable(11) }, d = 12, e = 13, f = writable(14);
+
+  export { i, k, l, n, a, c, d, f };
+</script>
+
+<div>i: {i}, j: {j}, k: {$k}, l: {l}, m: {m}, n: {$n}, a: {a}, b: {b}, c: {$c}, d: {d}, e: {e}, f: {$f}</div>
\ No newline at end of file
diff --git a/test/runtime/samples/destructured-props-3/_config.js b/test/runtime/samples/destructured-props-3/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-3/_config.js
@@ -0,0 +1,15 @@
+export default {
+	html: `
+		<div>i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: 9, b: 10, c: 11, d: 12, e: 13, f: 14</div>
+		<br>
+		<div>i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: a, b: 10, c: c, d: d, e: 13, f: f</div>
+	`,
+	async test({ component, target, assert }) {
+		await component.update();
+		assert.htmlEqual(target.innerHTML, `
+			<div>i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: 9, b: 10, c: 11, d: 12, e: 13, f: 14</div>
+			<br>
+			<div>i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: aa, b: 10, c: cc, d: dd, e: 13, f: ff</div>
+		`);
+	}
+};
diff --git a/test/runtime/samples/destructured-props-3/main.svelte b/test/runtime/samples/destructured-props-3/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/destructured-props-3/main.svelte
@@ -0,0 +1,29 @@
+<script>
+	import A from './A.svelte';
+	import { writable } from 'svelte/store';
+
+	let i = 'i',
+		k = writable('k'),
+		l = 'l',
+		n = writable('n'),
+		a = 'a',
+		c = writable('c'),
+		d = 'd',
+		f = writable('f');
+
+	export function update() {
+		i = 'ii';
+		k = writable('kk');
+		l = 'll';
+		n = writable('nn');
+		a = 'aa';
+		c = writable('cc');
+		d = 'dd';
+		f = writable('ff');
+	}
+
+</script>
+
+<A />
+<br />
+<A {i} {k} {l} {n} {a} {c} {d} {f} />