diff --git a/src/compile/Component.ts b/src/compile/Component.ts index e2412d74f3..cbda325690 100644 --- a/src/compile/Component.ts +++ b/src/compile/Component.ts @@ -750,7 +750,7 @@ export default class Component { }); } - rewrite_props() { + rewrite_props(get_insert: (name: string) => string) { const component = this; const { code, instance_scope, instance_scope_map: map, componentOptions } = this; let scope = instance_scope; @@ -771,73 +771,97 @@ export default class Component { if (node.type === 'VariableDeclaration') { if (node.kind === 'var' || scope === instance_scope) { - let has_exports = false; - let has_only_exports = true; + node.declarations.forEach((declarator, i) => { + const next = node.declarations[i + 1]; - node.declarations.forEach(declarator => { - extractNames(declarator.id).forEach(name => { - const variable = component.var_lookup.get(name); + if (declarator.id.type !== 'Identifier') { + const inserts = []; - if (name === componentOptions.props_object) { - if (variable.export_name) { - component.error(declarator, { - code: 'exported-options-props', - message: `Cannot export props binding` - }); - } + extractNames(declarator.id).forEach(name => { + const variable = component.var_lookup.get(name); - if (declarator.id.type !== 'Identifier') { + if (variable.export_name || name === componentOptions.props_object) { component.error(declarator, { code: 'destructured-prop', - message: `props binding in destructured declaration is not yet supported` + message: `Cannot declare props in destructured declaration` }); } - // can't use the @ trick here, because we're - // manipulating the underlying magic string - const exclude_internal_props = component.helper('exclude_internal_props'); - - const suffix = code.original[declarator.end] === ';' - ? ` = ${exclude_internal_props}($$props)` - : ` = ${exclude_internal_props}($$props);` + if (variable.subscribable) { + inserts.push(get_insert(name)); + } + }); - if (declarator.id.end === declarator.end) { - code.appendLeft(declarator.end, suffix); + if (inserts.length > 0) { + if (next) { + code.overwrite(declarator.end, next.start, `; ${inserts.join('; ')}; ${node.kind} `); } else { - code.overwrite(declarator.id.end, declarator.end, suffix); + code.appendLeft(declarator.end, `; ${inserts.join('; ')}`); } } + return; + } + + const { name } = declarator.id; + const variable = component.var_lookup.get(name); + + if (name === componentOptions.props_object) { if (variable.export_name) { - has_exports = true; + component.error(declarator, { + code: 'exported-options-props', + message: `Cannot export props binding` + }); } - if (!variable.export_name || variable.subscribable) { - has_only_exports = false; + // can't use the @ trick here, because we're + // manipulating the underlying magic string + const exclude_internal_props = component.helper('exclude_internal_props'); + + const suffix = code.original[declarator.end] === ';' + ? ` = ${exclude_internal_props}($$props)` + : ` = ${exclude_internal_props}($$props);` + + if (declarator.id.end === declarator.end) { + code.appendLeft(declarator.end, suffix); + } else { + code.overwrite(declarator.id.end, declarator.end, suffix); } - }); - }); + } + + if (variable.export_name) { + if (variable.subscribable) { + coalesced_declarations.push({ + kind: node.kind, + declarators: [declarator], + insert: get_insert(name) + }); + } else { + if (current_group && current_group.kind !== node.kind) { + current_group = null; + } + + if (!current_group) { + current_group = { kind: node.kind, declarators: [], insert: null }; + coalesced_declarations.push(current_group); + } - if (has_only_exports) { - if (current_group && current_group[current_group.length - 1].kind !== node.kind) { + current_group.declarators.push(declarator); + } + } else { current_group = null; - } - // rewrite as a group, later - if (!current_group) { - current_group = []; - coalesced_declarations.push(current_group); - } + if (variable.subscribable) { + let insert = get_insert(name); - current_group.push(node); - } else { - if (has_exports) { - // rewrite in place - throw new Error('TODO rewrite prop declaration in place'); + if (next) { + code.overwrite(declarator.end, next.start, `; ${insert}; ${node.kind} `); + } else { + code.appendLeft(declarator.end, `; ${insert}`); + } + } } - - current_group = null; - } + }); } } else { if (node.type !== 'ExportNamedDeclaration') { @@ -858,31 +882,25 @@ export default class Component { let combining = false; - group.forEach(node => { - node.declarations.forEach(declarator => { - const { id, init } = declarator; - - if (id.type === 'Identifier') { - const value = init - ? this.code.slice(id.start, init.end) - : this.code.slice(id.start, id.end); + group.declarators.forEach(declarator => { + const { id } = declarator; - if (combining) { - code.overwrite(c, id.start, ', '); - } else { - code.appendLeft(id.start, '{ '); - combining = true; - } - } else { - throw new Error('TODO destructured declarations'); - } + if (combining) { + code.overwrite(c, id.start, ', '); + } else { + code.appendLeft(id.start, '{ '); + combining = true; + } - c = declarator.end; - }); + c = declarator.end; }); if (combining) { - const suffix = code.original[c] === ';' ? ` } = $$props` : ` } = $$props;`; + const insert = group.insert + ? `; ${group.insert}` + : ''; + + const suffix = code.original[c] === ';' ? ` } = $$props${insert}` : ` } = $$props${insert};`; code.appendLeft(c, suffix); } }); diff --git a/src/compile/render-dom/index.ts b/src/compile/render-dom/index.ts index c13e59ac9b..708b235815 100644 --- a/src/compile/render-dom/index.ts +++ b/src/compile/render-dom/index.ts @@ -202,38 +202,6 @@ 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(';'); @@ -272,7 +240,19 @@ export default function dom( throw new Error(`TODO this should not happen!`); } - component.rewrite_props(); + component.rewrite_props(name => { + const value = `$${name}`; + + const subscribe = component.helper('subscribe'); + + 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}`; + } + + return insert; + }); } const args = ['$$self']; @@ -333,17 +313,34 @@ export default function dom( addToSet(all_reactive_dependencies, d.dependencies); }); - const user_code = component.javascript || ( - !component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props) - ? [ - component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`, - filtered_props.length > 0 && `let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;` - ].filter(Boolean).join('\n') - : null - ); + let user_code; + + if (component.javascript) { + user_code = component.javascript; + } else { + if (!component.ast.instance && !component.ast.module && (filtered_props.length > 0 || component.componentOptions.props)) { + const statements = []; + + if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`); + if (filtered_props.length > 0) statements.push(`let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;`); + + reactive_stores.forEach(({ name }) => { + if (component.compileOptions.dev) { + statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`); + } + + statements.push(`@subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); });`); + }); + + user_code = statements.join('\n'); + } + } const reactive_store_subscriptions = reactive_stores - .filter(store => component.var_lookup.get(store.name).hoistable) + .filter(store => { + const variable = component.var_lookup.get(store.name.slice(1)); + return variable.hoistable; + }) .map(({ name }) => deindent` ${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`} @subscribe($$self, ${name.slice(1)}, $$value => { ${name} = $$value; $$invalidate('${name}', ${name}); }); diff --git a/src/compile/render-ssr/index.ts b/src/compile/render-ssr/index.ts index f191c3c569..351b7540d1 100644 --- a/src/compile/render-ssr/index.ts +++ b/src/compile/render-ssr/index.ts @@ -24,57 +24,16 @@ export default function ssr( { code: null, map: null } : component.stylesheet.render(options.filename, true); - // 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}`); - } - } - }); - } - } + const reactive_stores = component.vars.filter(variable => variable.name[0] === '$'); + 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 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); @@ -82,26 +41,38 @@ export default function ssr( let user_code; if (component.javascript) { - component.rewrite_props(); + component.rewrite_props(name => { + const value = `$${name}`; + + const get_store_value = component.helper('get_store_value'); + + 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}`; + } + + return insert; + }); + user_code = component.javascript; } else if (!component.ast.instance && !component.ast.module && (props.length > 0 || component.componentOptions.props)) { - user_code = [ - component.componentOptions.props && `let ${component.componentOptions.props} = $$props;`, - props.length > 0 && `let { ${props.map(prop => prop.export_name).join(', ')} } = $$props;` - ].filter(Boolean).join('\n'); - } + const statements = []; - const reactive_stores = component.vars.filter(variable => variable.name[0] === '$'); - 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)});`; + if (component.componentOptions.props) statements.push(`let ${component.componentOptions.props} = $$props;`); + if (props.length > 0) statements.push(`let { ${props.map(x => x.name).join(', ')} } = $$props;`); - return component.compileOptions.dev - ? `@validate_store(${name.slice(1)}, '${name.slice(1)}'); ${assignment}` - : assignment; + reactive_stores.forEach(({ name }) => { + if (component.compileOptions.dev) { + statements.push(`${component.compileOptions.dev && `@validate_store(${name.slice(1)}, '${name.slice(1)}');`}`); + } + + statements.push(`const ${name} = @get_store_value(${name.slice(1)});`); }); + user_code = statements.join('\n'); + } + // TODO only do this for props with a default value const parent_bindings = component.javascript ? props.map(prop => {