chore: client transform visitors refactor (#12683)

* start refactoring client transform visitor code

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* more

* tweak

* painful

* more

* simplify

* more

* more

* more

* more

* more

* tidy up

* changeset
pull/12689/head
Rich Harris 1 year ago committed by GitHub
parent 9eca3d0736
commit 038754bfc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: internal refactoring of client transform visitors

@ -5,51 +5,122 @@
import { walk } from 'zimmerframe';
import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js';
import { template_visitors } from './visitors/template.js';
import { global_visitors } from './visitors/global.js';
import { javascript_visitors } from './visitors/javascript.js';
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
import { serialize_get_binding } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
import { AnimateDirective } from './visitors/AnimateDirective.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
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 { BreakStatement } from './visitors/BreakStatement.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Comment } from './visitors/Comment.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { FunctionDeclaration } from './visitors/FunctionDeclaration.js';
import { FunctionExpression } from './visitors/FunctionExpression.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { ImportDeclaration } from './visitors/ImportDeclaration.js';
import { KeyBlock } from './visitors/KeyBlock.js';
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 { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TitleElement } from './visitors/TitleElement.js';
import { TransitionDirective } from './visitors/TransitionDirective.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { UseDirective } from './visitors/UseDirective.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
const visitors = {
_: set_scope,
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
Attribute,
AwaitBlock,
BinaryExpression,
BindDirective,
BreakStatement,
CallExpression,
ClassBody,
Comment,
Component,
ConstTag,
DebugTag,
EachBlock,
ExportNamedDeclaration,
ExpressionStatement,
Fragment,
FunctionDeclaration,
FunctionExpression,
HtmlTag,
Identifier,
IfBlock,
ImportDeclaration,
KeyBlock,
LabeledStatement,
LetDirective,
MemberExpression,
OnDirective,
RegularElement,
RenderTag,
SlotElement,
SnippetBlock,
SpreadAttribute,
SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteHead,
SvelteSelf,
SvelteWindow,
TitleElement,
TransitionDirective,
UpdateExpression,
UseDirective,
VariableDeclaration
};
/**
* This function ensures visitor sets don't accidentally clobber each other
* @param {...Visitors} array
* @returns {Visitors}
*/
function combine_visitors(...array) {
/** @type {Record<string, any>} */
const visitors = {};
for (const member of array) {
for (const key in member) {
if (visitors[key]) {
throw new Error(`Duplicate visitor: ${key}`);
}
// @ts-ignore
visitors[key] = member[key];
}
}
return visitors;
}
/**
* @param {string} source
* @param {ComponentAnalysis} analysis
* @param {ValidatedCompileOptions} options
* @returns {ESTree.Program}
*/
export function client_component(source, analysis, options) {
export function client_component(analysis, options) {
/** @type {ComponentClientTransformState} */
const state = {
analysis,
options,
scope: analysis.module.scope,
scopes: analysis.template.scopes,
scopes: analysis.module.scopes,
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_statements: new Map(),
@ -78,57 +149,25 @@ export function client_component(source, analysis, options) {
};
const module = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.module.ast),
state,
combine_visitors(
set_scope(analysis.module.scopes),
global_visitors,
// @ts-expect-error TODO
javascript_visitors,
analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy
)
)
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors)
);
const instance_state = { ...state, scope: analysis.instance.scope };
const instance_state = {
...state,
scope: analysis.instance.scope,
scopes: analysis.instance.scopes,
is_instance: true
};
const instance = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.instance.ast),
instance_state,
combine_visitors(
set_scope(analysis.instance.scopes),
global_visitors,
// @ts-expect-error TODO
javascript_visitors,
analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy,
{
ImportDeclaration(node) {
state.hoisted.push(node);
return b.empty;
},
ExportNamedDeclaration(node, context) {
if (node.declaration) {
return context.visit(node.declaration);
}
return b.empty;
}
}
)
)
walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors)
);
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
{ ...state, scope: analysis.instance.scope },
combine_visitors(
set_scope(analysis.template.scopes),
global_visitors,
// @ts-expect-error TODO
template_visitors
)
{ ...state, scope: analysis.instance.scope, scopes: analysis.template.scopes },
visitors
)
);
@ -589,17 +628,7 @@ export function client_module(analysis, options) {
};
const module = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.module.ast),
state,
combine_visitors(
set_scope(analysis.module.scopes),
global_visitors,
// @ts-expect-error
javascript_visitors,
javascript_visitors_runes
)
)
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors)
);
return {

@ -35,6 +35,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
readonly options: ValidatedCompileOptions;
readonly hoisted: Array<Statement | ModuleDeclaration>;
readonly events: Set<string>;
readonly is_instance: boolean;
/** Stuff that happens before the render effect(s) */
readonly before_init: Statement[];
@ -78,7 +79,7 @@ export interface StateField {
}
export type Context = import('zimmerframe').Context<SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, ClientTransformState>;
export type Visitors = import('zimmerframe').Visitors<SvelteNode, any>;
export type ComponentContext = import('zimmerframe').Context<
SvelteNode,

@ -476,37 +476,6 @@ export function serialize_proxy_reassignment(value, proxy_reference) {
: b.call('$.proxy', value);
}
/**
* @param {ArrowFunctionExpression | FunctionExpression} node
* @param {ComponentContext} context
*/
export const function_visitor = (node, context) => {
const metadata = node.metadata;
let state = context.state;
if (node.type === 'FunctionExpression') {
const parent = /** @type {Node} */ (context.path.at(-1));
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
state = { ...context.state, in_constructor };
} else {
state = { ...context.state, in_constructor: false };
}
if (metadata?.hoistable === true) {
const params = serialize_hoistable_params(node, context);
return /** @type {FunctionExpression} */ ({
...node,
params,
body: context.visit(node.body, state)
});
}
context.next(state);
};
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context

@ -0,0 +1,28 @@
/** @import { Expression } from 'estree' */
/** @import { AnimateDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { parse_directive_name } from './shared/utils.js';
/**
* @param {AnimateDirective} node
* @param {ComponentContext} context
*/
export function AnimateDirective(node, context) {
const expression =
node.expression === null
? b.literal(null)
: b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
// in after_update to ensure it always happens after bind:this
context.state.after_update.push(
b.stmt(
b.call(
'$.animation',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))),
expression
)
)
);
}

@ -0,0 +1,11 @@
/** @import { ArrowFunctionExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {ArrowFunctionExpression} node
* @param {ComponentContext} context
*/
export function ArrowFunctionExpression(node, context) {
return visit_function(node, context);
}

@ -0,0 +1,11 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { Context } from '../types' */
import { serialize_set_binding } from '../utils.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
return serialize_set_binding(node, context, context.next);
}

@ -0,0 +1,14 @@
/** @import { Attribute } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_event_attribute } from '../../../../utils/ast.js';
import { serialize_event_attribute } from './shared/element.js';
/**
* @param {Attribute} node
* @param {ComponentContext} context
*/
export function Attribute(node, context) {
if (is_event_attribute(node)) {
serialize_event_attribute(node, context);
}
}

@ -0,0 +1,67 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { AwaitBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { create_derived_block_argument } from '../utils.js';
/**
* @param {AwaitBlock} node
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push('<!>');
let then_block;
let catch_block;
if (node.then) {
/** @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);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
then_block = b.arrow(args, block);
}
if (node.catch) {
/** @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);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
catch_block = b.arrow(args, block);
}
context.state.init.push(
b.stmt(
b.call(
'$.await',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
node.pending
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending)))
: b.literal(null),
then_block,
catch_block
)
)
);
}

@ -0,0 +1,34 @@
/** @import { Expression, BinaryExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {BinaryExpression} node
* @param {ComponentContext} context
*/
export function BinaryExpression(node, context) {
if (dev) {
const operator = node.operator;
if (operator === '===' || operator === '!==') {
return b.call(
'$.strict_equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
operator === '!==' && b.literal(false)
);
}
if (operator === '==' || operator === '!=') {
return b.call(
'$.equals',
/** @type {Expression} */ (context.visit(node.left)),
/** @type {Expression} */ (context.visit(node.right)),
operator === '!=' && b.literal(false)
);
}
}
context.next();
}

@ -0,0 +1,250 @@
/** @import { CallExpression, Expression, MemberExpression } from 'estree' */
/** @import { Attribute, BindDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
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 { serialize_set_binding } from '../utils.js';
import { serialize_attribute_value } from './shared/element.js';
import { serialize_bind_this, serialize_validate_binding } from './shared/utils.js';
/**
* @param {BindDirective} node
* @param {ComponentContext} context
*/
export function BindDirective(node, context) {
const expression = node.expression;
const property = binding_properties[node.name];
const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1));
if (
dev &&
context.state.analysis.runes &&
expression.type === 'MemberExpression' &&
(node.name !== 'this' ||
context.path.some(
({ type }) =>
type === 'IfBlock' || type === 'EachBlock' || type === 'AwaitBlock' || type === 'KeyBlock'
)) &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(
serialize_validate_binding(
context.state,
node,
/**@type {MemberExpression} */ (context.visit(expression))
)
);
}
const getter = b.thunk(/** @type {Expression} */ (context.visit(expression)));
const assignment = b.assignment('=', expression, b.id('$$value'));
const setter = b.arrow(
[b.id('$$value')],
serialize_set_binding(
assignment,
context,
() => /** @type {Expression} */ (context.visit(assignment)),
null,
{
skip_proxy_and_freeze: true
}
)
);
/** @type {CallExpression} */
let call;
if (property?.event) {
call = b.call(
'$.bind_property',
b.literal(node.name),
b.literal(property.event),
context.state.node,
setter,
property.bidirectional && getter
);
} else {
// special cases
switch (node.name) {
// window
case 'online':
call = b.call(`$.bind_online`, setter);
break;
case 'scrollX':
case 'scrollY':
call = b.call(
'$.bind_window_scroll',
b.literal(node.name === 'scrollX' ? 'x' : 'y'),
getter,
setter
);
break;
case 'innerWidth':
case 'innerHeight':
case 'outerWidth':
case 'outerHeight':
call = b.call('$.bind_window_size', b.literal(node.name), setter);
break;
// document
case 'activeElement':
call = b.call('$.bind_active_element', setter);
break;
// media
case 'muted':
call = b.call(`$.bind_muted`, context.state.node, getter, setter);
break;
case 'paused':
call = b.call(`$.bind_paused`, context.state.node, getter, setter);
break;
case 'volume':
call = b.call(`$.bind_volume`, context.state.node, getter, setter);
break;
case 'playbackRate':
call = b.call(`$.bind_playback_rate`, context.state.node, getter, setter);
break;
case 'currentTime':
call = b.call(`$.bind_current_time`, context.state.node, getter, setter);
break;
case 'buffered':
call = b.call(`$.bind_buffered`, context.state.node, setter);
break;
case 'played':
call = b.call(`$.bind_played`, context.state.node, setter);
break;
case 'seekable':
call = b.call(`$.bind_seekable`, context.state.node, setter);
break;
case 'seeking':
call = b.call(`$.bind_seeking`, context.state.node, setter);
break;
case 'ended':
call = b.call(`$.bind_ended`, context.state.node, setter);
break;
case 'readyState':
call = b.call(`$.bind_ready_state`, context.state.node, setter);
break;
// dimensions
case 'contentRect':
case 'contentBoxSize':
case 'borderBoxSize':
case 'devicePixelContentBoxSize':
call = b.call('$.bind_resize_observer', context.state.node, b.literal(node.name), setter);
break;
case 'clientWidth':
case 'clientHeight':
case 'offsetWidth':
case 'offsetHeight':
call = b.call('$.bind_element_size', context.state.node, b.literal(node.name), setter);
break;
// various
case 'value': {
if (parent?.type === 'RegularElement' && parent.name === 'select') {
call = b.call(`$.bind_select_value`, context.state.node, getter, setter);
} else {
call = b.call(`$.bind_value`, context.state.node, getter, setter);
}
break;
}
case 'files':
call = b.call(`$.bind_files`, context.state.node, getter, setter);
break;
case 'this':
call = serialize_bind_this(expression, context.state.node, context);
break;
case 'textContent':
case 'innerHTML':
case 'innerText':
call = b.call(
'$.bind_content_editable',
b.literal(node.name),
context.state.node,
getter,
setter
);
break;
// checkbox/radio
case 'checked':
call = b.call(`$.bind_checked`, context.state.node, getter, setter);
break;
case 'focused':
call = b.call(`$.bind_focused`, context.state.node, setter);
break;
case 'group': {
const indexes = node.metadata.parent_each_blocks.map((each) => {
// if we have a keyed block with an index, the index is wrapped in a source
return each.metadata.keyed && each.index
? b.call('$.get', each.metadata.index)
: each.metadata.index;
});
// We need to additionally invoke the value attribute signal to register it as a dependency,
// so that when the value is updated, the group binding is updated
let group_getter = getter;
if (parent?.type === 'RegularElement') {
const value = /** @type {any[]} */ (
/** @type {Attribute} */ (
parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
a.name === 'value' &&
!is_text_attribute(a) &&
a.value !== true
)
)?.value
);
if (value !== undefined) {
group_getter = b.thunk(
b.block([
b.stmt(serialize_attribute_value(value, context)[1]),
b.return(/** @type {Expression} */ (context.visit(expression)))
])
);
}
}
call = b.call(
'$.bind_group',
node.metadata.binding_group_name,
b.array(indexes),
context.state.node,
group_getter,
setter
);
break;
}
default:
throw new Error('unknown binding ' + node.name);
}
}
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
context.state.init.push(b.stmt(call));
} else {
const has_action_directive =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
context.state.after_update.push(
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(call)) : call)
);
}
}

@ -0,0 +1,20 @@
/** @import { BreakStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {BreakStatement} node
* @param {ComponentContext} context
*/
export function BreakStatement(node, context) {
if (context.state.analysis.runes || !node.label || node.label.name !== '$') {
return;
}
const in_reactive_statement =
context.path[1].type === 'LabeledStatement' && context.path[1].label.name === '$';
if (in_reactive_statement) {
return b.return();
}
}

