UNPKG

svelte

Version:

Cybernetically enhanced web apps

443 lines (380 loc) • 13.1 kB
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node * @param {string} component_name * @param {ComponentContext} context * @param {Expression} anchor * @returns {Statement} */ export function build_component(node, component_name, context, anchor = context.state.node) { /** @type {Array<Property[] | Expression>} */ const props_and_spreads = []; /** @type {Array<() => void>} */ const delayed_props = []; /** @type {ExpressionStatement[]} */ const lets = []; /** @type {Record<string, typeof context.state>} */ const states = { default: { ...context.state, scope: node.metadata.scopes.default, transform: { ...context.state.transform } } }; /** @type {Record<string, AST.TemplateNode[]>} */ const children = {}; /** @type {Record<string, Expression[]>} */ const events = {}; /** @type {Property[]} */ const custom_css_props = []; /** @type {Identifier | MemberExpression | SequenceExpression | null} */ let bind_this = null; /** @type {ExpressionStatement[]} */ const binding_initializers = []; /** * If this component has a slot property, it is a named slot within another component. In this case * the slot scope applies to the component itself, too, and not just its children. */ let slot_scope_applies_to_itself = !!determine_slot(node); /** * Components may have a children prop and also have child nodes. In this case, we assume * that the child component isn't using render tags yet and pass the slot as $$slots.default. * We're not doing it for spread attributes, as this would result in too many false positives. */ let has_children_prop = false; /** * @param {Property} prop * @param {boolean} [delay] */ function push_prop(prop, delay = false) { const do_push = () => { const current = props_and_spreads.at(-1); const current_is_props = Array.isArray(current); const props = current_is_props ? current : []; props.push(prop); if (!current_is_props) { props_and_spreads.push(props); } }; if (delay) { delayed_props.push(do_push); } else { do_push(); } } if (slot_scope_applies_to_itself) { for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } } } for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); } } else if (attribute.type === 'OnDirective') { if (!attribute.expression) { context.state.analysis.needs_props = true; } let handler = build_event_handler( attribute.expression, attribute.metadata.expression, context ); if (attribute.modifiers.includes('once')) { handler = b.call('$.once', handler); } (events[attribute.name] ||= []).push(handler); } else if (attribute.type === 'SpreadAttribute') { const expression = /** @type {Expression} */ (context.visit(attribute)); if (attribute.metadata.expression.has_state) { let value = expression; if (attribute.metadata.expression.has_call) { const id = b.id(context.state.scope.generate('spread_element')); context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); value = b.call('$.get', id); } props_and_spreads.push(b.thunk(value)); } else { props_and_spreads.push(expression); } } else if (attribute.type === 'Attribute') { if (attribute.name.startsWith('--')) { custom_css_props.push( b.init( attribute.name, build_attribute_value(attribute.value, context, (value, metadata) => // TODO put the derived in the local block metadata.has_call ? memoize_expression(context.state, value) : value ).value ) ); continue; } if (attribute.name === 'slot') { slot_scope_applies_to_itself = true; } if (attribute.name === 'children') { has_children_prop = true; } const { value, has_state } = build_attribute_value( attribute.value, context, (value, metadata) => { if (!metadata.has_state) return value; // When we have a non-simple computation, anything other than an Identifier or Member expression, // then there's a good chance it needs to be memoized to avoid over-firing when read within the // child component (e.g. `active={i === index}`) const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { return ( n.type === 'ExpressionTag' && n.expression.type !== 'Identifier' && n.expression.type !== 'MemberExpression' ); }); return should_wrap_in_derived ? memoize_expression(context.state, value) : value; } ); if (has_state) { push_prop(b.get(attribute.name, [b.return(value)])); } else { push_prop(b.init(attribute.name, value)); } } else if (attribute.type === 'BindDirective') { const expression = /** @type {Expression} */ (context.visit(attribute.expression)); if (dev && attribute.name !== 'this') { binding_initializers.push( b.stmt( b.call( b.id('$.add_owner_effect'), expression.type === 'SequenceExpression' ? expression.expressions[0] : b.thunk(expression), b.id(component_name), is_ignored(node, 'ownership_invalid_binding') && b.true ) ) ); } if (expression.type === 'SequenceExpression') { if (attribute.name === 'this') { bind_this = attribute.expression; } else { const [get, set] = expression.expressions; const get_id = b.id(context.state.scope.generate('bind_get')); const set_id = b.id(context.state.scope.generate('bind_set')); context.state.init.push(b.var(get_id, get)); context.state.init.push(b.var(set_id, set)); push_prop(b.get(attribute.name, [b.return(b.call(get_id))])); push_prop(b.set(attribute.name, [b.stmt(b.call(set_id, b.id('$$value')))])); } } else { if ( dev && expression.type === 'MemberExpression' && context.state.analysis.runes && !is_ignored(node, 'binding_property_non_reactive') ) { validate_binding(context.state, attribute, expression); } if (attribute.name === 'this') { bind_this = attribute.expression; } else { const is_store_sub = attribute.expression.type === 'Identifier' && context.state.scope.get(attribute.expression.name)?.kind === 'store_sub'; // Delay prop pushes so bindings come at the end, to avoid spreads overwriting them if (is_store_sub) { push_prop( b.get(attribute.name, [b.stmt(b.call('$.mark_store_binding')), b.return(expression)]), true ); } else { push_prop(b.get(attribute.name, [b.return(expression)]), true); } const assignment = b.assignment( '=', /** @type {Pattern} */ (attribute.expression), b.id('$$value') ); push_prop( b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))]), true ); } } } } delayed_props.forEach((fn) => fn()); if (slot_scope_applies_to_itself) { context.state.init.push(...lets); } if (Object.keys(events).length > 0) { const events_expression = b.object( Object.keys(events).map((name) => b.init(name, events[name].length > 1 ? b.array(events[name]) : events[name][0]) ) ); push_prop(b.init('$$events', events_expression)); } /** @type {Statement[]} */ const snippet_declarations = []; /** @type {import('estree').Property[]} */ const serialized_slots = []; // Group children by slot for (const child of node.fragment.nodes) { if (child.type === 'SnippetBlock') { // the SnippetBlock visitor adds a declaration to `init`, but if it's directly // inside a component then we want to hoist them into a block so that they // can be used as props without creating conflicts context.visit(child, { ...context.state, init: snippet_declarations }); push_prop(b.prop('init', child.expression, child.expression)); // Interop: allows people to pass snippets when component still uses slots serialized_slots.push( b.init(child.expression.name === 'children' ? 'default' : child.expression.name, b.true) ); continue; } let slot_name = determine_slot(child) ?? 'default'; (children[slot_name] ||= []).push(child); } // Serialize each slot for (const slot_name of Object.keys(children)) { const block = /** @type {BlockStatement} */ ( context.visit( { ...node.fragment, // @ts-expect-error nodes: children[slot_name] }, slot_name === 'default' ? slot_scope_applies_to_itself ? context.state : states.default : { ...context.state, scope: node.metadata.scopes[slot_name], transform: { ...context.state.transform } } ) ); if (block.body.length === 0) continue; const slot_fn = b.arrow( [b.id('$$anchor'), b.id('$$slotProps')], b.block([ ...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...block.body ]) ); if (slot_name === 'default' && !has_children_prop) { if ( lets.length === 0 && children.default.every( (node) => node.type !== 'SvelteFragment' || !node.attributes.some((attr) => attr.type === 'LetDirective') ) ) { // create `children` prop... push_prop( b.init( 'children', dev ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn) : slot_fn ) ); // and `$$slots.default: true` so that `<slot>` on the child works serialized_slots.push(b.init(slot_name, b.true)); } else { // create `$$slots.default`... serialized_slots.push(b.init(slot_name, slot_fn)); // and a `children` prop that errors push_prop(b.init('children', b.id('$.invalid_default_snippet'))); } } else { serialized_slots.push(b.init(slot_name, slot_fn)); } } if (serialized_slots.length > 0) { push_prop(b.init('$$slots', b.object(serialized_slots))); } if ( !context.state.analysis.runes && node.attributes.some((attribute) => attribute.type === 'BindDirective') ) { push_prop(b.init('$$legacy', b.true)); } const props_expression = props_and_spreads.length === 0 || (props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0])) ? b.object(/** @type {Property[]} */ (props_and_spreads[0]) || []) : b.call( '$.spread_props', ...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)) ); /** @param {Expression} node_id */ let fn = (node_id) => { return b.call( // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // will be handled separately through the `$.component` function, and then the component name will // always be referenced through just the identifier here. node.type === 'SvelteComponent' ? component_name : /** @type {Expression} */ (context.visit(b.member_id(component_name))), node_id, props_expression ); }; if (bind_this !== null) { const prev = fn; fn = (node_id) => { return build_bind_this(bind_this, prev(node_id), context); }; } const statements = [...snippet_declarations]; if (node.type === 'SvelteComponent') { const prev = fn; fn = (node_id) => { return b.call( '$.component', node_id, b.thunk(/** @type {Expression} */ (context.visit(node.expression))), b.arrow( [b.id('$$anchor'), b.id(component_name)], b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) ) ); }; } else { statements.push(...binding_initializers); } if (Object.keys(custom_css_props).length > 0) { context.state.template.push( context.state.metadata.namespace === 'svg' ? '<g><!></g>' : '<svelte-css-wrapper style="display: contents"><!></svelte-css-wrapper>' ); statements.push( b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))), b.stmt(fn(b.member(anchor, 'lastChild'))), b.stmt(b.call('$.reset', anchor)) ); } else { context.state.template.push('<!>'); statements.push(b.stmt(fn(anchor))); } return statements.length > 1 ? b.block(statements) : statements[0]; }