diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index a756f8e3c4cf..d2649a7d5980 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -28,7 +28,7 @@ import Slot from './nodes/Slot'; import { Node, ImportDeclaration, ExportNamedDeclaration, Identifier, ExpressionStatement, AssignmentExpression, Literal, Property, RestElement, ExportDefaultDeclaration, ExportAllDeclaration, FunctionDeclaration, FunctionExpression, Pattern, Expression } from 'estree'; import add_to_set from './utils/add_to_set'; import check_graph_for_cycles from './utils/check_graph_for_cycles'; -import { print, b } from 'code-red'; +import { print, b, x } from 'code-red'; import { is_reserved_keyword } from './utils/reserved_keywords'; import { apply_preprocessor_sourcemap } from '../utils/mapped_code'; import Element from './nodes/Element'; @@ -38,6 +38,7 @@ import compiler_warnings from './compiler_warnings'; import compiler_errors from './compiler_errors'; import { extract_ignores_above_position, extract_svelte_ignore_from_comments } from '../utils/extract_svelte_ignore'; import check_enable_sourcemap from './utils/check_enable_sourcemap'; +import { flatten } from '../utils/flatten'; interface ComponentOptions { namespace?: string; @@ -1007,7 +1008,7 @@ export default class Component { return null; } - rewrite_props(get_insert: (variable: Var) => Node[]) { + rewrite_props_and_add_subscriptions(get_subscriptions: (variable: Var) => Node[]) { if (!this.ast.instance) return; const component = this; @@ -1015,7 +1016,7 @@ export default class Component { let scope = instance_scope; walk(this.ast.instance.content, { - enter(node: Node) { + enter(node: Node, parent: Node, key, index) { if (regex_contains_term_function.test(node.type)) { return this.skip(); } @@ -1031,7 +1032,7 @@ export default class Component { if (node.type === 'VariableDeclaration') { // NOTE: `var` does not follow block scoping if (node.kind === 'var' || scope === instance_scope) { - const inserts = []; + const subscriptions = []; const props = []; function add_new_props(exported: Identifier, local: Pattern, default_value: Expression) { @@ -1067,7 +1068,7 @@ export default class Component { function get_new_name(local: Identifier): Identifier { const variable = component.var_lookup.get(local.name); if (variable.subscribable) { - inserts.push(get_insert(variable)); + subscriptions.push(get_subscriptions(variable)); } if (variable.export_name && variable.writable) { @@ -1136,17 +1137,88 @@ export default class Component { 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)); + + const for_in_of_loop_init = key === 'left' && (parent.type === 'ForInStatement' || parent.type === 'ForOfStatement'); + if (variable.subscribable && (is_props || declarator.init || for_in_of_loop_init)) { + subscriptions.push(get_subscriptions(variable)); + } + } + } + + // Assertion that if we see props, it must be at the top level + if (props.length > 0 && !(parent.type === 'Program' && Array.isArray(parent[key]))) { + throw new Error('export is not at the top level'); + } + + const flattened_subscriptions = flatten(subscriptions); + // parent.type === 'Program' or 'BlockStatement' or 'SwitchCase' and key === 'body' + if (Array.isArray(parent[key])) { + // If the variable declaration is part of some block, that is, among an array of statements + // then, we add the subscriptions and the $$props declaration after declaration + if (subscriptions.length > 0) { + parent[key].splice(index + 1, 0, ...flattened_subscriptions); + } + if (props.length > 0) { + // b`` might return a Node array, but the $$props declaration will be flattened later + parent[key].splice(index + 1, 0, b`let { ${props} } = $$props;`); + } + if (node.declarations.length == 0) { + // After extracting all the prop names, remove if there are no declarations left + parent[key].splice(index, 1); + } + } else if (subscriptions.length > 0) { + + if (key === 'left' && (parent.type === 'ForInStatement' || parent.type === 'ForOfStatement')) { + // if it is for (var x in array) or for (var x of array) + // we are transforming from: + // + // for (var x of array) { + // // body + // } + // or + // for (var x of array) + // // statement + // to: + // for (var x of array) { + // // subscription inserts + // // body or statement + // } + if (parent.body.type !== 'BlockStatement') { + parent.body = { + type: 'BlockStatement', + body: [parent.body] + }; } + parent.body.body.unshift(...flattened_subscriptions); + } else if (key === 'init' && parent.type === 'ForStatement') { + // If the variable declaration is like for (var i = writable(1); ...), we instead get a dummy variable setting + // calling an immediately-invoked function expression containing all the subscription functions + // e.g. for (var i = writable(1), $$subscriptions = (() => { subscription inserts })(); ...) + node.declarations.push({ + type: 'VariableDeclarator', + id: component.get_unique_name('$$subscriptions', scope), + init: x`(() => { + ${flattened_subscriptions} + })()` + }); + } else { + // Variable declaration is a statement without a parent list of statements, so we transform it by + // putting the var in a block statement and call subscription inserts + // e.g. + // if (condition) + // var a = writable(1); + // turns into + // if (condition) { + // var a = writable(1); + // // subscription inserts here + // } + parent[key] = { + type: 'BlockStatement', + body: [parent[key], ...flattened_subscriptions] + }; } } - this.replace(b` - ${node.declarations.length ? node : null} - ${ props.length > 0 && b`let { ${props} } = $$props;`} - ${inserts} - ` as any); return this.skip(); } } diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 58b7a8317b29..ad550d0862ca 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -306,7 +306,7 @@ export default function dom( } }); - component.rewrite_props(({ name, reassigned, export_name }) => { + component.rewrite_props_and_add_subscriptions(({ name, reassigned, export_name }) => { const value = `$${name}`; const i = renderer.context_lookup.get(`$${name}`).index; diff --git a/src/compiler/compile/render_ssr/index.ts b/src/compiler/compile/render_ssr/index.ts index e256ba78fbf0..46acca7f2f1b 100644 --- a/src/compiler/compile/render_ssr/index.ts +++ b/src/compiler/compile/render_ssr/index.ts @@ -123,7 +123,7 @@ export default function ssr( }); } - component.rewrite_props(({ name, reassigned }) => { + component.rewrite_props_and_add_subscriptions(({ name, reassigned }) => { const value = `$${name}`; let insert = reassigned diff --git a/test/js/samples/var-in-block/expected.js b/test/js/samples/var-in-block/expected.js new file mode 100644 index 000000000000..53e0e43530ae --- /dev/null +++ b/test/js/samples/var-in-block/expected.js @@ -0,0 +1,46 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + detach, + init, + insert, + noop, + safe_not_equal, + text +} from "svelte/internal"; + +function create_fragment(ctx) { + let t; + + return { + c() { + t = text(/*one*/ ctx[0]); + }, + m(target, anchor) { + insert(target, t, anchor); + }, + p: noop, + i: noop, + o: noop, + d(detaching) { + if (detaching) detach(t); + } + }; +} + +function instance($$self) { + { + var one = 1; + } + + return [one]; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, create_fragment, safe_not_equal, {}); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/var-in-block/input.svelte b/test/js/samples/var-in-block/input.svelte new file mode 100644 index 000000000000..5aa45f0aa4ec --- /dev/null +++ b/test/js/samples/var-in-block/input.svelte @@ -0,0 +1,7 @@ + + +{one} diff --git a/test/js/samples/variable-declaration-in-switch-case/expected.js b/test/js/samples/variable-declaration-in-switch-case/expected.js new file mode 100644 index 000000000000..71fbab6a3468 --- /dev/null +++ b/test/js/samples/variable-declaration-in-switch-case/expected.js @@ -0,0 +1,20 @@ +/* generated by Svelte vX.Y.Z */ +import { SvelteComponent, init, safe_not_equal } from "svelte/internal"; + +function instance($$self) { + switch (1) { + case 1: + const value = Math.random(); + } + + return []; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, null, safe_not_equal, {}); + } +} + +export default Component; diff --git a/test/js/samples/variable-declaration-in-switch-case/input.svelte b/test/js/samples/variable-declaration-in-switch-case/input.svelte new file mode 100644 index 000000000000..bef86a4967c0 --- /dev/null +++ b/test/js/samples/variable-declaration-in-switch-case/input.svelte @@ -0,0 +1,7 @@ + + diff --git a/test/runtime/samples/var-in-block/_config.js b/test/runtime/samples/var-in-block/_config.js new file mode 100644 index 000000000000..273d728e2762 --- /dev/null +++ b/test/runtime/samples/var-in-block/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

12345

67890

' +}; diff --git a/test/runtime/samples/var-in-block/main.svelte b/test/runtime/samples/var-in-block/main.svelte new file mode 100644 index 000000000000..ee3ca575a9aa --- /dev/null +++ b/test/runtime/samples/var-in-block/main.svelte @@ -0,0 +1,9 @@ + + +

{foo}

+

{bar}

diff --git a/test/runtime/samples/var-in-do-while-block/_config.js b/test/runtime/samples/var-in-do-while-block/_config.js new file mode 100644 index 000000000000..1ba2c535ec4a --- /dev/null +++ b/test/runtime/samples/var-in-do-while-block/_config.js @@ -0,0 +1,7 @@ +export default { + props: { + a: 13 + }, + + html: '

169

' +}; diff --git a/test/runtime/samples/var-in-do-while-block/main.svelte b/test/runtime/samples/var-in-do-while-block/main.svelte new file mode 100644 index 000000000000..cb4d4ff93e2c --- /dev/null +++ b/test/runtime/samples/var-in-do-while-block/main.svelte @@ -0,0 +1,9 @@ + + +

{b}

diff --git a/test/runtime/samples/var-in-for-in-loop/_config.js b/test/runtime/samples/var-in-for-in-loop/_config.js new file mode 100644 index 000000000000..fc3f1797c417 --- /dev/null +++ b/test/runtime/samples/var-in-for-in-loop/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

0

2

4

6

8

' +}; diff --git a/test/runtime/samples/var-in-for-in-loop/main.svelte b/test/runtime/samples/var-in-for-in-loop/main.svelte new file mode 100644 index 000000000000..8cc83d9305c7 --- /dev/null +++ b/test/runtime/samples/var-in-for-in-loop/main.svelte @@ -0,0 +1,10 @@ + + +{#each array as a} +

{a}

+{/each} diff --git a/test/runtime/samples/var-in-for-loop/_config.js b/test/runtime/samples/var-in-for-loop/_config.js new file mode 100644 index 000000000000..fd54cb4f85dd --- /dev/null +++ b/test/runtime/samples/var-in-for-loop/_config.js @@ -0,0 +1,3 @@ +export default { + html: '

0

1

2

3

4

' +}; diff --git a/test/runtime/samples/var-in-for-loop/main.svelte b/test/runtime/samples/var-in-for-loop/main.svelte new file mode 100644 index 000000000000..acf38eecaefe --- /dev/null +++ b/test/runtime/samples/var-in-for-loop/main.svelte @@ -0,0 +1,10 @@ + + +{#each array as a} +

{a}

+{/each} diff --git a/test/runtime/samples/var-in-function/_config.js b/test/runtime/samples/var-in-function/_config.js new file mode 100644 index 000000000000..f5c4a16cc046 --- /dev/null +++ b/test/runtime/samples/var-in-function/_config.js @@ -0,0 +1,11 @@ +export default { + html: '

0

', + + async test({ assert, target, window }) { + const button = target.querySelector('button'); + const clickEvent = new window.MouseEvent('click'); + + await button.dispatchEvent(clickEvent); + assert.htmlEqual(target.innerHTML, '

4

'); + } +}; diff --git a/test/runtime/samples/var-in-function/main.svelte b/test/runtime/samples/var-in-function/main.svelte new file mode 100644 index 000000000000..1dab3ff6b607 --- /dev/null +++ b/test/runtime/samples/var-in-function/main.svelte @@ -0,0 +1,10 @@ + + +