@ -0,0 +1,46 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { transform_inspect_rune } from '../../utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
switch (get_rune(node, context.state.scope)) {
case '$host':
return b.id('$$props.$$host');
case '$effect.tracking':
return b.call('$.effect_tracking');
case '$state.snapshot':
return b.call(
'$.snapshot',
/** @type {Expression} */ (context.visit(node.arguments[0])),
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
case '$state.is':
return b.call(
'$.is',
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
case '$effect.root':
return b.call(
'$.effect_root',
.../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)))
);
case '$inspect':
case '$inspect().with':
return transform_inspect_rune(node, context);
}
context.next();
}

@ -0,0 +1,223 @@
/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */
/** @import { } from '#compiler' */
/** @import { Context, StateField } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { serialize_proxy_reassignment, should_proxy_or_freeze } from '../utils.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
context.next();
return;
}
/** @type {Map<string, StateField>} */
const public_state = new Map();
/** @type {Map<string, StateField>} */
const private_state = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const type = definition.key.type;
const name = get_name(definition.key);
if (!name) continue;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, context.state.scope);
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by'
) {
/** @type {StateField} */
const field = {
kind:
rune === '$state'
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived.by'
? 'derived_call'
: 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_state.set(name, field);
} else {
public_state.set(name, field);
}
}
}
}
}
// each `foo = $state()` needs a backing `#foo` field
for (const [name, field] of public_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
const child_state = { ...context.state, public_state, private_state };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const name = get_name(definition.key);
if (!name) continue;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_state : public_state).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
let value = null;
if (definition.value.arguments.length > 0) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
);
value =
field.kind === 'state'
? b.call(
'$.source',
should_proxy_or_freeze(init, context.state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'frozen_state'
? b.call(
'$.source',
should_proxy_or_freeze(init, context.state.scope)
? b.call('$.freeze', init)
: init
)
: field.kind === 'derived_call'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.source');
}
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
if (field.kind === 'state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, serialize_proxy_reassignment(value, field.id)))]
)
);
}
if (field.kind === 'frozen_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
);
}
if (dev && (field.kind === 'derived' || field.kind === 'derived_call')) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
if (dev && public_state.size > 0) {
// add an `[$.ADD_OWNER]` method so that a class with state fields can widen ownership
body.push(
b.method(
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
Array.from(public_state.keys()).map((name) =>
b.stmt(
b.call(
'$.add_owner',
b.call('$.get', b.member(b.this, b.private_id(name))),
b.id('owner'),
b.literal(false),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
),
true
)
);
}
return { ...node, body };
}
/**
* @param {Identifier | PrivateIdentifier | Literal} node
*/
function get_name(node) {
if (node.type === 'Literal') {
return node.value?.toString().replace(regex_invalid_identifier_chars, '_');
} else {
return node.name;
}
}

@ -0,0 +1,11 @@
/** @import { Comment } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/**
* @param {Comment} node
* @param {ComponentContext} context
*/
export function Comment(node, context) {
// We'll only get here if comments are not filtered out, which they are unless preserveComments is true
context.state.template.push(`<!--${node.data}-->`);
}

@ -0,0 +1,32 @@
/** @import { Expression } from 'estree' */
/** @import { Component } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { serialize_component } from './shared/component.js';
/**
* @param {Component} node
* @param {ComponentContext} context
*/
export function Component(node, context) {
if (node.metadata.dynamic) {
// Handle dynamic references to what seems like static inline components
const component = serialize_component(node, '$$component', context, b.id('$$anchor'));
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))),
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
)
)
);
return;
}
const component = serialize_component(node, node.name, context);
context.state.init.push(component);
}

@ -0,0 +1,73 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { ConstTag } from '#compiler' */
/** @import { ComponentContext } from '../types' */
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';
/**
* @param {ConstTag} node
* @param {ComponentContext} context
*/
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') {
context.state.init.push(
b.const(
declaration.id,
create_derived(
context.state,
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
)
)
);
context.state.getters[declaration.id.name] = b.call('$.get', declaration.id);
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.init.push(b.stmt(b.call('$.get', declaration.id)));
}
} else {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
const getters = { ...context.state.getters };
// 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;
}
const child_state = { ...context.state, getters };
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const fn = b.arrow(
[],
b.block([
b.const(
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])
);
context.state.init.push(b.const(tmp, create_derived(context.state, fn)));
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
context.state.init.push(b.stmt(b.call('$.get', tmp)));
}
for (const node of identifiers) {
context.state.getters[node.name] = b.member(b.call('$.get', tmp), node);
}
}
}

@ -0,0 +1,22 @@
/** @import { Expression} from 'estree' */
/** @import { DebugTag } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {DebugTag} node
* @param {ComponentContext} context
*/
export function DebugTag(node, context) {
const object = b.object(
node.identifiers.map((identifier) =>
b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier)))
)
);
const call = b.call('console.log', object);
context.state.init.push(
b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger]))))
);
}

@ -0,0 +1,317 @@
/** @import { BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */
/** @import { Binding, EachBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED
} from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import {
get_assignment_value,
serialize_get_binding,
serialize_set_binding,
with_loc
} from '../utils.js';
/**
* @param {EachBlock} node
* @param {ComponentContext} context
*/
export function EachBlock(node, context) {
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
}
if (each_node_meta.array_name !== null) {
context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection)));
}
let flags = 0;
if (node.metadata.keyed) {
flags |= EACH_KEYED;
if (node.index) {
flags |= EACH_INDEX_REACTIVE;
}
// In runes mode, if key === item, we don't need to wrap the item in a source
const key_is_item =
/** @type {Expression} */ (node.key).type === 'Identifier' &&
node.context.type === 'Identifier' &&
node.context.name === node.key.name;
if (!context.state.analysis.runes || !key_is_item) {
flags |= EACH_ITEM_REACTIVE;
}
} else {
flags |= EACH_ITEM_REACTIVE;
}
// Since `animate:` can only appear on elements that are the sole child of a keyed each block,
// we can determine at compile time whether the each block is animated or not (in which
// case it should measure animated elements before and after reconciliation).
if (
node.key &&
node.body.nodes.some((child) => {
if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false;
return child.attributes.some((attr) => attr.type === 'AnimateDirective');
})
) {
flags |= EACH_IS_ANIMATED;
}
if (each_node_meta.is_controlled) {
flags |= EACH_IS_CONTROLLED;
}
if (context.state.analysis.runes) {
flags |= EACH_IS_STRICT_EQUALS;
}
// If the array is a store expression, we need to invalidate it when the array is changed.
// This doesn't catch all cases, but all the ones that Svelte 4 catches, too.
let store_to_invalidate = '';
if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') {
const id = object(node.expression);
if (id) {
const binding = context.state.scope.get(id.name);
if (binding?.kind === 'store_sub') {
store_to_invalidate = id.name;
}
}
}
// Legacy mode: find the parent each blocks which contain the arrays to invalidate
const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => {
const array = /** @type {Expression} */ (context.visit(block.expression));
const transitive_dependencies = serialize_transitive_dependencies(
block.metadata.references,
context
);
return [array, ...transitive_dependencies];
});
if (each_node_meta.array_name) {
indirect_dependencies.push(b.call(each_node_meta.array_name));
} else {
indirect_dependencies.push(collection);
const transitive_dependencies = serialize_transitive_dependencies(
each_node_meta.references,
context
);
indirect_dependencies.push(...transitive_dependencies);
}
const child_state = {
...context.state,
getters: { ...context.state.getters }
};
/** 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 {Binding['mutation']}
*/
const create_mutation = (expression_for_id) => {
return (assignment, context) => {
if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') {
// serialize_set_binding 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 serialize_set_binding(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 = context.visit(original_left);
const assign = b.assignment(assignment.operator, left, value);
sequence.unshift(assign);
return b.sequence(sequence);
}
};
};
// We need to generate a unique identifier in case there's a bind:group below
// which needs a reference to the index
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const item = each_node_meta.item;
const binding = /** @type {Binding} */ (context.state.scope.get(item.name));
const getter = (/** @type {Identifier} */ id) => {
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);
};
key_state.getters[node.index] = b.id(node.index);
}
/** @type {Statement[]} */
const declarations = [];
if (node.context.type === 'Identifier') {
binding.mutation = create_mutation(
b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
true
)
);
key_state.getters[node.context.name] = node.context;
} 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))
);
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;
binding.mutation = create_mutation(
/** @type {Pattern} */ (path.update_expression(unwrapped))
);
// 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));
}
key_state.getters[name] = path.node;
}
}
const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state));
/** @type {Expression} */
let key_function = b.id('$.index');
if (node.metadata.keyed) {
const expression = /** @type {Expression} */ (
context.visit(/** @type {Expression} */ (node.key), key_state)
);
key_function = b.arrow([node.context, index], expression);
}
if (node.index && each_node_meta.contains_group_binding) {
// We needed to create a unique identifier for the index above, but we want to use the
// original index name in the template, therefore create another binding
declarations.push(b.let(node.index, index));
}
if (dev && (flags & EACH_KEYED) !== 0) {
context.state.init.push(
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
);
}
/** @type {Expression[]} */
const args = [
context.state.node,
b.literal(flags),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(block.body)))
];
if (node.fallback) {
args.push(
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fallback)))
);
}
context.state.init.push(b.stmt(b.call('$.each', ...args)));
}
/**
* @param {ComponentContext} context
*/
function collect_parent_each_blocks(context) {
return /** @type {EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock'));
}
/**
* @param {Binding[]} references
* @param {ComponentContext} context
*/
function serialize_transitive_dependencies(references, context) {
/** @type {Set<Binding>} */
const dependencies = new Set();
for (const ref of references) {
const deps = collect_transitive_dependencies(ref);
for (const dep of deps) {
dependencies.add(dep);
}
}
return [...dependencies].map((dep) => serialize_get_binding({ ...dep.node }, context.state));
}
/**
* @param {Binding} binding
* @param {Set<Binding>} seen
* @returns {Binding[]}
*/
function collect_transitive_dependencies(binding, seen = new Set()) {
if (binding.kind !== 'legacy_reactive') return [];
for (const dep of binding.legacy_dependencies) {
if (!seen.has(dep)) {
seen.add(dep);
for (const transitive_dep of collect_transitive_dependencies(dep, seen)) {
seen.add(transitive_dep);
}
}
}
return [...seen];
}

@ -0,0 +1,19 @@
/** @import { ExportNamedDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {ExportNamedDeclaration} node
* @param {ComponentContext} context
*/
export function ExportNamedDeclaration(node, context) {
if (context.state.is_instance) {
if (node.declaration) {
return context.visit(node.declaration);
}
return b.empty;
}
return context.next();
}

@ -0,0 +1,23 @@
/** @import { Expression, ExpressionStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {ExpressionStatement} node
* @param {ComponentContext} context
*/
export function ExpressionStatement(node, context) {
if (node.expression.type === 'CallExpression') {
const rune = get_rune(node.expression, context.state.scope);
if (rune === '$effect' || rune === '$effect.pre') {
const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect';
const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0]));
return b.stmt(b.call(callee, /** @type {Expression} */ (func)));
}
}
context.next();
}

@ -0,0 +1,236 @@
/** @import { Expression, Identifier, Statement } from 'estree' */
/** @import { Fragment, Namespace, RegularElement } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js';
import { dev } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { clean_nodes, infer_namespace } from '../../utils.js';
import { process_children } from './shared/fragment.js';
import { serialize_render_stmt } from './shared/utils.js';
/**
* @param {Fragment} node
* @param {ComponentContext} context
*/
export function Fragment(node, context) {
// Creates a new block which looks roughly like this:
// ```js
// // hoisted:
// const block_name = $.template(`...`);
//
// // for the main block:
// const id = block_name();
// // init stuff and possibly render effect
// $.append($$anchor, id);
// ```
// Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block.
const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
namespace,
context.state,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
return b.block([]);
}
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {Statement[]} */
const body = [];
/** @type {Statement | undefined} */
let close = undefined;
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
before_init: [],
init: [],
update: [],
after_update: [],
template: [],
locations: [],
getters: { ...context.state.getters },
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
};
for (const node of hoisted) {
context.visit(node, state);
}
if (is_text_first) {
// skip over inserted comment
body.push(b.stmt(b.call('$.next')));
}
/**
* @param {Identifier} template_name
* @param {Expression[]} args
*/
const add_template = (template_name, args) => {
let call = b.call(get_template_function(namespace, state), ...args);
if (dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(context.state.analysis.name), b.id('$.FILENAME'), true),
serialize_locations(state.locations)
);
}
context.state.hoisted.push(b.var(template_name, call));
};
if (is_single_element) {
const element = /** @type {RegularElement} */ (trimmed[0]);
const id = b.id(context.state.scope.generate(element.name));
context.visit(element, {
...state,
node: id
});
/** @type {Expression[]} */
const args = [b.template([b.quasi(state.template.join(''), true)], [])];
if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
}
add_template(template_name, args);
body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init);
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
...context,
state
});
body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
if (is_standalone) {
// no need to create a template, we can just use the existing block's anchor
process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
} else {
/** @type {(is_text: boolean) => Expression} */
const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
process_children(trimmed, expression, false, { ...context, state });
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
if (state.template.length === 1 && state.template[0] === '<!>') {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []),
b.literal(flags)
]);
body.push(b.var(id, b.call(template_name)));
}
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
body.push(...state.before_init, ...state.init);
}
} else {
body.push(...state.before_init, ...state.init);
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state.update));
}
body.push(...state.after_update);
if (close !== undefined) {
// It's important that close is the last statement in the block, as any previous statements
// could contain element insertions into the template, which the close statement needs to
// know of when constructing the list of current inner elements.
body.push(close);
}
return b.block(body);
}
/**
*
* @param {Namespace} namespace
* @param {ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
const contains_script_tag = state.metadata.context.template_contains_script_tag;
return namespace === 'svg'
? contains_script_tag
? '$.svg_template_with_script'
: '$.ns_template'
: namespace === 'mathml'
? '$.mathml_template'
: contains_script_tag
? '$.template_with_script'
: '$.template';
}
/**
* @param {SourceLocation[]} locations
*/
function serialize_locations(locations) {
return b.array(
locations.map((loc) => {
const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]);
if (loc.length === 3) {
expression.elements.push(serialize_locations(loc[2]));
}
return expression;
})
);
}

