|
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'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
( |
|
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}"); |
|
} |
|
`); |
|
} |
|
|
|
|
|
const blocks = renderer.blocks.slice().reverse(); |
|
push_array( |
|
body, |
|
blocks.map((block) => { |
|
|
|
|
|
if ( (block).render) |
|
return (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; |
|
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`; |
|
|
|
|
|
let missing_props_check; |
|
|
|
|
|
let inject_state; |
|
|
|
|
|
let capture_state; |
|
|
|
|
|
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 '<svelte:options accessors/>'"); |
|
}` |
|
}); |
|
} |
|
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 '<svelte:options accessors/>'"); |
|
}` |
|
}); |
|
} |
|
}); |
|
component.instance_exports_from.forEach((exports_from) => { |
|
const import_declaration = { |
|
...exports_from, |
|
type: 'ImportDeclaration', |
|
specifiers: [], |
|
source: exports_from.source |
|
}; |
|
component.imports.push( (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 '<svelte:options accessors/>'"); |
|
}` |
|
}); |
|
} |
|
}); |
|
}); |
|
if (component.compile_options.dev) { |
|
|
|
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); |
|
} |
|
`; |
|
} |
|
} |
|
|
|
if (component.ast.instance) { |
|
let scope = component.instance_scope; |
|
const map = component.instance_scope_map; |
|
|
|
|
|
let execution_context = null; |
|
walk(component.ast.instance.content, { |
|
enter(node) { |
|
if (map.has(node)) { |
|
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; |
|
|
|
|
|
|
|
|
|
const names = new Set(extract_names( (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) { |
|
|
|
args.push(x`$$props`); |
|
} |
|
|
|
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) { |
|
|
|
const reactive_declarations = []; |
|
|
|
|
|
const fixed_reactive_declarations = []; |
|
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; |
|
if (condition) |
|
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};`; |
|
}); |
|
|
|
|
|
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: '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 = ( |
|
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 = ( |
|
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( (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 }; |
|
} |
|
|