From b720f0e6202f57cc6e4a16106aa16c620db86dfa Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Tue, 27 Jul 2021 21:53:55 +0800 Subject: [PATCH] [feat] support destructured declaration of props (#6578) --- src/compiler/compile/Component.ts | 159 ++++++++++++------ src/compiler/compile/compiler_errors.ts | 4 - .../samples/destructured-props-1/A.svelte | 24 +++ .../samples/destructured-props-1/_config.js | 9 + .../samples/destructured-props-1/main.svelte | 7 + .../samples/destructured-props-2/A.svelte | 21 +++ .../samples/destructured-props-2/_config.js | 19 +++ .../samples/destructured-props-2/main.svelte | 30 ++++ .../samples/destructured-props-3/A.svelte | 10 ++ .../samples/destructured-props-3/_config.js | 15 ++ .../samples/destructured-props-3/main.svelte | 29 ++++ 11 files changed, 271 insertions(+), 56 deletions(-) create mode 100644 test/runtime/samples/destructured-props-1/A.svelte create mode 100644 test/runtime/samples/destructured-props-1/_config.js create mode 100644 test/runtime/samples/destructured-props-1/main.svelte create mode 100644 test/runtime/samples/destructured-props-2/A.svelte create mode 100644 test/runtime/samples/destructured-props-2/_config.js create mode 100644 test/runtime/samples/destructured-props-2/main.svelte create mode 100644 test/runtime/samples/destructured-props-3/A.svelte create mode 100644 test/runtime/samples/destructured-props-3/_config.js create mode 100644 test/runtime/samples/destructured-props-3/main.svelte diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index fa5a8d555e..6a8d5b178e 100644 --- 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 index fac7029c80..54263c3eb9 100644 --- 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(' → ')}` diff --git a/test/runtime/samples/destructured-props-1/A.svelte b/test/runtime/samples/destructured-props-1/A.svelte new file mode 100644 index 0000000000..d1b392ef34 --- /dev/null +++ b/test/runtime/samples/destructured-props-1/A.svelte @@ -0,0 +1,24 @@ + + +
+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} +
+
{JSON.stringify(THING)}
\ 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 index 0000000000..29feaaabab --- /dev/null +++ b/test/runtime/samples/destructured-props-1/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +
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
+
{"a":1,"b":{"c":2,"d":[3,4,{}]},"e":[6],"h":8}
+
+
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
+
{"a":1,"b":{"c":2,"d":[3,4,{}]},"e":[6],"h":8}
+ ` +}; diff --git a/test/runtime/samples/destructured-props-1/main.svelte b/test/runtime/samples/destructured-props-1/main.svelte new file mode 100644 index 0000000000..dbe7c88d33 --- /dev/null +++ b/test/runtime/samples/destructured-props-1/main.svelte @@ -0,0 +1,7 @@ + + + +
+
diff --git a/test/runtime/samples/destructured-props-2/A.svelte b/test/runtime/samples/destructured-props-2/A.svelte new file mode 100644 index 0000000000..51167edbc4 --- /dev/null +++ b/test/runtime/samples/destructured-props-2/A.svelte @@ -0,0 +1,21 @@ + + +
+ 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} +
+
{JSON.stringify(LIST)}
\ 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 index 0000000000..c647f31be1 --- /dev/null +++ b/test/runtime/samples/destructured-props-2/_config.js @@ -0,0 +1,19 @@ +export default { + html: ` +
x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: 1, n: 2, o: 5, p: 3, q: 4
+
[1,{"a":2},[3,{}]]
+
x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: m, n: n, o: o, p: p, q: q
+
[1,{"a":2},[3,{}]]
+ `, + + async test({ component, assert, target }) { + await component.update(); + + assert.htmlEqual(target.innerHTML, ` +
x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: 1, n: 2, o: 5, p: 3, q: 4
+
[1,{"a":2},[3,{}]]
+
x: 1, list_two_a: 2, list_two_b: 5, y: 4, m: MM, n: NN, o: OO, p: PP, q: QQ
+
[1,{"a":2},[3,{}]]
+ `); + } +}; diff --git a/test/runtime/samples/destructured-props-2/main.svelte b/test/runtime/samples/destructured-props-2/main.svelte new file mode 100644 index 0000000000..f0044bb567 --- /dev/null +++ b/test/runtime/samples/destructured-props-2/main.svelte @@ -0,0 +1,30 @@ + + +
+
+
diff --git a/test/runtime/samples/destructured-props-3/A.svelte b/test/runtime/samples/destructured-props-3/A.svelte new file mode 100644 index 0000000000..6c5aca05b0 --- /dev/null +++ b/test/runtime/samples/destructured-props-3/A.svelte @@ -0,0 +1,10 @@ + + +
i: {i}, j: {j}, k: {$k}, l: {l}, m: {m}, n: {$n}, a: {a}, b: {b}, c: {$c}, d: {d}, e: {e}, f: {$f}
\ 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 index 0000000000..6d2e516e0e --- /dev/null +++ b/test/runtime/samples/destructured-props-3/_config.js @@ -0,0 +1,15 @@ +export default { + html: ` +
i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: 9, b: 10, c: 11, d: 12, e: 13, f: 14
+
+
i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: a, b: 10, c: c, d: d, e: 13, f: f
+ `, + async test({ component, target, assert }) { + await component.update(); + assert.htmlEqual(target.innerHTML, ` +
i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: 9, b: 10, c: 11, d: 12, e: 13, f: 14
+
+
i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, a: aa, b: 10, c: cc, d: dd, e: 13, f: ff
+ `); + } +}; diff --git a/test/runtime/samples/destructured-props-3/main.svelte b/test/runtime/samples/destructured-props-3/main.svelte new file mode 100644 index 0000000000..8152d61aab --- /dev/null +++ b/test/runtime/samples/destructured-props-3/main.svelte @@ -0,0 +1,29 @@ + + +
+
+