@ -0,0 +1,31 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { serialize_hoistable_params } from '../utils.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {FunctionDeclaration} node
* @param {ComponentContext} context
*/
export function FunctionDeclaration(node, context) {
const metadata = node.metadata;
const state = { ...context.state, in_constructor: false };
if (metadata?.hoistable === true) {
const params = serialize_hoistable_params(node, context);
context.state.hoisted.push(
/** @type {FunctionDeclaration} */ ({
...node,
id: node.id !== null ? context.visit(node.id, state) : null,
params,
body: context.visit(node.body, state)
})
);
return b.empty;
}
context.next(state);
}

@ -0,0 +1,11 @@
/** @import { FunctionExpression } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { visit_function } from './shared/function.js';
/**
* @param {FunctionExpression} node
* @param {ComponentContext} context
*/
export function FunctionExpression(node, context) {
return visit_function(node, context);
}

@ -0,0 +1,27 @@
/** @import { Expression } from 'estree' */
/** @import { HtmlTag } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {HtmlTag} node
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
context.state.template.push('<!>');
// push into init, so that bindings run afterwards, which might trigger another run and override hydration
context.state.init.push(
b.stmt(
b.call(
'$.html',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
b.literal(context.state.metadata.namespace === 'svg'),
b.literal(context.state.metadata.namespace === 'mathml'),
is_ignored(node, 'hydration_html_changed') && b.true
)
)
);
}

@ -0,0 +1,41 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import * as b from '../../../../utils/builders.js';
import { serialize_get_binding } from '../utils.js';
/**
* @param {Identifier} node
* @param {Context} context
*/
export function Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
if (is_reference(node, parent)) {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = context.state.scope.get(node.name);
if (
context.state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const grand_parent = context.path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, context.state);
}
}

@ -0,0 +1,55 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { IfBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {IfBlock} node
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
context.state.template.push('<!>');
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const args = [
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.test))),
b.arrow([b.id('$$anchor')], consequent)
];
if (node.alternate || node.elseif) {
args.push(
node.alternate
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.alternate)))
: b.literal(null)
);
}
if (node.elseif) {
// We treat this...
//
// {#if x}
// ...
// {:else}
// {#if y}
// <div transition:foo>...</div>
// {/if}
// {/if}
//
// ...slightly differently to this...
//
// {#if x}
// ...
// {:else if y}
// <div transition:foo>...</div>
// {/if}
//
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.literal(true));
}
context.state.init.push(b.stmt(b.call('$.if', ...args)));
}

@ -0,0 +1,15 @@
/** @import { ImportDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {ImportDeclaration} node
* @param {ComponentContext} context
*/
export function ImportDeclaration(node, context) {
if ('hoisted' in context.state) {
context.state.hoisted.push(node);
}
return b.empty;
}

@ -0,0 +1,19 @@
/** @import { Expression } from 'estree' */
/** @import { KeyBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {KeyBlock} node
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
context.state.template.push('<!>');
const key = /** @type {Expression} */ (context.visit(node.expression));
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)))
);
}

@ -0,0 +1,64 @@
/** @import { Expression, LabeledStatement, Statement } from 'estree' */
/** @import { ReactiveStatement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { serialize_get_binding } from '../utils.js';
/**
* @param {LabeledStatement} node
* @param {ComponentContext} context
*/
export function LabeledStatement(node, context) {
if (context.state.analysis.runes || context.path.length > 1 || node.label.name !== '$') {
context.next();
return;
}
// To recreate Svelte 4 behaviour, we track the dependencies
// the compiler can 'see', but we untrack the effect itself
const reactive_statement = /** @type {ReactiveStatement} */ (
context.state.analysis.reactive_statements.get(node)
);
if (!reactive_statement) return; // not the instance context
let serialized_body = /** @type {Statement} */ (context.visit(node.body));
if (serialized_body.type !== 'BlockStatement') {
serialized_body = b.block([serialized_body]);
}
const body = serialized_body.body;
/** @type {Expression[]} */
const sequence = [];
for (const binding of reactive_statement.dependencies) {
if (binding.kind === 'normal') continue;
const name = binding.node.name;
let serialized = serialize_get_binding(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);
}
// these statements will be topologically ordered later
context.state.legacy_reactive_statements.set(
node,
b.stmt(
b.call(
'$.legacy_pre_effect',
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
b.thunk(b.block(body))
)
)
);
return b.empty;
}

@ -0,0 +1,50 @@
/** @import { Expression } from 'estree' */
/** @import { LetDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { create_derived } from '../utils.js';
/**
* @param {LetDirective} node
* @param {ComponentContext} context
*/
export function LetDirective(node, context) {
// let:x --> const x = $.derived(() => $$slotProps.x);
// let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $$slotProps.x; return { y, z }));
if (node.expression && node.expression.type !== 'Identifier') {
const name = context.state.scope.generate(node.name);
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)
);
}
return b.const(
name,
b.call(
'$.derived',
b.thunk(
b.block([
b.let(
/** @type {Expression} */ (node.expression).type === 'ObjectExpression'
? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.object_pattern(node.expression.properties)
: // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine
b.array_pattern(node.expression.elements),
b.member(b.id('$$slotProps'), b.id(node.name))
),
b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node))))
])
)
)
);
} else {
return b.const(
node.expression === null ? node.name : node.expression.name,
create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
);
}
}

@ -0,0 +1,28 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {MemberExpression} node
* @param {Context} context
*/
export function MemberExpression(node, context) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = context.state.private_state.get(node.property.name);
if (field) {
return context.state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
}
} else if (node.object.type === 'ThisExpression') {
// rewrite `this.foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'Identifier' && !node.computed) {
const field = context.state.public_state.get(node.property.name);
if (field && context.state.in_constructor) {
return b.member(b.member(b.this, field.id), b.id('v'));
}
}
}
context.next();
}

@ -0,0 +1,11 @@
/** @import { OnDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { serialize_event } from './shared/element.js';
/**
* @param {OnDirective} node
* @param {ComponentContext} context
*/
export function OnDirective(node, context) {
serialize_event(node, node.metadata.expression, context);
}

