diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index c21359a13e..a7622e61a9 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -1,529 +1,417 @@ -import type { RawSourceMap, DecodedSourceMap } from '@ampproject/remapping'; import { b, x, p } from 'code-red'; -import Component from '../Component'; -import Renderer from './Renderer'; -import { CompileOptions, CssResult } from '../../interfaces'; +import Renderer from './Renderer.js'; import { walk } from 'estree-walker'; -import { extract_names, Scope } from 'periscopic'; -import { invalidate } from './invalidate'; -import Block from './Block'; -import { - ImportDeclaration, - ClassDeclaration, - Node, - Statement, - ObjectExpression, - Expression -} from 'estree'; -import { apply_preprocessor_sourcemap } from '../../utils/mapped_code'; -import { flatten } from '../../utils/flatten'; -import check_enable_sourcemap from '../utils/check_enable_sourcemap'; -import { push_array } from '../../utils/push_array'; - -export default function dom( - component: Component, - options: CompileOptions -): { js: Node[]; css: CssResult } { - 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, - options.sourcemap as string | RawSourceMap | DecodedSourceMap - ); - } 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` +import { extract_names } from 'periscopic'; +import { invalidate } from './invalidate.js'; +import { apply_preprocessor_sourcemap } from '../../utils/mapped_code.js'; +import { flatten } from '../../utils/flatten.js'; +import check_enable_sourcemap from '../utils/check_enable_sourcemap.js'; +import { push_array } from '../../utils/push_array.js'; + +/** + * @param {import('../Component.js').default} component + * @param {import('../../interfaces.js').CompileOptions} options + * @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 ` 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 ((block as Block).render) return (block as 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'); - let compute_slots: Node[] | undefined; - 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`; - let missing_props_check: Node[] | Node; - let inject_state: Expression; - let capture_state: Expression; - let props_inject: Node[] | Node; - - 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(import_declaration as ImportDeclaration); - - 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; - let execution_context: Node | null = null; - - walk(component.ast.instance.content, { - enter(node: Node) { - if (map.has(node)) { - scope = map.get(node) as Scope; - - if (!execution_context && !scope.block) { - execution_context = node; - } - } else if ( - !execution_context && - node.type === 'LabeledStatement' && - node.label.name === '$' - ) { - execution_context = node; - } - }, - - leave(node: 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(assignee as Node)); - - 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) { - const reactive_declarations: Node | Node[] = []; - const fixed_reactive_declarations: Node[] = []; // 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 = b`if (${condition}) { ${statement} }`[0] as Statement; - - 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};`; - }); - - let unknown_props_check: Node[] | undefined; - 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: 'Identifier', - name: member.name - } as Expression) - ) - }; - - 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} @@ -533,17 +421,13 @@ export default function dom( ${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} @@ -551,119 +435,97 @@ export default function dom( ${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 = 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}`)} - }` as ObjectExpression; - - 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 = 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] as ClassDeclaration; + `[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 }; +} + - 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 }; -}