import { b, x, p } from 'code-red'; import Renderer from './Renderer.js'; import { walk } from 'estree-walker'; 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('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; /** @type {import('estree').Node[][]} */ 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` 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 ${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` ${$$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` )};` } } ` : 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() { 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}) { 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) { 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) { 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() { return ${name} }` }); } 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` $$self.$$.on_mount.push(function () { ${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` ${$$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}` )};` )} } `; 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` function create_fragment(#ctx) { ${block.get_contents()} } `); } 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}'`)}]; @_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}'\`); }); `; } 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};`)} ${rest} ${reactive_store_declarations} ${reactive_store_subscriptions} ${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(',')}]);` } ${compute_slots} ${instance_javascript} ${missing_props_check} ${unknown_props_check} ${ renderer.binding_groups.size > 0 && b`const $$binding_groups = [${[...renderer.binding_groups.keys()].map((_) => x`[]`)}];` } ${component.partly_hoisted} ${set && b`$$self.$$set = ${set};`} ${capture_state && b`$$self.$capture_state = ${capture_state};`} ${inject_state && b`$$self.$inject_state = ${inject_state};`} ${/* before reactive declarations */ props_inject} ${ reactive_declarations.length > 0 && b` $$self.$$.update = () => { ${reactive_declarations} }; ` } ${fixed_reactive_declarations} ${uses_props && b`$$props = @exclude_internal_props($$props);`} return ${return_value}; } `); } 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) { /** @type {any} */ (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 });` } } } `[0] ); push_array(declaration.body.body, accessors); body.push(/** @type {any} */ (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) => accessor.kind === 'get' && !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'; const create_ce = x`@create_custom_element(${name}, ${JSON.stringify( props_str )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}, ${ component.component_options.customElement?.extend })`; if (component.component_options.customElement?.tag) { body.push( b`@_customElements.define("${component.component_options.customElement.tag}", ${create_ce});` ); } else { body.push(b`${create_ce}`); } } if (options.discloseVersion === true) { component.imports.unshift({ type: 'ImportDeclaration', specifiers: [], source: { type: 'Literal', value: `${options.sveltePath ?? 'svelte'}/internal/disclose-version` } }); } return { js: flatten(body), css }; }