@ -0,0 +1,719 @@
/** @import { Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { Attribute, BindDirective, ClassDirective, RegularElement, SpreadAttribute, StyleDirective } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import { DOMBooleanAttributes } from '../../../../../constants.js';
import { escape_html } from '../../../../../escaping.js';
import { dev, is_ignored, locator } from '../../../../state.js';
import {
get_attribute_expression,
is_event_attribute,
is_text_attribute
} from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { DOMProperties, LoadErrorElements, VoidElements } from '../../../constants.js';
import { is_custom_element_node } from '../../../nodes.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { serialize_get_binding } from '../utils.js';
import {
get_attribute_name,
serialize_attribute_value,
serialize_class_directives,
serialize_event_attribute,
serialize_style_directives
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
serialize_render_stmt,
serialize_update,
serialize_update_assignment
} from './shared/utils.js';
/**
* @param {RegularElement} node
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
/** @type {SourceLocation} */
let location = [-1, -1];
if (dev) {
const loc = locator(node.start);
if (loc) {
location[0] = loc.line;
location[1] = loc.column;
context.state.locations.push(location);
}
}
if (node.name === 'noscript') {
context.state.template.push('<noscript></noscript>');
return;
}
if (node.name === 'script') {
context.state.metadata.context.template_contains_script_tag = true;
}
const metadata = context.state.metadata;
const child_metadata = {
...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
};
context.state.template.push(`<${node.name}`);
/** @type {Array<Attribute | SpreadAttribute>} */
const attributes = [];
/** @type {ClassDirective[]} */
const class_directives = [];
/** @type {StyleDirective[]} */
const style_directives = [];
/** @type {ExpressionStatement[]} */
const lets = [];
const is_custom_element = is_custom_element_node(node);
let needs_input_reset = false;
let needs_content_reset = false;
/** @type {BindDirective | null} */
let value_binding = null;
/** If true, needs `__value` for inputs */
let needs_special_value_handling = node.name === 'option' || node.name === 'select';
let is_content_editable = false;
let has_content_editable_binding = false;
let img_might_be_lazy = false;
let might_need_event_replaying = false;
let has_direction_attribute = false;
let has_style_attribute = false;
if (is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
// custom element until the template is connected to the dom, which would
// cause problems when setting properties on the custom element.
// Therefore we need to use importNode instead, which doesn't have this caveat.
metadata.context.template_needs_import_node = true;
}
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
if (node.name === 'img' && attribute.name === 'loading') {
img_might_be_lazy = true;
}
if (attribute.name === 'dir') {
has_direction_attribute = true;
}
if (attribute.name === 'style') {
has_style_attribute = true;
}
if (
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
) {
needs_input_reset = true;
needs_content_reset = true;
} else if (
attribute.name === 'contenteditable' &&
(attribute.value === true ||
(is_text_attribute(attribute) && attribute.value[0].data === 'true'))
) {
is_content_editable = true;
}
} else if (attribute.type === 'SpreadAttribute') {
attributes.push(attribute);
needs_input_reset = true;
needs_content_reset = true;
if (LoadErrorElements.includes(node.name)) {
might_need_event_replaying = true;
}
} else if (attribute.type === 'ClassDirective') {
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 === 'BindDirective') {
if (attribute.name === 'group' || attribute.name === 'checked') {
needs_special_value_handling = true;
needs_input_reset = true;
} else if (attribute.name === 'value') {
value_binding = attribute;
needs_content_reset = true;
needs_input_reset = true;
} else if (
attribute.name === 'innerHTML' ||
attribute.name === 'innerText' ||
attribute.name === 'textContent'
) {
has_content_editable_binding = true;
}
} else if (attribute.type === 'UseDirective' && LoadErrorElements.includes(node.name)) {
might_need_event_replaying = true;
}
context.visit(attribute);
}
}
if (child_metadata.namespace === 'foreign') {
// input/select etc could mean something completely different in foreign namespace, so don't special-case them
needs_content_reset = false;
needs_input_reset = false;
needs_special_value_handling = false;
value_binding = null;
}
if (is_content_editable && has_content_editable_binding) {
child_metadata.bound_contenteditable = true;
}
if (needs_input_reset && node.name === 'input') {
context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node)));
}
if (needs_content_reset && node.name === 'textarea') {
context.state.init.push(b.stmt(b.call('$.remove_textarea_child', context.state.node)));
}
if (value_binding !== null && node.name === 'select') {
setup_select_synchronization(value_binding, context);
}
const node_id = context.state.node;
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
// Then do attributes
let is_attributes_reactive = false;
if (node.metadata.has_spread) {
if (node.name === 'img') {
img_might_be_lazy = true;
}
serialize_element_spread_attributes(
attributes,
context,
node,
node_id,
// If value binding exists, that one takes care of calling $.init_select
value_binding === null && node.name === 'select' && child_metadata.namespace !== 'foreign'
);
is_attributes_reactive = true;
} else {
for (const attribute of /** @type {Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
if (
(attribute.name === 'onload' || attribute.name === 'onerror') &&
LoadErrorElements.includes(node.name)
) {
might_need_event_replaying = true;
}
serialize_event_attribute(attribute, context);
continue;
}
if (needs_special_value_handling && attribute.name === 'value') {
serialize_element_special_value_attribute(node.name, node_id, attribute, context);
continue;
}
if (
!is_custom_element &&
attribute.name !== 'autofocus' &&
(attribute.value === true || is_text_attribute(attribute))
) {
const name = get_attribute_name(node, attribute, context);
const literal_value = /** @type {Literal} */ (
serialize_attribute_value(attribute.value, context)[1]
).value;
if (name !== 'class' || literal_value) {
// TODO namespace=foreign probably doesn't want to do template stuff at all and instead use programmatic methods
// to create the elements it needs.
context.state.template.push(
` ${attribute.name}${
DOMBooleanAttributes.includes(name) && literal_value === true
? ''
: `="${literal_value === true ? '' : escape_html(literal_value, true)}"`
}`
);
continue;
}
}
const is =
is_custom_element && child_metadata.namespace !== 'foreign'
? serialize_custom_element_attribute_update_assignment(node_id, attribute, context)
: serialize_element_attribute_update_assignment(node, node_id, attribute, context);
if (is) is_attributes_reactive = true;
}
}
// Apply the src and loading attributes for <img> elements after the element is appended to the document
if (img_might_be_lazy) {
context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
}
// class/style directives must be applied last since they could override class/style attributes
serialize_class_directives(class_directives, node_id, context, is_attributes_reactive);
serialize_style_directives(
style_directives,
node_id,
context,
is_attributes_reactive,
has_style_attribute || node.metadata.has_spread
);
if (might_need_event_replaying) {
context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id)));
}
context.state.template.push('>');
/** @type {SourceLocation[]} */
const child_locations = [];
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
metadata: child_metadata,
locations: child_locations,
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace ||
((node.name === 'pre' || node.name === 'textarea') && child_metadata.namespace !== 'foreign')
};
const { hoisted, trimmed } = clean_nodes(
node,
node.fragment.nodes,
context.path,
child_metadata.namespace,
state,
node.name === 'script' || state.preserve_whitespace,
state.options.preserveComments
);
/** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */
const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock');
const child_state = has_declaration
? { ...state, init: [], update: [], after_update: [] }
: state;
for (const node of hoisted) {
context.visit(node, child_state);
}
/** @type {Expression} */
let arg = context.state.node;
// If `hydrate_node` is set inside the element, we need to reset it
// after the element has been hydrated
let needs_reset = trimmed.some((node) => node.type !== 'Text');
// The same applies if it's a `<template>` element, since we need to
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
needs_reset = true;
arg = b.member(arg, b.id('content'));
child_state.init.push(b.stmt(b.call('$.reset', arg)));
}
process_children(trimmed, () => b.call('$.child', arg), true, {
...context,
state: child_state
});
if (needs_reset) {
child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
}
if (has_declaration) {
context.state.init.push(
b.block([
...child_state.init,
child_state.update.length > 0 ? serialize_render_stmt(child_state.update) : b.empty,
...child_state.after_update
])
);
}
if (has_direction_attribute) {
// This fixes an issue with Chromium where updates to text content within an element
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
context.state.update.push(
b.stmt(b.assignment('=', b.member(node_id, b.id('dir')), b.member(node_id, b.id('dir'))))
);
}
if (child_locations.length > 0) {
// @ts-expect-error
location.push(child_locations);
}
if (!VoidElements.includes(node.name)) {
context.state.template.push(`</${node.name}>`);
}
}
/**
* Special case: if we have a value binding on a select element, we need to set up synchronization
* between the value binding and inner signals, for indirect updates
* @param {BindDirective} value_binding
* @param {ComponentContext} context
*/
function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
let bound = value_binding.expression;
while (bound.type === 'MemberExpression') {
bound = /** @type {Identifier | MemberExpression} */ (bound.object);
}
/** @type {string[]} */
const names = [];
for (const [name, refs] of context.state.scope.references) {
if (
refs.length > 0 &&
// prevent infinite loop
name !== bound.name
) {
names.push(name);
}
}
const invalidator = b.call(
'$.invalidate_inner_signals',
b.thunk(
b.block(
names.map((name) => {
const serialized = serialize_get_binding(b.id(name), context.state);
return b.stmt(serialized);
})
)
)
);
context.state.init.push(
b.stmt(
b.call(
'$.template_effect',
b.thunk(
b.block([
b.stmt(/** @type {Expression} */ (context.visit(value_binding.expression))),
b.stmt(invalidator)
])
)
)
)
);
}
/**
* @param {Array<Attribute | SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {RegularElement} element
* @param {Identifier} element_id
* @param {boolean} needs_select_handling
*/
function serialize_element_spread_attributes(
attributes,
context,
element,
element_id,
needs_select_handling
) {
let needs_isolation = false;
/** @type {ObjectExpression['properties']} */
const values = [];
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context);
// TODO: handle has_call
const [, value] = serialize_attribute_value(attribute.value, context);
if (
name === 'is' &&
value.type === 'Literal' &&
context.state.metadata.namespace === 'html'
) {
context.state.template.push(` is="${escape_html(value.value, true)}"`);
continue;
}
if (
is_event_attribute(attribute) &&
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
get_attribute_expression(attribute).type === 'FunctionExpression')
) {
// Give the event handler a stable ID so it isn't removed and readded on every update
const id = context.state.scope.generate('event_handler');
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(name, value));
}
} else {
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
}
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call;
}
const lowercase_attributes =
element.metadata.svg || element.metadata.mathml || is_custom_element_node(element)
? b.false
: b.true;
const id = context.state.scope.generate('attributes');
const update = b.stmt(
b.assignment(
'=',
b.id(id),
b.call(
'$.set_attributes',
element_id,
b.id(id),
b.object(values),
lowercase_attributes,
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
)
)
);
context.state.init.push(b.let(id));
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
context.state.init.push(serialize_update(update));
} else {
context.state.update.push(update);
}
if (needs_select_handling) {
context.state.init.push(
b.stmt(b.call('$.init_select', element_id, b.thunk(b.member(b.id(id), b.id('value')))))
);
context.state.update.push(
b.if(
b.binary('in', b.literal('value'), b.id(id)),
b.block([
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.stmt(b.call('$.select_option', element_id, b.member(b.id(id), b.id('value'))))
])
)
);
}
}
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
* Resulting code for static looks something like this:
* ```js
* element.property = value;
* // or
* $.set_attribute(element, property, value);
* });
* ```
* Resulting code for dynamic looks something like this:
* ```js
* let value;
* $.template_effect(() => {
* if (value !== (value = 'new value')) {
* element.property = value;
* // or
* $.set_attribute(element, property, value);
* }
* });
* ```
* Returns true if attribute is deemed reactive, false otherwise.
* @param {RegularElement} element
* @param {Identifier} node_id
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_element_attribute_update_assignment(element, node_id, attribute, context) {
const state = context.state;
const name = get_attribute_name(element, attribute, context);
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';
let [has_call, value] = serialize_attribute_value(attribute.value, context);
// The foreign namespace doesn't have any special handling, everything goes through the attr function
if (context.state.metadata.namespace === 'foreign') {
const statement = b.stmt(
b.call(
'$.set_attribute',
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
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;
} else {
state.init.push(statement);
return false;
}
}
if (name === 'autofocus') {
state.init.push(b.stmt(b.call('$.autofocus', node_id, value)));
return false;
}
/** @type {Statement} */
let update;
if (name === 'class') {
update = b.stmt(
b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id,
value
)
);
} else if (name === 'value') {
update = b.stmt(b.call('$.set_value', node_id, value));
} else if (name === 'checked') {
update = b.stmt(b.call('$.set_checked', node_id, value));
} else if (DOMProperties.includes(name)) {
update = b.stmt(b.assignment('=', b.member(node_id, b.id(name)), value));
} else {
const callee = name.startsWith('xlink') ? '$.set_xlink_attribute' : '$.set_attribute';
update = b.stmt(
b.call(
callee,
node_id,
b.literal(name),
value,
is_ignored(element, 'hydration_attribute_changed') && b.true
)
);
}
if (attribute.metadata.expression.has_state) {
if (has_call) {
state.init.push(serialize_update(update));
} else {
state.update.push(update);
}
return true;
} else {
state.init.push(update);
return false;
}
}
/**
* Like `serialize_element_attribute_update_assignment` but without any special attribute treatment.
* @param {Identifier} node_id
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_custom_element_attribute_update_assignment(node_id, attribute, context) {
const state = context.state;
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let [has_call, value] = serialize_attribute_value(attribute.value, context);
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
if (attribute.metadata.expression.has_state) {
if (has_call) {
state.init.push(serialize_update(update));
} else {
state.update.push(update);
}
return true;
} else {
state.init.push(update);
return false;
}
}
/**
* Serializes an assignment to the value property of a `<select>`, `<option>` or `<input>` element
* that needs the hidden `__value` property.
* Returns true if attribute is deemed reactive, false otherwise.
* @param {string} element
* @param {Identifier} node_id
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state;
const [, value] = serialize_attribute_value(attribute.value, context);
const inner_assignment = b.assignment(
'=',
b.member(node_id, b.id('value')),
b.conditional(
b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, b.id('__value')), value)),
b.literal(''), // render null/undefined values as empty string to support placeholder options
value
)
);
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
element === 'select' && attribute.value !== true && !is_text_attribute(attribute);
const update = b.stmt(
is_select_with_value
? b.sequence([
inner_assignment,
// This ensures a one-way street to the DOM in case it's <select {value}>
// and not <select bind:value>. We need it in addition to $.init_select
// because the select value is not reflected as an attribute, so the
// mutation observer wouldn't notice.
b.call('$.select_option', node_id, value)
])
: inner_assignment
);
if (is_select_with_value) {
state.init.push(b.stmt(b.call('$.init_select', node_id, b.thunk(value))));
}
if (is_reactive) {
const id = state.scope.generate(`${node_id.name}_value`);
serialize_update_assignment(
state,
id,
// `<option>` is a special case: The value property reflects to the DOM. If the value is set to undefined,
// that means the value should be set to the empty string. To be able to do that when the value is
// initially undefined, we need to set a value that is guaranteed to be different.
element === 'option' ? b.object([]) : undefined,
value,
update
);
return true;
} else {
state.init.push(update);
return false;
}
}

@ -0,0 +1,47 @@
/** @import { Expression } from 'estree' */
/** @import { RenderTag } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {RenderTag} node
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
context.state.template.push('<!>');
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
const raw = raw_args[i];
const arg = /** @type {Expression} */ (context.visit(raw));
if (node.metadata.args_with_call_expression.has(i)) {
const id = b.id(context.state.scope.generate('render_arg'));
context.state.init.push(b.var(id, b.call('$.derived_safe_equal', b.thunk(arg))));
args.push(b.thunk(b.call('$.get', id)));
} else {
args.push(b.thunk(arg));
}
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
if (node.metadata.dynamic) {
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
);
} else {
context.state.init.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
)
);
}
}

@ -0,0 +1,66 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Property } from 'estree' */
/** @import { SlotElement } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { serialize_attribute_value } from './shared/element.js';
/**
* @param {SlotElement} node
* @param {ComponentContext} context
*/
export function SlotElement(node, context) {
// <slot {a}>fallback</slot> --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback);
context.state.template.push('<!>');
/** @type {Property[]} */
const props = [];
/** @type {Expression[]} */
const spreads = [];
/** @type {ExpressionStatement[]} */
const lets = [];
let is_default = true;
/** @type {Expression} */
let name = b.literal('default');
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute))));
} else if (attribute.type === 'Attribute') {
const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.name === 'name') {
name = value;
is_default = false;
} else if (attribute.name !== 'slot') {
if (attribute.metadata.expression.has_state) {
props.push(b.get(attribute.name, [b.return(value)]));
} else {
props.push(b.init(attribute.name, value));
}
}
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets);
const props_expression =
spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads);
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const expression = is_default
? b.call('$.default_slot', b.id('$$props'))
: b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true);
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);
context.state.init.push(b.stmt(slot));
}

@ -0,0 +1,86 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { SnippetBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
/**
* @param {SnippetBlock} node
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
// TODO hoist where possible
/** @type {Pattern[]} */
const args = [b.id('$$anchor')];
/** @type {BlockStatement} */
let body;
/** @type {Statement[]} */
const declarations = [];
const getters = { ...context.state.getters };
const child_state = { ...context.state, getters };
for (let i = 0; i < node.parameters.length; i++) {
const argument = node.parameters[i];
if (!argument) continue;
if (argument.type === 'Identifier') {
args.push({
type: 'AssignmentPattern',
left: argument,
right: b.id('$.noop')
});
getters[argument.name] = b.call(argument);
continue;
}
let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias));
const paths = extract_paths(argument);
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).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?.(b.maybe_call(b.id(arg_alias)))))
);
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);
// 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]));
}
}
}
body = b.block([
...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
]);
/** @type {Expression} */
let snippet = b.arrow(args, body);
if (dev) {
snippet = b.call('$.wrap_snippet', b.id(context.state.analysis.name), snippet);
}
const declaration = b.const(node.expression, snippet);
// Top-level snippets are hoisted so they can be referenced in the `<script>`
if (context.path.length === 1 && context.path[0].type === 'Fragment') {
context.state.analysis.top_level_snippets.push(declaration);
} else {
context.state.init.push(declaration);
}
}

@ -0,0 +1,10 @@
/** @import { SpreadAttribute } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/**
* @param {SpreadAttribute} node
* @param {ComponentContext} context
*/
export function SpreadAttribute(node, context) {
return context.visit(node.expression);
}

@ -0,0 +1,14 @@
/** @import { SvelteBody } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SvelteBody} node
* @param {ComponentContext} context
*/
export function SvelteBody(node, context) {
context.next({
...context.state,
node: b.id('$.document.body')
});
}

@ -0,0 +1,12 @@
/** @import { SvelteComponent } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { serialize_component } from './shared/component.js';
/**
* @param {SvelteComponent} node
* @param {ComponentContext} context
*/
export function SvelteComponent(node, context) {
const component = serialize_component(node, '$$component', context);
context.state.init.push(component);
}

@ -0,0 +1,14 @@
/** @import { SvelteDocument } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SvelteDocument} node
* @param {ComponentContext} context
*/
export function SvelteDocument(node, context) {
context.next({
...context.state,
node: b.id('$.document')
});
}

