chore: transformers ()

* clear out getters on new scope

* fix

* fix

* fix

* fix

* consolidate legacy_reactive_import logic

* unused

* $$sanitized_props

* use getters mechanism for store_sub

* snapshot

* fix

* tests passing

* remove some stuff

* more

* fix

* tidy up

* simplify

* simplify

* getters -> transformers

* update

* use update transformers

* add assign transformer

* more

* tweak

* remove junk

* unused

* simplify

* tidy up

* tweak

* assign_property

* fix

* tidy up

* tidy up

* move store code

* this appears to be unused

* tidy up

* tweak

* simplify

* move code

* move stuff

* note to self

* move stuff

* each blocks

* note to self

* lengthen stack trace

* tweak

* more

* tidy up

* tidy up

* remove some junk

* tidy up

* move stuff

* remove stuff

* tweak

* tweak

* fix

* tweak

* tidy up

* tidy up

* tidy up

* tweak

* simplify

* tidy up

* simplify

* tidy up

* improve output

* delete comments

* more

* unused

* tidy up

* tidy up

* fix

* move some stuff

* tweak

* tidy up

* DRY

* synchronise

* DRY out

* tidy up

* tidy up

* tidy up

* add test that fails on main

* snapshot test

* changesets

* lint

* ugh
pull/12794/head
Rich Harris 8 months ago committed by GitHub
parent f13c722ce4
commit 57a7b0cd48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: invalidate signals following ++/-- inside each block

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: better code generation for destructuring assignments

@ -187,9 +187,7 @@ function get_delegated_event(event_name, handler, context) {
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal' ||
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
binding.kind === 'legacy_reactive_import') &&
binding.kind === 'normal') &&
binding.mutated))
) {
return unhoisted;

@ -1,5 +1,6 @@
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
@ -25,5 +26,13 @@ export function EachBlock(node, context) {
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
context.next();
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
});
context.visit(node.body);
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
}

