diff --git a/src/compiler/compile/render_dom/index.js b/src/compiler/compile/render_dom/index.js index a7622e61a9..e6faa458ec 100644 --- a/src/compiler/compile/render_dom/index.js +++ b/src/compiler/compile/render_dom/index.js @@ -14,404 +14,459 @@ import { push_array } from '../../utils/push_array.js'; * @returns {{ js: import("C:/repos/svelte/svelte/node_modules/.pnpm/@types+estree@1.0.0/node_modules/@types/estree/index").import('estree').Node[]; css: import('../../interfaces.js').CssResult; }} */ export default function dom(component, options) { - const { name } = component; - const renderer = new Renderer(component, options); - const { block } = renderer; - block.has_outro_method = true; - const body = []; - if (renderer.file_var) { - const file = component.file ? x `"${component.file}"` : x `undefined`; - body.push(b `const ${renderer.file_var} = ${file};`); - } - const css = component.stylesheet.render(options.filename); - const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css'); - if (css_sourcemap_enabled) { - css.map = apply_preprocessor_sourcemap(options.filename, css.map, - /** @type {string | import('@ampproject/remapping').RawSourceMap | import('@ampproject/remapping').DecodedSourceMap} */ (options.sourcemap)); - } - else { - css.map = null; - } - const styles = css_sourcemap_enabled && component.stylesheet.has_styles && options.dev - ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` - : css.code; - const add_css = component.get_unique_name('add_css'); - const should_add_css = !!styles && (options.customElement || options.css === 'injected'); - if (should_add_css) { - body.push(b ` + const { name } = component; + const renderer = new Renderer(component, options); + const { block } = renderer; + block.has_outro_method = true; + const body = []; + if (renderer.file_var) { + const file = component.file ? x`"${component.file}"` : x`undefined`; + body.push(b`const ${renderer.file_var} = ${file};`); + } + const css = component.stylesheet.render(options.filename); + const css_sourcemap_enabled = check_enable_sourcemap(options.enableSourcemap, 'css'); + if (css_sourcemap_enabled) { + css.map = apply_preprocessor_sourcemap( + options.filename, + css.map, + /** @type {string | import('@ampproject/remapping').RawSourceMap | import('@ampproject/remapping').DecodedSourceMap} */ ( + options.sourcemap + ) + ); + } else { + css.map = null; + } + const styles = + css_sourcemap_enabled && component.stylesheet.has_styles && options.dev + ? `${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` + : css.code; + const add_css = component.get_unique_name('add_css'); + const should_add_css = !!styles && (options.customElement || options.css === 'injected'); + if (should_add_css) { + body.push(b` function ${add_css}(target) { @append_styles(target, "${component.stylesheet.id}", "${styles}"); } `); - } - // fix order - // TODO the deconflicted names of blocks are reversed... should set them here - const blocks = renderer.blocks.slice().reverse(); - push_array(body, blocks.map((block) => { - // TODO this is a horrible mess — renderer.blocks - // contains a mixture of Blocks and Nodes - if (( /** @type {import('./Block.js').default} */(block)).render) - return ( /** @type {import('./Block.js').default} */(block)).render(); - return block; - })); - if (options.dev && !options.hydratable) { - block.chunks.claim.push(b `throw new @_Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");`); - } - const uses_slots = component.var_lookup.has('$$slots'); - - /** @type {import('estree').Node[] | undefined} */ - let compute_slots; - if (uses_slots) { - compute_slots = b ` + } + // fix order + // TODO the deconflicted names of blocks are reversed... should set them here + const blocks = renderer.blocks.slice().reverse(); + push_array( + body, + blocks.map((block) => { + // TODO this is a horrible mess — renderer.blocks + // contains a mixture of Blocks and Nodes + if (/** @type {import('./Block.js').default} */ (block).render) + return /** @type {import('./Block.js').default} */ (block).render(); + return block; + }) + ); + if (options.dev && !options.hydratable) { + block.chunks.claim.push( + b`throw new @_Error("options.hydrate only works if the component was compiled with the \`hydratable: true\` option");` + ); + } + const uses_slots = component.var_lookup.has('$$slots'); + + /** @type {import('estree').Node[] | undefined} */ + let compute_slots; + if (uses_slots) { + compute_slots = b` const $$slots = @compute_slots(#slots); `; - } - const uses_props = component.var_lookup.has('$$props'); - const uses_rest = component.var_lookup.has('$$restProps'); - const $$props = uses_props || uses_rest ? '$$new_props' : '$$props'; - const props = component.vars.filter((variable) => !variable.module && variable.export_name); - const writable_props = props.filter((variable) => variable.writable); - const omit_props_names = component.get_unique_name('omit_props_names'); - const compute_rest = x `@compute_rest_props($$props, ${omit_props_names.name})`; - const rest = uses_rest - ? b ` + } + const uses_props = component.var_lookup.has('$$props'); + const uses_rest = component.var_lookup.has('$$restProps'); + const $$props = uses_props || uses_rest ? '$$new_props' : '$$props'; + const props = component.vars.filter((variable) => !variable.module && variable.export_name); + const writable_props = props.filter((variable) => variable.writable); + const omit_props_names = component.get_unique_name('omit_props_names'); + const compute_rest = x`@compute_rest_props($$props, ${omit_props_names.name})`; + const rest = uses_rest + ? b` const ${omit_props_names.name} = [${props.map((prop) => `"${prop.export_name}"`).join(',')}]; let $$restProps = ${compute_rest}; ` - : null; - const set = uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0 - ? x ` + : null; + const set = + uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0 + ? x` ${$$props} => { - ${uses_props && - renderer.invalidate('$$props', x `$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)} - ${uses_rest && - !uses_props && - x `$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`} - ${uses_rest && renderer.invalidate('$$restProps', x `$$restProps = ${compute_rest}`)} - ${writable_props.map((prop) => b `if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate(prop.name, x `${prop.name} = ${$$props}.${prop.export_name}`)};`)} - ${component.slots.size > 0 && - b `if ('$$scope' in ${$$props}) ${renderer.invalidate('$$scope', x `$$scope = ${$$props}.$$scope`)};`} + ${ + uses_props && + renderer.invalidate( + '$$props', + x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))` + ) + } + ${ + uses_rest && + !uses_props && + x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))` + } + ${uses_rest && renderer.invalidate('$$restProps', x`$$restProps = ${compute_rest}`)} + ${writable_props.map( + (prop) => + b`if ('${prop.export_name}' in ${$$props}) ${renderer.invalidate( + prop.name, + x`${prop.name} = ${$$props}.${prop.export_name}` + )};` + )} + ${ + component.slots.size > 0 && + b`if ('$$scope' in ${$$props}) ${renderer.invalidate( + '$$scope', + x`$$scope = ${$$props}.$$scope` + )};` + } } ` - : null; - const accessors = []; - const not_equal = component.component_options.immutable ? x `@not_equal` : x `@safe_not_equal`; - - /** @type {import('estree').Node[] | import('estree').Node} */ - let missing_props_check; - - /** @type {import('estree').Expression} */ - let inject_state; - - /** @type {import('estree').Expression} */ - let capture_state; - - /** @type {import('estree').Node[] | import('estree').Node} */ - let props_inject; - props.forEach((prop) => { - const variable = component.var_lookup.get(prop.name); - if (!variable.writable || component.component_options.accessors) { - accessors.push({ - type: 'MethodDefinition', - kind: 'get', - key: { type: 'Identifier', name: prop.export_name }, - value: x `function() { - return ${prop.hoistable - ? prop.name - : x `this.$$.ctx[${renderer.context_lookup.get(prop.name).index}]`} + : null; + const accessors = []; + const not_equal = component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`; + + /** @type {import('estree').Node[] | import('estree').Node} */ + let missing_props_check; + + /** @type {import('estree').Expression} */ + let inject_state; + + /** @type {import('estree').Expression} */ + let capture_state; + + /** @type {import('estree').Node[] | import('estree').Node} */ + let props_inject; + props.forEach((prop) => { + const variable = component.var_lookup.get(prop.name); + if (!variable.writable || component.component_options.accessors) { + accessors.push({ + type: 'MethodDefinition', + kind: 'get', + key: { type: 'Identifier', name: prop.export_name }, + value: x`function() { + return ${ + prop.hoistable + ? prop.name + : x`this.$$.ctx[${renderer.context_lookup.get(prop.name).index}]` + } }` - }); - } - else if (component.compile_options.dev) { - accessors.push({ - type: 'MethodDefinition', - kind: 'get', - key: { type: 'Identifier', name: prop.export_name }, - value: x `function() { + }); + } else if (component.compile_options.dev) { + accessors.push({ + type: 'MethodDefinition', + kind: 'get', + key: { type: 'Identifier', name: prop.export_name }, + value: x`function() { throw new @_Error("<${component.tag}>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''"); }` - }); - } - if (component.component_options.accessors) { - if (variable.writable && !renderer.readonly.has(prop.name)) { - accessors.push({ - type: 'MethodDefinition', - kind: 'set', - key: { type: 'Identifier', name: prop.export_name }, - value: x `function(${prop.name}) { + }); + } + if (component.component_options.accessors) { + if (variable.writable && !renderer.readonly.has(prop.name)) { + accessors.push({ + type: 'MethodDefinition', + kind: 'set', + key: { type: 'Identifier', name: prop.export_name }, + value: x`function(${prop.name}) { this.$$set({ ${prop.export_name}: ${prop.name} }); @flush(); }` - }); - } - else if (component.compile_options.dev) { - accessors.push({ - type: 'MethodDefinition', - kind: 'set', - key: { type: 'Identifier', name: prop.export_name }, - value: x `function(value) { + }); + } else if (component.compile_options.dev) { + accessors.push({ + type: 'MethodDefinition', + kind: 'set', + key: { type: 'Identifier', name: prop.export_name }, + value: x`function(value) { throw new @_Error("<${component.tag}>: Cannot set read-only property '${prop.export_name}'"); }` - }); - } - } - else if (component.compile_options.dev) { - accessors.push({ - type: 'MethodDefinition', - kind: 'set', - key: { type: 'Identifier', name: prop.export_name }, - value: x `function(value) { + }); + } + } else if (component.compile_options.dev) { + accessors.push({ + type: 'MethodDefinition', + kind: 'set', + key: { type: 'Identifier', name: prop.export_name }, + value: x`function(value) { throw new @_Error("<${component.tag}>: Props cannot be set directly on the component instance unless compiling with 'accessors: true' or ''"); }` - }); - } - }); - component.instance_exports_from.forEach((exports_from) => { - const import_declaration = { - ...exports_from, - type: 'ImportDeclaration', - specifiers: [], - source: exports_from.source - }; - component.imports.push(/** @type {import('estree').ImportDeclaration} */ (import_declaration)); - exports_from.specifiers.forEach((specifier) => { - if (component.component_options.accessors) { - const name = component.get_unique_name(specifier.exported.name); - import_declaration.specifiers.push({ - ...specifier, - type: 'ImportSpecifier', - imported: specifier.local, - local: name - }); - accessors.push({ - type: 'MethodDefinition', - kind: 'get', - key: { type: 'Identifier', name: specifier.exported.name }, - value: x `function() { + }); + } + }); + component.instance_exports_from.forEach((exports_from) => { + const import_declaration = { + ...exports_from, + type: 'ImportDeclaration', + specifiers: [], + source: exports_from.source + }; + component.imports.push(/** @type {import('estree').ImportDeclaration} */ (import_declaration)); + exports_from.specifiers.forEach((specifier) => { + if (component.component_options.accessors) { + const name = component.get_unique_name(specifier.exported.name); + import_declaration.specifiers.push({ + ...specifier, + type: 'ImportSpecifier', + imported: specifier.local, + local: name + }); + accessors.push({ + type: 'MethodDefinition', + kind: 'get', + key: { type: 'Identifier', name: specifier.exported.name }, + value: x`function() { return ${name} }` - }); - } - else if (component.compile_options.dev) { - accessors.push({ - type: 'MethodDefinition', - kind: 'get', - key: { type: 'Identifier', name: specifier.exported.name }, - value: x `function() { + }); + } else if (component.compile_options.dev) { + accessors.push({ + type: 'MethodDefinition', + kind: 'get', + key: { type: 'Identifier', name: specifier.exported.name }, + value: x`function() { throw new @_Error("<${component.tag}>: Props cannot be read directly from the component instance unless compiling with 'accessors: true' or ''"); }` - }); - } - }); - }); - if (component.compile_options.dev) { - // checking that expected ones were passed - const expected = props.filter((prop) => prop.writable && !prop.initialised); - if (expected.length) { - missing_props_check = b ` + }); + } + }); + }); + if (component.compile_options.dev) { + // checking that expected ones were passed + const expected = props.filter((prop) => prop.writable && !prop.initialised); + if (expected.length) { + missing_props_check = b` $$self.$$.on_mount.push(function () { - ${expected.map((prop) => b ` + ${expected.map( + (prop) => b` if (${prop.name} === undefined && !(('${prop.export_name}' in $$props) || $$self.$$.bound[$$self.$$.props['${prop.export_name}']])) { @_console.warn("<${component.tag}> was created without expected prop '${prop.export_name}'"); - }`)} + }` + )} }); `; - } - const capturable_vars = component.vars.filter((v) => !v.internal && !v.global && !v.name.startsWith('$$')); - if (capturable_vars.length > 0) { - capture_state = x `() => ({ ${capturable_vars.map((prop) => p `${prop.name}`)} })`; - } - const injectable_vars = capturable_vars.filter((v) => !v.module && v.writable && v.name[0] !== '$'); - if (uses_props || injectable_vars.length > 0) { - inject_state = x ` + } + const capturable_vars = component.vars.filter( + (v) => !v.internal && !v.global && !v.name.startsWith('$$') + ); + if (capturable_vars.length > 0) { + capture_state = x`() => ({ ${capturable_vars.map((prop) => p`${prop.name}`)} })`; + } + const injectable_vars = capturable_vars.filter( + (v) => !v.module && v.writable && v.name[0] !== '$' + ); + if (uses_props || injectable_vars.length > 0) { + inject_state = x` ${$$props} => { - ${uses_props && - renderer.invalidate('$$props', x `$$props = @assign(@assign({}, $$props), $$new_props)`)} - ${injectable_vars.map((v) => b `if ('${v.name}' in $$props) ${renderer.invalidate(v.name, x `${v.name} = ${$$props}.${v.name}`)};`)} + ${ + uses_props && + renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), $$new_props)`) + } + ${injectable_vars.map( + (v) => + b`if ('${v.name}' in $$props) ${renderer.invalidate( + v.name, + x`${v.name} = ${$$props}.${v.name}` + )};` + )} } `; - props_inject = b ` + props_inject = b` if ($$props && "$$inject" in $$props) { $$self.$inject_state($$props.$$inject); } `; - } - } - // instrument assignments - if (component.ast.instance) { - let scope = component.instance_scope; - const map = component.instance_scope_map; - - /** @type {import('estree').Node | null} */ - let execution_context = null; - walk(component.ast.instance.content, { - enter(node) { - if (map.has(node)) { - scope = /** @type {import('periscopic').Scope} */ (map.get(node)); - if (!execution_context && !scope.block) { - execution_context = node; - } - } - else if (!execution_context && - node.type === 'LabeledStatement' && - node.label.name === '$') { - execution_context = node; - } - }, - leave(node) { - if (map.has(node)) { - scope = scope.parent; - } - if (execution_context === node) { - execution_context = null; - } - if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { - const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; - // normally (`a = 1`, `b.c = 2`), there'll be a single name - // (a or b). In destructuring cases (`[d, e] = [e, d]`) there - // may be more, in which case we need to tack the extra ones - // onto the initial function call - const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee))); - this.replace(invalidate(renderer, scope, node, names, execution_context === null)); - } - } - }); - component.rewrite_props(({ name, reassigned, export_name }) => { - const value = `$${name}`; - const i = renderer.context_lookup.get(`$${name}`).index; - const insert = reassigned || export_name - ? b `${`$$subscribe_${name}`}()` - : b `@component_subscribe($$self, ${name}, #value => $$invalidate(${i}, ${value} = #value))`; - if (component.compile_options.dev) { - return b `@validate_store(${name}, '${name}'); ${insert}`; - } - return insert; - }); - } - const args = [x `$$self`]; - const has_invalidate = props.length > 0 || - component.has_reactive_assignments || - component.slots.size > 0 || - capture_state || - inject_state; - if (has_invalidate) { - args.push(x `$$props`, x `$$invalidate`); - } - else if (component.compile_options.dev) { - // $$props arg is still needed for unknown prop check - args.push(x `$$props`); - } - // has_create_fragment is intentionally to be true in dev mode. - const has_create_fragment = component.compile_options.dev || block.has_content(); - if (has_create_fragment) { - body.push(b ` + } + } + // instrument assignments + if (component.ast.instance) { + let scope = component.instance_scope; + const map = component.instance_scope_map; + + /** @type {import('estree').Node | null} */ + let execution_context = null; + walk(component.ast.instance.content, { + enter(node) { + if (map.has(node)) { + scope = /** @type {import('periscopic').Scope} */ (map.get(node)); + if (!execution_context && !scope.block) { + execution_context = node; + } + } else if ( + !execution_context && + node.type === 'LabeledStatement' && + node.label.name === '$' + ) { + execution_context = node; + } + }, + leave(node) { + if (map.has(node)) { + scope = scope.parent; + } + if (execution_context === node) { + execution_context = null; + } + if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { + const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; + // normally (`a = 1`, `b.c = 2`), there'll be a single name + // (a or b). In destructuring cases (`[d, e] = [e, d]`) there + // may be more, in which case we need to tack the extra ones + // onto the initial function call + const names = new Set(extract_names(/** @type {import('estree').Node} */ (assignee))); + this.replace(invalidate(renderer, scope, node, names, execution_context === null)); + } + } + }); + component.rewrite_props(({ name, reassigned, export_name }) => { + const value = `$${name}`; + const i = renderer.context_lookup.get(`$${name}`).index; + const insert = + reassigned || export_name + ? b`${`$$subscribe_${name}`}()` + : b`@component_subscribe($$self, ${name}, #value => $$invalidate(${i}, ${value} = #value))`; + if (component.compile_options.dev) { + return b`@validate_store(${name}, '${name}'); ${insert}`; + } + return insert; + }); + } + const args = [x`$$self`]; + const has_invalidate = + props.length > 0 || + component.has_reactive_assignments || + component.slots.size > 0 || + capture_state || + inject_state; + if (has_invalidate) { + args.push(x`$$props`, x`$$invalidate`); + } else if (component.compile_options.dev) { + // $$props arg is still needed for unknown prop check + args.push(x`$$props`); + } + // has_create_fragment is intentionally to be true in dev mode. + const has_create_fragment = component.compile_options.dev || block.has_content(); + if (has_create_fragment) { + body.push(b` function create_fragment(#ctx) { ${block.get_contents()} } `); - } - body.push(b ` + } + body.push(b` ${component.extract_javascript(component.ast.module)} ${component.fully_hoisted} `); - const filtered_props = props.filter((prop) => { - const variable = component.var_lookup.get(prop.name); - if (variable.hoistable) - return false; - return prop.name[0] !== '$'; - }); - const reactive_stores = component.vars.filter((variable) => variable.name[0] === '$' && variable.name[1] !== '$'); - const instance_javascript = component.extract_javascript(component.ast.instance); - const has_definition = component.compile_options.dev || - (instance_javascript && instance_javascript.length > 0) || - filtered_props.length > 0 || - uses_props || - component.partly_hoisted.length > 0 || - renderer.initial_context.length > 0 || - component.reactive_declarations.length > 0 || - capture_state || - inject_state; - const definition = has_definition - ? component.alias('instance') - : { type: 'Literal', value: null }; - const reactive_store_subscriptions = reactive_stores - .filter((store) => { - const variable = component.var_lookup.get(store.name.slice(1)); - return !variable || variable.hoistable; - }) - .map(({ name }) => b ` - ${component.compile_options.dev && b `@validate_store(${name.slice(1)}, '${name.slice(1)}');`} - @component_subscribe($$self, ${name.slice(1)}, $$value => $$invalidate(${renderer.context_lookup.get(name).index}, ${name} = $$value)); - `); - const resubscribable_reactive_store_unsubscribers = reactive_stores - .filter((store) => { - const variable = component.var_lookup.get(store.name.slice(1)); - return variable && (variable.reassigned || variable.export_name); - }) - .map(({ name }) => b `$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); - if (has_definition) { - - /** @type {import('estree').Node | import('estree').Node[]} */ - const reactive_declarations = []; - - /** @type {import('estree').Node[]} */ - const fixed_reactive_declarations = []; // not really 'reactive' but whatever - component.reactive_declarations.forEach((d) => { - const dependencies = Array.from(d.dependencies); - const uses_rest_or_props = !!dependencies.find((n) => n === '$$props' || n === '$$restProps'); - const writable = dependencies.filter((n) => { - const variable = component.var_lookup.get(n); - return variable && (variable.export_name || variable.mutated || variable.reassigned); - }); - const condition = !uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true); - let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced - if (condition) - statement = /** @type {import('estree').Statement} */ (b `if (${condition}) { ${statement} }`[0]); - if (condition || uses_rest_or_props) { - reactive_declarations.push(statement); - } - else { - fixed_reactive_declarations.push(statement); - } - }); - const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => { - const variable = component.var_lookup.get(name); - return variable.injected && variable.name[0] !== '$'; - }); - const reactive_store_declarations = reactive_stores.map((variable) => { - const $name = variable.name; - const name = $name.slice(1); - const store = component.var_lookup.get(name); - if (store && (store.reassigned || store.export_name)) { - const unsubscribe = `$$unsubscribe_${name}`; - const subscribe = `$$subscribe_${name}`; - const i = renderer.context_lookup.get($name).index; - return b `let ${$name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${name}, $$value => $$invalidate(${i}, ${$name} = $$value)), ${name})`; - } - return b `let ${$name};`; - }); - - /** @type {import('estree').Node[] | undefined} */ - let unknown_props_check; - if (component.compile_options.dev && !(uses_props || uses_rest)) { - unknown_props_check = b ` - const writable_props = [${writable_props.map((prop) => x `'${prop.export_name}'`)}]; + const filtered_props = props.filter((prop) => { + const variable = component.var_lookup.get(prop.name); + if (variable.hoistable) return false; + return prop.name[0] !== '$'; + }); + const reactive_stores = component.vars.filter( + (variable) => variable.name[0] === '$' && variable.name[1] !== '$' + ); + const instance_javascript = component.extract_javascript(component.ast.instance); + const has_definition = + component.compile_options.dev || + (instance_javascript && instance_javascript.length > 0) || + filtered_props.length > 0 || + uses_props || + component.partly_hoisted.length > 0 || + renderer.initial_context.length > 0 || + component.reactive_declarations.length > 0 || + capture_state || + inject_state; + const definition = has_definition + ? component.alias('instance') + : { type: 'Literal', value: null }; + const reactive_store_subscriptions = reactive_stores + .filter((store) => { + const variable = component.var_lookup.get(store.name.slice(1)); + return !variable || variable.hoistable; + }) + .map( + ({ name }) => b` + ${component.compile_options.dev && b`@validate_store(${name.slice(1)}, '${name.slice(1)}');`} + @component_subscribe($$self, ${name.slice(1)}, $$value => $$invalidate(${ + renderer.context_lookup.get(name).index + }, ${name} = $$value)); + ` + ); + const resubscribable_reactive_store_unsubscribers = reactive_stores + .filter((store) => { + const variable = component.var_lookup.get(store.name.slice(1)); + return variable && (variable.reassigned || variable.export_name); + }) + .map(({ name }) => b`$$self.$$.on_destroy.push(() => ${`$$unsubscribe_${name.slice(1)}`}());`); + if (has_definition) { + /** @type {import('estree').Node | import('estree').Node[]} */ + const reactive_declarations = []; + + /** @type {import('estree').Node[]} */ + const fixed_reactive_declarations = []; // not really 'reactive' but whatever + component.reactive_declarations.forEach((d) => { + const dependencies = Array.from(d.dependencies); + const uses_rest_or_props = !!dependencies.find((n) => n === '$$props' || n === '$$restProps'); + const writable = dependencies.filter((n) => { + const variable = component.var_lookup.get(n); + return variable && (variable.export_name || variable.mutated || variable.reassigned); + }); + const condition = + !uses_rest_or_props && writable.length > 0 && renderer.dirty(writable, true); + let statement = d.node; // TODO remove label (use d.node.body) if it's not referenced + if (condition) + statement = /** @type {import('estree').Statement} */ ( + b`if (${condition}) { ${statement} }`[0] + ); + if (condition || uses_rest_or_props) { + reactive_declarations.push(statement); + } else { + fixed_reactive_declarations.push(statement); + } + }); + const injected = Array.from(component.injected_reactive_declaration_vars).filter((name) => { + const variable = component.var_lookup.get(name); + return variable.injected && variable.name[0] !== '$'; + }); + const reactive_store_declarations = reactive_stores.map((variable) => { + const $name = variable.name; + const name = $name.slice(1); + const store = component.var_lookup.get(name); + if (store && (store.reassigned || store.export_name)) { + const unsubscribe = `$$unsubscribe_${name}`; + const subscribe = `$$subscribe_${name}`; + const i = renderer.context_lookup.get($name).index; + return b`let ${$name}, ${unsubscribe} = @noop, ${subscribe} = () => (${unsubscribe}(), ${unsubscribe} = @subscribe(${name}, $$value => $$invalidate(${i}, ${$name} = $$value)), ${name})`; + } + return b`let ${$name};`; + }); + + /** @type {import('estree').Node[] | undefined} */ + let unknown_props_check; + if (component.compile_options.dev && !(uses_props || uses_rest)) { + unknown_props_check = b` + const writable_props = [${writable_props.map((prop) => x`'${prop.export_name}'`)}]; @_Object.keys($$props).forEach(key => { - if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') @_console.warn(\`<${component.tag}> was created with unknown prop '\${key}'\`); + if (!~writable_props.indexOf(key) && key.slice(0, 2) !== '$$' && key !== 'slot') @_console.warn(\`<${ + component.tag + }> was created with unknown prop '\${key}'\`); }); `; - } - const return_value = { - type: 'ArrayExpression', - elements: renderer.initial_context.map((member) => ( /** @type {import('estree').Expression} */({ - type: 'Identifier', - name: member.name - }))) - }; - body.push(b ` + } + const return_value = { + type: 'ArrayExpression', + elements: renderer.initial_context.map( + (member) => + /** @type {import('estree').Expression} */ ({ + type: 'Identifier', + name: member.name + }) + ) + }; + body.push(b` function ${definition}(${args}) { - ${injected.map((name) => b `let ${name};`)} + ${injected.map((name) => b`let ${name};`)} ${rest} @@ -421,13 +476,17 @@ export default function dom(component, options) { ${resubscribable_reactive_store_unsubscribers} - ${component.slots.size || component.compile_options.dev || uses_slots - ? b `let { $$slots: #slots = {}, $$scope } = $$props;` - : null} - ${component.compile_options.dev && - b `@validate_slots('${component.tag}', #slots, [${[...component.slots.keys()] - .map((key) => `'${key}'`) - .join(',')}]);`} + ${ + component.slots.size || component.compile_options.dev || uses_slots + ? b`let { $$slots: #slots = {}, $$scope } = $$props;` + : null + } + ${ + component.compile_options.dev && + b`@validate_slots('${component.tag}', #slots, [${[...component.slots.keys()] + .map((key) => `'${key}'`) + .join(',')}]);` + } ${compute_slots} ${instance_javascript} @@ -435,97 +494,114 @@ export default function dom(component, options) { ${missing_props_check} ${unknown_props_check} - ${renderer.binding_groups.size > 0 && - b `const $$binding_groups = [${[...renderer.binding_groups.keys()].map((_) => x `[]`)}];`} + ${ + renderer.binding_groups.size > 0 && + b`const $$binding_groups = [${[...renderer.binding_groups.keys()].map((_) => x`[]`)}];` + } ${component.partly_hoisted} - ${set && b `$$self.$$set = ${set};`} + ${set && b`$$self.$$set = ${set};`} - ${capture_state && b `$$self.$capture_state = ${capture_state};`} + ${capture_state && b`$$self.$capture_state = ${capture_state};`} - ${inject_state && b `$$self.$inject_state = ${inject_state};`} + ${inject_state && b`$$self.$inject_state = ${inject_state};`} - ${ /* before reactive declarations */props_inject} + ${/* before reactive declarations */ props_inject} - ${reactive_declarations.length > 0 && - b ` + ${ + reactive_declarations.length > 0 && + b` $$self.$$.update = () => { ${reactive_declarations} }; - `} + ` + } ${fixed_reactive_declarations} - ${uses_props && b `$$props = @exclude_internal_props($$props);`} + ${uses_props && b`$$props = @exclude_internal_props($$props);`} return ${return_value}; } `); - } - const prop_indexes = /** @type {import('estree').ObjectExpression} */ (x `{ + } + const prop_indexes = /** @type {import('estree').ObjectExpression} */ ( + x`{ ${props - .filter((v) => v.export_name && !v.module) - .map((v) => p `${v.export_name}: ${renderer.context_lookup.get(v.name).index}`)} - }`); - let dirty; - if (renderer.context_overflow) { - dirty = x `[]`; - for (let i = 0; i < renderer.context.length; i += 31) { - dirty.elements.push(x `-1`); - } - } - const superclass = { - type: 'Identifier', - name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' - }; - const optional_parameters = []; - if (should_add_css) { - optional_parameters.push(add_css); - } - else if (dirty) { - optional_parameters.push(x `null`); - } - if (dirty) { - optional_parameters.push(dirty); - } - const declaration = /** @type {import('estree').ClassDeclaration} */ (b ` + .filter((v) => v.export_name && !v.module) + .map((v) => p`${v.export_name}: ${renderer.context_lookup.get(v.name).index}`)} + }` + ); + let dirty; + if (renderer.context_overflow) { + dirty = x`[]`; + for (let i = 0; i < renderer.context.length; i += 31) { + dirty.elements.push(x`-1`); + } + } + const superclass = { + type: 'Identifier', + name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' + }; + const optional_parameters = []; + if (should_add_css) { + optional_parameters.push(add_css); + } else if (dirty) { + optional_parameters.push(x`null`); + } + if (dirty) { + optional_parameters.push(dirty); + } + const declaration = /** @type {import('estree').ClassDeclaration} */ ( + b` class ${name} extends ${superclass} { constructor(options) { super(${options.dev && 'options'}); - @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters}); - ${options.dev && - b `@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} + @init(this, options, ${definition}, ${ + has_create_fragment ? 'create_fragment' : 'null' + }, ${not_equal}, ${prop_indexes}, ${optional_parameters}); + ${ + options.dev && + b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });` + } } } - `[0]); - push_array(declaration.body.body, accessors); - body.push(declaration); - if (options.customElement) { - const props_str = writable_props.reduce((def, prop) => { - def[prop.export_name] = - component.component_options.customElement?.props?.[prop.export_name] || {}; - if (prop.is_boolean && !def[prop.export_name].type) { - def[prop.export_name].type = 'Boolean'; - } - return def; - }, {}); - const slots_str = [...component.slots.keys()].map((key) => `"${key}"`).join(','); - const accessors_str = accessors - .filter((accessor) => !writable_props.some((prop) => prop.export_name === accessor.key.name)) - .map((accessor) => `"${accessor.key.name}"`) - .join(','); - const use_shadow_dom = component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false'; - if (component.component_options.customElement?.tag) { - body.push(b `@_customElements.define("${component.component_options.customElement.tag}", @create_custom_element(${name}, ${JSON.stringify(props_str)}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}));`); - } - else { - body.push(b `@create_custom_element(${name}, ${JSON.stringify(props_str)}, [${slots_str}], [${accessors_str}], ${use_shadow_dom});`); - } - } - return { js: flatten(body), css }; + `[0] + ); + push_array(declaration.body.body, accessors); + body.push(declaration); + if (options.customElement) { + const props_str = writable_props.reduce((def, prop) => { + def[prop.export_name] = + component.component_options.customElement?.props?.[prop.export_name] || {}; + if (prop.is_boolean && !def[prop.export_name].type) { + def[prop.export_name].type = 'Boolean'; + } + return def; + }, {}); + const slots_str = [...component.slots.keys()].map((key) => `"${key}"`).join(','); + const accessors_str = accessors + .filter((accessor) => !writable_props.some((prop) => prop.export_name === accessor.key.name)) + .map((accessor) => `"${accessor.key.name}"`) + .join(','); + const use_shadow_dom = + component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false'; + if (component.component_options.customElement?.tag) { + body.push( + b`@_customElements.define("${ + component.component_options.customElement.tag + }", @create_custom_element(${name}, ${JSON.stringify( + props_str + )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}));` + ); + } else { + body.push( + b`@create_custom_element(${name}, ${JSON.stringify( + props_str + )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom});` + ); + } + } + return { js: flatten(body), css }; } - - - -