chore: unified expression metadata (#12644)

* add ExpressionMetadata type

* use unified reactivity metadata

* simplify component

* rename

* more consistency

* store bindings on expression metadata

* tidy

* regenerate types

* remove junk from options node

* reuse

* tweak jsdoc
animation-params
Rich Harris 2 months ago committed by GitHub
parent 363535c849
commit 65234b80c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -119,13 +119,23 @@ export function parse(source, { filename, rootDir, modern } = {}) {
*/ */
function to_public_ast(source, ast, modern) { function to_public_ast(source, ast, modern) {
if (modern) { if (modern) {
const clean = (/** @type {any} */ node) => {
delete node.metadata;
delete node.parent;
};
ast.options?.attributes.forEach((attribute) => {
clean(attribute);
clean(attribute.value);
if (Array.isArray(attribute.value)) {
attribute.value.forEach(clean);
}
});
// remove things that we don't want to treat as public API // remove things that we don't want to treat as public API
return zimmerframe_walk(ast, null, { return zimmerframe_walk(ast, null, {
_(node, { next }) { _(node, { next }) {
// @ts-ignore clean(node);
delete node.parent;
// @ts-ignore
delete node.metadata;
next(); next();
} }
}); });

@ -9,7 +9,7 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { create_attribute } from '../../nodes.js'; import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js';
@ -127,7 +127,7 @@ export default function element(parser) {
const type = meta_tags.has(name) const type = meta_tags.has(name)
? meta_tags.get(name) ? meta_tags.get(name)
: regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component' : regex_capital_letter.test(name[0])
? 'Component' ? 'Component'
: name === 'title' && parent_is_head(parser.stack) : name === 'title' && parent_is_head(parser.stack)
? 'TitleElement' ? 'TitleElement'
@ -140,7 +140,7 @@ export default function element(parser) {
const element = const element =
type === 'RegularElement' type === 'RegularElement'
? { ? {
type: type, type,
start, start,
end: -1, end: -1,
name, name,
@ -163,7 +163,7 @@ export default function element(parser) {
fragment: create_fragment(true), fragment: create_fragment(true),
parent: null, parent: null,
metadata: { metadata: {
svg: false // unpopulated at first, differs between types
} }
}); });
@ -508,8 +508,7 @@ function read_attribute(parser) {
expression, expression,
parent: null, parent: null,
metadata: { metadata: {
contains_call_expression: false, expression: create_expression_metadata()
dynamic: false
} }
}; };
@ -538,8 +537,7 @@ function read_attribute(parser) {
}, },
parent: null, parent: null,
metadata: { metadata: {
dynamic: false, expression: create_expression_metadata()
contains_call_expression: false
} }
}; };
@ -584,7 +582,7 @@ function read_attribute(parser) {
value, value,
parent: null, parent: null,
metadata: { metadata: {
dynamic: false expression: create_expression_metadata()
} }
}; };
} }
@ -616,17 +614,10 @@ function read_attribute(parser) {
modifiers, modifiers,
expression, expression,
metadata: { metadata: {
dynamic: false, expression: create_expression_metadata()
contains_call_expression: false
} }
}; };
if (directive.type === 'ClassDirective') {
directive.metadata = {
dynamic: false
};
}
if (directive.type === 'TransitionDirective') { if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index); const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition'; directive.intro = direction === 'in' || direction === 'transition';
@ -789,8 +780,7 @@ function read_sequence(parser, done, location) {
expression, expression,
parent: null, parent: null,
metadata: { metadata: {
contains_call_expression: false, expression: create_expression_metadata()
dynamic: false
} }
}; };

@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse_expression_at } from '../acorn.js'; import { parse_expression_at } from '../acorn.js';
import { create_expression_metadata } from '../../nodes.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/; const regex_whitespace_with_closing_curly_brace = /^\s*}/;
@ -39,8 +40,7 @@ export default function tag(parser) {
end: parser.index, end: parser.index,
expression, expression,
metadata: { metadata: {
contains_call_expression: false, expression: create_expression_metadata()
dynamic: false
} }
}); });
} }