@ -0,0 +1,235 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { Attribute, ClassDirective, SpreadAttribute, StyleDirective, SvelteElement } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import { dev, locator } from '../../../../state.js';
import {
get_attribute_expression,
is_event_attribute,
is_text_attribute
} from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
serialize_attribute_value,
serialize_class_directives,
serialize_style_directives
} from './shared/element.js';
import { serialize_render_stmt, serialize_update } from './shared/utils.js';
/**
* @param {SvelteElement} node
* @param {ComponentContext} context
*/
export function SvelteElement(node, context) {
context.state.template.push(`<!>`);
/** @type {Array<Attribute | SpreadAttribute>} */
const attributes = [];
/** @type {Attribute['value'] | undefined} */
let dynamic_namespace = undefined;
/** @type {ClassDirective[]} */
const class_directives = [];
/** @type {StyleDirective[]} */
const style_directives = [];
/** @type {ExpressionStatement[]} */
const lets = [];
// Create a temporary context which picks up the init/update statements.
// They'll then be added to the function parameter of $.element
const element_id = b.id(context.state.scope.generate('$$element'));
/** @type {ComponentContext} */
const inner_context = {
...context,
state: {
...context.state,
node: element_id,
before_init: [],
init: [],
update: [],
after_update: []
}
};
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) {
dynamic_namespace = attribute.value;
}
attributes.push(attribute);
} else if (attribute.type === 'SpreadAttribute') {
attributes.push(attribute);
} else if (attribute.type === 'ClassDirective') {
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 {
context.visit(attribute, inner_context.state);
}
}
// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot
// Then do attributes
// Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime.
const is_attributes_reactive =
serialize_dynamic_element_attributes(attributes, inner_context, element_id) !== null;
// class/style directives must be applied last since they could override class/style attributes
serialize_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
serialize_style_directives(
style_directives,
element_id,
inner_context,
is_attributes_reactive,
true
);
const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));
if (dev && context.state.metadata.namespace !== 'foreign') {
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}
/** @type {Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
inner.push(serialize_render_stmt(inner_context.state.update));
}
inner.push(...inner_context.state.after_update);
inner.push(
.../** @type {BlockStatement} */ (
context.visit(node.fragment, {
...context.state,
metadata: {
...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
}
})
).body
);
const location = dev && locator(node.start);
context.state.init.push(
b.stmt(
b.call(
'$.element',
context.state.node,
get_tag,
node.metadata.svg || node.metadata.mathml ? b.true : b.false,
inner.length > 0 && b.arrow([element_id, b.id('$$anchor')], b.block(inner)),
dynamic_namespace && b.thunk(serialize_attribute_value(dynamic_namespace, context)[1]),
location && b.array([b.literal(location.line), b.literal(location.column)])
)
)
);
}
/**
* Serializes dynamic element attribute assignments.
* Returns the `true` if spread is deemed reactive.
* @param {Array<Attribute | SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {Identifier} element_id
* @returns {boolean}
*/
function serialize_dynamic_element_attributes(attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
return false;
}
// TODO why are we always treating this as a spread? needs docs, if that's not an error
let needs_isolation = false;
let is_reactive = false;
/** @type {ObjectExpression['properties']} */
const values = [];
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const [, value] = serialize_attribute_value(attribute.value, context);
if (
is_event_attribute(attribute) &&
(get_attribute_expression(attribute).type === 'ArrowFunctionExpression' ||
get_attribute_expression(attribute).type === 'FunctionExpression')
) {
// Give the event handler a stable ID so it isn't removed and readded on every update
const id = context.state.scope.generate('event_handler');
context.state.init.push(b.var(id, value));
values.push(b.init(attribute.name, b.id(id)));
} else {
values.push(b.init(attribute.name, value));
}
} else {
values.push(b.spread(/** @type {Expression} */ (context.visit(attribute))));
}
is_reactive ||=
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.expression.has_call;
}
if (needs_isolation || is_reactive) {
const id = context.state.scope.generate('attributes');
context.state.init.push(b.let(id));
const update = b.stmt(
b.assignment(
'=',
b.id(id),
b.call(
'$.set_dynamic_element_attributes',
element_id,
b.id(id),
b.object(values),
b.literal(context.state.analysis.css.hash)
)
)
);
if (needs_isolation) {
context.state.init.push(serialize_update(update));
return false;
}
context.state.update.push(update);
return true;
}
context.state.init.push(
b.stmt(
b.call(
'$.set_dynamic_element_attributes',
element_id,
b.literal(null),
b.object(values),
b.literal(context.state.analysis.css.hash)
)
)
);
return false;
}

@ -0,0 +1,17 @@
/** @import { BlockStatement, ExpressionStatement } from 'estree' */
/** @import { SvelteFragment } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/**
* @param {SvelteFragment} node
* @param {ComponentContext} context
*/
export function SvelteFragment(node, context) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
context.state.init.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
context.state.init.push(.../** @type {BlockStatement} */ (context.visit(node.fragment)).body);
}

@ -0,0 +1,20 @@
/** @import { BlockStatement } from 'estree' */
/** @import { SvelteHead } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SvelteHead} node
* @param {ComponentContext} context
*/
export function SvelteHead(node, context) {
// TODO attributes?
context.state.init.push(
b.stmt(
b.call(
'$.head',
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)))
)
)
);
}

@ -0,0 +1,12 @@
/** @import { SvelteSelf } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { serialize_component } from './shared/component.js';
/**
* @param {SvelteSelf} node
* @param {ComponentContext} context
*/
export function SvelteSelf(node, context) {
const component = serialize_component(node, context.state.analysis.name, context);
context.state.init.push(component);
}

@ -0,0 +1,14 @@
/** @import { SvelteWindow } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SvelteWindow} node
* @param {ComponentContext} context
*/
export function SvelteWindow(node, context) {
context.next({
...context.state,
node: b.id('$.window')
});
}

@ -0,0 +1,39 @@
/** @import { TitleElement, Text } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { serialize_template_literal } from './shared/utils.js';
/**
* @param {TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
// TODO throw validation error when attributes present / when children something else than text/expression tags
// TODO only create update when expression is dynamic
if (node.fragment.nodes.length === 1 && node.fragment.nodes[0].type === 'Text') {
context.state.init.push(
b.stmt(
b.assignment(
'=',
b.member(b.id('$.document'), b.id('title')),
b.literal(/** @type {Text} */ (node.fragment.nodes[0]).data)
)
)
);
} else {
context.state.update.push(
b.stmt(
b.assignment(
'=',
b.member(b.id('$.document'), b.id('title')),
serialize_template_literal(
/** @type {any} */ (node.fragment.nodes),
context.visit,
context.state
)[1]
)
)
);
}
}

@ -0,0 +1,29 @@
/** @import { Expression } from 'estree' */
/** @import { TransitionDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../../constants.js';
import * as b from '../../../../utils/builders.js';
import { parse_directive_name } from './shared/utils.js';
/**
* @param {TransitionDirective} node
* @param {ComponentContext} context
*/
export function TransitionDirective(node, context) {
let flags = node.modifiers.includes('global') ? TRANSITION_GLOBAL : 0;
if (node.intro) flags |= TRANSITION_IN;
if (node.outro) flags |= TRANSITION_OUT;
const args = [
b.literal(flags),
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name))))
];
if (node.expression) {
args.push(b.thunk(/** @type {Expression} */ (context.visit(node.expression))));
}
// in after_update to ensure it always happens after bind:this
context.state.after_update.push(b.stmt(b.call('$.transition', ...args)));
}

@ -0,0 +1,115 @@
/** @import { Expression, Pattern, Statement, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { serialize_get_binding, serialize_set_binding } from '../utils.js';
/**
* @param {UpdateExpression} node
* @param {Context} context
*/
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(serialize_get_binding(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' &&
argument.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(argument.property.name)
) {
let fn = '$.update';
if (node.prefix) fn += '_pre';
/** @type {Expression[]} */
const args = [argument];
if (node.operator === '--') {
args.push(b.literal(-1));
}
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;
}
// 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 serialized_assignment = serialize_set_binding(
assignment,
context,
() => assignment,
node.prefix
);
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 (context.state.analysis.runes) {
return maybe_skip_ownership_validation(serialized_assignment);
}
/** @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))];
}
return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements))));
}

@ -0,0 +1,34 @@
/** @import { Expression } from 'estree' */
/** @import { UseDirective } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { parse_directive_name } from './shared/utils.js';
/**
* @param {UseDirective} node
* @param {ComponentContext} context
*/
export function UseDirective(node, context) {
const params = [b.id('$$node')];
if (node.expression) {
params.push(b.id('$$action_arg'));
}
/** @type {Expression[]} */
const args = [
context.state.node,
b.arrow(
params,
b.call(/** @type {Expression} */ (context.visit(parse_directive_name(node.name))), ...params)
)
];
if (node.expression) {
args.push(b.thunk(/** @type {Expression} */ (context.visit(node.expression))));
}
// actions need to run after attribute updates in order with bindings/events
context.state.after_update.push(b.stmt(b.call('$.action', ...args)));
context.next();
}

@ -0,0 +1,328 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import {
get_prop_source,
is_prop_source,
is_state_source,
should_proxy_or_freeze
} from '../utils.js';
import { is_hoistable_function } from '../../utils.js';
/**
* @param {VariableDeclaration} node
* @param {ComponentContext} context
*/
export function VariableDeclaration(node, context) {
/** @type {VariableDeclarator[]} */
const declarations = [];
if (context.state.analysis.runes) {
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, context.state.scope);
if (
!rune ||
rune === '$effect.tracking' ||
rune === '$effect.root' ||
rune === '$inspect' ||
rune === '$state.snapshot' ||
rune === '$state.is'
) {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = context.visit(init);
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (rune === '$props') {
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
if (context.state.analysis.custom_element) {
seen.push('$$host');
}
if (declarator.id.type === 'Identifier') {
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {Identifier | Literal} */ (property.key);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {Binding} */ (context.state.scope.get(id.name));
let initial =
binding.initial && /** @type {Expression} */ (context.visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (
initial &&
binding.kind === 'bindable_prop' &&
should_proxy_or_freeze(initial, context.state.scope)
) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, context.state)) {
declarations.push(
b.declarator(id, get_prop_source(binding, context.state, name, initial))
);
}
} else {
// RestElement
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(/** @type {Identifier} */ (property.argument).name));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}
}
// TODO
continue;
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
if (rune === '$state' || rune === '$state.frozen') {
/**
* @param {Identifier} id
* @param {Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name)
);
if (should_proxy_or_freeze(value, context.state.scope)) {
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
}
if (is_state_source(binding, context.state)) {
value = b.call('$.source', value);
}
return value;
};
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
);
} else {
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}
continue;
}
if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
} else {
const bindings = context.state.scope.get_bindings(declarator);
const object_id = context.state.scope.generate('derived_object');
const values_id = context.state.scope.generate('derived_values');
declarations.push(
b.declarator(
b.id(object_id),
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
declarations.push(
b.declarator(
b.id(values_id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, b.call('$.get', b.id(object_id))),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
)
)
);
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
declarations.push(
b.declarator(
binding.node,
b.call(
'$.derived',
b.thunk(b.member(b.call('$.get', b.id(values_id)), b.literal(i), true))
)
)
);
}
}
continue;
}
}
} else {
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && is_hoistable_function(init)) {
const hoistable_function = context.visit(init);
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(
b.id(tmp),
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const value = path.expression?.(b.id(tmp));
declarations.push(
b.declarator(
path.node,
binding.kind === 'bindable_prop'
? get_prop_source(binding, context.state, binding.prop_alias ?? name, value)
: value
)
);
}
continue;
}
const binding = /** @type {Binding} */ (context.state.scope.get(declarator.id.name));
declarations.push(
b.declarator(
declarator.id,
get_prop_source(
binding,
context.state,
binding.prop_alias ?? declarator.id.name,
declarator.init && /** @type {Expression} */ (context.visit(declarator.init))
)
)
);
continue;
}
declarations.push(
...create_state_declarators(
declarator,
context.state.scope,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
)
);
}
}
if (declarations.length === 0) {
return b.empty;
}
return {
...node,
declarations
};
}
/**
* Creates the output for a state declaration.
* @param {VariableDeclarator} declarator
* @param {Scope} scope
* @param {Expression} value
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
return [
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}

@ -1,163 +0,0 @@
/** @import { Expression, Node, Pattern, Statement } from 'estree' */
/** @import { Visitors } from '../types' */
import is_reference from 'is-reference';
import { serialize_get_binding, serialize_set_binding } from '../utils.js';
import * as b from '../../../../utils/builders.js';
import { is_ignored } from '../../../../state.js';
/** @type {Visitors} */
export const global_visitors = {
Identifier(node, { path, state }) {
if (is_reference(node, /** @type {Node} */ (path.at(-1)))) {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state);
}
},
MemberExpression(node, { state, next }) {
// rewrite `this.#foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'PrivateIdentifier') {
const field = state.private_state.get(node.property.name);
if (field) {
return state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node);
}
} else if (node.object.type === 'ThisExpression') {
// rewrite `this.foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'Identifier' && !node.computed) {
const field = state.public_state.get(node.property.name);
if (field && state.in_constructor) {
return b.member(b.member(b.this, field.id), b.id('v'));
}
}
}
next();
},
AssignmentExpression(node, context) {
return serialize_set_binding(node, context, context.next);
},
UpdateExpression(node, context) {
const { state, next, visit } = context;
const argument = node.argument;
if (argument.type === 'Identifier') {
const binding = 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(serialize_get_binding(b.id(name), 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 next();
} else if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
argument.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(argument.property.name)
) {
let fn = '$.update';
if (node.prefix) fn += '_pre';
/** @type {Expression[]} */
const args = [argument];
if (node.operator === '--') {
args.push(b.literal(-1));
}
return b.call(fn, ...args);
} else {
/** @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;
}
// turn it into an IIFEE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; })
const assignment = b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
);
const serialized_assignment = serialize_set_binding(
assignment,
context,
() => assignment,
node.prefix
);
const value = /** @type {Expression} */ (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(next());
} else if (context.state.analysis.runes) {
return maybe_skip_ownership_validation(serialized_assignment);
} else {
/** @type {Statement[]} */
let statements;
if (node.prefix) {
statements = [b.stmt(serialized_assignment), b.return(value)];
} else {
const tmp_id = state.scope.generate('$$value');
statements = [
b.const(tmp_id, value),
b.stmt(serialized_assignment),
b.return(b.id(tmp_id))
];
}
return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements))));
}
}
}
};

