use unified reactivity metadata

pull/12644/head
Rich Harris 2 years ago
parent 7c9dea4b65
commit 7dc674525e

@ -9,7 +9,7 @@ import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.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 { closing_tag_omitted } from '../../../../html-tree-validation.js';
@ -127,7 +127,7 @@ export default function element(parser) {
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_capital_letter.test(name[0]) || name === 'svelte:self' || name === 'svelte:component'
: regex_capital_letter.test(name[0])
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
@ -140,7 +140,7 @@ export default function element(parser) {
const element =
type === 'RegularElement'
? {
type: type,
type,
start,
end: -1,
name,
@ -163,7 +163,7 @@ export default function element(parser) {
fragment: create_fragment(true),
parent: null,
metadata: {
svg: false
expression: create_expression_metadata()
}
});
@ -507,10 +507,8 @@ function read_attribute(parser) {
end: parser.index,
expression,
parent: null,
effect: {
is_dynamic: false,
has_call_expression: false,
bindings: new Set()
metadata: {
expression: create_expression_metadata()
}
};
@ -538,10 +536,8 @@ function read_attribute(parser) {
name
},
parent: null,
effect: {
has_call_expression: false,
is_dynamic: false,
bindings: new Set()
metadata: {
expression: create_expression_metadata()
}
};
@ -586,7 +582,7 @@ function read_attribute(parser) {
value,
parent: null,
metadata: {
dynamic: false
expression: create_expression_metadata()
}
};
}
@ -618,17 +614,10 @@ function read_attribute(parser) {
modifiers,
expression,
metadata: {
dynamic: false,
contains_call_expression: false
expression: create_expression_metadata()
}
};
if (directive.type === 'ClassDirective') {
directive.metadata = {
dynamic: false
};
}
if (directive.type === 'TransitionDirective') {
const direction = name.slice(0, colon_index);
directive.intro = direction === 'in' || direction === 'transition';
@ -791,8 +780,7 @@ function read_sequence(parser, done, location) {
expression,
parent: null,
metadata: {
contains_call_expression: false,
dynamic: false
expression: create_expression_metadata()
}
};

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

@ -1178,7 +1178,7 @@ const common_visitors = {
context.next();
node.metadata.dynamic = get_attribute_chunks(node.value).some((chunk) => {
node.metadata.expression.has_state = get_attribute_chunks(node.value).some((chunk) => {
if (chunk.type !== 'ExpressionTag') {
return false;
}
@ -1190,7 +1190,7 @@ const common_visitors = {
return false;
}
return chunk.metadata.dynamic || chunk.metadata.contains_call_expression;
return chunk.metadata.expression.has_state;
});
if (is_event_attribute(node)) {
@ -1211,10 +1211,10 @@ const common_visitors = {
}
},
ClassDirective(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
SpreadAttribute(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
SlotElement(node, context) {
let name = 'default';
@ -1230,17 +1230,17 @@ const common_visitors = {
if (node.value === true) {
const binding = context.state.scope.get(node.name);
if (binding?.kind !== 'normal') {
node.metadata.dynamic = true;
node.metadata.expression.has_state = true;
}
} else {
context.next();
node.metadata.dynamic = get_attribute_chunks(node.value).some(
(node) => node.type === 'ExpressionTag' && node.metadata.dynamic
node.metadata.expression.has_state = get_attribute_chunks(node.value).some(
(node) => node.type === 'ExpressionTag' && node.metadata.expression.has_state
);
}
},
ExpressionTag(node, context) {
context.next({ ...context.state, expression: node });
context.next({ ...context.state, expression: node.metadata.expression });
},
Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
@ -1264,7 +1264,7 @@ const common_visitors = {
// if no binding, means some global variable
if (binding && binding.kind !== 'normal') {
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
@ -1297,13 +1297,10 @@ const common_visitors = {
},
CallExpression(node, context) {
const { expression, render_tag } = context.state;
if (
(expression?.type === 'ExpressionTag' ||
expression?.type === 'SpreadAttribute' ||
expression?.type === 'OnDirective') &&
!is_known_safe_call(node, context)
) {
expression.metadata.contains_call_expression = true;
if (expression && !is_known_safe_call(node, context)) {
expression.has_call = true;
expression.has_state = true;
}
if (render_tag) {
@ -1354,7 +1351,7 @@ const common_visitors = {
},
MemberExpression(node, context) {
if (context.state.expression) {
context.state.expression.metadata.dynamic = true;
context.state.expression.has_state = true;
}
if (!is_safe_identifier(node, context.state.scope)) {
@ -1368,7 +1365,7 @@ const common_visitors = {
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node;
}
next({ ...state, expression: node });
next({ ...state, expression: node.metadata.expression });
},
BindDirective(node, context) {
let i = context.path.length;
@ -1543,7 +1540,7 @@ const common_visitors = {
node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name
);
node.metadata.dynamic = binding !== null && binding.kind !== 'normal';
node.metadata.expression.has_state = binding !== null && binding.kind !== 'normal';
},
RenderTag(node, context) {
context.next({ ...context.state, render_tag: node });

@ -2,6 +2,7 @@ import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type {
ClassDirective,
ExpressionMetadata,
ExpressionTag,
OnDirective,
RenderTag,
@ -21,7 +22,7 @@ export interface AnalysisState {
/** Which slots the current parent component has */
component_slots: Set<string>;
/** The current {expression}, if any */
expression: ExpressionTag | ClassDirective | OnDirective | SpreadAttribute | null;
expression: ExpressionMetadata | null;
/** The current {@render ...} tag, if any */
render_tag: null | RenderTag;
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 { 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 { Scope } from '../../../scope.js' */
/** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */
@ -104,12 +104,16 @@ function serialize_style_directives(style_directives, element_id, context, is_at
);
const contains_call_expression = get_attribute_chunks(directive.value).some(
(v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression
(v) => v.type === 'ExpressionTag' && v.metadata.expression.has_call
);
if (!is_attributes_reactive && contains_call_expression) {
state.init.push(serialize_update(update));
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
} else if (
is_attributes_reactive ||
directive.metadata.expression.has_state ||
contains_call_expression
) {
state.update.push(update);
} else {
state.init.push(update);
@ -155,7 +159,11 @@ function serialize_class_directives(class_directives, element_id, context, is_at
if (!is_attributes_reactive && contains_call_expression) {
state.init.push(serialize_update(update));
} else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) {
} else if (
is_attributes_reactive ||
directive.metadata.expression.has_state ||
contains_call_expression
) {
state.update.push(update);
} else {
state.init.push(update);
@ -306,7 +314,7 @@ function serialize_element_spread_attributes(
}
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
}
const lowercase_attributes =
@ -405,11 +413,11 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
}
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
attribute.type === 'SpreadAttribute';
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
}
if (needs_isolation || is_reactive) {
@ -492,7 +500,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
if (context.state.metadata.namespace === 'foreign') {
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}`);
serialize_update_assignment(state, id, undefined, value, statement);
return true;
@ -529,7 +537,7 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
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) {
state.init.push(serialize_update(update));
} else {
@ -556,7 +564,7 @@ function serialize_custom_element_attribute_update_assignment(node_id, attribute
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) {
state.init.push(serialize_update(update));
} else {
@ -592,7 +600,7 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
value
)
);
const is_reactive = attribute.metadata.dynamic;
const is_reactive = attribute.metadata.expression.has_state;
const is_select_with_value =
// 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
@ -723,10 +731,10 @@ function serialize_inline_component(node, component_name, context, anchor = cont
events[attribute.name].push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.dynamic) {
if (attribute.metadata.expression.has_state) {
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'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id);
@ -754,7 +762,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.metadata.dynamic) {
if (attribute.metadata.expression.has_state) {
let arg = value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
@ -1118,7 +1126,7 @@ function serialize_render_stmt(update) {
/**
* Serializes the event handler function of the `on:` directive
* @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {null | { contains_call_expression: boolean; dynamic: boolean; } | null} metadata
* @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
function serialize_event_handler(node, metadata, { state, visit }) {
@ -1145,7 +1153,7 @@ function serialize_event_handler(node, metadata, { state, visit }) {
);
if (
metadata?.contains_call_expression &&
metadata?.has_call &&
!(
(handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') &&
handler.metadata.hoistable
@ -1227,7 +1235,7 @@ function serialize_event_handler(node, metadata, { state, visit }) {
/**
* 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 {null | { contains_call_expression: boolean; dynamic: boolean; }} metadata
* @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
function serialize_event(node, metadata, context) {
@ -1353,7 +1361,9 @@ function serialize_event_attribute(node, context) {
modifiers,
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
);
}
@ -1397,9 +1407,9 @@ function process_children(nodes, expression, is_element, { visit, 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));
} else if (node.metadata.dynamic && !within_bound_contenteditable) {
} else if (node.metadata.expression.has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
state.init.push(
@ -1427,7 +1437,9 @@ function process_children(nodes, expression, is_element, { visit, state }) {
if (contains_call_expression && !within_bound_contenteditable) {
state.init.push(serialize_update(update));
} 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
) {
state.update.push(update);
@ -1528,7 +1540,7 @@ function serialize_attribute_value(value, context) {
}
return [
chunk.metadata.contains_call_expression,
chunk.metadata.expression.has_call,
/** @type {Expression} */ (context.visit(chunk.expression))
];
}
@ -1555,7 +1567,7 @@ function serialize_template_literal(values, visit, state) {
for (let i = 0; i < values.length; 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) {
contains_multiple_call_expression = true;
}
@ -2851,7 +2863,7 @@ export const template_visitors = {
context.next({ ...context.state, in_constructor: false });
},
OnDirective(node, context) {
serialize_event(node, node.metadata, context);
serialize_event(node, node.metadata.expression, context);
},
UseDirective(node, { state, next, visit }) {
const params = [b.id('$$node')];
@ -3115,7 +3127,7 @@ export const template_visitors = {
}
},
Component(node, context) {
if (node.metadata.dynamic) {
if (node.metadata.expression.has_state) {
// Handle dynamic references to what seems like static inline components
const component = serialize_inline_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
@ -3234,7 +3246,7 @@ export const template_visitors = {
name = value;
is_default = false;
} else if (attribute.name !== 'slot') {
if (attribute.metadata.dynamic) {
if (attribute.metadata.expression.has_state) {
props.push(b.get(attribute.name, [b.return(value)]));
} else {
props.push(b.init(attribute.name, value));

@ -233,7 +233,8 @@ export function serialize_inline_component(node, expression, context) {
}
const dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
node.type === 'SvelteComponent' ||
(node.type === 'Component' && node.metadata.expression.has_state);
if (custom_css_props.length > 0) {
context.state.template.push(

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

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

@ -317,8 +317,11 @@ export interface Binding {
}
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;
}

@ -1,4 +1,4 @@
import type { Binding, Css, EffectMetadata } from '#compiler';
import type { Binding, Css, ExpressionMetadata } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
@ -110,7 +110,9 @@ export interface Text extends BaseNode {
export interface ExpressionTag extends BaseNode {
type: 'ExpressionTag';
expression: Expression;
effect: EffectMetadata;
metadata: {
expression: ExpressionMetadata;
};
}
/** A (possibly reactive) HTML template expression — `{@html ...}` */
@ -183,7 +185,7 @@ export interface ClassDirective extends BaseNode {
/** The 'y' in `class:x={y}`, or the `x` in `class:x` */
expression: Expression;
metadata: {
dynamic: false;
expression: ExpressionMetadata;
};
}
@ -205,8 +207,7 @@ export interface OnDirective extends BaseNode {
expression: null | Expression;
modifiers: string[]; // TODO specify
metadata: {
contains_call_expression: boolean;
dynamic: boolean;
expression: ExpressionMetadata;
};
}
@ -226,7 +227,7 @@ export interface StyleDirective extends BaseNode {
value: true | ExpressionTag | Array<ExpressionTag | Text>;
modifiers: Array<'important'>;
metadata: {
dynamic: boolean;
expression: ExpressionMetadata;
};
}
@ -273,7 +274,7 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
dynamic: boolean;
expression: ExpressionMetadata;
};
}
@ -447,7 +448,7 @@ export interface Attribute extends BaseNode {
name: string;
value: true | ExpressionTag | Array<Text | ExpressionTag>;
metadata: {
dynamic: boolean;
expression: ExpressionMetadata;
/** May be set if this is an event attribute */
delegated: null | DelegatedEvent;
};
@ -456,7 +457,9 @@ export interface Attribute extends BaseNode {
export interface SpreadAttribute extends BaseNode {
type: 'SpreadAttribute';
expression: Expression;
effect: EffectMetadata;
metadata: {
expression: ExpressionMetadata;
};
}
export type TemplateNode =

@ -45,7 +45,11 @@
],
"parent": null,
"metadata": {
"dynamic": false,
"expression": {
"dependencies": {},
"has_state": false,
"has_call": false
},
"delegated": null
}
},
@ -77,13 +81,20 @@
},
"parent": null,
"metadata": {
"contains_call_expression": false,
"dynamic": false
"expression": {
"dependencies": {},
"has_state": false,
"has_call": false
}
}
},
"parent": null,
"metadata": {
"dynamic": false,
"expression": {
"dependencies": {},
"has_state": false,
"has_call": false
},
"delegated": null
}
}

Loading…
Cancel
Save