@ -1178,20 +1178,19 @@ const common_visitors = {
context.next(); context.next();
node.metadata.dynamic = get_attribute_chunks(node.value).some((chunk) => { for (const chunk of get_attribute_chunks(node.value)) {
if (chunk.type !== 'ExpressionTag') { if (chunk.type !== 'ExpressionTag') continue;
return false;
}
if ( if (
chunk.expression.type === 'FunctionExpression' || chunk.expression.type === 'FunctionExpression' ||
chunk.expression.type === 'ArrowFunctionExpression' chunk.expression.type === 'ArrowFunctionExpression'
) { ) {
return false; continue;
} }
return chunk.metadata.dynamic || chunk.metadata.contains_call_expression; node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
}); node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
if (is_event_attribute(node)) { if (is_event_attribute(node)) {
const parent = context.path.at(-1); const parent = context.path.at(-1);
@ -1211,10 +1210,10 @@ const common_visitors = {
} }
}, },
ClassDirective(node, context) { ClassDirective(node, context) {
context.next({ ...context.state, expression: node }); context.next({ ...context.state, expression: node.metadata.expression });
}, },
SpreadAttribute(node, context) { SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node }); context.next({ ...context.state, expression: node.metadata.expression });
}, },
SlotElement(node, context) { SlotElement(node, context) {
let name = 'default'; let name = 'default';
@ -1230,17 +1229,21 @@ const common_visitors = {
if (node.value === true) { if (node.value === true) {
const binding = context.state.scope.get(node.name); const binding = context.state.scope.get(node.name);
if (binding?.kind !== 'normal') { if (binding?.kind !== 'normal') {
node.metadata.dynamic = true; node.metadata.expression.has_state = true;
} }
} else { } else {
context.next(); context.next();
node.metadata.dynamic = get_attribute_chunks(node.value).some(
(node) => node.type === 'ExpressionTag' && node.metadata.dynamic for (const chunk of get_attribute_chunks(node.value)) {
); if (chunk.type !== 'ExpressionTag') continue;
node.metadata.expression.has_state ||= chunk.metadata.expression.has_state;
node.metadata.expression.has_call ||= chunk.metadata.expression.has_call;
}
} }
}, },
ExpressionTag(node, context) { ExpressionTag(node, context) {
context.next({ ...context.state, expression: node }); context.next({ ...context.state, expression: node.metadata.expression });
}, },
Identifier(node, context) { Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1)); const parent = /** @type {Node} */ (context.path.at(-1));
@ -1261,10 +1264,18 @@ const common_visitors = {
const binding = context.state.scope.get(node.name); const binding = context.state.scope.get(node.name);
if (binding && context.state.expression) {
context.state.expression.dependencies.add(binding);
if (binding.kind !== 'normal') {
context.state.expression.has_state = true;
}
}
// if no binding, means some global variable // if no binding, means some global variable
if (binding && binding.kind !== 'normal') { if (binding && binding.kind !== 'normal') {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.metadata.dynamic = true; context.state.expression.has_state = true;
} }
// TODO it would be better to just bail out when we hit the ExportSpecifier node but that's // TODO it would be better to just bail out when we hit the ExportSpecifier node but that's
@ -1297,13 +1308,10 @@ const common_visitors = {
}, },
CallExpression(node, context) { CallExpression(node, context) {
const { expression, render_tag } = context.state; const { expression, render_tag } = context.state;
if (
(expression?.type === 'ExpressionTag' || if (expression && !is_known_safe_call(node, context)) {
expression?.type === 'SpreadAttribute' || expression.has_call = true;
expression?.type === 'OnDirective') && expression.has_state = true;
!is_known_safe_call(node, context)
) {
expression.metadata.contains_call_expression = true;
} }
if (render_tag) { if (render_tag) {
@ -1354,7 +1362,7 @@ const common_visitors = {
}, },
MemberExpression(node, context) { MemberExpression(node, context) {
if (context.state.expression) { if (context.state.expression) {
context.state.expression.metadata.dynamic = true; context.state.expression.has_state = true;
} }
if (!is_safe_identifier(node, context.state.scope)) { if (!is_safe_identifier(node, context.state.scope)) {
@ -1368,7 +1376,7 @@ const common_visitors = {
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') { if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node; state.analysis.event_directive_node ??= node;
} }
next({ ...state, expression: node }); next({ ...state, expression: node.metadata.expression });
}, },
BindDirective(node, context) { BindDirective(node, context) {
let i = context.path.length; let i = context.path.length;

@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { import type {
ClassDirective, ClassDirective,
ExpressionMetadata,
ExpressionTag, ExpressionTag,
OnDirective, OnDirective,
RenderTag, RenderTag,
@ -20,8 +21,8 @@ export interface AnalysisState {
has_props_rune: boolean; has_props_rune: boolean;
/** Which slots the current parent component has */ /** Which slots the current parent component has */
component_slots: Set<string>; component_slots: Set<string>;
/** The current {expression}, if any */ /** Information about the current expression/directive/block value */
expression: ExpressionTag | ClassDirective | OnDirective | SpreadAttribute | null; expression: ExpressionMetadata | null;
/** The current {@render ...} tag, if any */ /** The current {@render ...} tag, if any */
render_tag: null | RenderTag; render_tag: null | RenderTag;
private_derived_state: string[]; private_derived_state: string[];

@ -1,5 +1,5 @@
/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */ /** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { Attribute, BindDirective, Binding, ClassDirective, Component, DelegatedEvent, EachBlock, ExpressionTag, Namespace, OnDirective, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text } from '#compiler' */ /** @import { Attribute, BindDirective, Binding, ClassDirective, Component, DelegatedEvent, EachBlock, ExpressionMetadata, ExpressionTag, Namespace, OnDirective, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text } from '#compiler' */
/** @import { SourceLocation } from '#shared' */ /** @import { SourceLocation } from '#shared' */
/** @import { Scope } from '../../../scope.js' */ /** @import { Scope } from '../../../scope.js' */
/** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */ /** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */
@ -103,13 +103,11 @@ function serialize_style_directives(style_directives, element_id, context, is_at
) )
); );
const contains_call_expression = get_attribute_chunks(directive.value).some( const { has_state, has_call } = directive.metadata.expression;
(v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression
);
if (!is_attributes_reactive && contains_call_expression) { if (!is_attributes_reactive && has_call) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { } else if (is_attributes_reactive || has_state || has_call) {
state.update.push(update); state.update.push(update);
} else { } else {
state.init.push(update); state.init.push(update);
@ -151,11 +149,12 @@ function serialize_class_directives(class_directives, element_id, context, is_at
for (const directive of class_directives) { for (const directive of class_directives) {
const value = /** @type {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 update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
const contains_call_expression = directive.expression.type === 'CallExpression';
if (!is_attributes_reactive && contains_call_expression) { const { has_state, has_call } = directive.metadata.expression;
if (!is_attributes_reactive && has_call) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { } else if (is_attributes_reactive || has_state || has_call) {
state.update.push(update); state.update.push(update);
} else { } else {
state.init.push(update); state.init.push(update);
@ -277,7 +276,7 @@ function serialize_element_spread_attributes(
for (const attribute of attributes) { for (const attribute of attributes) {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context); const name = get_attribute_name(element, attribute, context);
// TODO: handle contains_call_expression // TODO: handle has_call
const [, value] = serialize_attribute_value(attribute.value, context); const [, value] = serialize_attribute_value(attribute.value, context);
if ( if (
@ -306,7 +305,7 @@ function serialize_element_spread_attributes(
} }
needs_isolation ||= needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
} }
const lowercase_attributes = const lowercase_attributes =
@ -405,11 +404,11 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
} }
is_reactive ||= is_reactive ||=
attribute.metadata.dynamic || attribute.metadata.expression.has_state ||
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive // objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
attribute.type === 'SpreadAttribute'; attribute.type === 'SpreadAttribute';
needs_isolation ||= needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
} }
if (needs_isolation || is_reactive) { if (needs_isolation || is_reactive) {
@ -486,13 +485,13 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
const name = get_attribute_name(element, attribute, context); const name = get_attribute_name(element, attribute, context);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml'; const is_mathml = context.state.metadata.namespace === 'mathml';
let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); let [has_call, value] = serialize_attribute_value(attribute.value, context);
// The foreign namespace doesn't have any special handling, everything goes through the attr function // The foreign namespace doesn't have any special handling, everything goes through the attr function
if (context.state.metadata.namespace === 'foreign') { if (context.state.metadata.namespace === 'foreign') {
const statement = b.stmt(b.call('$.set_attribute', node_id, b.literal(name), value)); const statement = b.stmt(b.call('$.set_attribute', node_id, b.literal(name), value));
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
const id = state.scope.generate(`${node_id.name}_${name}`); const id = state.scope.generate(`${node_id.name}_${name}`);
serialize_update_assignment(state, id, undefined, value, statement); serialize_update_assignment(state, id, undefined, value, statement);
return true; return true;
@ -529,8 +528,8 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
update = b.stmt(b.call(callee, node_id, b.literal(name), value)); update = b.stmt(b.call(callee, node_id, b.literal(name), value));
} }
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
if (contains_call_expression) { if (has_call) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else { } else {
state.update.push(update); state.update.push(update);
@ -552,12 +551,12 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
function serialize_custom_element_attribute_update_assignment(node_id, attribute, context) { function serialize_custom_element_attribute_update_assignment(node_id, attribute, context) {
const state = context.state; const state = context.state;
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); let [has_call, value] = serialize_attribute_value(attribute.value, context);
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
if (contains_call_expression) { if (has_call) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else { } else {
state.update.push(update); state.update.push(update);
@ -592,7 +591,7 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
value value
) )
); );
const is_reactive = attribute.metadata.dynamic; const is_reactive = attribute.metadata.expression.has_state;
const is_select_with_value = const is_select_with_value =
// attribute.metadata.dynamic would give false negatives because even if the value does not change, // attribute.metadata.dynamic would give false negatives because even if the value does not change,
// the inner options could still change, so we need to always treat it as reactive // the inner options could still change, so we need to always treat it as reactive
@ -723,10 +722,10 @@ function serialize_inline_component(node, component_name, context, anchor = cont
events[attribute.name].push(handler); events[attribute.name].push(handler);
} else if (attribute.type === 'SpreadAttribute') { } else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute)); const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
let value = expression; let value = expression;
if (attribute.metadata.contains_call_expression) { if (attribute.metadata.expression.has_call) {
const id = b.id(context.state.scope.generate('spread_element')); const id = b.id(context.state.scope.generate('spread_element'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id); value = b.call('$.get', id);
@ -754,7 +753,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
const [, value] = serialize_attribute_value(attribute.value, context); const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
let arg = value; let arg = value;
// When we have a non-simple computation, anything other than an Identifier or Member expression, // When we have a non-simple computation, anything other than an Identifier or Member expression,
@ -1118,7 +1117,7 @@ function serialize_render_stmt(update) {
/** /**
* Serializes the event handler function of the `on:` directive * Serializes the event handler function of the `on:` directive
* @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node * @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {null | { contains_call_expression: boolean; dynamic: boolean; } | null} metadata * @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
function serialize_event_handler(node, metadata, { state, visit }) { function serialize_event_handler(node, metadata, { state, visit }) {
@ -1145,7 +1144,7 @@ function serialize_event_handler(node, metadata, { state, visit }) {
); );
if ( if (
metadata?.contains_call_expression && metadata?.has_call &&
!( !(
(handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') && (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') &&
handler.metadata.hoistable handler.metadata.hoistable
@ -1227,7 +1226,7 @@ function serialize_event_handler(node, metadata, { state, visit }) {
/** /**
* Serializes an event handler function of the `on:` directive or an attribute starting with `on` * Serializes an event handler function of the `on:` directive or an attribute starting with `on`
* @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: DelegatedEvent | null;}} node * @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: DelegatedEvent | null;}} node
* @param {null | { contains_call_expression: boolean; dynamic: boolean; }} metadata * @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
function serialize_event(node, metadata, context) { function serialize_event(node, metadata, context) {
@ -1357,7 +1356,9 @@ function serialize_event_attribute(node, context) {
modifiers, modifiers,
delegated: node.metadata.delegated delegated: node.metadata.delegated
}, },
!Array.isArray(node.value) && node.value?.type === 'ExpressionTag' ? node.value.metadata : null, !Array.isArray(node.value) && node.value?.type === 'ExpressionTag'
? node.value.metadata.expression
: null,
context context
); );
} }
@ -1401,9 +1402,9 @@ function process_children(nodes, expression, is_element, { visit, state }) {
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state))) b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state)))
); );
if (node.metadata.contains_call_expression && !within_bound_contenteditable) { if (node.metadata.expression.has_call && !within_bound_contenteditable) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else if (node.metadata.dynamic && !within_bound_contenteditable) { } else if (node.metadata.expression.has_state && !within_bound_contenteditable) {
state.update.push(update); state.update.push(update);
} else { } else {
state.init.push( state.init.push(
@ -1424,14 +1425,16 @@ function process_children(nodes, expression, is_element, { visit, state }) {
state.template.push(' '); state.template.push(' ');
const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state); const [has_call, value] = serialize_template_literal(sequence, visit, state);
const update = b.stmt(b.call('$.set_text', text_id, value)); const update = b.stmt(b.call('$.set_text', text_id, value));
if (contains_call_expression && !within_bound_contenteditable) { if (has_call && !within_bound_contenteditable) {
state.init.push(serialize_update(update)); state.init.push(serialize_update(update));
} else if ( } else if (
sequence.some((node) => node.type === 'ExpressionTag' && node.metadata.dynamic) && sequence.some(
(node) => node.type === 'ExpressionTag' && node.metadata.expression.has_state
) &&
!within_bound_contenteditable !within_bound_contenteditable
) { ) {
state.update.push(update); state.update.push(update);
@ -1517,7 +1520,7 @@ function get_node_id(expression, state, name) {
/** /**
* @param {Attribute['value']} value * @param {Attribute['value']} value
* @param {ComponentContext} context * @param {ComponentContext} context
* @returns {[contains_call_expression: boolean, Expression]} * @returns {[has_call: boolean, Expression]}
*/ */
function serialize_attribute_value(value, context) { function serialize_attribute_value(value, context) {
if (value === true) { if (value === true) {
@ -1532,7 +1535,7 @@ function serialize_attribute_value(value, context) {
} }
return [ return [
chunk.metadata.contains_call_expression, chunk.metadata.expression.has_call,
/** @type {Expression} */ (context.visit(chunk.expression)) /** @type {Expression} */ (context.visit(chunk.expression))
]; ];
} }
@ -1552,18 +1555,18 @@ function serialize_template_literal(values, visit, state) {
/** @type {Expression[]} */ /** @type {Expression[]} */
const expressions = []; const expressions = [];
let contains_call_expression = false; let has_call = false;
let contains_multiple_call_expression = false; let contains_multiple_call_expression = false;
quasis.push(b.quasi('')); quasis.push(b.quasi(''));
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const node = values[i]; const node = values[i];
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { if (node.type === 'ExpressionTag' && node.metadata.expression.has_call) {
if (contains_call_expression) { if (has_call) {
contains_multiple_call_expression = true; contains_multiple_call_expression = true;
} }
contains_call_expression = true; has_call = true;
} }
} }
@ -1600,7 +1603,7 @@ function serialize_template_literal(values, visit, state) {
} }
// TODO instead of this tuple, return a `{ dynamic, complex, value }` object. will DRY stuff out // TODO instead of this tuple, return a `{ dynamic, complex, value }` object. will DRY stuff out
return [contains_call_expression, b.template(quasis, expressions)]; return [has_call, b.template(quasis, expressions)];
} }
/** @type {ComponentVisitors} */ /** @type {ComponentVisitors} */
@ -2855,7 +2858,7 @@ export const template_visitors = {
context.next({ ...context.state, in_constructor: false }); context.next({ ...context.state, in_constructor: false });
}, },
OnDirective(node, context) { OnDirective(node, context) {
serialize_event(node, node.metadata, context); serialize_event(node, node.metadata.expression, context);
}, },
UseDirective(node, { state, next, visit }) { UseDirective(node, { state, next, visit }) {
const params = [b.id('$$node')]; const params = [b.id('$$node')];
@ -3238,7 +3241,7 @@ export const template_visitors = {
name = value; name = value;
is_default = false; is_default = false;
} else if (attribute.name !== 'slot') { } else if (attribute.name !== 'slot') {
if (attribute.metadata.dynamic) { if (attribute.metadata.expression.has_state) {
props.push(b.get(attribute.name, [b.return(value)])); props.push(b.get(attribute.name, [b.return(value)]));
} else { } else {
props.push(b.init(attribute.name, value)); props.push(b.init(attribute.name, value));

@ -12,7 +12,11 @@ import {
LoadErrorElements, LoadErrorElements,
WhitespaceInsensitiveAttributes WhitespaceInsensitiveAttributes
} from '../../../../constants.js'; } from '../../../../constants.js';
import { create_attribute, is_custom_element_node } from '../../../../nodes.js'; import {
create_attribute,
create_expression_metadata,
is_custom_element_node
} from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js'; import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '../../../../../utils/builders.js'; import * as b from '../../../../../utils/builders.js';
import { import {
@ -142,8 +146,7 @@ export function serialize_element_attributes(node, context) {
serialize_attribute_value(value_attribute.value, context) serialize_attribute_value(value_attribute.value, context)
), ),
metadata: { metadata: {
contains_call_expression: false, expression: create_expression_metadata()
dynamic: false
} }
} }
]) ])
@ -158,8 +161,7 @@ export function serialize_element_attributes(node, context) {
parent: attribute, parent: attribute,
expression: attribute.expression, expression: attribute.expression,
metadata: { metadata: {
contains_call_expression: false, expression: create_expression_metadata()
dynamic: false
} }
} }
]) ])
@ -399,7 +401,9 @@ function serialize_class_directives(class_directives, class_attribute) {
), ),
b.literal(' ') b.literal(' ')
), ),
metadata: { contains_call_expression: false, dynamic: false } metadata: {
expression: create_expression_metadata()
}
}); });
class_attribute.value = chunks; class_attribute.value = chunks;

