import deindent from '../../utils/deindent'; import { stringify, escape } from '../../utils/stringify'; import CodeBuilder from '../../utils/CodeBuilder'; import Component from '../Component'; import Renderer from './Renderer'; import { CompileOptions } from '../../interfaces'; import { walk } from 'estree-walker'; import stringifyProps from '../../utils/stringifyProps'; import addToSet from '../../utils/addToSet'; import getObject from '../../utils/getObject'; import { extractNames } from '../../utils/annotateWithScopes'; import { nodes_match } from '../../utils/nodes_match'; export default function dom( component: Component, options: CompileOptions ) { const { name, code } = component; const renderer = new Renderer(component, options); const { block } = renderer; block.hasOutroMethod = true; // prevent fragment being created twice (#1063) if (options.customElement) block.builders.create.addLine(`this.c = @noop;`); const builder = new CodeBuilder(); if (component.options.dev) { builder.addLine(`const ${renderer.fileVar} = ${JSON.stringify(component.file)};`); } const css = component.stylesheet.render(options.filename, !options.customElement); const styles = component.stylesheet.hasStyles && stringify(options.dev ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` : css.code, { onlyEscapeAtSymbol: true }); if (styles && component.options.css !== false && !options.customElement) { builder.addBlock(deindent` function @add_css() { var style = @createElement("style"); style.id = '${component.stylesheet.id}-style'; style.textContent = ${styles}; @append(document.head, style); } `); } // fix order // TODO the deconflicted names of blocks are reversed... should set them here const blocks = renderer.blocks.slice().reverse(); blocks.forEach(block => { builder.addBlock(block.toString()); }); if (options.dev && !options.hydratable) { block.builders.claim.addLine( 'throw new Error("options.hydrate only works if the component was compiled with the `hydratable: true` option");' ); } // TODO injecting CSS this way is kinda dirty. Maybe it should be an // explicit opt-in, or something? const should_add_css = ( !options.customElement && component.stylesheet.hasStyles && options.css !== false ); const props = component.props.filter(x => component.writable_declarations.has(x.name)); const set = component.meta.props || props.length > 0 ? deindent` $$props => { ${component.meta.props && deindent` if (!${component.meta.props}) ${component.meta.props} = {}; @assign(${component.meta.props}, $$props); $$invalidate('${component.meta.props_object}', ${component.meta.props_object}); `} ${props.map(prop => `if ('${prop.as}' in $$props) $$invalidate('${prop.name}', ${prop.name} = $$props.${prop.as});`)} } ` : null; const body = []; const not_equal = component.meta.immutable ? `@not_equal` : `@safe_not_equal`; let dev_props_check; component.props.forEach(x => { if (component.imported_declarations.has(x.name) || component.hoistable_names.has(x.name)) { body.push(deindent` get ${x.as}() { return ${x.name}; } `); } else { body.push(deindent` get ${x.as}() { return this.$$.ctx.${x.name}; } `); } if (component.writable_declarations.has(x.as) && !renderer.readonly.has(x.as)) { body.push(deindent` set ${x.as}(${x.name}) { this.$set({ ${x.name} }); @flush(); } `); } else if (component.options.dev) { body.push(deindent` set ${x.as}(value) { throw new Error("<${component.tag}>: Cannot set read-only property '${x.as}'"); } `); } }); if (component.options.dev) { // TODO check no uunexpected props were passed, as well as // checking that expected ones were passed const expected = component.props .map(x => x.name) .filter(name => !component.initialised_declarations.has(name)); if (expected.length) { dev_props_check = deindent` const { ctx } = this.$$; const props = ${options.customElement ? `this.attributes` : `options.props || {}`}; ${expected.map(name => deindent` if (ctx.${name} === undefined && !('${name}' in props)) { console.warn("<${component.tag}> was created without expected prop '${name}'"); }`)} `; } } // instrument assignments if (component.instance_script) { let scope = component.instance_scope; let map = component.instance_scope_map; let pending_assignments = new Set(); walk(component.instance_script.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 === 'AssignmentExpression') { const names = node.left.type === 'MemberExpression' ? [getObject(node.left).name] : extractNames(node.left); if (node.operator === '=' && nodes_match(node.left, node.right)) { const dirty = names.filter(name => { return scope.findOwner(name) === component.instance_scope; }); if (dirty.length) component.has_reactive_assignments = true; code.overwrite(node.start, node.end, dirty.map(n => `$$invalidate('${n}', ${n})`).join('; ')); } else { names.forEach(name => { if (component.imported_declarations.has(name)) return; if (scope.findOwner(name) !== component.instance_scope) return; pending_assignments.add(name); component.has_reactive_assignments = true; }); } } else if (node.type === 'UpdateExpression') { const { name } = getObject(node.argument); if (component.imported_declarations.has(name)) return; if (scope.findOwner(name) !== component.instance_scope) return; pending_assignments.add(name); component.has_reactive_assignments = true; } if (pending_assignments.size > 0) { if (node.type === 'ArrowFunctionExpression') { const insert = [...pending_assignments].map(name => `$$invalidate('${name}', ${name})`).join(';'); pending_assignments = new Set(); code.prependRight(node.body.start, `{ const $$result = `); code.appendLeft(node.body.end, `; ${insert}; return $$result; }`); pending_assignments = new Set(); } else if (/Statement/.test(node.type)) { const insert = [...pending_assignments].map(name => `$$invalidate('${name}', ${name})`).join('; '); if (/^(Break|Continue|Return)Statement/.test(node.type)) { if (node.argument) { code.overwrite(node.start, node.argument.start, `var $$result = `); code.appendLeft(node.argument.end, `; ${insert}; return $$result`); } else { code.prependRight(node.start, `${insert}; `); } } else if (parent && /(If|For(In|Of)?|While)Statement/.test(parent.type) && node.type !== 'BlockStatement') { code.prependRight(node.start, '{ '); code.appendLeft(node.end, `${code.original[node.end - 1] === ';' ? '' : ';'} ${insert}; }`); } else { code.appendLeft(node.end, `${code.original[node.end - 1] === ';' ? '' : ';'} ${insert};`); } pending_assignments = new Set(); } } } }); if (pending_assignments.size > 0) { throw new Error(`TODO this should not happen!`); } component.rewrite_props(); } const args = ['$$self']; if (component.props.length > 0 || component.has_reactive_assignments) args.push('$$props', '$$invalidate'); builder.addBlock(deindent` function create_fragment($$, ctx) { ${block.getContents()} } ${component.module_javascript} ${component.fully_hoisted.length > 0 && component.fully_hoisted.join('\n\n')} `); const filtered_declarations = component.declarations.filter(name => { if (component.hoistable_names.has(name)) return false; if (component.imported_declarations.has(name)) return false; if (component.props.find(p => p.as === name)) return true; return component.template_references.has(name); }); const filtered_props = component.props.filter(prop => { if (component.hoistable_names.has(prop.name)) return false; if (component.imported_declarations.has(prop.name)) return false; if (prop.name[0] === '$') return false; return true; }); const reactive_stores = Array.from(component.template_references).filter(n => n[0] === '$'); filtered_declarations.push(...reactive_stores); const has_definition = ( component.javascript || filtered_props.length > 0 || component.partly_hoisted.length > 0 || filtered_declarations.length > 0 || reactive_stores.length > 0 || component.reactive_declarations.length > 0 ); const definition = has_definition ? component.alias('instance') : 'null'; const all_reactive_dependencies = new Set(); component.reactive_declarations.forEach(d => { addToSet(all_reactive_dependencies, d.dependencies); }); const user_code = component.javascript || ( component.ast.js.length === 0 && filtered_props.length > 0 ? `let { ${filtered_props.map(x => x.name).join(', ')} } = $$props;` : null ); const reactive_store_subscriptions = reactive_stores.length > 0 && reactive_stores .map(name => deindent` let ${name}; ${component.options.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'); if (has_definition) { builder.addBlock(deindent` function ${definition}(${args.join(', ')}) { ${user_code} ${component.partly_hoisted.length > 0 && component.partly_hoisted.join('\n\n')} ${reactive_store_subscriptions} ${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}`)} }; `} return ${stringifyProps(filtered_declarations)}; } `); } if (options.customElement) { builder.addBlock(deindent` class ${name} extends @SvelteElement { constructor(options) { super(); ${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { onlyEscapeAtSymbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`} @init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal}); ${dev_props_check} if (options) { if (options.target) { @insert(options.target, this, options.anchor); } ${(component.props.length > 0 || component.meta.props) && deindent` if (options.props) { this.$set(options.props); @flush(); }`} } } static get observedAttributes() { return ${JSON.stringify(component.props.map(x => x.as))}; } ${body.length > 0 && body.join('\n\n')} } customElements.define("${component.tag}", ${name}); `); } else { const superclass = options.dev ? 'SvelteComponentDev' : 'SvelteComponent'; builder.addBlock(deindent` class ${name} extends @${superclass} { constructor(options) { super(${options.dev && `options`}); ${should_add_css && `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`} @init(this, options, ${definition}, create_fragment, ${not_equal}); ${dev_props_check} } ${body.length > 0 && body.join('\n\n')} } `); } return builder.toString(); }