@ -1,188 +0,0 @@
/** @import { Binding, ReactiveStatement } from '#compiler' */
/** @import { ComponentVisitors } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
/** @import { VariableDeclarator, Expression, Identifier, Statement } from 'estree' */
import { is_hoistable_function } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import { get_prop_source, serialize_get_binding } from '../utils.js';
/**
* Creates the output for a state declaration.
* @param {VariableDeclarator} declarator
* @param {Scope} scope
* @param {Expression} value
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
return [
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}
/** @type {ComponentVisitors} */
export const javascript_visitors_legacy = {
VariableDeclaration(node, { state, visit }) {
/** @type {VariableDeclarator[]} */
const declarations = [];
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
}
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(
b.id(tmp),
/** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (state.scope.get(name));
const value = path.expression?.(b.id(tmp));
declarations.push(
b.declarator(
path.node,
binding.kind === 'bindable_prop'
? get_prop_source(binding, state, binding.prop_alias ?? name, value)
: value
)
);
}
continue;
}
const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
declarations.push(
b.declarator(
declarator.id,
get_prop_source(
binding,
state,
binding.prop_alias ?? declarator.id.name,
declarator.init && /** @type {Expression} */ (visit(declarator.init))
)
)
);
continue;
}
declarations.push(
...create_state_declarators(
declarator,
state.scope,
/** @type {Expression} */ (declarator.init && visit(declarator.init))
)
);
}
if (declarations.length === 0) {
return b.empty;
}
return {
...node,
declarations
};
},
LabeledStatement(node, context) {
if (context.path.length > 1 || node.label.name !== '$') {
context.next();
return;
}
const state = context.state;
// To recreate Svelte 4 behaviour, we track the dependencies
// the compiler can 'see', but we untrack the effect itself
const reactive_stmt = /** @type {ReactiveStatement} */ (
state.analysis.reactive_statements.get(node)
);
if (!reactive_stmt) return; // not the instance context
const { dependencies } = reactive_stmt;
let serialized_body = /** @type {Statement} */ (context.visit(node.body));
if (serialized_body.type !== 'BlockStatement') {
serialized_body = b.block([serialized_body]);
}
const body = serialized_body.body;
/** @type {Expression[]} */
const sequence = [];
for (const binding of dependencies) {
if (binding.kind === 'normal') continue;
const name = binding.node.name;
let serialized = serialize_get_binding(b.id(name), 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);
}
// these statements will be topologically ordered later
state.legacy_reactive_statements.set(
node,
b.stmt(
b.call(
'$.legacy_pre_effect',
sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])),
b.thunk(b.block(body))
)
)
);
return b.empty;
},
BreakStatement(node, context) {
if (!node.label || node.label.name !== '$') return;
const in_reactive_statement =
context.path[1].type === 'LabeledStatement' && context.path[1].label.name === '$';
if (in_reactive_statement) {
return b.return();
}
}
};

@ -1,513 +0,0 @@
/** @import { CallExpression, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentVisitors, StateField } from '../types.js' */
import { dev, is_ignored } from '../../../../state.js';
import * as assert from '../../../../utils/assert.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import {
get_prop_source,
is_prop_source,
is_state_source,
serialize_proxy_reassignment,
should_proxy_or_freeze
} from '../utils.js';
/** @type {ComponentVisitors} */
export const javascript_visitors_runes = {
ClassBody(node, { state, visit }) {
/** @type {Map<string, StateField>} */
const public_state = new Map();
/** @type {Map<string, StateField>} */
const private_state = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const type = definition.key.type;
const name = get_name(definition.key);
if (!name) continue;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope);
if (
rune === '$state' ||
rune === '$state.frozen' ||
rune === '$derived' ||
rune === '$derived.by'
) {
/** @type {StateField} */
const field = {
kind:
rune === '$state'
? 'state'
: rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived.by'
? 'derived_call'
: 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_state.set(name, field);
} else {
public_state.set(name, field);
}
}
}
}
}
// each `foo = $state()` needs a backing `#foo` field
for (const [name, field] of public_state) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
const child_state = { ...state, public_state, private_state };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' ||
definition.key.type === 'PrivateIdentifier' ||
definition.key.type === 'Literal')
) {
const name = get_name(definition.key);
if (!name) continue;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_state : public_state).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
let value = null;
if (definition.value.arguments.length > 0) {
const init = /** @type {Expression} **/ (
visit(definition.value.arguments[0], child_state)
);
value =
field.kind === 'state'
? b.call(
'$.source',
should_proxy_or_freeze(init, state.scope) ? b.call('$.proxy', init) : init
)
: field.kind === 'frozen_state'
? b.call(
'$.source',
should_proxy_or_freeze(init, state.scope) ? b.call('$.freeze', init) : init
)
: field.kind === 'derived_call'
? b.call('$.derived', init)
: b.call('$.derived', b.thunk(init));
} else {
// if no arguments, we know it's state as `$derived()` is a compile error
value = b.call('$.source');
}
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))]));
if (field.kind === 'state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, serialize_proxy_reassignment(value, field.id)))]
)
);
}
if (field.kind === 'frozen_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
);
}
if (dev && (field.kind === 'derived' || field.kind === 'derived_call')) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (visit(definition, child_state)));
}
if (dev && public_state.size > 0) {
// add an `[$.ADD_OWNER]` method so that a class with state fields can widen ownership
body.push(
b.method(
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
Array.from(public_state.keys()).map((name) =>
b.stmt(
b.call(
'$.add_owner',
b.call('$.get', b.member(b.this, b.private_id(name))),
b.id('owner'),
b.literal(false),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
),
true
)
);
}
return { ...node, body };
},
VariableDeclaration(node, { state, visit }) {
const declarations = [];
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, state.scope);
if (
!rune ||
rune === '$effect.tracking' ||
rune === '$effect.root' ||
rune === '$inspect' ||
rune === '$state.snapshot' ||
rune === '$state.is'
) {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
}
if (rune === '$props') {
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
if (state.analysis.custom_element) {
seen.push('$$host');
}
if (declarator.id.type === 'Identifier') {
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {Identifier | Literal} */ (property.key);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {Binding} */ (state.scope.get(id.name));
let initial = binding.initial && /** @type {Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (
initial &&
binding.kind === 'bindable_prop' &&
should_proxy_or_freeze(initial, state.scope)
) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(/** @type {Identifier} */ (property.argument).name));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}
}
// TODO
continue;
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (visit(args[0]));
if (rune === '$state' || rune === '$state.frozen') {
/**
* @param {Identifier} id
* @param {Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {Binding} */ (state.scope.get(id.name));
if (should_proxy_or_freeze(value, state.scope)) {
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
}
if (is_state_source(binding, state)) {
value = b.call('$.source', value);
}
return value;
};
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
);
} else {
const tmp = state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}
continue;
}
if (rune === '$derived' || rune === '$derived.by') {
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(
declarator.id,
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
} else {
const bindings = state.scope.get_bindings(declarator);
const object_id = state.scope.generate('derived_object');
const values_id = state.scope.generate('derived_values');
declarations.push(
b.declarator(
b.id(object_id),
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
declarations.push(
b.declarator(
b.id(values_id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, b.call('$.get', b.id(object_id))),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
)
)
);
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
declarations.push(
b.declarator(
binding.node,
b.call(
'$.derived',
b.thunk(b.member(b.call('$.get', b.id(values_id)), b.literal(i), true))
)
)
);
}
}
continue;
}
}
if (declarations.length === 0) {
return b.empty;
}
return {
...node,
declarations
};
},
ExpressionStatement(node, context) {
if (node.expression.type === 'CallExpression') {
const callee = node.expression.callee;
if (
callee.type === 'Identifier' &&
callee.name === '$effect' &&
!context.state.scope.get('$effect')
) {
const func = context.visit(node.expression.arguments[0]);
return {
...node,
expression: b.call('$.user_effect', /** @type {Expression} */ (func))
};
}
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === '$effect' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'pre' &&
!context.state.scope.get('$effect')
) {
const func = context.visit(node.expression.arguments[0]);
return {
...node,
expression: b.call('$.user_pre_effect', /** @type {Expression} */ (func))
};
}
}
context.next();
},
CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('$$props.$$host');
}
if (rune === '$effect.tracking') {
return b.call('$.effect_tracking');
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
/** @type {Expression} */ (context.visit(node.arguments[0])),
is_ignored(node, 'state_snapshot_uncloneable') && b.true
);
}
if (rune === '$state.is') {
return b.call(
'$.is',
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
}
if (rune === '$effect.root') {
const args = /** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)));
return b.call('$.effect_root', ...args);
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
}
context.next();
},
BinaryExpression(node, { state, visit, next }) {
const operator = node.operator;
if (dev) {
if (operator === '===' || operator === '!==') {
return b.call(
'$.strict_equals',
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right)),
operator === '!==' && b.literal(false)
);
}
if (operator === '==' || operator === '!=') {
return b.call(
'$.equals',
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right)),
operator === '!=' && b.literal(false)
);
}
}
next();
}
};
/**
* @param {Identifier | PrivateIdentifier | Literal} node
*/
function get_name(node) {
if (node.type === 'Literal') {
return node.value?.toString().replace(regex_invalid_identifier_chars, '_');
} else {
return node.name;
}
}

@ -1,30 +0,0 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentVisitors } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { function_visitor, serialize_hoistable_params } from '../utils.js';
/** @type {ComponentVisitors} */
export const javascript_visitors = {
FunctionExpression: function_visitor,
ArrowFunctionExpression: function_visitor,
FunctionDeclaration(node, context) {
const metadata = node.metadata;
const state = { ...context.state, in_constructor: false };
if (metadata?.hoistable === true) {
const params = serialize_hoistable_params(node, context);
context.state.hoisted.push(
/** @type {FunctionDeclaration} */ ({
...node,
id: node.id !== null ? context.visit(node.id, state) : null,
params,
body: context.visit(node.body, state)
})
);
return b.empty;
}
context.next(state);
}
};

@ -0,0 +1,366 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */
/** @import { Attribute, Component, SvelteComponent, SvelteSelf, TemplateNode, Text } 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, serialize_set_binding } from '../../utils.js';
import {
serialize_bind_this,
serialize_event_handler,
serialize_validate_binding
} from '../shared/utils.js';
import { serialize_attribute_value } from '../shared/element.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {string} component_name
* @param {ComponentContext} context
* @param {Expression} anchor
* @returns {Statement}
*/
export function serialize_component(node, component_name, context, anchor = context.state.node) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {ExpressionStatement[]} */
const lets = [];
/** @type {Record<string, TemplateNode[]>} */
const children = {};
/** @type {Record<string, Expression[]>} */
const events = {};
/** @type {Property[]} */
const custom_css_props = [];
/** @type {Identifier | MemberExpression | null} */
let bind_this = null;
/**
* @type {ExpressionStatement[]}
*/
const binding_initializers = [];
/**
* 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;
/**
* Components may have a children prop and also have child nodes. In this case, we assume
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
* We're not doing it for spread attributes, as this would result in too many false positives.
*/
let has_children_prop = false;
/**
* @param {Property} prop
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
events[attribute.name] ||= [];
let handler = serialize_event_handler(attribute, null, context);
if (attribute.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
events[attribute.name].push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
let value = 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);
}
props_and_spreads.push(b.thunk(value));
} else {
props_and_spreads.push(expression);
}
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
custom_css_props.push(
b.init(attribute.name, serialize_attribute_value(attribute.value, context)[1])
);
continue;
}
if (attribute.name === 'slot') {
slot_scope_applies_to_itself = true;
}
if (attribute.name === 'children') {
has_children_prop = true;
}
const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.metadata.expression.has_state) {
let arg = value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// then there's a good chance it needs to be memoized to avoid over-firing when read within the
// child component.
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
return (
n.type === 'ExpressionTag' &&
n.expression.type !== 'Identifier' &&
n.expression.type !== 'MemberExpression'
);
});
if (should_wrap_in_derived) {
const id = b.id(context.state.scope.generate(attribute.name));
context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value))));
arg = b.call('$.get', id);
}
push_prop(b.get(attribute.name, [b.return(arg)]));
} else {
push_prop(b.init(attribute.name, value));
}
} else if (attribute.type === 'BindDirective') {
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (
dev &&
expression.type === 'MemberExpression' &&
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(serialize_validate_binding(context.state, attribute, expression));
}
if (attribute.name === 'this') {
bind_this = attribute.expression;
} else {
if (dev) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
)
);
}
push_prop(b.get(attribute.name, [b.return(expression)]));
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [
b.stmt(serialize_set_binding(assignment, context, () => context.visit(assignment)))
])
);
}
}
}
if (slot_scope_applies_to_itself) {
context.state.init.push(...lets);
}
if (Object.keys(events).length > 0) {
const events_expression = b.object(
Object.keys(events).map((name) =>
b.init(name, events[name].length > 1 ? b.array(events[name]) : events[name][0])
)
);
push_prop(b.init('$$events', events_expression));
}
/** @type {Statement[]} */
const snippet_declarations = [];
// Group children by slot
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
// inside a component then we want to hoist them into a block so that they
// can be used as props without creating conflicts
context.visit(child, {
...context.state,
init: snippet_declarations
});
push_prop(b.prop('init', child.expression, child.expression));
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;
}
}
(children[slot_name] ||= []).push(child);
}
// Serialize each slot
/** @type {Property[]} */
const serialized_slots = [];
for (const slot_name of Object.keys(children)) {
const block = /** @type {BlockStatement} */ (
context.visit(
{
...node.fragment,
// @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
}
)
);
if (block.body.length === 0) continue;
const slot_fn = b.arrow(
[b.id('$$anchor'), b.id('$$slotProps')],
b.block([
...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []),
...block.body
])
);
if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop...
push_prop(
b.init(
'children',
dev ? b.call('$.wrap_snippet', b.id(context.state.analysis.name), slot_fn) : slot_fn
)
);
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
serialized_slots.push(b.init(slot_name, slot_fn));
}
}
if (serialized_slots.length > 0) {
push_prop(b.init('$$slots', b.object(serialized_slots)));
}
if (!context.state.analysis.runes) {
push_prop(b.init('$$legacy', b.true));
}
const props_expression =
props_and_spreads.length === 0 ||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0]))
? b.object(/** @type {Property[]} */ (props_and_spreads[0]) || [])
: b.call(
'$.spread_props',
...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))
);
/** @param {Expression} node_id */
let fn = (node_id) => {
return b.call(
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
node.type === 'SvelteComponent'
? component_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
props_expression
);
};
if (bind_this !== null) {
const prev = fn;
fn = (node_id) => {
return serialize_bind_this(bind_this, prev(node_id), context);
};
}
const statements = [...snippet_declarations];
if (node.type === 'SvelteComponent') {
const prev = fn;
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id('$$anchor'), b.id(component_name)],
b.block([
...binding_initializers,
b.stmt(
dev
? b.call('$.validate_dynamic_component', b.thunk(prev(b.id('$$anchor'))))
: prev(b.id('$$anchor'))
)
])
)
);
};
} else {
statements.push(...binding_initializers);
}
if (Object.keys(custom_css_props).length > 0) {
context.state.template.push(
context.state.metadata.namespace === 'svg'
? '<g><!></g>'
: '<div style="display: contents"><!></div>'
);
statements.push(
b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))),
b.stmt(fn(b.member(anchor, b.id('lastChild')))),
b.stmt(b.call('$.reset', anchor))
);
} else {
context.state.template.push('<!>');
statements.push(b.stmt(fn(anchor)));
}
return statements.length > 1 ? b.block(statements) : statements[0];
}

