diff --git a/.changeset/tricky-avocados-play.md b/.changeset/tricky-avocados-play.md new file mode 100644 index 0000000000..40af26bc4a --- /dev/null +++ b/.changeset/tricky-avocados-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: cache call expressions in render tag arguments diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index f5beb7b79a..cca01d17ad 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -600,7 +600,8 @@ function special(parser) { end: parser.index, expression: expression, metadata: { - dynamic: false + dynamic: false, + args_with_call_expression: new Set() } }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d48fa34561..565b2fd5cb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -8,7 +8,8 @@ import { extract_paths, is_event_attribute, is_text_attribute, - object + object, + unwrap_optional } from '../../utils/ast.js'; import * as b from '../../utils/builders.js'; import { MathMLElements, ReservedKeywords, Runes, SVGElements } from '../constants.js'; @@ -441,6 +442,7 @@ export function analyze_component(root, source, options) { has_props_rune: false, component_slots: new Set(), expression: null, + render_tag: null, private_derived_state: [], function_depth: scope.function_depth }; @@ -512,6 +514,7 @@ export function analyze_component(root, source, options) { reactive_statements: analysis.reactive_statements, component_slots: new Set(), expression: null, + render_tag: null, private_derived_state: [], function_depth: scope.function_depth }; @@ -1289,7 +1292,7 @@ const common_visitors = { } }, CallExpression(node, context) { - const { expression } = context.state; + const { expression, render_tag } = context.state; if ( (expression?.type === 'ExpressionTag' || expression?.type === 'SpreadAttribute') && !is_known_safe_call(node, context) @@ -1297,6 +1300,18 @@ const common_visitors = { expression.metadata.contains_call_expression = true; } + if (render_tag) { + // Find out which of the render tag arguments contains this call expression + const arg_idx = unwrap_optional(render_tag.expression).arguments.findIndex( + (arg) => arg === node || context.path.includes(arg) + ); + + // -1 if this is the call expression of the render tag itself + if (arg_idx !== -1) { + render_tag.metadata.args_with_call_expression.add(arg_idx); + } + } + const callee = node.callee; const rune = get_rune(node, context.state.scope); @@ -1523,6 +1538,9 @@ const common_visitors = { ); node.metadata.dynamic = binding !== null && binding.kind !== 'normal'; + }, + RenderTag(node, context) { + context.next({ ...context.state, render_tag: node }); } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index d2c503e8b1..461be5df39 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -3,6 +3,7 @@ import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { ClassDirective, ExpressionTag, + RenderTag, SpreadAttribute, SvelteNode, ValidatedCompileOptions @@ -20,6 +21,8 @@ export interface AnalysisState { component_slots: Set; /** The current {expression}, if any */ expression: ExpressionTag | ClassDirective | SpreadAttribute | null; + /** The current {@render ...} tag, if any */ + render_tag: null | RenderTag; private_derived_state: string[]; function_depth: number; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 8e545e2d0d..9c9cc90662 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1,3 +1,4 @@ +/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */ import { extract_identifiers, extract_paths, @@ -72,7 +73,7 @@ function get_attribute_name(element, attribute, context) { * Serializes each style directive into something like `$.set_style(element, style_property, value)` * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. * @param {import('#compiler').StyleDirective[]} style_directives - * @param {import('estree').Identifier} element_id + * @param {Identifier} element_id * @param {import('../types.js').ComponentContext} context * @param {boolean} is_attributes_reactive */ @@ -91,9 +92,7 @@ function serialize_style_directives(style_directives, element_id, context, is_at element_id, b.literal(directive.name), value, - /** @type {import('estree').Expression} */ ( - directive.modifiers.includes('important') ? b.true : undefined - ) + /** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined) ) ); @@ -123,7 +122,7 @@ function parse_directive_name(name) { const parts = name.split('.'); let part = /** @type {string} */ (parts.shift()); - /** @type {import('estree').Identifier | import('estree').MemberExpression} */ + /** @type {Identifier | MemberExpression} */ let expression = b.id(part); while ((part = /** @type {string} */ (parts.shift()))) { @@ -138,14 +137,14 @@ function parse_directive_name(name) { * Serializes each class directive into something like `$.class_toogle(element, class_name, value)` * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. * @param {import('#compiler').ClassDirective[]} class_directives - * @param {import('estree').Identifier} element_id + * @param {Identifier} element_id * @param {import('../types.js').ComponentContext} context * @param {boolean} is_attributes_reactive */ function serialize_class_directives(class_directives, element_id, context, is_attributes_reactive) { const state = context.state; for (const directive of class_directives) { - const value = /** @type {import('estree').Expression} */ (context.visit(directive.expression)); + const value = /** @type {Expression} */ (context.visit(directive.expression)); const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); const contains_call_expression = directive.expression.type === 'CallExpression'; @@ -208,9 +207,7 @@ function setup_select_synchronization(value_binding, context) { let bound = value_binding.expression; while (bound.type === 'MemberExpression') { - bound = /** @type {import('estree').Identifier | import('estree').MemberExpression} */ ( - bound.object - ); + bound = /** @type {Identifier | MemberExpression} */ (bound.object); } /** @type {string[]} */ @@ -244,9 +241,7 @@ function setup_select_synchronization(value_binding, context) { '$.template_effect', b.thunk( b.block([ - b.stmt( - /** @type {import('estree').Expression} */ (context.visit(value_binding.expression)) - ), + b.stmt(/** @type {Expression} */ (context.visit(value_binding.expression))), b.stmt(invalidator) ]) ) @@ -259,7 +254,7 @@ function setup_select_synchronization(value_binding, context) { * @param {Array} attributes * @param {import('../types.js').ComponentContext} context * @param {import('#compiler').RegularElement} element - * @param {import('estree').Identifier} element_id + * @param {Identifier} element_id * @param {boolean} needs_select_handling */ function serialize_element_spread_attributes( @@ -271,7 +266,7 @@ function serialize_element_spread_attributes( ) { let needs_isolation = false; - /** @type {import('estree').ObjectExpression['properties']} */ + /** @type {ObjectExpression['properties']} */ const values = []; for (const attribute of attributes) { @@ -302,7 +297,7 @@ function serialize_element_spread_attributes( values.push(b.init(name, value)); } } else { - values.push(b.spread(/** @type {import('estree').Expression} */ (context.visit(attribute)))); + values.push(b.spread(/** @type {Expression} */ (context.visit(attribute)))); } needs_isolation ||= @@ -363,7 +358,7 @@ function serialize_element_spread_attributes( * Returns the `true` if spread is deemed reactive. * @param {Array} attributes * @param {import('../types.js').ComponentContext} context - * @param {import('estree').Identifier} element_id + * @param {Identifier} element_id * @returns {boolean} */ function serialize_dynamic_element_attributes(attributes, context, element_id) { @@ -381,7 +376,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) { let needs_isolation = false; let is_reactive = false; - /** @type {import('estree').ObjectExpression['properties']} */ + /** @type {ObjectExpression['properties']} */ const values = []; for (const attribute of attributes) { @@ -401,7 +396,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) { values.push(b.init(attribute.name, value)); } } else { - values.push(b.spread(/** @type {import('estree').Expression} */ (context.visit(attribute)))); + values.push(b.spread(/** @type {Expression} */ (context.visit(attribute)))); } is_reactive ||= @@ -476,7 +471,7 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) { * ``` * Returns true if attribute is deemed reactive, false otherwise. * @param {import('#compiler').RegularElement} element - * @param {import('estree').Identifier} node_id + * @param {Identifier} node_id * @param {import('#compiler').Attribute} attribute * @param {import('../types.js').ComponentContext} context * @returns {boolean} @@ -507,7 +502,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu return false; } - /** @type {import('estree').Statement} */ + /** @type {Statement} */ let update; if (name === 'class') { @@ -544,7 +539,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu /** * Like `serialize_element_attribute_update_assignment` but without any special attribute treatment. - * @param {import('estree').Identifier} node_id + * @param {Identifier} node_id * @param {import('#compiler').Attribute} attribute * @param {import('../types.js').ComponentContext} context * @returns {boolean} @@ -574,7 +569,7 @@ function serialize_custom_element_attribute_update_assignment(node_id, attribute * that needs the hidden `__value` property. * Returns true if attribute is deemed reactive, false otherwise. * @param {string} element - * @param {import('estree').Identifier} node_id + * @param {Identifier} node_id * @param {import('#compiler').Attribute} attribute * @param {import('../types.js').ComponentContext} context * @returns {boolean} @@ -637,9 +632,9 @@ function serialize_element_special_value_attribute(element, node_id, attribute, /** * @param {import('../types.js').ComponentClientTransformState} state * @param {string} id - * @param {import('estree').Expression | undefined} init - * @param {import('estree').Expression} value - * @param {import('estree').ExpressionStatement} update + * @param {Expression | undefined} init + * @param {Expression} value + * @param {ExpressionStatement} update */ function serialize_update_assignment(state, id, init, value, update) { state.init.push(b.var(id, init)); @@ -661,26 +656,26 @@ function collect_parent_each_blocks(context) { * @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node * @param {string} component_name * @param {import('../types.js').ComponentContext} context - * @param {import('estree').Expression} anchor - * @returns {import('estree').Statement} + * @param {Expression} anchor + * @returns {Statement} */ function serialize_inline_component(node, component_name, context, anchor = context.state.node) { - /** @type {Array} */ + /** @type {Array} */ const props_and_spreads = []; - /** @type {import('estree').ExpressionStatement[]} */ + /** @type {ExpressionStatement[]} */ const lets = []; /** @type {Record} */ const children = {}; - /** @type {Record} */ + /** @type {Record} */ const events = {}; - /** @type {import('estree').Property[]} */ + /** @type {Property[]} */ const custom_css_props = []; - /** @type {import('estree').Identifier | import('estree').MemberExpression | null} */ + /** @type {Identifier | MemberExpression | null} */ let bind_this = null; /** @@ -702,7 +697,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont let has_children_prop = false; /** - * @param {import('estree').Property} prop + * @param {Property} prop */ function push_prop(prop) { const current = props_and_spreads.at(-1); @@ -715,7 +710,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont } for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - lets.push(/** @type {import('estree').ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else if (attribute.type === 'OnDirective') { events[attribute.name] ||= []; let handler = serialize_event_handler(attribute, context); @@ -724,7 +719,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont } events[attribute.name].push(handler); } else if (attribute.type === 'SpreadAttribute') { - const expression = /** @type {import('estree').Expression} */ (context.visit(attribute)); + const expression = /** @type {Expression} */ (context.visit(attribute)); if (attribute.metadata.dynamic) { let value = expression; @@ -786,9 +781,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont if (attribute.name === 'this') { bind_this = attribute.expression; } else { - const expression = /** @type {import('estree').Expression} */ ( - context.visit(attribute.expression) - ); + const expression = /** @type {Expression} */ (context.visit(attribute.expression)); if (context.state.options.dev) { binding_initializers.push( @@ -821,7 +814,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont push_prop(b.init('$$events', events_expression)); } - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const snippet_declarations = []; // Group children by slot @@ -858,10 +851,10 @@ function serialize_inline_component(node, component_name, context, anchor = cont } // Serialize each slot - /** @type {import('estree').Property[]} */ + /** @type {Property[]} */ const serialized_slots = []; for (const slot_name of Object.keys(children)) { - const block = /** @type {import('estree').BlockStatement} */ ( + const block = /** @type {BlockStatement} */ ( context.visit( { ...node.fragment, @@ -915,13 +908,13 @@ function serialize_inline_component(node, component_name, context, anchor = cont const props_expression = props_and_spreads.length === 0 || (props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0])) - ? b.object(/** @type {import('estree').Property[]} */ (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 {import('estree').Expression} node_id */ + /** @param {Expression} node_id */ let fn = (node_id) => { return b.call( context.state.options.dev @@ -937,7 +930,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont fn = (node_id) => { return serialize_bind_this( - /** @type {import('estree').Identifier | import('estree').MemberExpression} */ (bind_this), + /** @type {Identifier | MemberExpression} */ (bind_this), context, prev(node_id) ); @@ -953,7 +946,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont return b.call( '$.component', node_id, - b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), + b.thunk(/** @type {Expression} */ (context.visit(node.expression))), b.arrow( [b.id('$$anchor'), b.id(component_name)], b.block([ @@ -993,14 +986,14 @@ function serialize_inline_component(node, component_name, context, anchor = cont /** * Serializes `bind:this` for components and elements. - * @param {import('estree').Identifier | import('estree').MemberExpression} bind_this + * @param {Identifier | MemberExpression} bind_this * @param {import('zimmerframe').Context} context - * @param {import('estree').Expression} node + * @param {Expression} node * @returns */ function serialize_bind_this(bind_this, context, node) { let i = 0; - /** @type {Map} */ + /** @type {Map} */ const each_ids = new Map(); // Transform each reference to an each block context variable into a $$value_ variable // by temporarily changing the `expression` of the corresponding binding. @@ -1022,7 +1015,7 @@ function serialize_bind_this(bind_this, context, node) { if (associated_node?.type === 'EachBlock') { each_ids.set(binding, [ i, - /** @type {import('estree').Expression} */ (context.visit(node)), + /** @type {Expression} */ (context.visit(node)), binding.expression ]); binding.expression = b.id('$$value_' + i); @@ -1032,7 +1025,7 @@ function serialize_bind_this(bind_this, context, node) { } ); - const bind_this_id = /** @type {import('estree').Expression} */ (context.visit(bind_this)); + const bind_this_id = /** @type {Expression} */ (context.visit(bind_this)); const ids = Array.from(each_ids.values()).map((id) => b.id('$$value_' + id[0])); const assignment = b.assignment('=', bind_this, b.id('$$value')); const update = serialize_set_binding(assignment, context, () => context.visit(assignment)); @@ -1042,11 +1035,11 @@ function serialize_bind_this(bind_this, context, node) { binding.expression = expression; } - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const args = [node, b.arrow([b.id('$$value'), ...ids], update), b.arrow([...ids], bind_this_id)]; // If we're mutating a property, then it might already be non-existent. // If we make all the object nodes optional, then it avoids any runtime exceptions. - /** @type {import('estree').Expression | import('estree').Super} */ + /** @type {Expression | Super} */ let bind_node = bind_this_id; while (bind_node?.type === 'MemberExpression') { @@ -1098,7 +1091,7 @@ function get_template_function(namespace, state) { /** * - * @param {import('estree').Statement} statement + * @param {Statement} statement */ function serialize_update(statement) { const body = @@ -1108,7 +1101,7 @@ function serialize_update(statement) { } /** - * @param {import('estree').Statement[]} update + * @param {Statement[]} update */ function serialize_render_stmt(update) { return update.length === 1 @@ -1122,7 +1115,7 @@ function serialize_render_stmt(update) { * @param {import('../types.js').ComponentContext} context */ function serialize_event_handler(node, { state, visit }) { - /** @type {import('estree').Expression} */ + /** @type {Expression} */ let handler; if (node.expression) { @@ -1134,7 +1127,7 @@ function serialize_event_handler(node, { state, visit }) { null, [b.rest(b.id('$$args'))], b.block([ - b.const('$$callback', /** @type {import('estree').Expression} */ (visit(handler))), + b.const('$$callback', /** @type {Expression} */ (visit(handler))), b.return( b.call(b.member(b.id('$$callback'), b.id('apply'), false, true), b.this, b.id('$$args')) ) @@ -1157,12 +1150,12 @@ function serialize_event_handler(node, { state, visit }) { ) { handler = dynamic_handler(); } else { - handler = /** @type {import('estree').Expression} */ (visit(handler)); + handler = /** @type {Expression} */ (visit(handler)); } } else if (handler.type === 'ConditionalExpression' || handler.type === 'LogicalExpression') { handler = dynamic_handler(); } else { - handler = /** @type {import('estree').Expression} */ (visit(handler)); + handler = /** @type {Expression} */ (visit(handler)); } } else { state.analysis.needs_props = true; @@ -1196,13 +1189,13 @@ function serialize_event_handler(node, { state, visit }) { /** * Serializes an event handler function of the `on:` directive or an attribute starting with `on` - * @param {{name: string; modifiers: string[]; expression: import('estree').Expression | null; delegated?: import('#compiler').DelegatedEvent | null; }} node + * @param {{name: string; modifiers: string[]; expression: Expression | null; delegated?: import('#compiler').DelegatedEvent | null; }} node * @param {import('../types.js').ComponentContext} context */ function serialize_event(node, context) { const state = context.state; - /** @type {import('estree').Statement} */ + /** @type {Statement} */ let statement; if (node.expression) { @@ -1226,7 +1219,7 @@ function serialize_event(node, context) { if (node.modifiers.includes('once')) { handler = b.call('$.once', handler); } - const hoistable_params = /** @type {import('estree').Expression[]} */ ( + const hoistable_params = /** @type {Expression[]} */ ( delegated.function.metadata.hoistable_params ); // When we hoist a function we assign an array with the function and all @@ -1322,7 +1315,7 @@ function serialize_event_attribute(node, context) { * (e.g. `{a} b {c}`) into a single update function. Along the way it creates * corresponding template node references these updates are applied to. * @param {import('#compiler').SvelteNode[]} nodes - * @param {(is_text: boolean) => import('estree').Expression} expression + * @param {(is_text: boolean) => Expression} expression * @param {boolean} is_element * @param {import('../types.js').ComponentContext} context */ @@ -1353,11 +1346,7 @@ function process_children(nodes, expression, is_element, { visit, state }) { const text_id = get_node_id(expression(true), state, 'text'); const update = b.stmt( - b.call( - '$.set_text', - text_id, - /** @type {import('estree').Expression} */ (visit(node.expression)) - ) + b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression))) ); if (node.metadata.contains_call_expression && !within_bound_contenteditable) { @@ -1370,7 +1359,7 @@ function process_children(nodes, expression, is_element, { visit, state }) { b.assignment( '=', b.member(text_id, b.id('nodeValue')), - /** @type {import('estree').Expression} */ (visit(node.expression)) + /** @type {Expression} */ (visit(node.expression)) ) ) ); @@ -1458,7 +1447,7 @@ function process_children(nodes, expression, is_element, { visit, state }) { } /** - * @param {import('estree').Expression} expression + * @param {Expression} expression * @param {import('../types.js').ComponentClientTransformState} state * @param {string} name */ @@ -1476,7 +1465,7 @@ function get_node_id(expression, state, name) { /** * @param {true | Array} value * @param {import('../types').ComponentContext} context - * @returns {[contains_call_expression: boolean, import('estree').Expression]} + * @returns {[contains_call_expression: boolean, Expression]} */ function serialize_attribute_value(value, context) { if (value === true) { @@ -1492,7 +1481,7 @@ function serialize_attribute_value(value, context) { return [ chunk.metadata.contains_call_expression, - /** @type {import('estree').Expression} */ (context.visit(chunk.expression)) + /** @type {Expression} */ (context.visit(chunk.expression)) ]; } @@ -1503,13 +1492,13 @@ function serialize_attribute_value(value, context) { * @param {Array} values * @param {(node: import('#compiler').SvelteNode, state: any) => any} visit * @param {import("../types.js").ComponentClientTransformState} state - * @returns {[boolean, import('estree').TemplateLiteral]} + * @returns {[boolean, TemplateLiteral]} */ function serialize_template_literal(values, visit, state) { - /** @type {import('estree').TemplateElement[]} */ + /** @type {TemplateElement[]} */ const quasis = []; - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const expressions = []; let contains_call_expression = false; let contains_multiple_call_expression = false; @@ -1530,10 +1519,10 @@ function serialize_template_literal(values, visit, state) { const node = values[i]; if (node.type === 'Text') { - const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); + const last = /** @type {TemplateElement} */ (quasis.at(-1)); last.value.raw += sanitize_template_string(node.data); } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { - const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); + const last = /** @type {TemplateElement} */ (quasis.at(-1)); if (node.expression.value != null) { last.value.raw += sanitize_template_string(node.expression.value + ''); } @@ -1546,7 +1535,7 @@ function serialize_template_literal(values, visit, state) { id, create_derived( state, - b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression, state))) + b.thunk(/** @type {Expression} */ (visit(node.expression, state))) ) ) ); @@ -1602,10 +1591,10 @@ export const template_visitors = { const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const body = []; - /** @type {import('estree').Statement | undefined} */ + /** @type {Statement | undefined} */ let close = undefined; /** @type {import('../types').ComponentClientTransformState} */ @@ -1637,8 +1626,8 @@ export const template_visitors = { } /** - * @param {import('estree').Identifier} template_name - * @param {import('estree').Expression[]} args + * @param {Identifier} template_name + * @param {Expression[]} args */ const add_template = (template_name, args) => { let call = b.call(get_template_function(namespace, state), ...args); @@ -1664,7 +1653,7 @@ export const template_visitors = { node: id }); - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const args = [b.template([b.quasi(state.template.join(''), true)], [])]; if (state.metadata.context.template_needs_import_node) { @@ -1701,7 +1690,7 @@ export const template_visitors = { // no need to create a template, we can just use the existing block's anchor process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); } else { - /** @type {(is_text: boolean) => import('estree').Expression} */ + /** @type {(is_text: boolean) => Expression} */ const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); process_children(trimmed, expression, false, { ...context, state }); @@ -1761,7 +1750,7 @@ export const template_visitors = { b.call( '$.html', context.state.node, - b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), + b.thunk(/** @type {Expression} */ (context.visit(node.expression))), b.literal(context.state.metadata.namespace === 'svg'), b.literal(context.state.metadata.namespace === 'mathml') ) @@ -1775,10 +1764,7 @@ export const template_visitors = { state.init.push( b.const( declaration.id, - create_derived( - state, - b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) - ) + create_derived(state, b.thunk(/** @type {Expression} */ (visit(declaration.init)))) ) ); @@ -1804,8 +1790,8 @@ export const template_visitors = { [], b.block([ b.const( - /** @type {import('estree').Pattern} */ (visit(declaration.id)), - /** @type {import('estree').Expression} */ (visit(declaration.init)) + /** @type {Pattern} */ (visit(declaration.id)), + /** @type {Expression} */ (visit(declaration.init)) ), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) @@ -1837,11 +1823,7 @@ export const template_visitors = { 'console.log', b.object( node.identifiers.map((identifier) => - b.prop( - 'init', - identifier, - /** @type {import('estree').Expression} */ (visit(identifier)) - ) + b.prop('init', identifier, /** @type {Expression} */ (visit(identifier))) ) ) ) @@ -1858,11 +1840,21 @@ export const template_visitors = { const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; - const args = raw_args.map((arg) => - b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))) - ); + /** @type {Expression[]} */ + let args = []; + for (let i = 0; i < raw_args.length; i++) { + const raw = raw_args[i]; + const arg = /** @type {Expression} */ (context.visit(raw)); + if (node.metadata.args_with_call_expression.has(i)) { + const id = b.id(context.state.scope.generate('render_arg')); + context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg)))); + args.push(b.thunk(b.call('$.get', id))); + } else { + args.push(b.thunk(arg)); + } + } - let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee)); + let snippet_function = /** @type {Expression} */ (context.visit(callee)); if (context.state.options.dev) { snippet_function = b.call( '$.validate_snippet', @@ -1891,16 +1883,14 @@ export const template_visitors = { const expression = node.expression === null ? b.literal(null) - : b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression))); + : b.thunk(/** @type {Expression} */ (visit(node.expression))); state.init.push( b.stmt( b.call( '$.animation', state.node, - b.thunk( - /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))) - ), + b.thunk(/** @type {Expression} */ (visit(parse_directive_name(node.name)))), expression ) ) @@ -1920,11 +1910,11 @@ export const template_visitors = { const args = [ b.literal(flags), state.node, - b.thunk(/** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name)))) + b.thunk(/** @type {Expression} */ (visit(parse_directive_name(node.name)))) ]; if (node.expression) { - args.push(b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))); + args.push(b.thunk(/** @type {Expression} */ (visit(node.expression)))); } state.init.push(b.stmt(b.call('$.transition', ...args))); @@ -1967,7 +1957,7 @@ export const template_visitors = { /** @type {import('#compiler').StyleDirective[]} */ const style_directives = []; - /** @type {import('estree').ExpressionStatement[]} */ + /** @type {ExpressionStatement[]} */ const lets = []; const is_custom_element = is_custom_element_node(node); @@ -2027,7 +2017,7 @@ export const template_visitors = { } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {import('estree').ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else { if (attribute.type === 'BindDirective') { if (attribute.name === 'group' || attribute.name === 'checked') { @@ -2118,7 +2108,7 @@ export const template_visitors = { (attribute.value === true || is_text_attribute(attribute)) ) { const name = get_attribute_name(node, attribute, context); - const literal_value = /** @type {import('estree').Literal} */ ( + const literal_value = /** @type {Literal} */ ( serialize_attribute_value(attribute.value, context)[1] ).value; if (name !== 'class' || literal_value) { @@ -2196,7 +2186,7 @@ export const template_visitors = { context.visit(node, child_state); } - /** @type {import('estree').Expression} */ + /** @type {Expression} */ let arg = context.state.node; // If `hydrate_node` is set inside the element, we need to reset it @@ -2263,7 +2253,7 @@ export const template_visitors = { /** @type {import('#compiler').StyleDirective[]} */ const style_directives = []; - /** @type {import('estree').ExpressionStatement[]} */ + /** @type {ExpressionStatement[]} */ const lets = []; // Create a temporary context which picks up the init/update statements. @@ -2296,7 +2286,7 @@ export const template_visitors = { } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {import('estree').ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else { context.visit(attribute, inner_context.state); } @@ -2315,7 +2305,7 @@ export const template_visitors = { serialize_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); serialize_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); - const get_tag = b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.tag))); + const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); if (context.state.options.dev && context.state.metadata.namespace !== 'foreign') { if (node.fragment.nodes.length > 0) { @@ -2324,14 +2314,14 @@ export const template_visitors = { context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag))); } - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { inner.push(serialize_render_stmt(inner_context.state.update)); } inner.push(...inner_context.state.after_update); inner.push( - .../** @type {import('estree').BlockStatement} */ ( + .../** @type {BlockStatement} */ ( context.visit(node.fragment, { ...context.state, metadata: { @@ -2360,7 +2350,7 @@ export const template_visitors = { }, EachBlock(node, context) { const each_node_meta = node.metadata; - const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression)); + const collection = /** @type {Expression} */ (context.visit(node.expression)); let each_item_is_reactive = true; if (!each_node_meta.is_controlled) { @@ -2439,7 +2429,7 @@ export const template_visitors = { // Legacy mode: find the parent each blocks which contain the arrays to invalidate const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => { - const array = /** @type {import('estree').Expression} */ (context.visit(block.expression)); + const array = /** @type {Expression} */ (context.visit(block.expression)); const transitive_dependencies = serialize_transitive_dependencies( block.metadata.references, context @@ -2458,7 +2448,7 @@ export const template_visitors = { } /** - * @param {import('estree').Pattern} expression_for_id + * @param {Pattern} expression_for_id * @returns {import('#compiler').Binding['mutation']} */ const create_mutation = (expression_for_id) => { @@ -2487,7 +2477,7 @@ export const template_visitors = { sequence.unshift(assign); return b.sequence(sequence); } else { - const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left); + const original_left = /** @type {MemberExpression} */ (assignment.left); const left = context.visit(original_left); const assign = b.assignment(assignment.operator, left, value); sequence.unshift(assign); @@ -2518,7 +2508,7 @@ export const template_visitors = { }; } - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const declarations = []; if (node.context.type === 'Identifier') { @@ -2534,19 +2524,17 @@ export const template_visitors = { const paths = extract_paths(node.context); for (const path of paths) { - const name = /** @type {import('estree').Identifier} */ (path.node).name; + const name = /** @type {Identifier} */ (path.node).name; const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name)); const needs_derived = path.has_default_value; // to ensure that default value is only called once - const fn = b.thunk( - /** @type {import('estree').Expression} */ (context.visit(path.expression?.(unwrapped))) - ); + const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression?.(unwrapped)))); declarations.push( b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn) ); binding.expression = needs_derived ? b.call('$.get', b.id(name)) : b.call(name); binding.mutation = create_mutation( - /** @type {import('estree').Pattern} */ (path.update_expression(unwrapped)) + /** @type {Pattern} */ (path.update_expression(unwrapped)) ); // we need to eagerly evaluate the expression in order to hit any @@ -2557,18 +2545,16 @@ export const template_visitors = { } } - const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.body)); + const block = /** @type {BlockStatement} */ (context.visit(node.body)); const key_function = node.key ? b.arrow( [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], declarations.length > 0 ? b.block( - declarations.concat( - b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) - ) + declarations.concat(b.return(/** @type {Expression} */ (context.visit(node.key)))) ) - : /** @type {import('estree').Expression} */ (context.visit(node.key)) + : /** @type {Expression} */ (context.visit(node.key)) ) : b.id('$.index'); @@ -2584,7 +2570,7 @@ export const template_visitors = { ); } - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const args = [ context.state.node, b.literal(each_type), @@ -2595,10 +2581,7 @@ export const template_visitors = { if (node.fallback) { args.push( - b.arrow( - [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.fallback)) - ) + b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fallback))) ); } @@ -2607,13 +2590,11 @@ export const template_visitors = { IfBlock(node, context) { context.state.template.push(''); - const consequent = /** @type {import('estree').BlockStatement} */ ( - context.visit(node.consequent) - ); + const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); const args = [ context.state.node, - b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.test))), + b.thunk(/** @type {Expression} */ (context.visit(node.test))), b.arrow([b.id('$$anchor')], consequent) ]; @@ -2622,7 +2603,7 @@ export const template_visitors = { node.alternate ? b.arrow( [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) + /** @type {BlockStatement} */ (context.visit(node.alternate)) ) : b.literal(null) ); @@ -2662,9 +2643,9 @@ export const template_visitors = { let catch_block; if (node.then) { - /** @type {import('estree').Pattern[]} */ + /** @type {Pattern[]} */ const args = [b.id('$$anchor')]; - const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.then)); + const block = /** @type {BlockStatement} */ (context.visit(node.then)); if (node.value) { const argument = create_derived_block_argument(node.value, context); @@ -2680,9 +2661,9 @@ export const template_visitors = { } if (node.catch) { - /** @type {import('estree').Pattern[]} */ + /** @type {Pattern[]} */ const args = [b.id('$$anchor')]; - const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.catch)); + const block = /** @type {BlockStatement} */ (context.visit(node.catch)); if (node.error) { const argument = create_derived_block_argument(node.error, context); @@ -2702,11 +2683,11 @@ export const template_visitors = { b.call( '$.await', context.state.node, - b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), + b.thunk(/** @type {Expression} */ (context.visit(node.expression))), node.pending ? b.arrow( [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.pending)) + /** @type {BlockStatement} */ (context.visit(node.pending)) ) : b.literal(null), then_block, @@ -2717,21 +2698,21 @@ export const template_visitors = { }, KeyBlock(node, context) { context.state.template.push(''); - const key = /** @type {import('estree').Expression} */ (context.visit(node.expression)); - const body = /** @type {import('estree').Expression} */ (context.visit(node.fragment)); + const key = /** @type {Expression} */ (context.visit(node.expression)); + const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) ); }, SnippetBlock(node, context) { // TODO hoist where possible - /** @type {import('estree').Pattern[]} */ + /** @type {Pattern[]} */ const args = [b.id('$$anchor')]; - /** @type {import('estree').BlockStatement} */ + /** @type {BlockStatement} */ let body; - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const declarations = []; for (let i = 0; i < node.parameters.length; i++) { @@ -2758,11 +2739,11 @@ export const template_visitors = { const paths = extract_paths(argument); for (const path of paths) { - const name = /** @type {import('estree').Identifier} */ (path.node).name; + const name = /** @type {Identifier} */ (path.node).name; const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name)); const needs_derived = path.has_default_value; // to ensure that default value is only called once const fn = b.thunk( - /** @type {import('estree').Expression} */ ( + /** @type {Expression} */ ( context.visit(path.expression?.(b.maybe_call(b.id(arg_alias)))) ) ); @@ -2782,10 +2763,10 @@ export const template_visitors = { body = b.block([ ...declarations, - .../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body + .../** @type {BlockStatement} */ (context.visit(node.body)).body ]); - /** @type {import('estree').Expression} */ + /** @type {Expression} */ let snippet = b.arrow(args, body); if (context.state.options.dev) { @@ -2816,20 +2797,17 @@ export const template_visitors = { params.push(b.id('$$action_arg')); } - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const args = [ state.node, b.arrow( params, - b.call( - /** @type {import('estree').Expression} */ (visit(parse_directive_name(node.name))), - ...params - ) + b.call(/** @type {Expression} */ (visit(parse_directive_name(node.name))), ...params) ) ]; if (node.expression) { - args.push(b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression)))); + args.push(b.thunk(/** @type {Expression} */ (visit(node.expression)))); } // actions need to run after attribute updates in order with bindings/events @@ -2839,14 +2817,14 @@ export const template_visitors = { BindDirective(node, context) { const { state, path, visit } = context; const expression = node.expression; - const getter = b.thunk(/** @type {import('estree').Expression} */ (visit(expression))); + const getter = b.thunk(/** @type {Expression} */ (visit(expression))); const assignment = b.assignment('=', expression, b.id('$$value')); const setter = b.arrow( [b.id('$$value')], serialize_set_binding( assignment, context, - () => /** @type {import('estree').Expression} */ (visit(assignment)), + () => /** @type {Expression} */ (visit(assignment)), null, { skip_proxy_and_freeze: true @@ -2854,7 +2832,7 @@ export const template_visitors = { ) ); - /** @type {import('estree').CallExpression} */ + /** @type {CallExpression} */ let call_expr; const property = binding_properties[node.name]; @@ -2985,7 +2963,7 @@ export const template_visitors = { call_expr = b.call(`$.bind_focused`, state.node, setter); break; case 'group': { - /** @type {import('estree').CallExpression[]} */ + /** @type {CallExpression[]} */ const indexes = []; for (const parent_each_block of node.metadata.parent_each_blocks) { indexes.push(b.call('$.unwrap', parent_each_block.metadata.index)); @@ -3011,7 +2989,7 @@ export const template_visitors = { group_getter = b.thunk( b.block([ b.stmt(serialize_attribute_value(value, context)[1]), - b.return(/** @type {import('estree').Expression} */ (visit(expression))) + b.return(/** @type {Expression} */ (visit(expression))) ]) ); } @@ -3052,9 +3030,7 @@ export const template_visitors = { context.state.node, // TODO use untrack here to not update when binding changes? // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this - b.thunk( - /** @type {import('estree').Expression} */ (context.visit(b.member_id(node.name))) - ), + b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))), b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component])) ) ) @@ -3097,8 +3073,7 @@ export const template_visitors = { b.thunk( b.block([ b.let( - /** @type {import('estree').Expression} */ (node.expression).type === - 'ObjectExpression' + /** @type {Expression} */ (node.expression).type === 'ObjectExpression' ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine b.object_pattern(node.expression.properties) : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine @@ -3122,43 +3097,39 @@ export const template_visitors = { return visit(node.expression); }, SvelteFragment(node, context) { - /** @type {import('estree').Statement[]} */ + /** @type {Statement[]} */ const lets = []; for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - lets.push(/** @type {import('estree').ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } } context.state.init.push(...lets); - context.state.init.push( - .../** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)).body - ); + context.state.init.push(.../** @type {BlockStatement} */ (context.visit(node.fragment)).body); }, SlotElement(node, context) { // fallback --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback); context.state.template.push(''); - /** @type {import('estree').Property[]} */ + /** @type {Property[]} */ const props = []; - /** @type {import('estree').Expression[]} */ + /** @type {Expression[]} */ const spreads = []; - /** @type {import('estree').ExpressionStatement[]} */ + /** @type {ExpressionStatement[]} */ const lets = []; let is_default = true; - /** @type {import('estree').Expression} */ + /** @type {Expression} */ let name = b.literal('default'); for (const attribute of node.attributes) { if (attribute.type === 'SpreadAttribute') { - spreads.push( - b.thunk(/** @type {import('estree').Expression} */ (context.visit(attribute))) - ); + spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute)))); } else if (attribute.type === 'Attribute') { const [, value] = serialize_attribute_value(attribute.value, context); if (attribute.name === 'name') { @@ -3172,7 +3143,7 @@ export const template_visitors = { } } } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {import('estree').ExpressionStatement} */ (context.visit(attribute))); + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } } @@ -3187,10 +3158,7 @@ export const template_visitors = { const fallback = node.fragment.nodes.length === 0 ? b.literal(null) - : b.arrow( - [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)) - ); + : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); const expression = is_default ? b.call('$.default_slot', b.id('$$props')) @@ -3205,10 +3173,7 @@ export const template_visitors = { b.stmt( b.call( '$.head', - b.arrow( - [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)) - ) + b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))) ) ) ); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 25947dcf2b..c5ec44e1df 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -154,6 +154,7 @@ export interface RenderTag extends BaseNode { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); metadata: { dynamic: boolean; + args_with_call_expression: Set; }; } diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/_config.js new file mode 100644 index 0000000000..e298061ef4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, logs }) { + assert.deepEqual(logs, ['invoked']); + }, + test_ssr({ assert, logs }) { + assert.deepEqual(logs, ['invoked']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/main.svelte new file mode 100644 index 0000000000..229a04fbb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-argument-call-expression/main.svelte @@ -0,0 +1,13 @@ + + +{#snippet foo(a)} + {a.foo} {a.bar} +{/snippet} + +{@render foo(fn(el))} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3991078153..6163a4f42a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1548,6 +1548,7 @@ declare module 'svelte/compiler' { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); metadata: { dynamic: boolean; + args_with_call_expression: Set; }; }