pull/16197/head
Rich Harris 4 months ago
commit 449ca1455e

@ -28,6 +28,8 @@ jobs:
os: ubuntu-latest
- node-version: 22
os: ubuntu-latest
- node-version: 24
os: ubuntu-latest
steps:
- uses: actions/checkout@v4

@ -8,7 +8,7 @@ title: <svelte:body>
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](use) on the `<body>` element.
As with `<svelte:window>` and `<svelte:document>`, this element may only appear the top level of your component and must never be inside a block or element.
As with `<svelte:window>` and `<svelte:document>`, this element may only appear at the top level of your component and must never be inside a block or element.
```svelte
<svelte:body onmouseenter={handleMouseenter} onmouseleave={handleMouseleave} use:someAction />

@ -1,5 +1,45 @@
# svelte
## 5.33.10
### Patch Changes
- fix: use `fill: 'forwards'` on transition animations to prevent flicker ([#16035](https://github.com/sveltejs/svelte/pull/16035))
## 5.33.9
### Patch Changes
- fix: put expressions in effects unless known to be static ([#15792](https://github.com/sveltejs/svelte/pull/15792))
## 5.33.8
### Patch Changes
- fix: only `select_option` if `'value'` is in `next` ([#16032](https://github.com/sveltejs/svelte/pull/16032))
## 5.33.7
### Patch Changes
- fix: `bind:value` to select with stores ([#16028](https://github.com/sveltejs/svelte/pull/16028))
## 5.33.6
### Patch Changes
- fix: falsy attachments on components ([#16021](https://github.com/sveltejs/svelte/pull/16021))
- fix: correctly mark <option> elements as selected during SSR ([#16017](https://github.com/sveltejs/svelte/pull/16017))
## 5.33.5
### Patch Changes
- fix: handle derived destructured iterators ([#16015](https://github.com/sveltejs/svelte/pull/16015))
- fix: avoid rerunning attachments when unrelated spread attributes change ([#15961](https://github.com/sveltejs/svelte/pull/15961))
## 5.33.4
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.33.4",
"version": "5.33.10",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -606,15 +606,15 @@ const instance_script = {
);
// 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);
// const tmp = b.id(state.scope.generate('tmp'));
// const paths = extract_paths(declarator.id, tmp);
// state.props_pre.push(
// b.declaration('const', b.id(tmp), visit(declarator.init!) as Expression)
// b.declaration('const', tmp, visit(declarator.init!) as Expression)
// );
// for (const path of paths) {
// const name = (path.node as Identifier).name;
// const binding = state.scope.get(name)!;
// const value = path.expression!(b.id(tmp));
// const value = path.expression;
// if (binding.kind === 'bindable_prop' || binding.kind === 'rest_prop') {
// state.props.push({
// local: name,

@ -90,7 +90,10 @@ export function Identifier(node, context) {
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.has_state ||= binding.kind !== 'normal';
context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&
!context.state.scope.evaluate(node).is_known;
}
if (

@ -7,6 +7,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';
import { equal } from '../../../utils/assert.js';
import * as b from '#compiler/builders';
/**
* @param {VariableDeclarator} node
@ -18,7 +19,7 @@ export function VariableDeclarator(node, context) {
if (context.state.analysis.runes) {
const init = node.init;
const rune = get_rune(init, context.state.scope);
const paths = extract_paths(node.id);
const { paths } = extract_paths(node.id, b.id('dummy'));
for (const path of paths) {
validate_identifier_name(context.state.scope.get(/** @type {Identifier} */ (path.node).name));

@ -234,13 +234,21 @@ export function EachBlock(node, context) {
} else if (node.context) {
const unwrapped = (flags & EACH_ITEM_REACTIVE) !== 0 ? b.call('$.get', item) : item;
for (const path of extract_paths(node.context)) {
const { inserts, paths } = extract_paths(node.context, unwrapped);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
child_state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value), child_state));
declarations.push(b.var(id, b.call('$.derived', expression)));
}
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?.(unwrapped), child_state))
);
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression, child_state)));
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
@ -249,7 +257,7 @@ export function EachBlock(node, context) {
child_state.transform[name] = {
read,
assign: (_, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
const left = /** @type {Pattern} */ (path.update_expression);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => {

@ -1,6 +1,6 @@
/** @import { ArrayExpression, Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
/** @import { ComponentClientTransformState, ComponentContext, MemoizedExpression } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
cannot_be_set_statically,
@ -17,7 +17,7 @@ import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_set_attributes,
build_attribute_effect,
build_set_class,
build_set_style
} from './shared/element.js';
@ -201,37 +201,7 @@ export function RegularElement(node, context) {
const node_id = context.state.node;
if (has_spread) {
const attributes_id = b.id(context.state.scope.generate('attributes'));
build_set_attributes(
attributes,
class_directives,
style_directives,
context,
node,
node_id,
attributes_id
);
// If value binding exists, that one takes care of calling $.init_select
if (node.name === 'select' && !bindings.has('value')) {
context.state.init.push(
b.stmt(b.call('$.init_select', node_id, b.thunk(b.member(attributes_id, 'value'))))
);
context.state.update.push(
b.if(
b.binary('in', b.literal('value'), attributes_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', node_id, b.member(attributes_id, 'value')))
])
)
);
}
build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id);
} else {
/** If true, needs `__value` for inputs */
const needs_special_value_handling =
@ -492,10 +462,17 @@ function setup_select_synchronization(value_binding, context) {
/**
* @param {AST.ClassDirective[]} class_directives
* @param {MemoizedExpression[]} async_expressions
* @param {MemoizedExpression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | Identifier}
*/
export function build_class_directives_object(class_directives, context) {
export function build_class_directives_object(
class_directives,
async_expressions,
expressions,
context
) {
let properties = [];
let has_call_or_state = false;
let has_async = false;
@ -510,19 +487,23 @@ export function build_class_directives_object(class_directives, context) {
const directives = b.object(properties);
return has_call_or_state || has_async
? get_expression_id(
has_async ? context.state.async_expressions : context.state.expressions,
directives
)
? get_expression_id(has_async ? async_expressions : expressions, directives)
: directives;
}
/**
* @param {AST.StyleDirective[]} style_directives
* @param {MemoizedExpression[]} async_expressions
* @param {MemoizedExpression[]} expressions
* @param {ComponentContext} context
* @return {ObjectExpression | ArrayExpression}}
*/
export function build_style_directives_object(style_directives, context) {
export function build_style_directives_object(
style_directives,
async_expressions,
expressions,
context
) {
let normal_properties = [];
let important_properties = [];
@ -532,10 +513,7 @@ export function build_style_directives_object(style_directives, context) {
? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, metadata) =>
metadata.has_call
? get_expression_id(
metadata.has_await ? context.state.async_expressions : context.state.expressions,
value
)
? get_expression_id(metadata.has_await ? async_expressions : expressions, value)
: value
).value;
const property = b.init(directive.name, expression);

@ -43,14 +43,21 @@ export function SnippetBlock(node, context) {
let arg_alias = `$$arg${i}`;
args.push(b.id(arg_alias));
const paths = extract_paths(argument);
const { inserts, paths } = extract_paths(argument, b.maybe_call(b.id(arg_alias)));
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
transform[id.name] = { read: get_value };
declarations.push(
b.var(id, b.call('$.derived', /** @type {Expression} */ (context.visit(b.thunk(value)))))
);
}
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)))))
);
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression)));
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));

@ -5,7 +5,11 @@ import { dev, locator } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js';
import { build_attribute_value, build_set_attributes, build_set_class } from './shared/element.js';
import {
build_attribute_value,
build_attribute_effect,
build_set_class
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
/**
@ -78,18 +82,15 @@ export function SvelteElement(node, context) {
) {
build_set_class(node, element_id, attributes[0], class_directives, inner_context, false);
} else if (attributes.length) {
const attributes_id = b.id(context.state.scope.generate('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.
build_set_attributes(
build_attribute_effect(
attributes,
class_directives,
style_directives,
inner_context,
node,
element_id,
attributes_id
element_id
);
}

@ -1,13 +1,19 @@
/** @import { CallExpression, Expression, Identifier, Literal, VariableDeclaration, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
<<<<<<< HEAD
import { dev, is_ignored, locate_node } from '../../../../state.js';
import { build_pattern, extract_paths } from '../../../../utils/ast.js';
=======
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
>>>>>>> main
import * as b from '#compiler/builders';
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 } from '../utils.js';
import { is_hoisted_function } from '../../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {VariableDeclaration} node
@ -116,7 +122,7 @@ export function VariableDeclaration(node, context) {
}
const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
const value = /** @type {Expression} */ (args[0]) ?? b.void0; // TODO do we need the void 0? can we just omit it altogether?
if (rune === '$state' || rune === '$state.raw') {
/**
@ -137,24 +143,34 @@ export function VariableDeclaration(node, context) {
};
if (declarator.id.type === 'Identifier') {
const expression = /** @type {Expression} */ (context.visit(value));
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
b.declarator(declarator.id, create_state_declarator(declarator.id, expression))
);
} else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(pattern, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
([original, replacement]) => {
const binding = context.state.scope.get(original.name);
return b.declarator(
original,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, replacement)
: replacement
);
}
)
b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}
@ -169,6 +185,7 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type === 'Identifier') {
if (is_async) {
const location = dev && is_ignored(init, 'await_waterfall') && locate_node(init);
const expression = /** @type {Expression} */ (context.visit(value));
declarations.push(
b.declarator(
@ -179,7 +196,7 @@ export function VariableDeclaration(node, context) {
'$.save',
b.call(
'$.async_derived',
b.thunk(value, true),
b.thunk(expression, true),
location ? b.literal(location) : undefined
)
)
@ -188,6 +205,9 @@ export function VariableDeclaration(node, context) {
)
);
} else {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
declarations.push(
b.declarator(
declarator.id,
@ -196,37 +216,36 @@ export function VariableDeclaration(node, context) {
);
}
} else {
const [pattern, replacements] = build_pattern(declarator.id, context.state.scope);
const init = /** @type {CallExpression} */ (declarator.init);
/** @type {Identifier} */
let id;
let rhs = value;
if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') {
const id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id);
declarations.push(
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)))
);
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}
for (let i = 0; i < replacements.size; i++) {
const [original, replacement] = [...replacements][i];
declarations.push(
b.declarator(
original,
b.call(
'$.derived',
b.arrow([], b.block([b.let(pattern, rhs), b.return(replacement)]))
)
)
);
const { inserts, paths } = extract_paths(declarator.id, rhs);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}
for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
}
}
continue;
}
}
@ -255,20 +274,29 @@ export function VariableDeclaration(node, context) {
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);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(
b.id(tmp),
tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
}
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));
const value = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(
b.declarator(
path.node,
@ -302,7 +330,7 @@ export function VariableDeclaration(node, context) {
declarations.push(
...create_state_declarators(
declarator,
context.state,
context,
/** @type {Expression} */ (declarator.init && context.visit(declarator.init))
)
);
@ -322,32 +350,41 @@ export function VariableDeclaration(node, context) {
/**
* Creates the output for a state declaration in legacy mode.
* @param {VariableDeclarator} declarator
* @param {ComponentClientTransformState} scope
* @param {ComponentContext} context
* @param {Expression} value
*/
function create_state_declarators(declarator, { scope, analysis }, value) {
function create_state_declarators(declarator, context, value) {
if (declarator.id.type === 'Identifier') {
return [
b.declarator(
declarator.id,
b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined)
b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
)
];
}
const [pattern, replacements] = build_pattern(declarator.id, scope);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
return [
b.declarator(pattern, value),
.../** @type {[Identifier, Identifier][]} */ ([...replacements]).map(
([original, replacement]) => {
const binding = scope.get(original.name);
return b.declarator(
original,
binding?.kind === 'state'
? b.call('$.mutable_source', replacement, analysis.immutable ? b.true : undefined)
: replacement
);
}
)
b.declarator(tmp, value),
...inserts.map(({ id, value }) => {
id.name = context.state.scope.generate('$$array');
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
const binding = context.state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state'
? b.call('$.mutable_source', value, context.state.analysis.immutable ? b.true : undefined)
: value
);
})
];
}

@ -282,10 +282,18 @@ export function build_component(node, component_name, context, anchor = context.
}
}
} else if (attribute.type === 'AttachTag') {
const evaluated = context.state.scope.evaluate(attribute.expression);
let expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (attribute.metadata.expression.has_state) {
expression = b.arrow([b.id('$$node')], b.call(expression, b.id('$$node')));
expression = b.arrow(
[b.id('$$node')],
b.call(
evaluated.is_function ? expression : b.logical('||', expression, b.id('$.noop')),
b.id('$$node')
)
);
}
push_prop(b.prop('get', b.call('$.attachment'), expression, true));

@ -1,6 +1,6 @@
/** @import { ArrayExpression, Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
/** @import { ComponentContext, MemoizedExpression } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
@ -16,34 +16,30 @@ import { build_template_chunk, get_expression_id } from './utils.js';
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {Identifier} attributes_id
*/
export function build_set_attributes(
export function build_attribute_effect(
attributes,
class_directives,
style_directives,
context,
element,
element_id,
attributes_id
element_id
) {
let is_dynamic = false;
/** @type {ObjectExpression['properties']} */
const values = [];
/** @type {MemoizedExpression[]} */
const async_expressions = [];
/** @type {MemoizedExpression[]} */
const expressions = [];
for (const attribute of attributes) {
if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(
attribute.value,
context,
(value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(
metadata.has_await ? context.state.async_expressions : context.state.expressions,
value
)
: value
const { value } = build_attribute_value(attribute.value, context, (value, metadata) =>
metadata.has_call || metadata.has_await
? get_expression_id(metadata.has_await ? async_expressions : expressions, value)
: value
);
if (
@ -57,19 +53,12 @@ export function build_set_attributes(
} else {
values.push(b.init(attribute.name, value));
}
is_dynamic ||= has_state;
} else {
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
is_dynamic = true;
let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) {
value = get_expression_id(
attribute.metadata.expression.has_await
? context.state.async_expressions
: context.state.expressions,
attribute.metadata.expression.has_await ? async_expressions : expressions,
value
);
}
@ -83,12 +72,9 @@ export function build_set_attributes(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context)
build_class_directives_object(class_directives, async_expressions, expressions, context)
)
);
is_dynamic ||=
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
}
if (style_directives.length) {
@ -96,31 +82,29 @@ export function build_set_attributes(
b.prop(
'init',
b.array([b.id('$.STYLE')]),
build_style_directives_object(style_directives, context)
build_style_directives_object(style_directives, async_expressions, expressions, context)
)
);
is_dynamic ||= style_directives.some((directive) => directive.metadata.expression.has_state);
}
const call = b.call(
'$.set_attributes',
element_id,
is_dynamic ? attributes_id : b.null,
b.object(values),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
context.state.init.push(
b.stmt(
b.call(
'$.attribute_effect',
element_id,
b.arrow(
expressions.map(({ id }) => id),
b.object(values)
),
// TODO need to handle async expressions too
expressions.length > 0 && b.array(expressions.map(({ expression }) => b.thunk(expression))),
element.metadata.scoped &&
context.state.analysis.css.hash !== '' &&
b.literal(context.state.analysis.css.hash),
is_ignored(element, 'hydration_attribute_changed') && b.true
)
)
);
if (is_dynamic) {
context.state.init.push(b.let(attributes_id));
const update = b.stmt(b.assignment('=', attributes_id, call));
context.state.update.push(update);
} else {
context.state.init.push(b.stmt(call));
}
}
/**
@ -196,7 +180,12 @@ export function build_set_class(element, node_id, attribute, class_directives, c
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
next = build_class_directives_object(
class_directives,
context.state.async_expressions,
context.state.expressions,
context
);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
@ -269,7 +258,12 @@ export function build_set_style(node_id, attribute, style_directives, context) {
let next;
if (style_directives.length) {
next = build_style_directives_object(style_directives, context);
next = build_style_directives_object(
style_directives,
context.state.async_expressions,
context.state.expressions,
context
);
has_state ||= style_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {

@ -1,3 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
@ -6,8 +7,8 @@ import { is_void } from '../../../../../utils.js';
import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
import { process_children, build_template } from './shared/utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.js';
import { process_children, build_template, build_attribute_value } from './shared/utils.js';
/**
* @param {AST.RegularElement} node
@ -71,21 +72,96 @@ export function RegularElement(node, context) {
);
}
if (body === null) {
process_children(trimmed, { ...context, state });
} else {
let id = body;
let select_with_value = false;
if (body.type !== 'Identifier') {
id = b.id(state.scope.generate('$$body'));
state.template.push(b.const(id, body));
if (node.name === 'select') {
const value = node.attributes.find(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value'
);
if (node.attributes.some((attribute) => attribute.type === 'SpreadAttribute')) {
select_with_value = true;
state.template.push(
b.stmt(
b.assignment(
'=',
b.id('$$payload.select_value'),
b.member(
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
)
);
} else if (value) {
select_with_value = true;
const left = b.id('$$payload.select_value');
if (value.type === 'Attribute') {
state.template.push(
b.stmt(b.assignment('=', left, build_attribute_value(value.value, context)))
);
} else if (value.type === 'BindDirective') {
state.template.push(
b.stmt(
b.assignment(
'=',
left,
value.expression.type === 'SequenceExpression'
? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(value.expression))
)
)
);
}
}
}
if (
node.name === 'option' &&
!node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
)
) {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$payload'),
b.thunk(b.block([...inner_state.init, ...build_template(inner_state.template)]))
)
)
);
} else if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
let id = /** @type {Expression} */ (body);
if (body.type !== 'Identifier') {
id = b.id(state.scope.generate('$$body'));
state.template.push(b.const(id, body));
}
// Use the body expression as the body if it's truthy, otherwise use the inner template
state.template.push(
b.if(
@ -94,6 +170,12 @@ export function RegularElement(node, context) {
b.block([...inner_state.init, ...build_template(inner_state.template)])
)
);
} else {
process_children(trimmed, { ...context, state });
}
if (select_with_value) {
state.template.push(b.stmt(b.assignment('=', b.id('$$payload.select_value'), b.void0)));
}
if (!node_is_void) {

@ -3,7 +3,7 @@
/** @import { Context } from '../types.js' */
/** @import { ComponentAnalysis } from '../../../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { build_pattern, build_fallback, extract_paths } from '../../../../utils/ast.js';
import { build_fallback, extract_paths } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
@ -120,21 +120,29 @@ export function VariableDeclaration(node, context) {
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);
const tmp = b.id(context.state.scope.generate('tmp'));
const { inserts, paths } = extract_paths(declarator.id, tmp);
declarations.push(
b.declarator(
b.id(tmp),
tmp,
/** @type {Expression} */ (context.visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const { id, value } of inserts) {
id.name = context.state.scope.generate('$$array');
declarations.push(b.declarator(id, value));
}
for (const path of paths) {
const value = path.expression?.(b.id(tmp));
const value = path.expression;
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, build_fallback(prop, value)));
}
continue;
}
@ -188,10 +196,13 @@ function create_state_declarators(declarator, scope, value) {
return [b.declarator(declarator.id, value)];
}
const [pattern, replacements] = build_pattern(declarator.id, scope);
const tmp = b.id(scope.generate('tmp'));
const { paths } = extract_paths(declarator.id, tmp);
return [
b.declarator(pattern, value),
// TODO inject declarator for opts, so we can use it below
...[...replacements].map(([original, replacement]) => b.declarator(original, replacement))
b.declarator(tmp, value), // TODO inject declarator for opts, so we can use it below
...paths.map((path) => {
const value = path.expression;
return b.declarator(path.node, value);
})
];
}

@ -202,6 +202,29 @@ export function build_element_attributes(node, context) {
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
if (node.name === 'option') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
b.member(
build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context
),
'value',
false,
true
)
)
);
}
} else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
@ -236,6 +259,16 @@ export function build_element_attributes(node, context) {
);
}
if (node.name === 'option' && name === 'value') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$payload'),
literal_value != null ? b.literal(/** @type {any} */ (literal_value)) : b.void0
)
);
}
continue;
}
@ -260,6 +293,10 @@ export function build_element_attributes(node, context) {
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
if (name === 'value' && node.name === 'option') {
context.state.template.push(b.call('$.maybe_selected', b.id('$$payload'), value));
}
}
}
@ -274,7 +311,7 @@ export function build_element_attributes(node, context) {
/**
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {AST.Attribute} attribute
* @param {AST.Attribute | AST.BindDirective} attribute
*/
function get_attribute_name(element, attribute) {
let name = attribute.name;
@ -286,6 +323,36 @@ function get_attribute_name(element, attribute) {
return name;
}
/**
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context
*/
export function build_spread_object(element, attributes, context) {
return b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute);
const value = build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute);
const value =
attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
})
);
}
/**
*
* @param {AST.RegularElement | AST.SvelteElement} element
@ -336,21 +403,7 @@ function build_element_spread_attributes(
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
}
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute);
const value = build_attribute_value(
attribute.value,
context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
})
);
const object = build_spread_object(element, attributes, context);
const css_hash =
element.metadata.scoped && context.state.analysis.css.hash

@ -1,8 +1,9 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Node, Pattern } from 'estree' */
/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern, Statement } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
import { build_pattern, is_expression_async } from '../../../utils/ast.js';
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
import * as b from '#compiler/builders';
import { get_value } from '../client/visitors/shared/declarations.js';
/**
* @template {ClientContext | ServerContext} Context
@ -23,23 +24,27 @@ export function visit_assignment_expression(node, context, build_assignment) {
let changed = false;
const [pattern, replacements] = build_pattern(node.left, context.state.scope);
const assignments = [
b.let(pattern, rhs),
...[...replacements].map(([original, replacement]) => {
let assignment = build_assignment(node.operator, original, replacement, context);
if (assignment !== null) changed = true;
return b.stmt(
assignment ??
b.assignment(
node.operator,
/** @type {Identifier} */ (context.visit(original)),
/** @type {Expression} */ (context.visit(replacement))
)
);
})
];
const { inserts, paths } = extract_paths(node.left, rhs);
for (const { id } of inserts) {
id.name = context.state.scope.generate('$$array');
}
const assignments = paths.map((path) => {
const value = path.expression;
let assignment = build_assignment('=', path.node, value, context);
if (assignment !== null) changed = true;
return (
assignment ??
b.assignment(
'=',
/** @type {Pattern} */ (context.visit(path.node)),
/** @type {Expression} */ (context.visit(value))
)
);
});
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
@ -47,36 +52,36 @@ export function visit_assignment_expression(node, context, build_assignment) {
}
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
const block = b.block(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
block.body.push(b.return(rhs));
}
if (inserts.length > 0 || should_cache) {
/** @type {Statement[]} */
const statements = [
...inserts.map(({ id, value }) => b.var(id, value)),
...assignments.map(b.stmt)
];
if (is_standalone && !should_cache) {
return block;
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
statements.push(b.return(rhs));
}
const iife = b.arrow([rhs], b.block(statements));
const iife_is_async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
}
const iife = b.arrow(should_cache ? [rhs] : [], block);
const iife_is_async =
is_expression_async(value) ||
assignments.some(
(assignment) =>
(assignment.type === 'ExpressionStatement' &&
is_expression_async(assignment.expression)) ||
(assignment.type === 'VariableDeclaration' &&
assignment.declarations.some(
(declaration) =>
is_expression_async(declaration.id) ||
(declaration.init && is_expression_async(declaration.init))
))
);
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
return iife_is_async
? b.await(b.call(b.async(iife), should_cache ? value : undefined))
: b.call(iife, should_cache ? value : undefined);
return sequence;
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {

@ -20,6 +20,7 @@ const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
export const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
const globals = {
@ -200,6 +201,13 @@ class Evaluation {
*/
is_number = true;
/**
* True if the value is known to be a function
* @readonly
* @type {boolean}
*/
is_function = true;
/**
* @readonly
* @type {any}
@ -209,10 +217,12 @@ class Evaluation {
/**
*
* @param {Scope} scope
* @param {Expression} expression
* @param {Expression | FunctionDeclaration} expression
* @param {Set<any>} values
*/
constructor(scope, expression, values) {
current_evaluations.set(expression, this);
this.values = values;
switch (expression.type) {
@ -500,6 +510,13 @@ class Evaluation {
break;
}
case 'ArrowFunctionExpression':
case 'FunctionExpression':
case 'FunctionDeclaration': {
this.values.add(FUNCTION);
break;
}
default: {
this.values.add(UNKNOWN);
}
@ -516,6 +533,10 @@ class Evaluation {
this.is_number = false;
}
if (value !== FUNCTION) {
this.is_function = false;
}
if (value == null || value === UNKNOWN) {
this.is_defined = false;
}
@ -524,6 +545,8 @@ class Evaluation {
if (this.values.size > 1 || typeof this.value === 'symbol') {
this.is_known = false;
}
current_evaluations.delete(expression);
}
}
@ -630,10 +653,9 @@ export class Scope {
/**
* @param {string} preferred_name
* @param {(name: string, counter: number) => string} [generator]
* @returns {string}
*/
generate(preferred_name, generator = (name, counter) => `${name}_${counter}`) {
generate(preferred_name) {
if (this.#porous) {
return /** @type {Scope} */ (this.parent).generate(preferred_name);
}
@ -648,7 +670,7 @@ export class Scope {
this.root.conflicts.has(name) ||
is_reserved(name)
) {
name = generator(preferred_name, n++);
name = `${preferred_name}_${n++}`;
}
this.references.set(name, []);
@ -716,10 +738,20 @@ export class Scope {
* @param {Set<any>} [values]
*/
evaluate(expression, values = new Set()) {
const current = current_evaluations.get(expression);
if (current) return current;
return new Evaluation(this, expression, values);
}
}
/**
* Track which expressions are currently being evaluated this allows
* us to prevent cyclical evaluations without passing the map around
* @type {Map<Expression | FunctionDeclaration, Evaluation>}
*/
const current_evaluations = new Map();
/** @type {Record<BinaryOperator, (left: any, right: any) => any>} */
const binary = {
'!=': (left, right) => left != right,
@ -1138,7 +1170,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const is_keyed =
node.key &&
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
scope.declare(b.id(node.index), is_keyed ? 'template' : 'normal', 'const', node);
scope.declare(b.id(node.index), is_keyed ? 'template' : 'static', 'const', node);
}
if (node.key) visit(node.key, { scope });

@ -270,7 +270,8 @@ export type BindingKind =
| 'snippet' // A snippet parameter
| 'store_sub' // A $store value
| 'legacy_reactive' // A `$:` declaration
| 'template'; // A binding declared in the template, e.g. in an `await` block or `const` tag
| 'template' // A binding declared in the template, e.g. in an `await` block or `const` tag
| 'static'; // A binding whose value is known to be static (i.e. each index)
export type DeclarationKind =
| 'var'

@ -1,8 +1,7 @@
/** @import { AST, Scope } from '#compiler' */
/** @import { AST } from '#compiler' */
/** @import * as ESTree from 'estree' */
import { walk } from 'zimmerframe';
import * as b from '#compiler/builders';
import is_reference from 'is-reference';
/**
* Gets the left-most identifier of a member expression or identifier.
@ -129,49 +128,6 @@ export function unwrap_pattern(pattern, nodes = []) {
return nodes;
}
/**
* @param {ESTree.Pattern} id
* @param {Scope} scope
* @returns {[ESTree.Pattern, Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>]}
*/
export function build_pattern(id, scope) {
/** @type {Map<ESTree.Identifier | ESTree.MemberExpression, ESTree.Identifier>} */
const map = new Map();
/** @type {Map<string, string>} */
const names = new Map();
let counter = 0;
for (const node of unwrap_pattern(id)) {
const name = scope.generate(`$$${++counter}`, (_, counter) => `$$${counter}`);
map.set(node, b.id(name));
if (node.type === 'Identifier') {
names.set(node.name, name);
}
}
const pattern = walk(id, null, {
Identifier(node, context) {
if (is_reference(node, /** @type {ESTree.Pattern} */ (context.path.at(-1)))) {
const name = names.get(node.name);
if (name) return b.id(name);
}
},
MemberExpression(node, context) {
const n = map.get(node);
if (n) return n;
context.next();
}
});
return [pattern, map];
}
/**
* Extracts all identifiers from a pattern.
* @param {ESTree.Pattern} pattern
@ -271,40 +227,50 @@ export function extract_identifiers_from_destructuring(node, nodes = []) {
* @property {ESTree.Identifier | ESTree.MemberExpression} node The node the destructuring path end in. Can be a member expression only for assignment expressions
* @property {boolean} is_rest `true` if this is a `...rest` destructuring
* @property {boolean} has_default_value `true` if this has a fallback value like `const { foo = 'bar } = ..`
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} expression Returns an expression which walks the path starting at the given expression.
* @property {ESTree.Expression} expression The value of the current path
* This will be a call expression if a rest element or default is involved e.g. `const { foo: { bar: baz = 42 }, ...rest } = quux` since we can't represent `baz` or `rest` purely as a path
* Will be an await expression in case of an async default value (`const { foo = await bar } = ...`)
* @property {(expression: ESTree.Expression) => ESTree.Identifier | ESTree.MemberExpression | ESTree.CallExpression | ESTree.AwaitExpression} update_expression Like `expression` but without default values.
* @property {ESTree.Expression} update_expression Like `expression` but without default values.
*/
/**
* Extracts all destructured assignments from a pattern.
* For each `id` in the returned `inserts`, make sure to adjust the `name`.
* @param {ESTree.Node} param
* @returns {DestructuredAssignment[]}
* @param {ESTree.Expression} initial
* @returns {{ inserts: Array<{ id: ESTree.Identifier, value: ESTree.Expression }>, paths: DestructuredAssignment[] }}
*/
export function extract_paths(param) {
return _extract_paths(
[],
param,
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
(node) => /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node),
false
);
export function extract_paths(param, initial) {
/**
* When dealing with array destructuring patterns (`let [a, b, c] = $derived(blah())`)
* we need an intermediate declaration that creates an array, since `blah()` could
* return a non-array-like iterator
* @type {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>}
*/
const inserts = [];
/** @type {DestructuredAssignment[]} */
const paths = [];
_extract_paths(paths, inserts, param, initial, initial, false);
return { inserts, paths };
}
/**
* @param {DestructuredAssignment[]} assignments
* @param {DestructuredAssignment[]} paths
* @param {Array<{ id: ESTree.Identifier, value: ESTree.Expression }>} inserts
* @param {ESTree.Node} param
* @param {DestructuredAssignment['expression']} expression
* @param {DestructuredAssignment['update_expression']} update_expression
* @param {ESTree.Expression} expression
* @param {ESTree.Expression} update_expression
* @param {boolean} has_default_value
* @returns {DestructuredAssignment[]}
*/
function _extract_paths(assignments = [], param, expression, update_expression, has_default_value) {
function _extract_paths(paths, inserts, param, expression, update_expression, has_default_value) {
switch (param.type) {
case 'Identifier':
case 'MemberExpression':
assignments.push({
paths.push({
node: param,
is_rest: false,
has_default_value,
@ -316,28 +282,25 @@ function _extract_paths(assignments = [], param, expression, update_expression,
case 'ObjectPattern':
for (const prop of param.properties) {
if (prop.type === 'RestElement') {
/** @type {DestructuredAssignment['expression']} */
const rest_expression = (object) => {
/** @type {ESTree.Expression[]} */
const props = [];
for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
if (p.key.type === 'Identifier' && !p.computed) {
props.push(b.literal(p.key.name));
} else if (p.key.type === 'Literal') {
props.push(b.literal(String(p.key.value)));
} else {
props.push(b.call('String', p.key));
}
/** @type {ESTree.Expression[]} */
const props = [];
for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
if (p.key.type === 'Identifier' && !p.computed) {
props.push(b.literal(p.key.name));
} else if (p.key.type === 'Literal') {
props.push(b.literal(String(p.key.value)));
} else {
props.push(b.call('String', p.key));
}
}
}
return b.call('$.exclude_from_object', expression(object), b.array(props));
};
const rest_expression = b.call('$.exclude_from_object', expression, b.array(props));
if (prop.argument.type === 'Identifier') {
assignments.push({
paths.push({
node: prop.argument,
is_rest: true,
has_default_value,
@ -346,7 +309,8 @@ function _extract_paths(assignments = [], param, expression, update_expression,
});
} else {
_extract_paths(
assignments,
paths,
inserts,
prop.argument,
rest_expression,
rest_expression,
@ -354,11 +318,15 @@ function _extract_paths(assignments = [], param, expression, update_expression,
);
}
} else {
/** @type {DestructuredAssignment['expression']} */
const object_expression = (object) =>
b.member(expression(object), prop.key, prop.computed || prop.key.type !== 'Identifier');
const object_expression = b.member(
expression,
prop.key,
prop.computed || prop.key.type !== 'Identifier'
);
_extract_paths(
assignments,
paths,
inserts,
prop.value,
object_expression,
object_expression,
@ -369,16 +337,27 @@ function _extract_paths(assignments = [], param, expression, update_expression,
break;
case 'ArrayPattern':
case 'ArrayPattern': {
// we create an intermediate declaration to convert iterables to arrays if necessary.
// the consumer is responsible for setting the name of the identifier
const id = b.id('#');
const value = b.call(
'$.to_array',
expression,
param.elements.at(-1)?.type === 'RestElement' ? undefined : b.literal(param.elements.length)
);
inserts.push({ id, value });
for (let i = 0; i < param.elements.length; i += 1) {
const element = param.elements[i];
if (element) {
if (element.type === 'RestElement') {
/** @type {DestructuredAssignment['expression']} */
const rest_expression = (object) =>
b.call(b.member(expression(object), 'slice'), b.literal(i));
const rest_expression = b.call(b.member(id, 'slice'), b.literal(i));
if (element.argument.type === 'Identifier') {
assignments.push({
paths.push({
node: element.argument,
is_rest: true,
has_default_value,
@ -387,7 +366,8 @@ function _extract_paths(assignments = [], param, expression, update_expression,
});
} else {
_extract_paths(
assignments,
paths,
inserts,
element.argument,
rest_expression,
rest_expression,
@ -395,10 +375,11 @@ function _extract_paths(assignments = [], param, expression, update_expression,
);
}
} else {
/** @type {DestructuredAssignment['expression']} */
const array_expression = (object) => b.member(expression(object), b.literal(i), true);
const array_expression = b.member(id, b.literal(i), true);
_extract_paths(
assignments,
paths,
inserts,
element,
array_expression,
array_expression,
@ -409,13 +390,13 @@ function _extract_paths(assignments = [], param, expression, update_expression,
}
break;
}
case 'AssignmentPattern': {
/** @type {DestructuredAssignment['expression']} */
const fallback_expression = (object) => build_fallback(expression(object), param.right);
const fallback_expression = build_fallback(expression, param.right);
if (param.left.type === 'Identifier') {
assignments.push({
paths.push({
node: param.left,
is_rest: false,
has_default_value: true,
@ -423,14 +404,14 @@ function _extract_paths(assignments = [], param, expression, update_expression,
update_expression
});
} else {
_extract_paths(assignments, param.left, fallback_expression, update_expression, true);
_extract_paths(paths, inserts, param.left, fallback_expression, update_expression, true);
}
break;
}
}
return assignments;
return paths;
}
/**

@ -1,3 +1,4 @@
/** @import { Effect } from '#client' */
import { DEV } from 'esm-env';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
@ -10,6 +11,7 @@ import { is_capture_event, is_delegated, normalize_attribute } from '../../../..
import {
active_effect,
active_reaction,
get,
set_active_effect,
set_active_reaction
} from '../../runtime.js';
@ -18,6 +20,9 @@ import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
import { set_style } from './style.js';
import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js';
import { block, branch, destroy_effect } from '../../reactivity/effects.js';
import { derived } from '../../reactivity/deriveds.js';
import { init_select, select_option } from './bindings/select.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
@ -447,13 +452,68 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
set_hydrating(true);
}
for (let symbol of Object.getOwnPropertySymbols(next)) {
if (symbol.description === ATTACHMENT_KEY) {
attach(element, () => next[symbol]);
return current;
}
/**
* @param {Element & ElementCSSInlineStyle} element
* @param {(...expressions: any) => Record<string | symbol, any>} fn
* @param {Array<() => any>} thunks
* @param {string} [css_hash]
* @param {boolean} [skip_warning]
*/
export function attribute_effect(
element,
fn,
thunks = [],
css_hash,
skip_warning = false,
d = derived
) {
const deriveds = thunks.map(d);
/** @type {Record<string | symbol, any> | undefined} */
var prev = undefined;
/** @type {Record<symbol, Effect>} */
var effects = {};
var is_select = element.nodeName === 'SELECT';
var inited = false;
block(() => {
var next = fn(...deriveds.map(get));
set_attributes(element, prev, next, css_hash, skip_warning);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value, false);
}
for (let symbol of Object.getOwnPropertySymbols(effects)) {
if (!next[symbol]) destroy_effect(effects[symbol]);
}
for (let symbol of Object.getOwnPropertySymbols(next)) {
var n = next[symbol];
if (symbol.description === ATTACHMENT_KEY && (!prev || n !== prev[symbol])) {
if (effects[symbol]) destroy_effect(effects[symbol]);
effects[symbol] = branch(() => attach(element, () => n));
}
}
prev = next;
});
if (is_select) {
init_select(
/** @type {HTMLSelectElement} */ (element),
() => /** @type {Record<string | symbol, any>} */ (prev).value
);
}
return current;
inited = true;
}
/**

@ -25,10 +25,14 @@ export function select_option(select, value, mounting) {
}
// Otherwise, update the selection
return select_options(select, value);
for (var option of select.options) {
option.selected = value.includes(get_option_value(option));
}
return;
}
for (var option of select.options) {
for (option of select.options) {
var option_value = get_option_value(option);
if (is(option_value, value)) {
option.selected = true;
@ -136,16 +140,6 @@ export function bind_select_value(select, get, set = get) {
init_select(select);
}
/**
* @param {HTMLSelectElement} select
* @param {unknown[]} value
*/
function select_options(select, value) {
for (var option of select.options) {
option.selected = value.includes(get_option_value(option));
}
}
/** @param {HTMLOptionElement} option */
function get_option_value(option) {
// __value only exists if the <option> has a value attribute

@ -381,9 +381,15 @@ function animate(element, options, counterpart, t2, on_finish) {
// create a dummy animation that lasts as long as the delay (but with whatever devtools
// multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
// the CSS keyframes aren't created until the DOM is updated
var animation = element.animate(keyframes, { duration: delay });
//
// fill forwards to prevent the element from rendering without styles applied
// see https://github.com/sveltejs/svelte/issues/14732
var animation = element.animate(keyframes, { duration: delay, fill: 'forwards' });
animation.onfinish = () => {
// remove dummy animation from the stack to prevent conflict with main animation
animation.cancel();
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;

@ -29,6 +29,7 @@ export {
remove_input_defaults,
set_attribute,
set_attributes,
attribute_effect,
set_custom_element_data,
set_xlink_attribute,
set_value,
@ -168,7 +169,7 @@ export {
} from './dom/operations.js';
export { attr, clsx } from '../shared/attributes.js';
export { snapshot } from '../shared/clone.js';
export { noop, fallback } from '../shared/utils.js';
export { noop, fallback, to_array } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,

@ -504,7 +504,7 @@ export { assign_payload, copy_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
export { fallback } from '../shared/utils.js';
export { fallback, to_array } from '../shared/utils.js';
export {
invalid_default_snippet,
@ -537,3 +537,30 @@ export function derived(fn) {
return updated_value;
};
}
/**
*
* @param {Payload} payload
* @param {*} value
*/
export function maybe_selected(payload, value) {
return value === payload.select_value ? ' selected' : '';
}
/**
* @param {Payload} payload
* @param {() => void} children
* @returns {void}
*/
export function valueless_option(payload, children) {
var i = payload.out.length;
children();
var body = payload.out.slice(i);
if (body.replace(/<!---->/g, '') === payload.select_value) {
// replace '>' with ' selected>' (closing tag will be added later)
payload.out = payload.out.slice(0, i - 1) + ' selected>' + body;
}
}

@ -18,6 +18,7 @@ export class Payload {
css = new Set();
out = '';
uid = () => '';
select_value = undefined;
head = new HeadPayload();

@ -81,3 +81,38 @@ export function fallback(value, fallback, lazy = false) {
: /** @type {V} */ (fallback)
: value;
}
/**
* When encountering a situation like `let [a, b, c] = $derived(blah())`,
* we need to stash an intermediate value that `a`, `b`, and `c` derive
* from, in case it's an iterable
* @template T
* @param {ArrayLike<T> | Iterable<T>} value
* @param {number} [n]
* @returns {Array<T>}
*/
export function to_array(value, n) {
// return arrays unchanged
if (Array.isArray(value)) {
return value;
}
// if value is not iterable, or `n` is unspecified (indicates a rest
// element, which means we're not concerned about unbounded iterables)
// convert to an array with `Array.from`
if (n === undefined || !(Symbol.iterator in value)) {
return Array.from(value);
}
// otherwise, populate an array with `n` values
/** @type {T[]} */
const array = [];
for (const element of value) {
array.push(element);
if (array.length === n) break;
}
return array;
}

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.33.4';
export const VERSION = '5.33.10';
export const PUBLIC_VERSION = '5';

@ -1,9 +1,9 @@
import { test } from '../../test';
export default test({
html: `
ssrHtml: `
<select>
<option value="[object Object]">wheeee</option>
<option selected value="[object Object]">wheeee</option>
</select>
`
});

@ -17,9 +17,9 @@ export default test({
return { tasks, selected: tasks[0] };
},
html: `
ssrHtml: `
<select>
<option value='[object Object]'>put your left leg in</option>
<option selected value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
@ -36,7 +36,28 @@ export default test({
<p>shake it all about</p>
`,
async test({ assert, component, target, window }) {
async test({ assert, component, target, window, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option ${variant === 'hydrate' ? 'selected ' : ''}value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
</select>
<label>
<input type='checkbox'> put your left leg in
</label>
<h2>Pending tasks</h2>
<p>put your left leg in</p>
<p>your left leg out</p>
<p>in, out, in, out</p>
<p>shake it all about</p>
`
);
const input = target.querySelector('input');
const select = target.querySelector('select');
const options = target.querySelectorAll('option');
@ -57,7 +78,7 @@ export default test({
target.innerHTML,
`
<select>
<option value='[object Object]'>put your left leg in</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>
@ -94,7 +115,7 @@ export default test({
target.innerHTML,
`
<select>
<option value='[object Object]'>put your left leg in</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value='[object Object]'>put your left leg in</option>
<option value='[object Object]'>your left leg out</option>
<option value='[object Object]'>in, out, in, out</option>
<option value='[object Object]'>shake it all about</option>

@ -6,17 +6,29 @@ export default test({
return { values: [1, 2, 3], foo: 2 };
},
html: `
ssrHtml: `
<select>
<option value="1">1</option>
<option value="2">2</option>
<option selected value="2">2</option>
<option value="3">3</option>
</select>
<p>foo: 2</p>
`,
test({ assert, component, target, window }) {
test({ assert, component, target, window, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="1">1</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
</select>
<p>foo: 2</p>
`
);
const select = target.querySelector('select');
ok(select);
const options = [...target.querySelectorAll('option')];
@ -36,7 +48,7 @@ export default test({
`
<select>
<option value="1">1</option>
<option value="2">2</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value="2">2</option>
<option value="3">3</option>
</select>

@ -1,15 +1,15 @@
import { test } from '../../test';
export default test({
html: `
ssrHtml: `
<select>
<option value='hullo'>Hullo</option>
<option selected value='hullo'>Hullo</option>
<option value='world'>World</option>
</select>
<select>
<option value='hullo'>Hullo</option>
<option value='world'>World</option>
<option selected value='world'>World</option>
</select>
`,
@ -19,7 +19,21 @@ export default test({
};
},
test({ assert, component, target, window }) {
test({ assert, component, target, window, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option ${variant === 'hydrate' ? 'selected ' : ''}value='hullo'>Hullo</option>
<option value='world'>World</option>
</select>
<select>
<option value='hullo'>Hullo</option>
<option ${variant === 'hydrate' ? 'selected ' : ''}value='world'>World</option>
</select>
`
);
const selects = [...target.querySelectorAll('select')];
const change = new window.Event('change');

@ -8,7 +8,7 @@ export default test({
<select>
<option>a</option>
<option selected="">b</option>
<option selected>b</option>
<option>c</option>
</select>

@ -1,12 +1,12 @@
import { ok, test } from '../../test';
export default test({
html: `
ssrHtml: `
<p>selected: b</p>
<select>
<option>a</option>
<option>b</option>
<option selected>b</option>
<option>c</option>
</select>
@ -17,7 +17,21 @@ export default test({
return { selected: 'b' };
},
test({ assert, target }) {
test({ assert, target, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<p>selected: b</p>
<select>
<option>a</option>
<option${variant === 'hydrate' ? ' selected' : ''}>b</option>
<option>c</option>
</select>
<p>selected: b</p>
`
);
const select = target.querySelector('select');
ok(select);
const options = [...target.querySelectorAll('option')];

@ -2,11 +2,11 @@ import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: `
ssrHtml: `
<p>selected: one</p>
<select>
<option>one</option>
<option selected>one</option>
<option>two</option>
<option>three</option>
</select>
@ -18,7 +18,21 @@ export default test({
return { selected: 'one' };
},
test({ assert, component, target, window }) {
test({ assert, component, target, window, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<p>selected: one</p>
<select>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$>
<option>two</option>
<option>three</option>
</select>
<p>selected: one</p>
`
);
const select = target.querySelector('select');
ok(select);
@ -40,7 +54,7 @@ export default test({
<p>selected: two</p>
<select>
<option>one</option>
<option${variant === 'hydrate' ? ' selected' : ''}>one</option$>
<option>two</option>
<option>three</option>
</select>

@ -2,27 +2,28 @@ import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
html: `
<select>
<option value="a">A</option>
<option value="b">B</option>
</select>
selected: a
`,
test({ assert, target }) {
test({ assert, target, variant }) {
assert.htmlEqual(
target.innerHTML,
`
<select>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$>
<option value="b">B</option>
</select>
selected: a
`
);
const select = target.querySelector('select');
ok(select);
const event = new window.Event('change');
select.value = 'b';
select.dispatchEvent(event);
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<select>
<option value="a">A</option>
<option${variant === 'hydrate' ? ' selected' : ''} value="a">A</option$>
<option value="b">B</option>
</select>
selected: b

@ -0,0 +1,5 @@
<script>
let props = $props();
</script>
<div {...props}></div>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
test() {}
});

@ -0,0 +1,13 @@
<script>
import Child from './Child.svelte';
function attachment(){
console.log("up");
}
let enabled = $state(false);
</script>
<button onclick={() => enabled = !enabled}></button>
<Child {@attach enabled && attachment} />

@ -0,0 +1,5 @@
<script>
let props = $props();
</script>
<p {...props}>hello</p>

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
assert.deepEqual(logs, ['up']);
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.deepEqual(logs, ['up']);
flushSync(() => button?.click());
assert.deepEqual(logs, ['up', 'down']);
}
});

@ -0,0 +1,17 @@
<script>
import Component from './Component.svelte';
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>
{#if count < 2}
<Component
data-count={count}
{@attach () => {
console.log('up');
return () => console.log('down');
}}
/>
{/if}

@ -3,8 +3,14 @@
let checkbox = $state(true);
let radio_group = $state('a');
let checkbox_group = $state(['a']);
let select = $state('b');
// this will be ssrd
let select = $state('a');
let textarea = $state('textarea');
$effect(()=>{
// changing the value of `select` on mount
select = 'b';
})
</script>
<p>{JSON.stringify({ text, checkbox, radio_group, checkbox_group, select, textarea })}</p>

@ -0,0 +1,16 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>increment</button><p>a: 1</p><p>b: 2</p><p>c: 3</p>`,
test({ assert, target }) {
const button = target.querySelector('button');
flushSync(() => button?.click());
assert.htmlEqual(
target.innerHTML,
`<button>increment</button><p>a: 2</p><p>b: 3</p><p>c: 4</p>`
);
}
});

@ -0,0 +1,16 @@
<script>
let offset = $state(1);
function* count(offset) {
let i = offset;
while (true) yield i++;
}
let [a, b, c] = $derived(count(offset));
</script>
<button onclick={() => offset += 1}>increment</button>
<p>a: {a}</p>
<p>b: {b}</p>
<p>c: {c}</p>

@ -0,0 +1,13 @@
import { flushSync } from '../../../../src/index-client';
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
test({ assert, target }) {
const btn = target.querySelector('button');
flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
}
});

@ -0,0 +1,13 @@
<script>
let count = $state(0);
let object = {
toString() {
return count;
}
};
</script>
<button onclick={() => count += 1}>
{object}
</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { ok, test } from '../../test';
export default test({
async test({ assert, target }) {
const select = target.querySelector('select');
ok(select);
const [option1, option2] = select;
assert.ok(option1.selected);
assert.ok(!option2.selected);
const btn = target.querySelector('button');
flushSync(() => {
btn?.click();
});
assert.ok(option1.selected);
assert.ok(!option2.selected);
}
});

@ -0,0 +1,10 @@
<script>
let { value = "Hello", spread = { disabled: false } } = $props();
</script>
<button onclick={()=> spread = { disabled: false }}></button>
<select bind:value {...spread}>
<option>Hello</option>
<option>World</option>
</select>

@ -0,0 +1 @@
<select><option value="">--Please choose an option--</option><option value="dog" selected>Dog</option><option value="cat">Cat</option></select>

@ -0,0 +1,10 @@
<script>
import { writable } from 'svelte/store';
const value = writable('dog');
</script>
<select bind:value={$value}>
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
</select>

@ -0,0 +1,5 @@
<script>
let props = $props();
</script>
<option {...props}>{@render props.children?.()}</option>

@ -0,0 +1 @@
<select><option value="">--Please choose an option--</option><option value="dog" selected>Dog</option><option value="cat">Cat</option></select>

@ -0,0 +1,8 @@
<script lang="ts">
import Option from './Option.svelte';
</script>
<select value="dog">
<Option value="">--Please choose an option--</Option>
<Option value="dog">Dog</Option>
<Option value="cat">Cat</Option>
</select>

@ -0,0 +1 @@
<select><option>--Please choose an option--</option><option selected>dog</option><option>cat</option></select>

@ -0,0 +1,13 @@
<select value="dog">
<option>
{@render option("--Please choose an option--")}
</option>
<option>
{@render option("dog")}
</option>
<option>
{@render option("cat")}
</option>
</select>
{#snippet option(val)}{val}{/snippet}

@ -0,0 +1 @@
<select><option>--Please choose an option--</option><option selected>dog</option><option>cat</option></select>

@ -0,0 +1,5 @@
<select value="dog">
<option>--Please choose an option--</option>
<option>dog</option>
<option>cat</option>
</select>

@ -0,0 +1 @@
<select><option value="">--Please choose an option--</option><option value="dog" selected>Dog</option><option value="cat">Cat</option></select>

@ -0,0 +1,5 @@
<select value="dog">
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
</select>

@ -7,12 +7,12 @@ let c = 3;
let d = 4;
export function update(array) {
{
let [$$1, $$2] = array;
((array) => {
var $$array = $.to_array(array, 2);
$.set(a, $$1, true);
$.set(b, $$2, true);
};
$.set(a, $$array[0], true);
$.set(b, $$array[1], true);
})(array);
[c, d] = array;
}

@ -9,10 +9,16 @@ export default function Main($$anchor) {
let y = () => 'test';
var fragment = root();
var div = $.first_child(fragment);
$.set_attribute(div, 'foobar', x);
var svg = $.sibling(div, 2);
$.set_attribute(svg, 'viewBox', x);
var custom_element = $.sibling(svg, 2);
$.template_effect(() => $.set_custom_element_data(custom_element, 'fooBar', x));
$.set_custom_element_data(custom_element, 'fooBar', x);
var div_1 = $.sibling(custom_element, 2);
var svg_1 = $.sibling(div_1, 2);
@ -22,8 +28,6 @@ export default function Main($$anchor) {
$.template_effect(
($0, $1) => {
$.set_attribute(div, 'foobar', x);
$.set_attribute(svg, 'viewBox', x);
$.set_attribute(div_1, 'foobar', $0);
$.set_attribute(svg_1, 'viewBox', $1);
},

@ -3,5 +3,5 @@ import * as $ from 'svelte/internal/server';
export default function Skip_static_subtree($$payload, $$props) {
let { title, content } = $$props;
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select><option value="a">a</option></select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`;
$$payload.out += `<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select><option value="a"${$.maybe_selected($$payload, 'a')}>a</option></select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`;
}

@ -93,7 +93,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) {
// generate with fragments: 'tree'
if (generate === 'client') {
const compiled = compile(source, {
dev: true,
dev: false,
filename: input,
generate,
runes: argv.values.runes,

Loading…
Cancel
Save