@ -0,0 +1,279 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Attribute, ClassDirective, DelegatedEvent, ExpressionMetadata, ExpressionTag, Namespace, OnDirective, RegularElement, StyleDirective, SvelteElement, SvelteNode } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { AttributeAliases, is_capture_event } from '../../../../../../constants.js';
import { is_ignored } from '../../../../../state.js';
import { get_attribute_expression } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { DOMProperties, PassiveEvents } from '../../../../constants.js';
import { serialize_get_binding } from '../../utils.js';
import {
serialize_event_handler,
serialize_template_literal,
serialize_update,
serialize_update_assignment
} from './utils.js';
/**
* Serializes each style directive into something like `$.set_style(element, style_property, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {StyleDirective[]} style_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
* @param {boolean} force_check Should be `true` if we can't rely on our cached value, because for example there's also a `style` attribute
*/
export function serialize_style_directives(
style_directives,
element_id,
context,
is_attributes_reactive,
force_check
) {
const state = context.state;
for (const directive of style_directives) {
let value =
directive.value === true
? serialize_get_binding({ name: directive.name, type: 'Identifier' }, context.state)
: serialize_attribute_value(directive.value, context)[1];
const update = b.stmt(
b.call(
'$.set_style',
element_id,
b.literal(directive.name),
value,
/** @type {Expression} */ (directive.modifiers.includes('important') ? b.true : undefined),
force_check ? b.true : undefined
)
);
const { has_state, has_call } = directive.metadata.expression;
if (!is_attributes_reactive && has_call) {
state.init.push(serialize_update(update));
} else if (is_attributes_reactive || has_state || has_call) {
state.update.push(update);
} else {
state.init.push(update);
}
}
}
/**
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {ClassDirective[]} class_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
export function serialize_class_directives(
class_directives,
element_id,
context,
is_attributes_reactive
) {
const state = context.state;
for (const directive of class_directives) {
const value = /** @type {Expression} */ (context.visit(directive.expression));
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
const { has_state, has_call } = directive.metadata.expression;
if (!is_attributes_reactive && has_call) {
state.init.push(serialize_update(update));
} else if (is_attributes_reactive || has_state || has_call) {
state.update.push(update);
} else {
state.init.push(update);
}
}
}
/**
* @param {Attribute['value']} value
* @param {ComponentContext} context
* @returns {[has_call: boolean, Expression]}
*/
export function serialize_attribute_value(value, context) {
if (value === true) {
return [false, b.literal(true)];
}
if (!Array.isArray(value) || value.length === 1) {
const chunk = Array.isArray(value) ? value[0] : value;
if (chunk.type === 'Text') {
return [false, b.literal(chunk.data)];
}
return [
chunk.metadata.expression.has_call,
/** @type {Expression} */ (context.visit(chunk.expression))
];
}
return serialize_template_literal(value, context.visit, context.state);
}
/**
* @param {RegularElement | SvelteElement} element
* @param {Attribute} attribute
* @param {{ state: { metadata: { namespace: Namespace }}}} context
*/
export function get_attribute_name(element, attribute, context) {
let name = attribute.name;
if (
!element.metadata.svg &&
!element.metadata.mathml &&
context.state.metadata.namespace !== 'foreign'
) {
name = name.toLowerCase();
if (name in AttributeAliases) {
name = AttributeAliases[name];
}
}
return name;
}
/**
* @param {Attribute & { value: ExpressionTag | [ExpressionTag] }} node
* @param {ComponentContext} context
*/
export function serialize_event_attribute(node, context) {
/** @type {string[]} */
const modifiers = [];
let event_name = node.name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
modifiers.push('capture');
}
serialize_event(
{
name: event_name,
expression: get_attribute_expression(node),
modifiers,
delegated: node.metadata.delegated
},
!Array.isArray(node.value) && node.value?.type === 'ExpressionTag'
? node.value.metadata.expression
: null,
context
);
}
/**
* 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 | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
export function serialize_event(node, metadata, context) {
const state = context.state;
/** @type {Expression} */
let expression;
if (node.expression) {
let handler = serialize_event_handler(node, metadata, context);
const event_name = node.name;
const delegated = node.delegated;
if (delegated != null) {
let delegated_assignment;
if (!state.events.has(event_name)) {
state.events.add(event_name);
}
// Hoist function if we can, otherwise we leave the function as is
if (delegated.type === 'hoistable') {
if (delegated.function === node.expression) {
const func_name = context.state.scope.root.unique('on_' + event_name);
state.hoisted.push(b.var(func_name, handler));
handler = func_name;
}
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
const hoistable_params = /** @type {Expression[]} */ (
delegated.function.metadata.hoistable_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
const args = [handler, ...hoistable_params];
delegated_assignment = b.array(args);
} else {
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
delegated_assignment = handler;
}
state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name)),
delegated_assignment
)
)
);
return;
}
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
const args = [
b.literal(event_name),
context.state.node,
handler,
b.literal(node.modifiers.includes('capture'))
];
if (node.modifiers.includes('passive')) {
args.push(b.literal(true));
} else if (node.modifiers.includes('nonpassive')) {
args.push(b.literal(false));
} else if (
PassiveEvents.includes(node.name) &&
/** @type {OnDirective} */ (node).type !== 'OnDirective'
) {
// For on:something events we don't apply passive behaviour to match Svelte 4.
args.push(b.literal(true));
}
// Events need to run in order with bindings/actions
expression = b.call('$.event', ...args);
} else {
expression = b.call(
'$.event',
b.literal(node.name),
state.node,
serialize_event_handler(node, metadata, context)
);
}
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
const has_action_directive =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
const statement = b.stmt(
has_action_directive ? b.call('$.effect', b.thunk(expression)) : expression
);
if (
parent.type === 'SvelteDocument' ||
parent.type === 'SvelteWindow' ||
parent.type === 'SvelteBody'
) {
// These nodes are above the component tree, and its events should run parent first
state.before_init.push(statement);
} else {
state.after_update.push(statement);
}
}

@ -0,0 +1,159 @@
/** @import { Expression } from 'estree' */
/** @import { ExpressionTag, SvelteNode, Text } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import * as b from '../../../../../utils/builders.js';
import { serialize_template_literal, serialize_update } from './utils.js';
/**
* Processes an array of template nodes, joining sibling text/expression nodes
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates
* corresponding template node references these updates are applied to.
* @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} expression
* @param {boolean} is_element
* @param {ComponentContext} context
*/
export function process_children(nodes, expression, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
/** @typedef {Array<Text | ExpressionTag>} Sequence */
/** @type {Sequence} */
let sequence = [];
/**
* @param {Sequence} sequence
*/
function flush_sequence(sequence) {
if (sequence.length === 1) {
const node = sequence[0];
if (node.type === 'Text') {
let prev = expression;
expression = () => b.call('$.sibling', prev(true));
state.template.push(node.raw);
return;
}
state.template.push(' ');
const text_id = get_node_id(expression(true), state, 'text');
const update = b.stmt(
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state)))
);
if (node.metadata.expression.has_call && !within_bound_contenteditable) {
state.init.push(serialize_update(update));
} else if (node.metadata.expression.has_state && !within_bound_contenteditable) {
state.update.push(update);
} else {
state.init.push(
b.stmt(
b.assignment(
'=',
b.member(text_id, b.id('nodeValue')),
/** @type {Expression} */ (visit(node.expression))
)
)
);
}
expression = (is_text) =>
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
} else {
const text_id = get_node_id(expression(true), state, 'text');
state.template.push(' ');
const [has_call, value] = serialize_template_literal(sequence, visit, state);
const update = b.stmt(b.call('$.set_text', text_id, value));
if (has_call && !within_bound_contenteditable) {
state.init.push(serialize_update(update));
} else if (
sequence.some(
(node) => node.type === 'ExpressionTag' && node.metadata.expression.has_state
) &&
!within_bound_contenteditable
) {
state.update.push(update);
} else {
state.init.push(b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), value)));
}
expression = (is_text) =>
is_text ? b.call('$.sibling', text_id, b.true) : b.call('$.sibling', text_id);
}
}
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.type === 'Text' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
if (sequence.length > 0) {
flush_sequence(sequence);
sequence = [];
}
if (
node.type === 'SvelteHead' ||
node.type === 'TitleElement' ||
node.type === 'SnippetBlock'
) {
// These nodes do not contribute to the sibling/child tree
// TODO what about e.g. ConstTag and all the other things that
// get hoisted inside clean_nodes?
visit(node, state);
} else {
if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
visit(node, state);
} else {
const id = get_node_id(
expression(false),
state,
node.type === 'RegularElement' ? node.name : 'node'
);
expression = (is_text) =>
is_text ? b.call('$.sibling', id, b.true) : b.call('$.sibling', id);
visit(node, {
...state,
node: id
});
}
}
}
}
if (sequence.length > 0) {
// if the final item in a fragment is static text,
// we need to force `hydrate_node` to advance
if (sequence.length === 1 && sequence[0].type === 'Text' && nodes.length > 1) {
state.init.push(b.stmt(b.call('$.next')));
}
flush_sequence(sequence);
}
}
/**
* @param {Expression} expression
* @param {ComponentClientTransformState} state
* @param {string} name
*/
function get_node_id(expression, state, name) {
let id = expression;
if (id.type !== 'Identifier') {
id = b.id(state.scope.generate(name));
state.init.push(b.var(id, expression));
}
return id;
}

@ -0,0 +1,34 @@
/** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */
/** @import { ComponentContext } from '../../types' */
import { serialize_hoistable_params } from '../../utils.js';
/**
* @param {ArrowFunctionExpression | FunctionExpression} node
* @param {ComponentContext} context
*/
export const visit_function = (node, context) => {
const metadata = node.metadata;
let state = context.state;
if (node.type === 'FunctionExpression') {
const parent = /** @type {Node} */ (context.path.at(-1));
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
state = { ...context.state, in_constructor };
} else {
state = { ...context.state, in_constructor: false };
}
if (metadata?.hoistable === true) {
const params = serialize_hoistable_params(node, context);
return /** @type {FunctionExpression} */ ({
...node,
params,
body: context.visit(node.body, state)
});
}
context.next(state);
};