@ -80,16 +80,8 @@ export function Identifier(node, context) {
binding.declaration_kind !== 'function') ||
binding.declaration_kind === 'import')
) {
if (binding.declaration_kind === 'import') {
if (
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template') &&
parent.type === 'MemberExpression'
) {
binding.kind = 'legacy_reactive_import';
}
} else if (
if (
binding.declaration_kind !== 'import' &&
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template')

@ -1,7 +1,8 @@
/** @import { Component, SvelteComponent, SvelteSelf } from '#compiler' */
/** @import { Comment, Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import { determine_slot } from '../../../../utils/slot.js';
import {
validate_attribute,
validate_attribute_name,
@ -60,9 +61,45 @@ export function visit_component(node, context) {
}
}
context.next({
...context.state,
parent_element: null,
component_slots: new Set()
});
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —
// then `let:` directives apply to other attributes, instead of just the
// top-level contents of the component. Yes, this is very weird.
const default_state = determine_slot(node)
? context.state
: { ...context.state, scope: node.metadata.scopes.default };
for (const attribute of node.attributes) {
context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state);
}
/** @type {Comment[]} */
let comments = [];
/** @type {Record<string, Fragment['nodes']>} */
const nodes = { default: [] };
for (const child of node.fragment.nodes) {
if (child.type === 'Comment') {
comments.push(child);
continue;
}
const slot_name = determine_slot(child) ?? 'default';
(nodes[slot_name] ??= []).push(...comments, child);
if (slot_name !== 'default') comments = [];
}
const component_slots = new Set();
for (const slot_name in nodes) {
const state = {
...context.state,
scope: node.metadata.scopes[slot_name],
parent_element: null,
component_slots
};
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}

@ -4,7 +4,6 @@
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js';
import { build_getter } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
@ -15,6 +14,7 @@ import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
import { BreakStatement } from './visitors/BreakStatement.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
@ -37,6 +37,7 @@ import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { Program } from './visitors/Program.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@ -58,7 +59,23 @@ import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
const visitors = {
_: set_scope,
_: function set_scope(node, { next, state }) {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal') {
delete transform[name];
}
}
next({ ...state, transform, scope });
} else {
next();
}
},
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
@ -66,6 +83,7 @@ const visitors = {
AwaitBlock,
BinaryExpression,
BindDirective,
BlockStatement,
BreakStatement,
CallExpression,
ClassBody,
@ -88,6 +106,7 @@ const visitors = {
LetDirective,
MemberExpression,
OnDirective,
Program,
RegularElement,
RenderTag,
SlotElement,
@ -123,6 +142,7 @@ export function client_component(analysis, options) {
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
context: {
@ -136,8 +156,7 @@ export function client_component(analysis, options) {
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
getters: {},
setters: {},
transform: {},
in_constructor: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
@ -155,6 +174,7 @@ export function client_component(analysis, options) {
const instance_state = {
...state,
transform: { ...state.transform },
scope: analysis.instance.scope,
scopes: analysis.instance.scopes,
is_instance: true
@ -167,21 +187,17 @@ export function client_component(analysis, options) {
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
{ ...state, scope: analysis.instance.scope, scopes: analysis.template.scopes },
{
...state,
transform: instance_state.transform,
scope: analysis.instance.scope,
scopes: analysis.template.scopes
},
visitors
)
);
// Very very dirty way of making import statements reactive in legacy mode if needed
if (!analysis.runes) {
for (const [name, binding] of analysis.module.scope.declarations) {
if (binding.kind === 'legacy_reactive_import') {
instance.body.unshift(
b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name))))
);
}
}
}
instance.body.unshift(...state.legacy_reactive_imports);
/** @type {ESTree.Statement[]} */
const store_setup = [];
@ -623,11 +639,9 @@ export function client_module(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
legacy_reactive_statements: new Map(),
public_state: new Map(),
private_state: new Map(),
getters: {},
setters: {},
transform: {},
in_constructor: false
};

@ -5,7 +5,8 @@ import type {
Identifier,
PrivateIdentifier,
Expression,
AssignmentExpression
AssignmentExpression,
UpdateExpression
} from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -22,19 +23,18 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
/**
* A map of `[name, node]` pairs, where `Identifier` nodes matching `name`
* will be replaced with `node` (e.g. `x` -> `$.get(x)`)
*/
readonly getters: Record<string, Expression | ((id: Identifier) => Expression)>;
/**
* Counterpart to `getters`
*/
readonly setters: Record<
readonly transform: Record<
string,
(assignment: AssignmentExpression, context: Context) => Expression
{
/** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
update?: (node: UpdateExpression) => Expression;
}
>;
}
@ -79,6 +79,12 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** The anchor node for the current context */
readonly node: Identifier;
/** Imports that should be re-evaluated in legacy mode following a mutation */
readonly legacy_reactive_imports: Statement[];
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
}
export interface StateField {

@ -1,59 +1,17 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BinaryOperator, Expression, FunctionDeclaration, FunctionExpression, Identifier, MemberExpression, Node, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { Binding, SvelteNode } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
import {
extract_identifiers,
extract_paths,
is_expression_async,
is_simple_expression,
object
} from '../../../utils/ast.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { is_ignored, dev } from '../../../state.js';
/**
* @template {ClientTransformState} State
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, State>} context
* @returns
*/
export function get_assignment_value(node, { state, visit }) {
if (node.left.type === 'Identifier') {
const operator = node.operator;
return operator === '='
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
build_getter(node.left, state),
/** @type {Expression} */ (visit(node.right))
);
} else if (
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
node.left.property.type === 'PrivateIdentifier' &&
state.private_state.has(node.left.property.name)
) {
const operator = node.operator;
return operator === '='
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
);
} else {
return /** @type {Expression} */ (visit(node.right));
}
}
import { dev } from '../../../state.js';
import { get_value } from './visitors/shared/declarations.js';
/**
* @param {Binding} binding
@ -73,392 +31,18 @@ export function is_state_source(binding, state) {
* @returns {Expression}
*/
export function build_getter(node, state) {
const binding = state.scope.get(node.name);
if (binding === null || node === binding.node) {
// No associated binding or the declaration itself which shouldn't be transformed
return node;
}
if (Object.hasOwn(state.transform, node.name)) {
const binding = state.scope.get(node.name);
if (Object.hasOwn(state.getters, node.name)) {
const getter = state.getters[node.name];
return typeof getter === 'function' ? getter(node) : getter;
}
if (binding.node.name === '$$props') {
// Special case for $$props which only exists in the old world
return b.id('$$sanitized_props');
}
if (binding.kind === 'store_sub') {
return b.call(node);
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (is_prop_source(binding, state)) {
return b.call(node);
// don't transform the declaration itself
if (node !== binding?.node) {
return state.transform[node.name].read(node);
}
if (binding.prop_alias) {
const key = b.key(binding.prop_alias);
return b.member(b.id('$$props'), key, key.type === 'Literal');
}
return b.member(b.id('$$props'), node);
}
if (binding.kind === 'legacy_reactive_import') {
return b.call('$$_import_' + node.name);
}
if (
is_state_source(binding, state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
return b.call('$.get', node);
}
return node;
}
/**
* @template {ClientTransformState} State
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, State>} context
* @param {() => any} fallback
* @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null`
* @param {{skip_proxy_and_freeze?: boolean}} [options]
* @returns {Expression}
*/
export function build_setter(node, context, fallback, prefix, options) {
const { state, visit } = context;
const assignee = node.left;
if (
assignee.type === 'ArrayPattern' ||
assignee.type === 'ObjectPattern' ||
assignee.type === 'RestElement'
) {
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
const tmp_id = context.state.scope.generate('tmp');
/** @type {AssignmentExpression[]} */
const original_assignments = [];
/** @type {Expression[]} */
const assignments = [];
const paths = extract_paths(assignee);
for (const path of paths) {
const value = path.expression?.(b.id(tmp_id));
const assignment = b.assignment('=', path.node, value);
original_assignments.push(assignment);
assignments.push(build_setter(assignment, context, () => assignment, prefix, options));
}
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
// No change to output -> nothing to transform -> we can keep the original assignment
return fallback();
}
const rhs_expression = /** @type {Expression} */ (visit(node.right));
const iife_is_async =
is_expression_async(rhs_expression) ||
assignments.some((assignment) => is_expression_async(assignment));
const iife = b.arrow(
[],
b.block([
b.const(tmp_id, rhs_expression),
b.stmt(b.sequence(assignments)),
// return because it could be used in a nested expression where the value is needed.
// example: { foo: ({ bar } = { bar: 1 })}
b.return(b.id(tmp_id))
])
);
if (iife_is_async) {
return b.await(b.call(b.async(iife)));
} else {
return b.call(iife);
}
}
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${assignee.type}`);
}
// Handle class private/public state assignment cases
if (assignee.type === 'MemberExpression') {
if (
assignee.object.type === 'ThisExpression' &&
assignee.property.type === 'PrivateIdentifier'
) {
const private_state = context.state.private_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
if (private_state !== undefined) {
if (state.in_constructor) {
// See if we should wrap value in $.proxy
if (
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id);
return assignment;
}
}
} else {
return b.call(
'$.set',
assignee,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id)
: value
);
}
}
} else if (
assignee.object.type === 'ThisExpression' &&
assignee.property.type === 'Identifier' &&
state.in_constructor
) {
const public_state = context.state.public_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy
if (
context.state.analysis.runes &&
public_state !== undefined &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, public_state.id);
return assignment;
}
}
}
}
const left = object(assignee);
if (left === null) {
return fallback();
}
const binding = state.scope.get(left.name);
if (!binding) return fallback();
if (Object.hasOwn(state.setters, left.name)) {
const setter = state.setters[left.name];
// @ts-expect-error
return setter(node, context);
}
if (binding.kind === 'legacy_reactive_import') {
return b.call(
'$$_import_' + binding.node.name,
b.assignment(
node.operator,
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
/**
* @param {any} serialized
* @returns
*/
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
}
if (binding.kind === 'derived') {
return maybe_skip_ownership_validation(fallback());
}
const is_store = binding.kind === 'store_sub';
const left_name = is_store ? left.name.slice(1) : left.name;
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
!is_store
) {
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
return fallback();
}
const value = get_assignment_value(node, context);
const serialize = () => {
if (left === node.left) {
const is_initial_proxy =
binding.initial !== null &&
should_proxy_or_freeze(/**@type {Expression}*/ (binding.initial), context.state.scope);
if ((binding.kind === 'prop' || binding.kind === 'bindable_prop') && !is_initial_proxy) {
return b.call(left, value);
} else if (is_store) {
return b.call('$.store_set', build_getter(b.id(left_name), state), value);
} else {
let call;
if (binding.kind === 'state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? build_proxy_reassignment(value, left_name)
: value
);
} else if (binding.kind === 'frozen_state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.freeze', value)
: value
);
} else if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
is_initial_proxy
) {
call = b.call(
left,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope) &&
binding.kind === 'bindable_prop'
? build_proxy_reassignment(value, left_name)
: value
);
} else {
call = b.call('$.set', b.id(left_name), value);
}
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores'));
} else {
return call;
}
}
} else {
if (is_store) {
// If we are assigning to a store property, we need to ensure we don't
// capture the read for the store as part of the member expression to
// keep consistency with how store $ shorthand reads work in Svelte 4.
/**
*
* @param {Expression | Pattern} node
* @returns {Expression}
*/
function visit_node(node) {
if (node.type === 'MemberExpression') {
return {
...node,
object: visit_node(/** @type {Expression} */ (node.object)),
property: /** @type {MemberExpression} */ (visit(node)).property
};
}
if (node.type === 'Identifier') {
const binding = state.scope.get(node.name);
if (binding !== null && binding.kind === 'store_sub') {
return b.call('$.untrack', b.thunk(/** @type {Expression} */ (visit(node))));
}
}
return /** @type {Expression} */ (visit(node));
}
return maybe_skip_ownership_validation(
b.call(
'$.store_mutate',
build_getter(b.id(left_name), state),
b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value),
b.call('$.untrack', b.id('$' + left_name))
)
);
} else if (
!state.analysis.runes ||
// this condition can go away once legacy mode is gone; only necessary for interop with legacy parent bindings
(binding.mutated && binding.kind === 'bindable_prop')
) {
if (binding.kind === 'bindable_prop') {
return maybe_skip_ownership_validation(
b.call(
left,
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value),
b.true
)
);
} else {
return maybe_skip_ownership_validation(
b.call(
'$.mutate',
b.id(left_name),
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
)
);
}
} else if (
node.right.type === 'Literal' &&
prefix != null &&
(node.operator === '+=' || node.operator === '-=')
) {
return maybe_skip_ownership_validation(
b.update(
node.operator === '+=' ? '++' : '--',
/** @type {Expression} */ (visit(node.left)),
prefix
)
);
} else {
return maybe_skip_ownership_validation(
b.assignment(
node.operator,
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
}
};
if (value.type === 'BinaryExpression' && /** @type {any} */ (value.operator) === '??') {
return b.logical('??', build_getter(b.id(left_name), state), serialize());
}
return serialize();
}
/**
* @param {Expression} value
* @param {PrivateIdentifier | string} proxy_reference
@ -507,7 +91,7 @@ function get_hoisted_params(node, context) {
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
const expression = context.state.getters[reference];
let expression = context.state.transform[reference]?.read(b.id(binding.node.name));
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
@ -706,6 +290,7 @@ export function with_loc(target, source) {
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
context.state.transform[node.name] = { read: get_value };
return { id: node, declarations: null };
}
@ -723,6 +308,8 @@ export function create_derived_block_argument(node, context) {
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);

@ -1,11 +1,129 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { Context } from '../types' */
import { build_setter } from '../utils.js';
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
return build_setter(node, context, context.next);
return visit_assignment_expression(node, context, build_assignment);
}
/**
* @param {AssignmentOperator} operator
* @param {Pattern} left
* @param {Expression} right
* @param {Context} context
* @returns {Expression | null}
*/
export function build_assignment(operator, left, right, context) {
// Handle class private/public state assignment cases
if (
context.state.analysis.runes &&
left.type === 'MemberExpression' &&
left.object.type === 'ThisExpression'
) {
if (left.property.type === 'PrivateIdentifier') {
const private_state = context.state.private_state.get(left.property.name);
if (private_state !== undefined) {
let transformed = false;
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
if (should_proxy_or_freeze(value, context.state.scope)) {
transformed = true;
value =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id);
}
if (!context.state.in_constructor) {
return b.call('$.set', left, value);
} else if (transformed) {
return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
}
}
} else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name);
if (public_state !== undefined && should_proxy_or_freeze(right, context.state.scope)) {
const value = /** @type {Expression} */ (context.visit(right));
return b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, public_state.id)
);
}
}
}
let object = left;
while (object.type === 'MemberExpression') {
// @ts-expect-error
object = object.object;
}
if (object.type !== 'Identifier') {
return null;
}
const binding = context.state.scope.get(object.name);
if (!binding) return null;
const transform = Object.hasOwn(context.state.transform, object.name)
? context.state.transform[object.name]
: null;
// reassignment
if (object === left && transform?.assign) {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
// special case — if an element binding, we know it's a primitive
const path = context.path.map((node) => node.type);
const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
if (
!is_primitive &&
binding.kind !== 'prop' &&
context.state.analysis.runes &&
should_proxy_or_freeze(value, context.state.scope)
) {
value =
binding.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, object.name);
}
return transform.assign(object, value);
}
/** @type {Expression} */
let mutation = b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
/** @type {Expression} */ (context.visit(right))
);
// mutation
if (transform?.mutate) {
mutation = transform.mutate(object, mutation);
}
return is_ignored(left, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(mutation))
: mutation;
}

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AwaitBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
@ -15,39 +15,29 @@ export function AwaitBlock(node, context) {
let catch_block;
if (node.then) {
const argument = node.value && create_derived_block_argument(node.value, context);
/** @type {Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {BlockStatement} */ (context.visit(node.then));
if (node.value) {
const argument = create_derived_block_argument(node.value, context);
if (argument) args.push(argument.id);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
const declarations = argument?.declarations ?? [];
const block = /** @type {BlockStatement} */ (context.visit(node.then));
then_block = b.arrow(args, block);
then_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
if (node.catch) {
const argument = node.error && create_derived_block_argument(node.error, context);
/** @type {Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {BlockStatement} */ (context.visit(node.catch));
if (node.error) {
const argument = create_derived_block_argument(node.error, context);
if (argument) args.push(argument.id);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
const declarations = argument?.declarations ?? [];
const block = /** @type {BlockStatement} */ (context.visit(node.catch));
catch_block = b.arrow(args, block);
catch_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
context.state.init.push(

@ -5,7 +5,6 @@ import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { binding_properties } from '../../../bindings.js';
import { build_setter } from '../utils.js';
import { build_attribute_value } from './shared/element.js';
import { build_bind_this, validate_binding } from './shared/utils.js';
@ -38,18 +37,10 @@ export function BindDirective(node, context) {
}
const getter = b.thunk(/** @type {Expression} */ (context.visit(expression)));
const assignment = b.assignment('=', expression, b.id('$$value'));
const setter = b.arrow(
[b.id('$$value')],
build_setter(
assignment,
context,
() => /** @type {Expression} */ (context.visit(assignment)),
null,
{
skip_proxy_and_freeze: true
}
)
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
);
/** @type {CallExpression} */

@ -0,0 +1,12 @@
/** @import { BlockStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {BlockStatement} node
* @param {ComponentContext} context
*/
export function BlockStatement(node, context) {
add_state_transformers(context);
context.next();
}

@ -5,6 +5,7 @@ import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {ConstTag} node
@ -24,7 +25,7 @@ export function ConstTag(node, context) {
)
);
context.state.getters[declaration.id.name] = b.call('$.get', declaration.id);
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
@ -35,15 +36,15 @@ export function ConstTag(node, context) {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
const getters = { ...context.state.getters };
const transform = { ...context.state.transform };
// Make all identifiers that are declared within the following computed regular
// variables, as they are not signals in that context yet
for (const node of identifiers) {
getters[node.name] = node;
delete transform[node.name];
}
const child_state = { ...context.state, getters };
const child_state = { ...context.state, transform };
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
@ -67,7 +68,9 @@ export function ConstTag(node, context) {
}
for (const node of identifiers) {
context.state.getters[node.name] = b.member(b.call('$.get', tmp), node);
context.state.transform[node.name] = {
read: (node) => b.member(b.call('$.get', tmp), node)
};
}
}
}

@ -1,6 +1,7 @@
/** @import { AssignmentExpression, BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { Binding, EachBlock } from '#compiler' */
/** @import { ComponentContext, Context } from '../types' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@ -12,7 +13,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_assignment_value, build_getter, build_setter, with_loc } from '../utils.js';
import { build_getter, with_loc } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {EachBlock} node
@ -20,7 +22,15 @@ import { get_assignment_value, build_getter, build_setter, with_loc } from '../u
*/
export function EachBlock(node, context) {
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
);
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
@ -110,54 +120,13 @@ export function EachBlock(node, context) {
const child_state = {
...context.state,
getters: { ...context.state.getters },
setters: { ...context.state.setters }
transform: { ...context.state.transform }
};
/** The state used when generating the key function, if necessary */
const key_state = {
...context.state,
getters: { ...context.state.getters }
};
/**
* @param {Pattern} expression_for_id
* @returns {(assignment: AssignmentExpression, context: Context) => Expression}
*/
const create_mutation = (expression_for_id) => {
return (assignment, context) => {
if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') {
// build_setter turns other patterns into IIFEs and separates the assignments
// into separate expressions, at which point this is called again with an identifier or member expression
return build_setter(assignment, context, () => assignment);
}
const left = object(assignment.left);
const value = get_assignment_value(assignment, context);
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (left === assignment.left) {
const assign = b.assignment('=', expression_for_id, value);
sequence.unshift(assign);
return b.sequence(sequence);
} else {
const original_left = /** @type {MemberExpression} */ (assignment.left);
const left = /** @type {Pattern} */ (context.visit(original_left));
const assign = b.assignment(assignment.operator, left, value);
sequence.unshift(assign);
return b.sequence(sequence);
}
};
transform: { ...context.state.transform }
};
// We need to generate a unique identifier in case there's a bind:group below
@ -170,37 +139,59 @@ export function EachBlock(node, context) {
const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc);
};
child_state.getters[item.name] = getter;
if (node.index) {
child_state.getters[node.index] = (id) => {
const index_with_loc = with_loc(index, id);
return (flags & EACH_INDEX_REACTIVE) === 0 ? index_with_loc : b.call('$.get', index_with_loc);
child_state.transform[node.index] = {
read: (id) => {
const index_with_loc = with_loc(index, id);
return (flags & EACH_INDEX_REACTIVE) === 0
? index_with_loc
: b.call('$.get', index_with_loc);
}
};
key_state.getters[node.index] = b.id(node.index);
delete key_state.transform[node.index];
}
/** @type {Statement[]} */
const declarations = [];
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
/** @type {Expression[]} */
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (node.context.type === 'Identifier') {
child_state.setters[node.context.name] = create_mutation(
b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
true
)
);
child_state.transform[node.context.name] = {
read: getter,
assign: (_, value) => {
const left = b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
true
);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => b.sequence([mutation, ...sequence])
};
key_state.getters[node.context.name] = node.context;
delete key_state.transform[node.context.name];
} else {
const unwrapped = getter(binding.node);
const paths = extract_paths(node.context);
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {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 {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
@ -208,19 +199,26 @@ export function EachBlock(node, context) {
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
const getter = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
child_state.getters[name] = getter;
child_state.setters[name] = create_mutation(
/** @type {Pattern} */ (path.update_expression(unwrapped))
);
const read = needs_derived ? get_value : b.call;
child_state.transform[name] = {
read,
assign: (node, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (node, mutation) => {
return b.sequence([mutation, ...sequence]);
}
};
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
declarations.push(b.stmt(getter));
declarations.push(b.stmt(read(b.id(name))));
}
key_state.getters[name] = path.node;
delete key_state.transform[name];
}
}

@ -66,7 +66,7 @@ export function Fragment(node, context) {
after_update: [],
template: [],
locations: [],
getters: { ...context.state.getters },
transform: { ...context.state.transform },
metadata: {
context: {
template_needs_import_node: false,

@ -34,7 +34,7 @@ export function LabeledStatement(node, context) {
const sequence = [];
for (const binding of reactive_statement.dependencies) {
if (binding.kind === 'normal') continue;
if (binding.kind === 'normal' && binding.declaration_kind !== 'import') continue;
const name = binding.node.name;
let serialized = build_getter(b.id(name), context.state);

@ -16,10 +16,9 @@ export function LetDirective(node, context) {
const bindings = context.state.scope.get_bindings(node);
for (const binding of bindings) {
context.state.getters[binding.node.name] = b.member(
b.call('$.get', b.id(name)),
b.id(binding.node.name)
);
context.state.transform[binding.node.name] = {
read: (node) => b.member(b.call('$.get', b.id(name)), node)
};
}
return b.const(
@ -42,8 +41,13 @@ export function LetDirective(node, context) {
)
);
} else {
const name = node.expression === null ? node.name : node.expression.name;
context.state.transform[name] = {
read: (node) => b.call('$.get', node)
};
return b.const(
node.expression === null ? node.name : node.expression.name,
name,
create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
);
}

@ -0,0 +1,123 @@
/** @import { Expression, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '../../../../utils/builders.js';
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {Program} _
* @param {ComponentContext} context
*/
export function Program(_, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
};
for (const [name, binding] of context.state.scope.declarations) {
if (binding.declaration_kind === 'import' && binding.mutated) {
const id = b.id('$$_import_' + name);
context.state.transform[name] = {
read: (_) => b.call(id),
mutate: (_, mutation) => b.call(id, mutation)
};
context.state.legacy_reactive_imports.push(
b.var(id, b.call('$.reactive_import', b.thunk(b.id(name))))
);
}
}
}
for (const [name, binding] of context.state.scope.declarations) {
if (binding.kind === 'store_sub') {
const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1))));
context.state.transform[name] = {
read: b.call,
assign: (_, value) => b.call('$.store_set', store, value),
mutate: (node, mutation) => {
// We need to untrack the store read, for consistency with Svelte 4
const untracked = b.call('$.untrack', node);
/**
*
* @param {Expression} n
* @returns {Expression}
*/
function replace(n) {
if (n.type === 'MemberExpression') {
return {
...n,
object: replace(/** @type {Expression} */ (n.object)),
property: n.property
};
}
return untracked;
}
return b.call(
'$.store_mutate',
store,
b.assignment(
mutation.operator,
/** @type {MemberExpression} */ (
replace(/** @type {MemberExpression} */ (mutation.left))
),
mutation.right
),
untracked
);
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre_store' : '$.update_store',
build_getter(b.id(name.slice(1)), context.state),
b.call(node.argument),
node.operator === '--' && b.literal(-1)
);
}
};
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (is_prop_source(binding, context.state)) {
context.state.transform[name] = {
read: b.call,
assign: (node, value) => b.call(node, value),
mutate: (node, value) => {
if (binding.kind === 'bindable_prop') {
// only necessary for interop with legacy parent bindings
return b.call(node, value, b.true);
}
return value;
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre_prop' : '$.update_prop',
node.argument,
node.operator === '--' && b.literal(-1)
);
}
};
} else if (binding.prop_alias) {
const key = b.key(binding.prop_alias);
context.state.transform[name] = {
read: (_) => b.member(b.id('$$props'), key, key.type === 'Literal')
};
} else {
context.state.transform[name] = {
read: (node) => b.member(b.id('$$props'), node)
};
}
}
}
add_state_transformers(context);
context.next();
}

@ -100,6 +100,13 @@ export function RegularElement(node, context) {
metadata.context.template_needs_import_node = true;
}
// visit let directives first, to set state
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
@ -136,8 +143,6 @@ export function RegularElement(node, context) {
class_directives.push(attribute);
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute));
const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective');
@ -145,7 +150,7 @@ export function RegularElement(node, context) {
context.state.after_update.push(
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler)
);
} else {
} else if (attribute.type !== 'LetDirective') {
if (attribute.type === 'BindDirective') {
if (attribute.name === 'group' || attribute.name === 'checked') {
needs_special_value_handling = true;

@ -4,6 +4,7 @@
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_value } from './shared/declarations.js';
/**
* @param {SnippetBlock} node
@ -20,8 +21,8 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
const getters = { ...context.state.getters };
const child_state = { ...context.state, getters };
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
for (let i = 0; i < node.parameters.length; i++) {
const argument = node.parameters[i];
@ -35,7 +36,8 @@ export function SnippetBlock(node, context) {
right: b.id('$.noop')
});
getters[argument.name] = b.call(argument);
transform[argument.name] = { read: b.call };
continue;
}
@ -53,12 +55,14 @@ export function SnippetBlock(node, context) {
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
getters[name] = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
transform[name] = {
read: needs_derived ? get_value : b.call
};
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
declarations.push(b.stmt(getters[name]));
declarations.push(b.stmt(transform[name].read(b.id(name))));
}
}
}

@ -1,8 +1,8 @@
/** @import { Expression, Pattern, Statement, UpdateExpression } from 'estree' */
/** @import { Expression, Node, Pattern, Statement, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_getter, build_setter } from '../utils.js';
/**
* @param {UpdateExpression} node
@ -11,45 +11,6 @@ import { build_getter, build_setter } from '../utils.js';
export function UpdateExpression(node, context) {
const argument = node.argument;
if (argument.type === 'Identifier') {
const binding = context.state.scope.get(argument.name);
const is_store = binding?.kind === 'store_sub';
const name = is_store ? argument.name.slice(1) : argument.name;
// use runtime functions for smaller output
if (
binding?.kind === 'state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||
binding?.kind === 'bindable_prop' ||
is_store
) {
/** @type {Expression[]} */
const args = [];
let fn = '$.update';
if (node.prefix) fn += '_pre';
if (is_store) {
fn += '_store';
args.push(build_getter(b.id(name), context.state), b.call('$' + name));
} else {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop';
args.push(b.id(name));
}
if (node.operator === '--') {
args.push(b.literal(-1));
}
return b.call(fn, ...args);
}
return context.next();
}
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
@ -68,43 +29,41 @@ export function UpdateExpression(node, context) {
return b.call(fn, ...args);
}
/** @param {any} serialized */
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
if (argument.type !== 'Identifier' && argument.type !== 'MemberExpression') {
throw new Error('An impossible state was reached');
}
// turn it into an IIFE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; })
const assignment = b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
);
const left = object(argument);
if (left === null) return context.next();
const serialized_assignment = build_setter(assignment, context, () => assignment, node.prefix);
if (left === argument) {
const transform = context.state.transform;
const update = transform[left.name]?.update;
const value = /** @type {Expression} */ (context.visit(argument));
if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression
return maybe_skip_ownership_validation(context.next());
if (update && Object.hasOwn(transform, left.name)) {
return update(node);
}
}
if (context.state.analysis.runes) {
return maybe_skip_ownership_validation(serialized_assignment);
}
const assignment = /** @type {Expression} */ (
context.visit(
b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
)
)
);
/** @type {Statement[]} */
let statements;
if (node.prefix) {
statements = [b.stmt(serialized_assignment), b.return(value)];
} else {
const tmp_id = context.state.scope.generate('$$value');
statements = [b.const(tmp_id, value), b.stmt(serialized_assignment), b.return(b.id(tmp_id))];
}
const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type === 'ExpressionStatement'; // TODO and possibly others, but not e.g. the `test` of a WhileStatement
const update =
node.prefix || is_standalone
? assignment
: b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1));
return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements))));
return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(update))
: update;
}

@ -1,14 +1,14 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */
/** @import { Attribute, Component, SvelteComponent, SvelteSelf, TemplateNode, Text } from '#compiler' */
/** @import { Component, SvelteComponent, SvelteSelf, TemplateNode } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { is_element_node } from '../../../../nodes.js';
import { create_derived, build_setter } from '../../utils.js';
import { create_derived } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
@ -24,6 +24,15 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {ExpressionStatement[]} */
const lets = [];
/** @type {Record<string, typeof context.state>} */
const states = {
default: {
...context.state,
scope: node.metadata.scopes.default,
transform: { ...context.state.transform }
}
};
/** @type {Record<string, TemplateNode[]>} */
const children = {};
@ -45,7 +54,7 @@ export function build_component(node, component_name, context, anchor = context.
* If this component has a slot property, it is a named slot within another component. In this case
* the slot scope applies to the component itself, too, and not just its children.
*/
let slot_scope_applies_to_itself = false;
let slot_scope_applies_to_itself = !!determine_slot(node);
/**
* Components may have a children prop and also have child nodes. In this case, we assume
@ -66,9 +75,20 @@ export function build_component(node, component_name, context, anchor = context.
props_and_spreads.push(props);
}
}
if (slot_scope_applies_to_itself) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
if (!slot_scope_applies_to_itself) {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default)));
}
} else if (attribute.type === 'OnDirective') {
if (!attribute.expression) {
context.state.analysis.needs_props = true;
@ -174,9 +194,7 @@ export function build_component(node, component_name, context, anchor = context.
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [
b.stmt(build_setter(assignment, context, () => context.visit(assignment)))
])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
);
}
}
@ -214,19 +232,7 @@ export function build_component(node, component_name, context, anchor = context.
continue;
}
let slot_name = 'default';
if (is_element_node(child)) {
const attribute = /** @type {Attribute | undefined} */ (
child.attributes.find(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
);
if (attribute !== undefined) {
slot_name = /** @type {Text[]} */ (attribute.value)[0].data;
}
}
let slot_name = determine_slot(child) ?? 'default';
(children[slot_name] ||= []).push(child);
}
@ -242,12 +248,15 @@ export function build_component(node, component_name, context, anchor = context.
// @ts-expect-error
nodes: children[slot_name]
},
{
...context.state,
scope:
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
context.state.scope
}
slot_name === 'default'
? slot_scope_applies_to_itself
? context.state
: states.default
: {
...context.state,
scope: node.metadata.scopes[slot_name],
transform: { ...context.state.transform }
}
)
);

@ -0,0 +1,53 @@
/** @import { BinaryOperator, Expression, Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */
import { build_proxy_reassignment, is_state_source, should_proxy_or_freeze } from '../../utils.js';
import * as b from '../../../../../utils/builders.js';
/**
* Turns `foo` into `$.get(foo)`
* @param {Identifier} node
*/
export function get_value(node) {
return b.call('$.get', node);
}
/**
*
* @param {Context | ComponentContext} context
*/
export function add_state_transformers(context) {
for (const [name, binding] of context.state.scope.declarations) {
if (
is_state_source(binding, context.state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
context.state.transform[name] = {
read: get_value,
assign: (node, value) => {
let call = b.call('$.set', node, value);
if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') {
call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores'));
}
return call;
},
mutate: (node, mutation) => {
if (context.state.analysis.runes) {
return mutation;
}
return b.call('$.mutate', node, mutation);
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre' : '$.update',
node.argument,
node.operator === '--' && b.literal(-1)
);
}
};
}
}
}

@ -101,7 +101,7 @@ export function build_attribute_value(value, context) {
}
return {
has_state: chunk.metadata.expression.has_call,
has_state: chunk.metadata.expression.has_state,
has_call: chunk.metadata.expression.has_call,
value: /** @type {Expression} */ (context.visit(chunk.expression))
};

@ -145,8 +145,10 @@ export function build_bind_this(expression, value, { state, visit }) {
/** @type {Expression[]} */
const values = [];
/** @type {typeof state.getters} */
const getters = {};
/** @type {string[]} */
const seen = [];
const transform = { ...state.transform };
// Pass in each context variables to the get/set functions, so that we can null out old values on teardown.
// Note that we only do this for each context variables, the consequence is that the value might be stale in
@ -154,7 +156,8 @@ export function build_bind_this(expression, value, { state, visit }) {
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
walk(expression, null, {
Identifier(node, { path }) {
if (Object.hasOwn(getters, node.name)) return;
if (seen.includes(node.name)) return;
seen.push(node.name);
const parent = /** @type {Expression} */ (path.at(-1));
if (!is_reference(node, parent)) return;
@ -166,14 +169,21 @@ export function build_bind_this(expression, value, { state, visit }) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
values.push(/** @type {Expression} */ (visit(node)));
getters[node.name] = node;
if (transform[node.name]) {
transform[node.name] = {
...transform[node.name],
read: (node) => node
};
}
break;
}
}
}
});
const child_state = { ...state, getters: { ...state.getters, ...getters } };
const child_state = { ...state, transform };
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (

@ -1,59 +1,16 @@
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import { build_getter } from './shared/utils.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type.endsWith('Statement');
if (
node.left.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' ||
node.left.type === 'RestElement'
) {
const value = /** @type {Expression} */ (context.visit(node.right));
const should_cache = value.type !== 'Identifier';
const rhs = should_cache ? b.id('$$value') : value;
let changed = false;
const assignments = extract_paths(node.left).map((path) => {
const value = path.expression?.(rhs);
let assignment = build_assignment('=', path.node, value, context);
if (assignment !== null) changed = true;
return assignment ?? b.assignment('=', path.node, value);
});
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
return context.next();
}
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
if (should_cache) {
// the right hand side is a complex expression, wrap in an IIFE to cache it
return b.call(b.arrow([rhs], sequence), value);
}
return sequence;
}
return build_assignment(node.operator, node.left, node.right, context) || context.next();
return visit_assignment_expression(node, context, build_assignment);
}
/**
@ -83,16 +40,9 @@ function build_assignment(operator, left, right, context) {
}
if (object === left) {
let value = /** @type {Expression} */ (context.visit(right));
if (operator !== '=') {
// turn `x += 1` into `x = x + 1`
value = b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
build_getter(left, context.state),
value
);
}
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
return b.call('$.store_set', b.id(name), value);
}

@ -20,6 +20,15 @@ export function build_inline_component(node, expression, context) {
/** @type {Record<string, LetDirective[]>} */
const lets = { default: [] };
/**
* Children in the default slot are evaluated in the component scope,
* children in named slots are evaluated in the parent scope
*/
const child_state = {
...context.state,
scope: node.metadata.scopes.default
};
/** @type {Record<string, TemplateNode[]>} */
const children = {};
@ -144,12 +153,12 @@ export function build_inline_component(node, expression, context) {
// @ts-expect-error
nodes: children[slot_name]
},
{
...context.state,
scope:
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
context.state.scope
}
slot_name === 'default'
? child_state
: {
...context.state,
scope: node.metadata.scopes[slot_name]
}
)
);

@ -0,0 +1,77 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
/**
* @template {ClientContext | ServerContext} Context
* @param {AssignmentExpression} node
* @param {Context} context
* @param {(operator: AssignmentOperator, left: Pattern, right: Expression, context: Context) => Expression | null} build_assignment
* @returns
*/
export function visit_assignment_expression(node, context, build_assignment) {
if (
node.left.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' ||
node.left.type === 'RestElement'
) {
const value = /** @type {Expression} */ (context.visit(node.right));
const should_cache = value.type !== 'Identifier';
const rhs = should_cache ? b.id('$$value') : value;
let changed = false;
const assignments = extract_paths(node.left).map((path) => {
const value = path.expression?.(rhs);
let assignment = build_assignment('=', path.node, value, context);
if (assignment !== null) changed = true;
return (
assignment ??
b.assignment(
'=',
/** @type {Pattern} */ (context.visit(path.node)),
/** @type {Expression} */ (context.visit(value))
)
);
});
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
return context.next();
}
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
if (should_cache) {
// the right hand side is a complex expression, wrap in an IIFE to cache it
const iife = b.arrow([rhs], sequence);
const iife_is_async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
}
return sequence;
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${node.left.type}`);
}
return (
build_assignment(node.operator, node.left, node.right, context) ??
/** @type {Expression} */ (context.next())
);
}

@ -425,7 +425,7 @@ export function transform_inspect_rune(node, context) {
const { state, visit } = context;
const as_fn = state.options.generate === 'client';
if (!dev) return b.unary('void', b.literal(0));
if (!dev) return b.empty;
if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;

@ -1,9 +1,8 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AnimateDirective, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */
/** @import { AnimateDirective, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { is_element_node } from './nodes.js';
import * as b from '../utils/builders.js';
import * as e from '../errors.js';
import {
@ -13,6 +12,7 @@ import {
unwrap_pattern
} from '../utils/ast.js';
import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
export class Scope {
/** @type {ScopeRoot} */
@ -290,52 +290,47 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @type {Visitor<ElementLike, State, SvelteNode>}
*/
const SvelteFragment = (node, { state, next }) => {
const [scope] = analyze_let_directives(node, state.scope);
const scope = state.scope.child();
scopes.set(node, scope);
next({ scope });
};
/**
* @param {ElementLike} node
* @param {Scope} parent
* @type {Visitor<Component | SvelteComponent | SvelteSelf, State, SvelteNode>}
*/
function analyze_let_directives(node, parent) {
const scope = parent.child();
let is_default_slot = true;
const Component = (node, context) => {
node.metadata.scopes = {
default: context.state.scope.child()
};
const default_state = determine_slot(node)
? context.state
: { scope: node.metadata.scopes.default };
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
/** @type {Binding[]} */
const bindings = [];
scope.declarators.set(attribute, bindings);
context.visit(attribute, default_state);
} else {
context.visit(attribute);
}
}
// attach the scope to the directive itself, as well as the
// contents to which it applies
scopes.set(attribute, scope);
for (const child of node.fragment.nodes) {
let state = default_state;
if (attribute.expression) {
for (const id of extract_identifiers_from_destructuring(attribute.expression)) {
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else {
/** @type {Identifier} */
const id = {
name: attribute.name,
type: 'Identifier',
start: attribute.start,
end: attribute.end
};
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
is_default_slot = false;
const slot_name = determine_slot(child);
if (slot_name !== null) {
node.metadata.scopes[slot_name] = context.state.scope.child();
state = {
scope: node.metadata.scopes[slot_name]
};
}
}
return /** @type {const} */ ([scope, is_default_slot]);
}
context.visit(child, state);
}
};
/**
* @type {Visitor<AnimateDirective | TransitionDirective | UseDirective, State, SvelteNode>}
@ -384,48 +379,37 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SvelteElement: SvelteFragment,
RegularElement: SvelteFragment,
Component(node, { state, visit, path }) {
state.scope.reference(b.id(node.name), path);
LetDirective(node, context) {
const scope = context.state.scope;
// let:x is super weird:
// - for the default slot, its scope only applies to children that are not slots themselves
// - for named slots, its scope applies to the component itself, too
const [scope, is_default_slot] = analyze_let_directives(node, state.scope);
if (is_default_slot) {
for (const attribute of node.attributes) {
visit(attribute);
}
} else {
scopes.set(node, scope);
/** @type {Binding[]} */
const bindings = [];
scope.declarators.set(node, bindings);
for (const attribute of node.attributes) {
visit(attribute, { ...state, scope });
if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else {
/** @type {Identifier} */
const id = {
name: node.name,
type: 'Identifier',
start: node.start,
end: node.end
};
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
},
for (const child of node.fragment.nodes) {
if (
is_element_node(child) &&
child.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
) {
// <div slot="..."> inherits the scope above the component unless the component is a named slot itself, because slots are hella weird
scopes.set(child, is_default_slot ? state.scope : scope);
visit(child, { scope: is_default_slot ? state.scope : scope });
} else {
if (child.type === 'ExpressionTag') {
// expression tag is a special case — we don't visit it directly, but via process_children,
// so we need to set the scope on the expression rather than the tag itself
scopes.set(child.expression, scope);
} else {
scopes.set(child, scope);
}
visit(child, { scope });
}
}
Component: (node, context) => {
context.state.scope.reference(b.id(node.name), context.path);
Component(node, context);
},
SvelteSelf: Component,
SvelteComponent: Component,
// updates
AssignmentExpression(node, { state, next }) {
@ -532,7 +516,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
references_within.add(id);
}
}
scopes.set(node.expression, state.scope);
// context and children are a new scope
const scope = state.scope.child();

@ -271,7 +271,6 @@ export interface Binding {
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `legacy_reactive_import`: An imported binding that is mutated inside the component
*/
kind:
| 'normal'
@ -284,8 +283,7 @@ export interface Binding {
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'legacy_reactive_import';
| 'legacy_reactive';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.

@ -16,6 +16,7 @@ import type {
ChainExpression,
SimpleCallExpression
} from 'estree';
import type { Scope } from '../phases/scope';
export interface BaseNode {
type: string;
@ -275,6 +276,7 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
scopes: Record<string, Scope>;
dynamic: boolean;
};
}
@ -311,6 +313,9 @@ export interface SvelteComponent extends BaseElement {
type: 'SvelteComponent';
name: 'svelte:component';
expression: Expression;
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteDocument extends BaseElement {
@ -356,6 +361,9 @@ export interface SvelteOptionsRaw extends BaseElement {
export interface SvelteSelf extends BaseElement {
type: 'SvelteSelf';
name: 'svelte:self';
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteWindow extends BaseElement {

@ -563,3 +563,15 @@ export function build_fallback(expression, fallback) {
? b.await(b.call('$.fallback', expression, b.thunk(fallback, true), b.true))
: b.call('$.fallback', expression, b.thunk(fallback), b.true);
}
/**
* @param {ESTree.AssignmentOperator} operator
* @param {ESTree.Identifier | ESTree.MemberExpression} left
* @param {ESTree.Expression} right
*/
export function build_assignment_value(operator, left, right) {
return operator === '='
? right
: // turn something like x += 1 into x = x + 1
b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}

@ -0,0 +1,20 @@
/** @import { SvelteNode } from '#compiler' */
import { is_element_node } from '../phases/nodes.js';
import { is_text_attribute } from './ast.js';
/**
* @param {SvelteNode} node
*/
export function determine_slot(node) {
if (!is_element_node(node)) return null;
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') continue;
if (attribute.name !== 'slot') continue;
if (!is_text_attribute(attribute)) continue;
return /** @type {string} */ (attribute.value[0].data);
}
return null;
}

@ -0,0 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<button>1</button>
<button>2</button>
<button>3</button>
<p>1, 2, 3</p>
`,
test({ assert, target }) {
let buttons = target.querySelectorAll('button');
flushSync(() => buttons[2].click());
assert.htmlEqual(
target.innerHTML,
`
<button>1</button>
<button>2</button>
<button>4</button>
<p>1, 2, 4</p>
`
);
}
});

@ -0,0 +1,9 @@
<script>
let arr = [1, 2, 3];
</script>
{#each arr as n}
<button on:click={() => n++}>{n}</button>
{/each}
<p>{arr.join(', ')}</p>

@ -0,0 +1,12 @@
/* index.svelte.js generated by Svelte VERSION */
import * as $ from "svelte/internal/client";
let a = $.source(1);
let b = $.source(2);
export function update(array) {
(
$.set(a, $.proxy(array[0])),
$.set(b, $.proxy(array[1]))
);
}

@ -0,0 +1,9 @@
/* index.svelte.js generated by Svelte VERSION */
import * as $ from "svelte/internal/server";
let a = 1;
let b = 2;
export function update(array) {
[a, b] = array;
}

@ -0,0 +1,6 @@
let a = $state(1);
let b = $state(2);
export function update(array) {
[a, b] = array;
}

@ -920,7 +920,6 @@ declare module 'svelte/compiler' {
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `legacy_reactive_import`: An imported binding that is mutated inside the component
*/
kind:
| 'normal'
@ -933,8 +932,7 @@ declare module 'svelte/compiler' {
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'legacy_reactive_import';
| 'legacy_reactive';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.
@ -1733,6 +1731,7 @@ declare module 'svelte/compiler' {
interface Component extends BaseElement {
type: 'Component';
metadata: {
scopes: Record<string, Scope>;
dynamic: boolean;
};
}
@ -1769,6 +1768,9 @@ declare module 'svelte/compiler' {
type: 'SvelteComponent';
name: 'svelte:component';
expression: Expression;
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteDocument extends BaseElement {
@ -1814,6 +1816,9 @@ declare module 'svelte/compiler' {
interface SvelteSelf extends BaseElement {
type: 'SvelteSelf';
name: 'svelte:self';
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteWindow extends BaseElement {

Loading…
Cancel
Save