From 9757fbfdb88b4a84a856290cea97fd051d182a50 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sun, 17 Feb 2019 20:16:08 -0500 Subject: [PATCH] move store subscriptions into a helper. broke a bunch of stuff, bear with me --- src/compile/Component.ts | 21 ++++-- src/compile/render-dom/index.ts | 44 ++++++++++-- src/compile/render-ssr/index.ts | 72 ++++++++++++++++--- src/interfaces.ts | 1 + src/internal/utils.js | 4 ++ .../_config.js | 9 +++ .../main.svelte | 7 ++ 7 files changed, 140 insertions(+), 18 deletions(-) create mode 100644 test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js create mode 100644 test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte diff --git a/src/compile/Component.ts b/src/compile/Component.ts index 09f5261c63..96a0b6bedb 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -183,7 +183,11 @@ export default class Component { writable: true }); - this.add_reference(name.slice(1)); + const subscribable_name = name.slice(1); + this.add_reference(subscribable_name); + + const variable = this.var_lookup.get(subscribable_name); + variable.subscribable = true; } else if (!this.ast.instance) { this.add_var({ name, @@ -213,6 +217,11 @@ export default class Component { return this.aliases.get(name); } + helper(name: string) { + this.helpers.add(name); + return this.alias(name); + } + generate(result: string) { const { compileOptions, name } = this; const { format = 'esm' } = compileOptions; @@ -641,6 +650,9 @@ export default class Component { }); this.add_reference(name.slice(1)); + + const variable = this.var_lookup.get(name.slice(1)); + variable.subscribable = true; } else { this.add_var({ name, @@ -783,8 +795,7 @@ export default class Component { // can't use the @ trick here, because we're // manipulating the underlying magic string - component.helpers.add('exclude_internal_props'); - const exclude_internal_props = component.alias('exclude_internal_props'); + const exclude_internal_props = component.helper('exclude_internal_props'); const suffix = code.original[declarator.end] === ';' ? ` = ${exclude_internal_props}($$props)` @@ -799,7 +810,9 @@ export default class Component { if (variable.export_name) { has_exports = true; - } else { + } + + if (!variable.export_name || variable.subscribable) { has_only_exports = false; } }); diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index b25a7ba7b8..c13e59ac9b 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -202,6 +202,38 @@ export default function dom( component.has_reactive_assignments = true; } + else if (node.type === 'VariableDeclarator') { + const names = extractNames(node.id); + names.forEach(name => { + const owner = scope.findOwner(name); + if (owner && owner !== component.instance_scope) return; + + const variable = component.var_lookup.get(name); + if (variable && variable.subscribable) { + const value = `$${name}`; + + const subscribe = component.helper('subscribe'); + + const index = parent.declarations.indexOf(node); + const next = parent.declarations[index + 1]; + + let insert = `${subscribe}($$self, ${name}, $$value => { ${value} = $$value; $$invalidate('${value}', ${value}) })`; + if (component.compileOptions.dev) { + const validate_store = component.helper('validate_store'); + insert = `${validate_store}(${name}, '${name}'); ${insert}`; + } + + // initialise store value here + if (next) { + code.overwrite(node.end, next.start, `; ${insert}; ${parent.kind} `); + } else { + // final (or only) declarator + code.appendLeft(node.end, `; ${insert}`); + } + } + }); + } + if (pending_assignments.size > 0) { if (node.type === 'ArrowFunctionExpression') { const insert = Array.from(pending_assignments).map(name => `$$invalidate('${name}', ${name})`).join(';'); @@ -310,12 +342,12 @@ export default function dom( : null ); - const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores + const reactive_store_subscriptions = reactive_stores + .filter(store => component.var_lookup.get(store.name).hoistable) .map(({ name }) => deindent` ${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`} - $$self.$$.on_destroy.push(${name.slice(1)}.subscribe($$value => { ${name} = $$value; $$invalidate('${name}', ${name}); })); - `) - .join('\n\n'); + @subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); }); + `); if (has_definition) { const reactive_declarations = component.reactive_declarations.map(d => { @@ -346,6 +378,8 @@ export default function dom( function ${definition}(${args.join(', ')}) { ${reactive_stores.length > 0 && `let ${reactive_stores.map(store => store.name).join(', ')};`} + ${reactive_store_subscriptions} + ${user_code} ${renderer.slots.size && `let { ${[...renderer.slots].map(name => `$$slot_${sanitize(name)}`).join(', ')}, $$scope } = $$props;`} @@ -354,8 +388,6 @@ export default function dom( ${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')} - ${reactive_store_subscriptions} - ${set && `$$self.$set = ${set};`} ${reactive_declarations.length > 0 && deindent` diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts index 0383e8931c..f191c3c569 100644 --- a/src/compile/render-ssr/index.ts +++ b/src/compile/render-ssr/index.ts @@ -3,6 +3,8 @@ import Component from '../Component'; import { CompileOptions } from '../../interfaces'; import { stringify } from '../../utils/stringify'; import Renderer from './Renderer'; +import { walk } from 'estree-walker'; +import { extractNames } from '../../utils/annotateWithScopes'; export default function ssr( component: Component, @@ -22,11 +24,63 @@ export default function ssr( { code: null, map: null } : component.stylesheet.render(options.filename, true); - let user_code; + // insert store values + if (component.ast.instance) { + let scope = component.instance_scope; + let map = component.instance_scope_map; + + walk(component.ast.instance.content, { + enter: (node, parent) => { + if (map.has(node)) { + scope = map.get(node); + } + }, + + leave(node, parent) { + if (map.has(node)) { + scope = scope.parent; + } + + if (node.type === 'VariableDeclarator') { + const names = extractNames(node.id); + names.forEach(name => { + const owner = scope.findOwner(name); + if (owner && owner !== component.instance_scope) return; + + const variable = component.var_lookup.get(name); + if (variable && variable.subscribable) { + const value = `$${name}`; + + const get_store_value = component.helper('get_store_value'); + + const index = parent.declarations.indexOf(node); + const next = parent.declarations[index + 1]; + + let insert = `const ${value} = ${get_store_value}(${name});`; + if (component.compileOptions.dev) { + const validate_store = component.helper('validate_store'); + insert = `${validate_store}(${name}, '${name}'); ${insert}`; + } + + // initialise store value here + if (next) { + component.code.overwrite(node.end, next.start, `; ${insert}; ${parent.kind} `); + } else { + // final (or only) declarator + component.code.appendLeft(node.end, `; ${insert}`); + } + } + }); + } + } + }); + } // TODO remove this, just use component.vars everywhere const props = component.vars.filter(variable => !variable.module && variable.export_name && variable.export_name !== component.componentOptions.props_object); + let user_code; + if (component.javascript) { component.rewrite_props(); user_code = component.javascript; @@ -38,13 +92,15 @@ export default function ssr( } const reactive_stores = component.vars.filter(variable => variable.name[0] === '$'); - const reactive_store_values = reactive_stores.map(({ name }) => { - const assignment = `const ${name} = @get_store_value(${name.slice(1)});`; - - return component.compileOptions.dev - ? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}` - : assignment; - }); + const reactive_store_values = reactive_stores + .filter(store => component.var_lookup.get(store.name).hoistable) + .map(({ name }) => { + const assignment = `const ${name} = @get_store_value(${name.slice(1)});`; + + return component.compileOptions.dev + ? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}` + : assignment; + }); // TODO only do this for props with a default value const parent_bindings = component.javascript diff --git a/src/interfaces.ts b/src/interfaces.ts index 11228a0d55..4ec258c94a 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -89,4 +89,5 @@ export interface Var { internal?: boolean; // event handlers, bindings initialised?: boolean; hoistable?: boolean; + subscribable?: boolean; } \ No newline at end of file diff --git a/src/internal/utils.js b/src/internal/utils.js index 2e93efb44b..d6b1bb6380 100644 --- a/src/internal/utils.js +++ b/src/internal/utils.js @@ -47,6 +47,10 @@ export function validate_store(store, name) { } } +export function subscribe(component, store, callback) { + component.$$.on_destroy.push(store.subscribe(callback)); +} + export function create_slot(definition, ctx, fn) { if (definition) { const slot_ctx = get_slot_context(definition, ctx, fn); diff --git a/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js new file mode 100644 index 0000000000..9cfd765cde --- /dev/null +++ b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +

42

+ `, + + async test({ assert, component }) { + assert.equal(component.initial_foo, 42); + } +}; \ No newline at end of file diff --git a/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte new file mode 100644 index 0000000000..e98b6002a1 --- /dev/null +++ b/test/runtime/samples/store-auto-subscribe-immediate-multiple-vars/main.svelte @@ -0,0 +1,7 @@ + + +

{initial_foo}

\ No newline at end of file