diff --git a/src/compile/Component.ts b/src/compile/Component.ts index 9d2e08a44d..9d2cf13a71 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -20,6 +20,7 @@ import isReference from 'is-reference'; import TemplateScope from './nodes/shared/TemplateScope'; import fuzzymatch from '../utils/fuzzymatch'; import { remove_indentation } from '../utils/remove_indentation'; +import getObject from '../utils/getObject'; type Meta = { namespace?: string; @@ -82,6 +83,8 @@ export default class Component { module_exports: Array<{ name: string, as: string }> = []; partly_hoisted: string[] = []; fully_hoisted: string[] = []; + reactive_declarations: Array<{ assignees: Set, dependencies: Set, snippet: string }> = []; + reactive_declaration_nodes: Set = new Set(); has_reactive_assignments = false; indirectDependencies: Map> = new Map(); @@ -428,6 +431,7 @@ export default class Component { extract_javascript(script) { const nodes_to_include = script.content.body.filter(node => { if (this.hoistable_nodes.has(node)) return false; + if (this.reactive_declaration_nodes.has(node)) return false; if (node.type === 'ImportDeclaration') return false; if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false; return true; @@ -440,10 +444,14 @@ export default class Component { let result = ''; - script.content.body.forEach(node => { - if (this.hoistable_nodes.has(node)) { - result += `[✂${a}-${node.start}✂]/* HOISTED */`; + script.content.body.forEach((node, i) => { + if (this.hoistable_nodes.has(node) || this.reactive_declaration_nodes.has(node)) { + result += `[✂${a}-${node.start}✂]`; a = node.end; + + if (i < script.content.body.length - 1) { + while (a < this.source.length && /\s/.test(this.source[a])) a += 1; + } } }); @@ -498,6 +506,7 @@ export default class Component { this.extract_imports_and_exports(script.content, this.imports, this.props); this.rewrite_props(); this.hoist_instance_declarations(); + this.extract_reactive_declarations(); this.javascript = this.extract_javascript(script); } @@ -739,6 +748,111 @@ export default class Component { } } + extract_reactive_declarations() { + const component = this; + + const unsorted_reactive_declarations = []; + + this.instance_script.content.body.forEach(node => { + if (node.type === 'LabeledStatement') { + this.reactive_declaration_nodes.add(node); + + const assignees = new Set(); + const dependencies = new Set(); + + let scope = this.instance_scope; + let map = this.instance_scope_map; + + walk(node.body, { + enter(node, parent) { + if (map.has(node)) { + scope = map.get(node); + } + + if (parent && parent.type === 'AssignmentExpression' && node === parent.left) { + return this.skip(); + } + + if (node.type === 'AssignmentExpression') { + assignees.add(getObject(node.left).name); + } else if (node.type === 'UpdateExpression') { + assignees.add(getObject(node.argument).name); + this.skip(); + } else if (isReference(node, parent)) { + const { name } = getObject(node); + if (component.declarations.indexOf(name) !== -1) { + dependencies.add(name); + } + this.skip(); + } + }, + + leave(node) { + if (map.has(node)) { + scope = scope.parent; + } + } + }); + + unsorted_reactive_declarations.push({ + assignees, + dependencies, + node, + snippet: node.body.type === 'BlockStatement' + ? `[✂${node.body.start}-${node.end}✂]` + : `{ [✂${node.body.start}-${node.end}✂] }` + }); + } + }); + + const lookup = new Map(); + let seen; + + unsorted_reactive_declarations.forEach(declaration => { + declaration.assignees.forEach(name => { + if (!lookup.has(name)) { + lookup.set(name, []); + } + + // TODO warn or error if a name is assigned to in + // multiple reactive declarations? + lookup.get(name).push(declaration); + }); + }); + + const add_declaration = declaration => { + if (seen.has(declaration)) { + this.error(declaration.node, { + code: 'cyclical-reactive-declaration', + message: 'Cyclical dependency detected' + }); + } + + seen.add(declaration); + + if (declaration.dependencies.size === 0) { + this.error(declaration.node, { + code: 'invalid-reactive-declaration', + message: 'Invalid reactive declaration — must depend on local state' + }); + } + + declaration.dependencies.forEach(name => { + const earlier_declarations = lookup.get(name); + if (earlier_declarations) earlier_declarations.forEach(add_declaration); + }); + + if (this.reactive_declarations.indexOf(declaration) === -1) { + this.reactive_declarations.push(declaration); + } + }; + + unsorted_reactive_declarations.forEach(declaration => { + seen = new Set(); + add_declaration(declaration); + }); + } + qualify(name) { if (this.hoistable_names.has(name)) return name; if (this.imported_declarations.has(name)) return name; diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index 93efa715f4..cd87e295ed 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -7,6 +7,7 @@ import { CompileOptions } from '../../interfaces'; import { walk } from 'estree-walker'; import flattenReference from '../../utils/flattenReference'; import stringifyProps from '../../utils/stringifyProps'; +import addToSet from '../../utils/addToSet'; export default function dom( component: Component, @@ -206,13 +207,19 @@ export default function dom( component.javascript || filtered_props.length > 0 || component.partly_hoisted.length > 0 || - filtered_declarations.length > 0 + filtered_declarations.length > 0 || + component.reactive_declarations.length > 0 ); const definition = has_definition ? component.alias('define') : '@noop'; + const all_reactive_dependencies = new Set(); + component.reactive_declarations.forEach(d => { + addToSet(all_reactive_dependencies, d.dependencies); + }); + if (has_definition) { builder.addBlock(deindent` function ${definition}(${args.join(', ')}) { @@ -227,6 +234,14 @@ export default function dom( ${set && `$$self.$$.set = ${set};`} + ${component.reactive_declarations.length > 0 && deindent` + $$self.$$.update = ($$dirty = { ${Array.from(all_reactive_dependencies).map(n => `${n}: 1`).join(', ')} }) => { + ${component.reactive_declarations.map(d => deindent` + if (${Array.from(d.dependencies).map(n => `$$dirty.${n}`).join(' || ')}) ${d.snippet} + `)} + }; + `} + ${inject_refs && `$$self.$$.inject_refs = ${inject_refs};`} } `); diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts index 83f8ee9629..71bc2f0155 100644 --- a/src/compile/render-ssr/index.ts +++ b/src/compile/render-ssr/index.ts @@ -51,12 +51,17 @@ export default function ssr( do { $$settled = true; + ${component.reactive_declarations.map(d => d.snippet)} + $$rendered = \`${renderer.code}\`; } while (!$$settled); return $$rendered; ` - : `return \`${renderer.code}\`;`; + : deindent` + ${component.reactive_declarations.map(d => d.snippet)} + + return \`${renderer.code}\`;`; const blocks = [ setup, diff --git a/src/internal/Component.js b/src/internal/Component.js index 508304b7db..89151bb637 100644 --- a/src/internal/Component.js +++ b/src/internal/Component.js @@ -74,6 +74,7 @@ export function init(component, options, define, create_fragment, not_equal) { // state get: empty, set: noop, + update: noop, inject_refs: noop, not_equal, bound: blankObject(), @@ -97,6 +98,7 @@ export function init(component, options, define, create_fragment, not_equal) { if (component.$$.bound[key]) component.$$.bound[key](component.$$.get()[key]); }); + component.$$.update(); run_all(component.$$.before_render); component.$$.fragment = create_fragment(component, component.$$.get()); diff --git a/src/internal/scheduler.js b/src/internal/scheduler.js index bb688c0222..fa6fc3a2e3 100644 --- a/src/internal/scheduler.js +++ b/src/internal/scheduler.js @@ -55,6 +55,7 @@ export function flush() { function update($$) { if ($$.fragment) { + $$.update($$.dirty); run_all($$.before_render); $$.fragment.p($$.dirty, $$.get()); $$.inject_refs($$.refs); diff --git a/test/js/samples/action-custom-event-handler/expected.js b/test/js/samples/action-custom-event-handler/expected.js index 4f32ceeb3b..7692dd3195 100644 --- a/test/js/samples/action-custom-event-handler/expected.js +++ b/test/js/samples/action-custom-event-handler/expected.js @@ -46,10 +46,6 @@ function foo(node, callback) { function define($$self, $$props) { let { bar } = $$props; - /* HOISTED */ - - /* HOISTED */ - function foo_function() { return handleFoo(bar); } diff --git a/test/js/samples/dev-warning-missing-data-computed/expected.js b/test/js/samples/dev-warning-missing-data-computed/expected.js index 788d582c29..2afbeadbf0 100644 --- a/test/js/samples/dev-warning-missing-data-computed/expected.js +++ b/test/js/samples/dev-warning-missing-data-computed/expected.js @@ -28,11 +28,11 @@ function create_fragment(component, ctx) { }, p: function update(changed, ctx) { - if (text0_value !== (text0_value = Math.max(0, ctx.foo))) { + if ((changed.foo) && text0_value !== (text0_value = Math.max(0, ctx.foo))) { setData(text0, text0_value); } - if ((changed.bar || changed.foo) && text2_value !== (text2_value = ctx.bar())) { + if ((changed.bar) && text2_value !== (text2_value = ctx.bar())) { setData(text2, text2_value); } }, diff --git a/test/js/samples/each-block-keyed-animated/expected.js b/test/js/samples/each-block-keyed-animated/expected.js index 8e94747d49..88694bbd23 100644 --- a/test/js/samples/each-block-keyed-animated/expected.js +++ b/test/js/samples/each-block-keyed-animated/expected.js @@ -123,8 +123,6 @@ function foo(node, animation, params) { function define($$self, $$props) { let { things } = $$props; - /* HOISTED */ - $$self.$$.get = () => ({ things }); $$self.$$.set = $$props => { diff --git a/test/js/samples/ssr-no-oncreate-etc/expected.js b/test/js/samples/ssr-no-oncreate-etc/expected.js index 3e8d2ecc81..aa2381e492 100644 --- a/test/js/samples/ssr-no-oncreate-etc/expected.js +++ b/test/js/samples/ssr-no-oncreate-etc/expected.js @@ -23,10 +23,6 @@ const SvelteComponent = create_ssr_component(($$result, $$props, $$bindings, $$s console.log('onDestroy'); }); - /* HOISTED */ - - /* HOISTED */ - return ``; }); diff --git a/test/runtime/samples/binding-input-checkbox-deep-contextual/_config.js b/test/runtime/samples/binding-input-checkbox-deep-contextual/_config.js index 9b634710fc..959b17777b 100644 --- a/test/runtime/samples/binding-input-checkbox-deep-contextual/_config.js +++ b/test/runtime/samples/binding-input-checkbox-deep-contextual/_config.js @@ -45,7 +45,7 @@ export default { inputs[1].checked = true; await inputs[1].dispatchEvent(event); - assert.equal(component.numCompleted(), 2); + assert.equal(component.numCompleted, 2); assert.htmlEqual(target.innerHTML, `

one

two

three

2 completed

diff --git a/test/runtime/samples/binding-input-checkbox-deep-contextual/main.html b/test/runtime/samples/binding-input-checkbox-deep-contextual/main.html index ad50ec0c32..1d51c2b140 100644 --- a/test/runtime/samples/binding-input-checkbox-deep-contextual/main.html +++ b/test/runtime/samples/binding-input-checkbox-deep-contextual/main.html @@ -1,13 +1,14 @@ {#each items as item}

{item.description}

{/each} -

{numCompleted()} completed

\ No newline at end of file +

{numCompleted} completed

\ No newline at end of file diff --git a/test/runtime/samples/component-binding-blowback-b/main.html b/test/runtime/samples/component-binding-blowback-b/main.html index 3c826723e2..327b64ca09 100644 --- a/test/runtime/samples/component-binding-blowback-b/main.html +++ b/test/runtime/samples/component-binding-blowback-b/main.html @@ -3,18 +3,17 @@ export let count; export let idToValue = Object.create(null); + let ids; - function ids() { - return new Array(count) - .fill(null) - .map((_, i) => 'id-' + i); - } + $: ids = new Array(count) + .fill(null) + .map((_, i) => 'id-' + i);
    - {#each ids() as id} + {#each ids as id} {id}: value is {idToValue[id]} diff --git a/test/runtime/samples/component-binding-infinite-loop/C.html b/test/runtime/samples/component-binding-infinite-loop/C.html index a8074ae07c..0bfc2f1d4e 100644 --- a/test/runtime/samples/component-binding-infinite-loop/C.html +++ b/test/runtime/samples/component-binding-infinite-loop/C.html @@ -2,18 +2,18 @@ export let currentIdentifier; export let identifier; - function isCurrentlySelected() { - return currentIdentifier === identifier; - } + let isCurrentlySelected; function toggle() { - currentIdentifier = isCurrentlySelected() ? null : identifier + currentIdentifier = isCurrentlySelected ? null : identifier } + + $: isCurrentlySelected = currentIdentifier === identifier; \ No newline at end of file diff --git a/test/runtime/samples/computed-function/main.html b/test/runtime/samples/computed-function/main.html index 6c57992ee4..e6f7465b7e 100644 --- a/test/runtime/samples/computed-function/main.html +++ b/test/runtime/samples/computed-function/main.html @@ -3,12 +3,12 @@ export let range = [0, 100]; export let x = 5; - function scale() { - return num => { - const t = domain[0] + (num - domain[0]) / (domain[1] - domain[0]); - return range[0] + t * (range[1] - range[0]); - } - } + let scale; + + $: scale = num => { + const t = domain[0] + (num - domain[0]) / (domain[1] - domain[0]); + return range[0] + t * (range[1] - range[0]); + }; -

    {scale()(x)}

    \ No newline at end of file +

    {scale(x)}

    \ No newline at end of file diff --git a/test/runtime/samples/computed-values-deconflicted/main.html b/test/runtime/samples/computed-values-deconflicted/main.html index 18937d8093..b7a6a6f85c 100644 --- a/test/runtime/samples/computed-values-deconflicted/main.html +++ b/test/runtime/samples/computed-values-deconflicted/main.html @@ -1,9 +1,8 @@ -{state()} \ No newline at end of file +{state} \ No newline at end of file diff --git a/test/runtime/samples/computed-values-function-dependency/_config.js b/test/runtime/samples/computed-values-function-dependency/_config.js index 1636adeceb..b9ece1f685 100644 --- a/test/runtime/samples/computed-values-function-dependency/_config.js +++ b/test/runtime/samples/computed-values-function-dependency/_config.js @@ -3,7 +3,7 @@ export default { test({ assert, component, target }) { component.y = 2; - assert.equal(component.x(), 4); + assert.equal(component.x, 4); assert.equal(target.innerHTML, '

    4

    '); } }; diff --git a/test/runtime/samples/computed-values-function-dependency/main.html b/test/runtime/samples/computed-values-function-dependency/main.html index ac52111bb4..3a8dc83263 100644 --- a/test/runtime/samples/computed-values-function-dependency/main.html +++ b/test/runtime/samples/computed-values-function-dependency/main.html @@ -7,14 +7,15 @@ export let y = 1; - function xGetter() { + let xGetter; + export let x; + + $: { _x = y * 2; - return getX; + xGetter = getX; } - export function x() { - return xGetter()(); - } + $: x = xGetter(); -

    {x()}

    \ No newline at end of file +

    {x}

    \ No newline at end of file diff --git a/test/runtime/samples/computed-values/_config.js b/test/runtime/samples/computed-values/_config.js index c86af2292d..345166046a 100644 --- a/test/runtime/samples/computed-values/_config.js +++ b/test/runtime/samples/computed-values/_config.js @@ -6,8 +6,8 @@ export default { test({ assert, component, target }) { component.a = 3; - assert.equal(component.c(), 5); - assert.equal(component.cSquared(), 25); + assert.equal(component.c, 5); + assert.equal(component.cSquared, 25); assert.htmlEqual(target.innerHTML, `

    3 + 2 = 5

    5 * 5 = 25

    diff --git a/test/runtime/samples/computed-values/main.html b/test/runtime/samples/computed-values/main.html index a847e254b1..61d2d20045 100644 --- a/test/runtime/samples/computed-values/main.html +++ b/test/runtime/samples/computed-values/main.html @@ -1,13 +1,12 @@ -

    {a} + {b} = {c()}

    -

    {c()} * {c()} = {cSquared()}

    \ No newline at end of file +

    {a} + {b} = {c}

    +

    {c} * {c} = {cSquared}

    \ No newline at end of file