pull/16100/head
Rich Harris 4 months ago
parent d2e14e328b
commit b42a89471b

@ -1,17 +1,14 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
const expression = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression_2(context, node.expression, node.metadata.expression);
const expression = build_expression(context, node.expression, node.metadata.expression);
context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
context.next();
}

@ -1,11 +1,11 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -15,11 +15,7 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(
context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression_2(context, node.expression, node.metadata.expression)
);
const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
let then_block;
let catch_block;

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -6,7 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression, build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
@ -16,9 +16,7 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(declaration.init))
: build_legacy_expression_2(context, declaration.init, node.metadata.expression);
const init = build_expression(context, declaration.init, node.metadata.expression);
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init))));
context.state.transform[declaration.id.name] = { read: get_value };
@ -44,13 +42,11 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const init = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(declaration.init, child_state))
: build_legacy_expression_2(
{ ...context, state: child_state },
declaration.init,
node.metadata.expression
);
const init = build_expression(
{ ...context, state: child_state },
declaration.init,
node.metadata.expression
);
const fn = b.arrow(
[],
b.block([

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -12,9 +12,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -29,16 +28,15 @@ export function EachBlock(node, context) {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
};
const collection = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state))
: build_legacy_expression_2(
{
...context,
state: parent_scope_state
},
node.expression,
node.metadata.expression
);
const collection = build_expression(
{
...context,
state: parent_scope_state
},
node.expression,
node.metadata.expression
);
if (!each_node_meta.is_controlled) {
context.state.template.push_comment();

@ -1,9 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -12,9 +11,7 @@ import { build_legacy_expression_2 } from './shared/utils.js';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const expression = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression_2(context, node.expression, node.metadata.expression);
const expression = build_expression(context, node.expression, node.metadata.expression);
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -32,9 +32,7 @@ export function IfBlock(node, context) {
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
const test = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.test))
: build_legacy_expression_2(context, node.test, node.metadata.expression);
const test = build_expression(context, node.test, node.metadata.expression);
/** @type {Expression[]} */
const args = [

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -11,9 +11,7 @@ import { build_legacy_expression_2 } from './shared/utils.js';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression_2(context, node.expression, node.metadata.expression);
const key = build_expression(context, node.expression, node.metadata.expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_legacy_expression_2 } from './shared/utils.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -32,13 +32,11 @@ export function RenderTag(node, context) {
}
}
let snippet_function = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(callee))
: build_legacy_expression_2(
context,
/** @type {Expression} */ (callee),
node.metadata.expression
);
let snippet_function = build_expression(
context,
/** @type {Expression} */ (callee),
node.metadata.expression
);
if (node.metadata.dynamic) {
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one

@ -361,89 +361,19 @@ export function validate_mutation(node, context, expression) {
);
}
/**
* Checks whether the expression contains assignments, function calls, or member accesses
* @param {Expression|Pattern} expression
* @returns {boolean}
*/
function is_pure_expression(expression) {
// It's supposed that values do not have custom @@toPrimitive() or toString(),
// which may be implicitly called in expressions like `a + b`, `a & b`, `+a`, `str: ${a}`
switch (expression.type) {
case 'ArrayExpression':
return expression.elements.every(
(element) =>
element == null ||
(element.type === 'SpreadElement' ? false : is_pure_expression(element))
);
case 'BinaryExpression':
return (
expression.left.type !== 'PrivateIdentifier' &&
is_pure_expression(expression.left) &&
is_pure_expression(expression.right)
);
case 'ConditionalExpression':
return (
is_pure_expression(expression.test) &&
is_pure_expression(expression.consequent) &&
is_pure_expression(expression.alternate)
);
case 'Identifier':
return true;
case 'Literal':
return true;
case 'LogicalExpression':
return is_pure_expression(expression.left) && is_pure_expression(expression.right);
case 'MetaProperty':
return true; // new.target
case 'ObjectExpression':
return expression.properties.every(
(property) =>
property.type !== 'SpreadElement' &&
property.key.type !== 'PrivateIdentifier' &&
is_pure_expression(property.key) &&
is_pure_expression(property.value)
);
case 'SequenceExpression':
return expression.expressions.every(is_pure_expression);
case 'TemplateLiteral':
return expression.expressions.every(is_pure_expression);
case 'ThisExpression':
return true;
case 'UnaryExpression':
return is_pure_expression(expression.argument);
case 'YieldExpression':
return expression.argument == null || is_pure_expression(expression.argument);
case 'ArrayPattern':
case 'ArrowFunctionExpression':
case 'AssignmentExpression':
case 'AssignmentPattern':
case 'AwaitExpression':
case 'CallExpression':
case 'ChainExpression':
case 'ClassExpression':
case 'FunctionExpression':
case 'ImportExpression':
case 'MemberExpression':
case 'NewExpression':
case 'ObjectPattern':
case 'RestElement':
case 'TaggedTemplateExpression':
case 'UpdateExpression':
return false;
}
}
/**
*
* @param {ComponentContext} context
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
export function build_legacy_expression_2(context, expression, metadata) {
export function build_expression(context, expression, metadata) {
const value = /** @type {Expression} */ (context.visit(expression));
if (context.state.analysis.runes) {
return value;
}
if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) {
return value;
}
@ -471,61 +401,3 @@ export function build_legacy_expression_2(context, expression, metadata) {
return sequence;
}
/**
* Serializes an expression with reactivity like in Svelte 4
* @param {Expression} expression
* @param {ComponentContext} context
*/
export function build_legacy_expression(expression, context) {
// To recreate Svelte 4 behaviour, we track the dependencies
// the compiler can 'see', but we untrack the effect itself
const serialized_expression = /** @type {Expression} */ (context.visit(expression));
if (is_pure_expression(expression)) return serialized_expression;
/** @type {Expression[]} */
const sequence = [];
for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding === null || (binding.kind === 'normal' && binding.declaration_kind !== 'import'))
continue;
let used = false;
for (const { node, path } of nodes) {
const expression_idx = path.indexOf(expression);
if (expression_idx < 0) continue;
// in Svelte 4, #if, #each and #await copy context, so assignments
// aren't propagated to the parent block / component root
const track_assignment = !path.find(
(node, i) =>
i < expression_idx - 1 && ['IfBlock', 'EachBlock', 'AwaitBlock'].includes(node.type)
);
if (track_assignment) {
used = true;
break;
}
const assignment = /** @type {AssignmentExpression|undefined} */ (
path.find((node, i) => i >= expression_idx && node.type === 'AssignmentExpression')
);
if (!assignment || (assignment.left !== node && !path.includes(assignment.left))) {
used = true;
break;
}
}
if (!used) continue;
let serialized = build_getter(b.id(name), context.state);
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') {
serialized = b.call('$.deep_read_state', serialized);
}
sequence.push(serialized);
}
return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]);
}

Loading…
Cancel
Save