@ -45,8 +45,19 @@ export function create_attribute(name, start, end, value) {
value, value,
parent: null, parent: null,
metadata: { metadata: {
dynamic: false, expression: create_expression_metadata(),
delegated: null delegated: null
} }
}; };
} }
/**
* @returns {Compiler.ExpressionMetadata}
*/
export function create_expression_metadata() {
return {
dependencies: new Set(),
has_state: false,
has_call: false
};
}

@ -316,6 +316,15 @@ export interface Binding {
} | null; } | null;
} }
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
dependencies: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
}
export * from './template.js'; export * from './template.js';
export { Css }; export { Css };

@ -1,4 +1,4 @@
import type { Binding, Css } from '#compiler'; import type { Binding, Css, ExpressionMetadata } from '#compiler';
import type { import type {
ArrayExpression, ArrayExpression,
ArrowFunctionExpression, ArrowFunctionExpression,
@ -111,12 +111,7 @@ export interface ExpressionTag extends BaseNode {
type: 'ExpressionTag'; type: 'ExpressionTag';
expression: Expression; expression: Expression;
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
/**
* Whether or not the expression contains any dynamic references
* determines whether it will be updated in a render effect or not
*/
dynamic: boolean;
}; };
} }
@ -190,7 +185,7 @@ export interface ClassDirective extends BaseNode {
/** The 'y' in `class:x={y}`, or the `x` in `class:x` */ /** The 'y' in `class:x={y}`, or the `x` in `class:x` */
expression: Expression; expression: Expression;
metadata: { metadata: {
dynamic: false; expression: ExpressionMetadata;
}; };
} }
@ -212,8 +207,7 @@ export interface OnDirective extends BaseNode {
expression: null | Expression; expression: null | Expression;
modifiers: string[]; // TODO specify modifiers: string[]; // TODO specify
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
dynamic: boolean;
}; };
} }
@ -233,7 +227,7 @@ export interface StyleDirective extends BaseNode {
value: true | ExpressionTag | Array<ExpressionTag | Text>; value: true | ExpressionTag | Array<ExpressionTag | Text>;
modifiers: Array<'important'>; modifiers: Array<'important'>;
metadata: { metadata: {
dynamic: boolean; expression: ExpressionMetadata;
}; };
} }
@ -454,7 +448,7 @@ export interface Attribute extends BaseNode {
name: string; name: string;
value: true | ExpressionTag | Array<Text | ExpressionTag>; value: true | ExpressionTag | Array<Text | ExpressionTag>;
metadata: { metadata: {
dynamic: boolean; expression: ExpressionMetadata;
/** May be set if this is an event attribute */ /** May be set if this is an event attribute */
delegated: null | DelegatedEvent; delegated: null | DelegatedEvent;
}; };
@ -464,8 +458,7 @@ export interface SpreadAttribute extends BaseNode {
type: 'SpreadAttribute'; type: 'SpreadAttribute';
expression: Expression; expression: Expression;
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
dynamic: boolean;
}; };
} }