@ -0,0 +1,335 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { BindDirective, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode, Text } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js';
import { create_derived } from '../../utils.js';
import is_reference from 'is-reference';
import { locator } from '../../../../../state.js';
/**
* @param {Array<Text | ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @returns {[boolean, TemplateLiteral]}
*/
export function serialize_template_literal(values, visit, state) {
/** @type {TemplateElement[]} */
const quasis = [];
/** @type {Expression[]} */
const expressions = [];
let has_call = false;
let contains_multiple_call_expression = false;
quasis.push(b.quasi(''));
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'ExpressionTag' && node.metadata.expression.has_call) {
if (has_call) {
contains_multiple_call_expression = true;
}
has_call = true;
}
}
for (let i = 0; i < values.length; i++) {
const node = values[i];
if (node.type === 'Text') {
const last = /** @type {TemplateElement} */ (quasis.at(-1));
last.value.raw += sanitize_template_string(node.data);
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
const last = /** @type {TemplateElement} */ (quasis.at(-1));
if (node.expression.value != null) {
last.value.raw += sanitize_template_string(node.expression.value + '');
}
} else {
if (contains_multiple_call_expression) {
const id = b.id(state.scope.generate('stringified_text'));
state.init.push(
b.const(
id,
create_derived(
state,
b.thunk(/** @type {Expression} */ (visit(node.expression, state)))
)
)
);
expressions.push(b.call('$.get', id));
} else {
expressions.push(b.logical('??', visit(node.expression, state), b.literal('')));
}
quasis.push(b.quasi('', i + 1 === values.length));
}
}
// TODO instead of this tuple, return a `{ dynamic, complex, value }` object. will DRY stuff out
return [has_call, b.template(quasis, expressions)];
}
/**
* @param {Statement} statement
*/
export function serialize_update(statement) {
const body =
statement.type === 'ExpressionStatement' ? statement.expression : b.block([statement]);
return b.stmt(b.call('$.template_effect', b.thunk(body)));
}
/**
* @param {Statement[]} update
*/
export function serialize_render_stmt(update) {
return update.length === 1
? serialize_update(update[0])
: b.stmt(b.call('$.template_effect', b.thunk(b.block(update))));
}
/**
* For unfortunate legacy reasons, directive names can look like this `use:a.b-c`
* This turns that string into a member expression
* @param {string} name
*/
export function parse_directive_name(name) {
// this allow for accessing members of an object
const parts = name.split('.');
let part = /** @type {string} */ (parts.shift());
/** @type {Identifier | MemberExpression} */
let expression = b.id(part);
while ((part = /** @type {string} */ (parts.shift()))) {
const computed = !regex_is_valid_identifier.test(part);
expression = b.member(expression, computed ? b.literal(part) : b.id(part), computed);
}
return expression;
}
/**
* @param {ComponentClientTransformState} state
* @param {string} id
* @param {Expression | undefined} init
* @param {Expression} value
* @param {ExpressionStatement} update
*/
export function serialize_update_assignment(state, id, init, value, update) {
state.init.push(b.var(id, init));
state.update.push(
b.if(b.binary('!==', b.id(id), b.assignment('=', b.id(id), value)), b.block([update]))
);
}
/**
* Serializes the event handler function of the `on:` directive
* @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
export function serialize_event_handler(node, metadata, { state, visit }) {
/** @type {Expression} */
let handler;
if (node.expression) {
handler = node.expression;
// Event handlers can be dynamic (source/store/prop/conditional etc)
const dynamic_handler = () =>
b.function(
null,
[b.rest(b.id('$$args'))],
b.block([
b.return(
b.call(
b.member(/** @type {Expression} */ (visit(handler)), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
if (
metadata?.has_call &&
!(
(handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') &&
handler.metadata.hoistable
)
) {
// Create a derived dynamic event handler
const id = b.id(state.scope.generate('event_handler'));
state.init.push(
b.var(id, b.call('$.derived', b.thunk(/** @type {Expression} */ (visit(handler)))))
);
handler = b.function(
null,
[b.rest(b.id('$$args'))],
b.block([
b.return(
b.call(
b.member(b.call('$.get', id), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
} else if (handler.type === 'Identifier' || handler.type === 'MemberExpression') {
const id = object(handler);
const binding = id === null ? null : state.scope.get(id.name);
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.declaration_kind === 'import' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'store_sub')
) {
handler = dynamic_handler();
} else {
handler = /** @type {Expression} */ (visit(handler));
}
} else if (handler.type === 'ConditionalExpression' || handler.type === 'LogicalExpression') {
handler = dynamic_handler();
} else {
handler = /** @type {Expression} */ (visit(handler));
}
} else {
state.analysis.needs_props = true;
// Function + .call to preserve "this" context as much as possible
handler = b.function(
null,
[b.id('$$arg')],
b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))])
);
}
if (node.modifiers.includes('stopPropagation')) {
handler = b.call('$.stopPropagation', handler);
}
if (node.modifiers.includes('stopImmediatePropagation')) {
handler = b.call('$.stopImmediatePropagation', handler);
}
if (node.modifiers.includes('preventDefault')) {
handler = b.call('$.preventDefault', handler);
}
if (node.modifiers.includes('self')) {
handler = b.call('$.self', handler);
}
if (node.modifiers.includes('trusted')) {
handler = b.call('$.trusted', handler);
}
return handler;
}
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} expression
* @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
*/
export function serialize_bind_this(expression, value, { state, visit }) {
/** @type {Identifier[]} */
const ids = [];
/** @type {Expression[]} */
const values = [];
/** @type {typeof state.getters} */
const getters = {};
// 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
// some scenarios where the value is a member expression with changing computed parts or using a combination of multiple
// 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;
const parent = /** @type {Expression} */ (path.at(-1));
if (!is_reference(node, parent)) return;
const binding = state.scope.get(node.name);
if (!binding) return;
for (const [owner, scope] of state.scopes) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
values.push(/** @type {Expression} */ (visit(node)));
getters[node.name] = node;
break;
}
}
}
});
const child_state = { ...state, getters: { ...state.getters, ...getters } };
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (
visit(b.assignment('=', expression, b.id('$$value')), child_state)
);
// If we're mutating a property, then it might already be non-existent.
// If we make all the object nodes optional, then it avoids any runtime exceptions.
/** @type {Expression | Super} */
let node = get;
while (node.type === 'MemberExpression') {
node.optional = true;
node = node.object;
}
return b.call(
'$.bind_this',
value,
b.arrow([b.id('$$value'), ...ids], set),
b.arrow([...ids], get),
values.length > 0 && b.thunk(b.array(values))
);
}
/**
* @param {ComponentClientTransformState} state
* @param {BindDirective} binding
* @param {MemberExpression} expression
*/
export function serialize_validate_binding(state, binding, expression) {
const string = state.analysis.source.slice(binding.start, binding.end);
const get_object = b.thunk(/** @type {Expression} */ (expression.object));
const get_property = b.thunk(
/** @type {Expression} */ (
expression.computed
? expression.property
: b.literal(/** @type {Identifier} */ (expression.property).name)
)
);
const loc = locator(binding.start);
return b.stmt(
b.call(
'$.validate_binding',
b.literal(string),
get_object,
get_property,
loc && b.literal(loc.line),
loc && b.literal(loc.column)
)
);
}

@ -30,7 +30,7 @@ export function transform_component(analysis, source, options) {
const program =
options.generate === 'server'
? server_component(analysis, options)
: client_component(source, analysis, options);
: client_component(analysis, options);
const js_source_name = get_source_name(options.filename, options.outputFilename, 'input.svelte');
const js = print(program, {

@ -11,20 +11,20 @@ import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBodyRunes } from './visitors/ClassBody.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
import { ConstTag } from './visitors/ConstTag.js';
import { DebugTag } from './visitors/DebugTag.js';
import { EachBlock } from './visitors/EachBlock.js';
import { ExpressionStatementRunes } from './visitors/ExpressionStatement.js';
import { ExpressionStatement } from './visitors/ExpressionStatement.js';
import { Fragment } from './visitors/Fragment.js';
import { HtmlTag } from './visitors/HtmlTag.js';
import { Identifier } from './visitors/Identifier.js';
import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatementLegacy } from './visitors/LabeledStatement.js';
import { MemberExpressionRunes } from './visitors/MemberExpression.js';
import { PropertyDefinitionRunes } from './visitors/PropertyDefinition.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@ -37,34 +37,21 @@ import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import {
VariableDeclarationLegacy,
VariableDeclarationRunes
} from './visitors/VariableDeclaration.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
const global_visitors = {
_: set_scope,
AssignmentExpression,
CallExpression,
ClassBody,
ExpressionStatement,
Identifier,
UpdateExpression
};
/** @type {Visitors} */
const javascript_visitors_runes = {
...global_visitors,
ClassBody: ClassBodyRunes,
ExpressionStatement: ExpressionStatementRunes,
MemberExpression: MemberExpressionRunes,
PropertyDefinition: PropertyDefinitionRunes,
VariableDeclaration: VariableDeclarationRunes
};
/** @type {Visitors} */
const javascript_visitors_legacy = {
...global_visitors,
LabeledStatement: LabeledStatementLegacy,
VariableDeclaration: VariableDeclarationLegacy
LabeledStatement,
MemberExpression,
PropertyDefinition,
UpdateExpression,
VariableDeclaration
};
/** @type {ComponentVisitors} */
@ -102,7 +89,7 @@ export function server_component(analysis, options) {
analysis,
options,
scope: analysis.module.scope,
scopes: analysis.template.scopes,
scopes: analysis.module.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')],
legacy_reactive_statements: new Map(),
// these are set inside the `Fragment` visitor, and cannot be used until then
@ -115,25 +102,15 @@ export function server_component(analysis, options) {
};
const module = /** @type {Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.module.ast),
state,
// @ts-expect-error TODO: zimmerframe types
{
...set_scope(analysis.module.scopes),
...(analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy)
}
)
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
const instance = /** @type {Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.instance.ast),
state,
// @ts-expect-error TODO: zimmerframe types
{ ...state, scopes: analysis.instance.scopes },
{
...set_scope(analysis.instance.scopes),
...(analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy),
...global_visitors,
ImportDeclaration(node) {
state.hoisted.push(node);
return b.empty;
@ -152,13 +129,9 @@ export function server_component(analysis, options) {
const template = /** @type {Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
state,
// @ts-expect-error TODO: zimmerframe types
{
...set_scope(analysis.template.scopes),
...global_visitors,
...template_visitors
}
{ ...state, scopes: analysis.template.scopes },
// @ts-expect-error don't know, don't care
{ ...global_visitors, ...template_visitors }
)
);
@ -423,10 +396,7 @@ export function server_module(analysis, options) {
};
const module = /** @type {Program} */ (
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, {
...set_scope(analysis.module.scopes),
...javascript_visitors_runes
})
walk(/** @type {SvelteNode} */ (analysis.module.ast), state, global_visitors)
);
return {

@ -9,7 +9,12 @@ import { get_rune } from '../../../scope.js';
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBodyRunes(node, context) {
export function ClassBody(node, context) {
if (!context.state.analysis.runes) {
context.next();
return;
}
/** @type {Map<string, StateField>} */
const public_derived = new Map();

@ -1,28 +1,17 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {ExpressionStatement} node
* @param {Context} context
*/
export function ExpressionStatementRunes(node, context) {
const expression = node.expression;
export function ExpressionStatement(node, context) {
const rune = get_rune(node.expression, context.state.scope);
if (expression.type === 'CallExpression') {
const callee = expression.callee;
if (callee.type === 'Identifier' && callee.name === '$effect') {
return b.empty;
}
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === '$effect'
) {
return b.empty;
}
if (rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root') {
return b.empty;
}
context.next();

@ -6,9 +6,10 @@ import * as b from '../../../../utils/builders.js';
* @param {LabeledStatement} node
* @param {Context} context
*/
export function LabeledStatementLegacy(node, context) {
if (context.path.length > 1) return;
if (node.label.name !== '$') return;
export function LabeledStatement(node, context) {
if (context.state.analysis.runes || context.path.length > 1 || node.label.name !== '$') {
return;
}
// TODO bail out if we're in module context

@ -6,8 +6,12 @@ import * as b from '../../../../utils/builders.js';
* @param {MemberExpression} node
* @param {Context} context
*/
export function MemberExpressionRunes(node, context) {
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
export function MemberExpression(node, context) {
if (
context.state.analysis.runes &&
node.object.type === 'ThisExpression' &&
node.property.type === 'PrivateIdentifier'
) {
const field = context.state.private_derived.get(node.property.name);
if (field) {

@ -7,8 +7,8 @@ import { get_rune } from '../../../scope.js';
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinitionRunes(node, context) {
if (node.value != null && node.value.type === 'CallExpression') {
export function PropertyDefinition(node, context) {
if (context.state.analysis.runes && node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, context.state.scope);
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {

@ -11,136 +11,127 @@ import { walk } from 'zimmerframe';
* @param {VariableDeclaration} node
* @param {Context} context
*/
export function VariableDeclarationRunes(node, context) {
export function VariableDeclaration(node, context) {
/** @type {VariableDeclarator[]} */
const declarations = [];
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, context.state.scope);
if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (context.state.analysis.runes) {
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, context.state.scope);
if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (rune === '$props') {
// remove $bindable() from props declaration
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
node.right.type === 'CallExpression' &&
get_rune(node.right, context.state.scope) === '$bindable'
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
: b.id('undefined');
return b.assignment_pattern(node.left, right);
if (rune === '$props') {
// remove $bindable() from props declaration
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
node.right.type === 'CallExpression' &&
get_rune(node.right, context.state.scope) === '$bindable'
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
}
}
});
declarations.push(b.declarator(id, b.id('$$props')));
continue;
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
if (rune === '$derived.by') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
);
continue;
}
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, value));
continue;
}
if (rune === '$derived') {
declarations.push(b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value));
continue;
}
});
declarations.push(b.declarator(id, b.id('$$props')));
continue;
}
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
return {
...node,
declarations
};
}
if (rune === '$derived.by') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
);
continue;
}
/**
* @param {VariableDeclaration} node
* @param {Context} context
*/
export function VariableDeclarationLegacy(node, { state, visit }) {
/** @type {VariableDeclarator[]} */
const declarations = [];
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, value));
continue;
}
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (rune === '$derived') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value)
);
continue;
}
if (!has_state && !has_props) {
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
}
} else {
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (context.state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(
b.id(tmp),
/** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const value = path.expression?.(b.id(tmp));
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (state.scope.get(name));
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = context.state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value)))
b.declarator(
b.id(tmp),
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const value = path.expression?.(b.id(tmp));
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
declarations.push(
b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value)))
);
}
continue;
}
continue;
}
const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
const prop = b.member(
b.id('$$props'),
b.literal(binding.prop_alias ?? declarator.id.name),
true
);
const binding = /** @type {Binding} */ (context.state.scope.get(declarator.id.name));
const prop = b.member(
b.id('$$props'),
b.literal(binding.prop_alias ?? declarator.id.name),
true
);
/** @type {Expression} */
let init = prop;
if (declarator.init) {
const default_value = /** @type {Expression} */ (visit(declarator.init));
init = is_expression_async(default_value)
? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true)))
: b.call('$.value_or_fallback', prop, b.thunk(default_value));
}
/** @type {Expression} */
let init = prop;
if (declarator.init) {
const default_value = /** @type {Expression} */ (context.visit(declarator.init));
init = is_expression_async(default_value)
? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true)))
: b.call('$.value_or_fallback', prop, b.thunk(default_value));
}
declarations.push(b.declarator(declarator.id, init));
declarations.push(b.declarator(declarator.id, init));
continue;
}
continue;
}
declarations.push(
...create_state_declarators(
declarator,
state.scope,
/** @type {Expression} */ (declarator.init && visit(declarator.init))
)
);
declarations.push(
...create_state_declarators(
declarator,
context.state.scope,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
)
);
}
}
return {

@ -49,8 +49,6 @@ function sort_const_tags(nodes, state) {
/** @type {Map<Compiler.Binding, Tag>} */
const tags = new Map();
const { _ } = set_scope(state.scopes);
for (const node of nodes) {
if (node.type === 'ConstTag') {
const declaration = node.declaration.declarations[0];
@ -63,7 +61,8 @@ function sort_const_tags(nodes, state) {
const deps = new Set();
walk(declaration.init, state, {
_,
// @ts-expect-error don't know, don't care
_: set_scope,
Identifier(node, context) {
const parent = /** @type {Expression} */ (context.path.at(-1));

@ -738,22 +738,13 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @template {{ scope: Scope }} State
* @param {Map<SvelteNode, Scope>} scopes
* @returns {Visitors<SvelteNode, State>}
* @template {{ scope: Scope, scopes: Map<SvelteNode, Scope> }} State
* @param {SvelteNode} node
* @param {Context<SvelteNode, State>} context
*/
export function set_scope(scopes) {
return {
/**
*
* @param {SvelteNode} node
* @param {Context<SvelteNode, State>} context
*/
_(node, { next, state }) {
const scope = scopes.get(node);
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
}
};
export function set_scope(node, { next, state }) {
const scope = state.scopes.get(node);
next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state);
}
/**

Loading…
Cancel
Save