@ -39,15 +39,9 @@
"end": 48, "end": 48,
"type": "Text", "type": "Text",
"raw": "my-custom-element", "raw": "my-custom-element",
"data": "my-custom-element", "data": "my-custom-element"
"parent": null
}
],
"parent": null,
"metadata": {
"dynamic": false,
"delegated": null
} }
]
}, },
{ {
"type": "Attribute", "type": "Attribute",
@ -74,17 +68,7 @@
}, },
"value": true, "value": true,
"raw": "true" "raw": "true"
},
"parent": null,
"metadata": {
"contains_call_expression": false,
"dynamic": false
} }
},
"parent": null,
"metadata": {
"dynamic": false,
"delegated": null
} }
} }
], ],

@ -965,6 +965,15 @@ declare module 'svelte/compiler' {
inside_rest?: boolean; inside_rest?: boolean;
} | null; } | null;
} }
interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
dependencies: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
}
/** /**
* The preprocess function provides convenient hooks for arbitrarily transforming component source code. * The preprocess function provides convenient hooks for arbitrarily transforming component source code.
* For example, it can be used to convert a <style lang="sass"> block into vanilla CSS. * For example, it can be used to convert a <style lang="sass"> block into vanilla CSS.
@ -1563,12 +1572,7 @@ declare module 'svelte/compiler' {
type: 'ExpressionTag'; type: 'ExpressionTag';
expression: Expression; expression: Expression;
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
/**
* Whether or not the expression contains any dynamic references
* determines whether it will be updated in a render effect or not
*/
dynamic: boolean;
}; };
} }
@ -1642,7 +1646,7 @@ declare module 'svelte/compiler' {
/** The 'y' in `class:x={y}`, or the `x` in `class:x` */ /** The 'y' in `class:x={y}`, or the `x` in `class:x` */
expression: Expression; expression: Expression;
metadata: { metadata: {
dynamic: false; expression: ExpressionMetadata;
}; };
} }
@ -1664,8 +1668,7 @@ declare module 'svelte/compiler' {
expression: null | Expression; expression: null | Expression;
modifiers: string[]; // TODO specify modifiers: string[]; // TODO specify
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
dynamic: boolean;
}; };
} }
@ -1685,7 +1688,7 @@ declare module 'svelte/compiler' {
value: true | ExpressionTag | Array<ExpressionTag | Text>; value: true | ExpressionTag | Array<ExpressionTag | Text>;
modifiers: Array<'important'>; modifiers: Array<'important'>;
metadata: { metadata: {
dynamic: boolean; expression: ExpressionMetadata;
}; };
} }
@ -1906,7 +1909,7 @@ declare module 'svelte/compiler' {
name: string; name: string;
value: true | ExpressionTag | Array<Text | ExpressionTag>; value: true | ExpressionTag | Array<Text | ExpressionTag>;
metadata: { metadata: {
dynamic: boolean; expression: ExpressionMetadata;
/** May be set if this is an event attribute */ /** May be set if this is an event attribute */
delegated: null | DelegatedEvent; delegated: null | DelegatedEvent;
}; };
@ -1916,8 +1919,7 @@ declare module 'svelte/compiler' {
type: 'SpreadAttribute'; type: 'SpreadAttribute';
expression: Expression; expression: Expression;
metadata: { metadata: {
contains_call_expression: boolean; expression: ExpressionMetadata;
dynamic: boolean;
}; };
} }

Loading…
Cancel
Save