hoist-unmodified-var
Ben McCann 1 year ago
commit 8cc7422a61

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow transition undefined payload

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve text node output

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve style parser whitespace handling

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow input elements within button elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply key animations on proxied arrays

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: provide `unstate` in server environment

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve key block reactivity detection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve internal signal dependency checking logic

@ -30,6 +30,8 @@
"dirty-garlics-design",
"dirty-tips-add",
"dry-clocks-grow",
"dry-eggs-play",
"dry-eggs-retire",
"dull-mangos-wave",
"early-ads-tie",
"eight-steaks-shout",
@ -49,6 +51,7 @@
"funny-wombats-argue",
"gentle-sheep-hug",
"giant-roses-press",
"good-cars-visit",
"good-pianos-jump",
"great-icons-retire",
"green-eggs-approve",
@ -74,6 +77,7 @@
"lazy-months-knock",
"lazy-spiders-think",
"lemon-geese-drum",
"light-humans-hang",
"light-pens-watch",
"long-buckets-lay",
"long-crews-return",
@ -91,7 +95,9 @@
"odd-schools-wait",
"odd-shoes-cheat",
"old-flies-jog",
"old-houses-drum",
"old-mails-sneeze",
"old-oranges-compete",
"orange-dingos-poke",
"polite-dolphins-care",
"polite-pumpkins-guess",
@ -114,6 +120,7 @@
"seven-deers-jam",
"seven-ravens-check",
"sharp-gorillas-impress",
"sharp-kids-happen",
"sharp-tomatoes-learn",
"shiny-baboons-play",
"shiny-shrimps-march",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: always treat spread attributes as reactive and separate them if needed

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly call exported state

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take into account setters when spreading and binding

@ -13,7 +13,7 @@
<!-- the text will flash red whenever
the `todo` object changes -->
<button bind:this={btn}>
<button bind:this={btn} on:click>
{todo.done ? '👍' : ''}
{todo.text}
</button>

@ -1,5 +1,23 @@
# svelte
## 5.0.0-next.29
### Patch Changes
- fix: improve text node output ([#10081](https://github.com/sveltejs/svelte/pull/10081))
- fix: improve style parser whitespace handling ([#10077](https://github.com/sveltejs/svelte/pull/10077))
- fix: allow input elements within button elements ([#10083](https://github.com/sveltejs/svelte/pull/10083))
- fix: support TypeScript's `satisfies` operator ([#10068](https://github.com/sveltejs/svelte/pull/10068))
- fix: provide `unstate` in server environment ([`877ff1ee7`](https://github.com/sveltejs/svelte/commit/877ff1ee7d637e2248145d975748e1012a977396))
- fix: improve key block reactivity detection ([#10092](https://github.com/sveltejs/svelte/pull/10092))
- fix: always treat spread attributes as reactive and separate them if needed ([#10071](https://github.com/sveltejs/svelte/pull/10071))
## 5.0.0-next.28
### Patch Changes

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

@ -7,7 +7,7 @@ const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF =
/^\s*(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))(\s*(?=[,)])|\s+of\s+)/;
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_BRACE_OR_SEMICOLON = /[{;]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
@ -153,6 +153,8 @@ function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {import('#compiler').Css.Selector[]} */
const children = [];
allow_comment_or_whitespace(parser);
const start = parser.index;
while (parser.index < parser.template.length) {
@ -286,9 +288,10 @@ function read_selector(parser, inside_pseudo_class = false) {
});
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
children.push({
type: 'Nth',
value: /** @type {string} */ (parser.read(REGEX_NTH_OF)),
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});

@ -121,15 +121,8 @@ function validate_code(code) {
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const interactive_elements = new Set([
'a',
'button',
'iframe',
'embed',
'input',
'select',
'textarea'
]);
// while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
const interactive_elements = new Set(['a', 'button', 'iframe', 'embed', 'select', 'textarea']);
/** @type {Record<string, Set<string>>} */
const disallowed_contents = {

@ -7,7 +7,7 @@ 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 { is_state_source, serialize_get_binding } from './utils.js';
import { remove_types } from '../typescript.js';
/**
@ -250,9 +250,7 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source =
(binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned);
const is_source = binding !== null && is_state_source(binding, state);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?

@ -46,6 +46,18 @@ export function get_assignment_value(node, { state, visit }) {
}
}
/**
* @param {import('#compiler').Binding} binding
* @param {import('./types').ClientTransformState} state
* @returns {boolean}
*/
export function is_state_source(binding, state) {
return (
(binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned || state.analysis.accessors)
);
}
/**
* @param {import('estree').Identifier} node
* @param {import('./types').ClientTransformState} state
@ -94,8 +106,7 @@ export function serialize_get_binding(node, state) {
}
if (
((binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
is_state_source(binding, state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
@ -492,33 +503,6 @@ export function get_prop_source(binding, state, name, initial) {
return b.call('$.prop', ...args);
}
/**
* Creates the output for a state declaration.
* @param {import('estree').VariableDeclarator} declarator
* @param {import('../../scope').Scope} scope
* @param {import('estree').Expression} value
*/
export function create_state_declarators(declarator, scope, value) {
// in the simple `let count = $state(0)` case, we rewrite `$state` as `$.source`
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), // TODO inject declarator for opts, so we can use it below
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = scope.get(/** @type {import('estree').Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}
/** @param {import('estree').Expression} node */
export function should_proxy_or_freeze(node) {
if (

@ -3,11 +3,36 @@ import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import {
can_hoist_declaration,
create_state_declarators,
get_prop_source,
serialize_get_binding
} from '../utils.js';
/**
* Creates the output for a state declaration.
* @param {import('estree').VariableDeclarator} declarator
* @param {import('../../../scope.js').Scope} scope
* @param {import('estree').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 {import('estree').Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value
);
})
];
}
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_legacy = {
VariableDeclaration(node, { state, visit }) {

@ -4,11 +4,11 @@ import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js';
import {
can_hoist_declaration,
create_state_declarators,
get_prop_source,
is_state_source,
should_proxy_or_freeze
} from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js';
import { extract_paths, unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */
export const javascript_visitors_runes = {
@ -237,66 +237,79 @@ export const javascript_visitors_runes = {
}
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
let value =
const value =
args.length === 0
? b.id('undefined')
: /** @type {import('estree').Expression} */ (visit(args[0]));
if (declarator.id.type === 'Identifier') {
if (rune === '$state') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (rune === '$state' || rune === '$state.frozen') {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
if (should_proxy_or_freeze(value)) {
value = b.call('$.proxy', value);
value = b.call(rune === '$state' ? '$.proxy' : '$.freeze', value);
}
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
if (is_state_source(binding, state)) {
value = b.call('$.source', value);
}
} else if (rune === '$state.frozen') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy_or_freeze(value)) {
value = b.call('$.freeze', value);
}
return value;
};
if (binding.reassigned) {
value = b.call('$.source', value);
}
if (declarator.id.type === 'Identifier') {
declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value))
);
} else {
value = b.call('$.derived', b.thunk(value));
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 {import('estree').Identifier} */ (path.node).name
);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? create_state_declarator(binding.node, value)
: value
);
})
);
}
declarations.push(b.declarator(declarator.id, value));
continue;
}
if (rune === '$derived') {
const bindings = state.scope.get_bindings(declarator);
const id = state.scope.generate('derived_value');
declarations.push(
b.declarator(
b.id(id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, value),
b.return(b.array(bindings.map((binding) => binding.node)))
])
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, b.call('$.derived', b.thunk(value))));
} else {
const bindings = state.scope.get_bindings(declarator);
const id = state.scope.generate('derived_value');
declarations.push(
b.declarator(
b.id(id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, value),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
)
)
)
);
for (let i = 0; i < bindings.length; i++) {
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
);
for (let i = 0; i < bindings.length; i++) {
bindings[i].expression = b.member(b.call('$.get', b.id(id)), b.literal(i), true);
}
}
continue;
}
declarations.push(...create_state_declarators(declarator, state.scope, value));
}
if (declarations.length === 0) {

@ -316,7 +316,7 @@ function setup_select_synchronization(value_binding, context) {
* value = $.spread_attributes(element, value, [...])
* });
* ```
* Returns the id of the spread_attribute variable if spread is deemed reactive, `null` otherwise.
* Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise.
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context
* @param {import('#compiler').RegularElement} element
@ -324,7 +324,7 @@ function setup_select_synchronization(value_binding, context) {
* @returns {string | null}
*/
function serialize_element_spread_attributes(attributes, context, element, element_id) {
let is_reactive = false;
let needs_isolation = false;
/** @type {import('estree').Expression[]} */
const values = [];
@ -339,18 +339,32 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
}
is_reactive ||=
attribute.metadata.dynamic ||
(attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression);
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
}
const lowercase_attributes =
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
if (is_reactive) {
const isolated = b.stmt(
b.call(
'$.spread_attributes_effect',
element_id,
b.thunk(b.array(values)),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
)
);
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
context.state.update_effects.push(isolated);
return null;
} else {
const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id, undefined));
context.state.init.push(b.let(id));
context.state.update.push({
singular: isolated,
grouped: b.stmt(
b.assignment(
'=',
@ -367,20 +381,6 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
)
});
return id;
} else {
context.state.init.push(
b.stmt(
b.call(
'$.spread_attributes',
element_id,
b.literal(null),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
)
)
);
return null;
}
}
@ -392,7 +392,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
* @param {import('estree').Identifier} element_id
* @returns {boolean}
*/
function serialize_dynamic_element_spread_attributes(attributes, context, element_id) {
function serialize_dynamic_element_attributes(attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.stylesheet.id) {
context.state.init.push(
@ -402,6 +402,7 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen
return false;
}
let needs_isolation = false;
let is_reactive = false;
/** @type {import('estree').Expression[]} */
@ -415,13 +416,31 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
}
is_reactive ||= attribute.metadata.dynamic;
is_reactive ||=
attribute.metadata.dynamic ||
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
attribute.type === 'SpreadAttribute';
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
}
if (is_reactive) {
const isolated = b.stmt(
b.call(
'$.spread_dynamic_element_attributes_effect',
element_id,
b.thunk(b.array(values)),
b.literal(context.state.analysis.stylesheet.id)
)
);
if (needs_isolation) {
context.state.update_effects.push(isolated);
return false;
} else if (is_reactive) {
const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id));
context.state.update.push({
singular: isolated,
grouped: b.stmt(
b.assignment(
'=',
@ -1605,6 +1624,7 @@ function process_children(nodes, parent, { visit, state }) {
expression = b.call('$.sibling', text_id);
}
let is_fragment = false;
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
@ -1612,7 +1632,12 @@ function process_children(nodes, parent, { visit, state }) {
sequence.push(node);
} else {
if (sequence.length > 0) {
flush_sequence(sequence, true);
flush_sequence(sequence, is_fragment);
// Ensure we move to the next sibling for the case where we move reference within a fragment
if (!is_fragment && sequence.length === 1 && sequence[0].type === 'ExpressionTag') {
expression = b.call('$.sibling', expression);
is_fragment = true;
}
sequence = [];
}
@ -2195,7 +2220,7 @@ export const template_visitors = {
// 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_spread_attributes(attributes, inner_context, element_id) !== null;
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);

@ -1,6 +1,7 @@
export const EACH_ITEM_REACTIVE = 1;
export const EACH_INDEX_REACTIVE = 1 << 1;
export const EACH_KEYED = 1 << 2;
export const EACH_PROXIED = 1 << 3;
export const EACH_IS_CONTROLLED = 1 << 3;
export const EACH_IS_ANIMATED = 1 << 4;
export const EACH_IS_IMMUTABLE = 1 << 6;

@ -351,13 +351,8 @@ function reconcile_tracked_array(
) {
var a_blocks = each_block.v;
const is_computed_key = keys !== null;
var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i;
var active_transitions = each_block.s;
if (is_proxied_array) {
flags &= ~EACH_ITEM_REACTIVE;
}
/** @type {number | void} */
var a = a_blocks.length;
@ -422,7 +417,9 @@ function reconcile_tracked_array(
insert_each_item_block(block, dom, is_controlled, null);
}
} else {
var should_update_block = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0;
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_update_block =
(flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0 || is_animated;
var start = 0;
/** @type {null | Text | Element | Comment} */
@ -505,7 +502,6 @@ function reconcile_tracked_array(
mark_lis(sources);
}
// If keys are animated, we need to do updates before actual moves
var is_animated = (flags & EACH_IS_ANIMATED) !== 0;
var should_create;
if (is_animated) {
var i = b_length;

@ -2428,10 +2428,26 @@ function get_setters(element) {
return setters;
}
/**
* Like `spread_attributes` but self-contained
* @param {Element & ElementCSSInlineStyle} dom
* @param {() => Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {string} css_hash
*/
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
/** @type {Record<string, any> | undefined} */
let current = undefined;
render_effect(() => {
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
});
}
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} dom
* @param {Record<string, unknown> | null} prev
* @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {string} css_hash
@ -2528,18 +2544,30 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
/**
* @param {Element} node
* @param {Record<string, unknown> | null} prev
* @param {() => Record<string, unknown>[]} attrs
* @param {string} css_hash
*/
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
/** @type {Record<string, any> | undefined} */
let current = undefined;
render_effect(() => {
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
});
}
/**
* @param {Element} node
* @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs
* @param {string} css_hash
*/
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
if (node.tagName.includes('-')) {
const next = object_assign({}, ...attrs);
if (prev !== null) {
for (const key in prev) {
if (!(key in next)) {
next[key] = null;
}
for (const key in prev) {
if (!(key in next)) {
next[key] = null;
}
}
for (const key in next) {
@ -2618,8 +2646,13 @@ const spread_props_handler = {
if (typeof p === 'object' && p !== null && key in p) return p[key];
}
},
getOwnPropertyDescriptor() {
return { enumerable: true, configurable: true };
getOwnPropertyDescriptor(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return get_descriptor(p, key);
}
},
has(target, key) {
for (let p of target.props) {

@ -50,6 +50,8 @@ let current_queued_effects = [];
/** @type {Array<() => void>} */
let current_queued_tasks = [];
/** @type {Array<() => void>} */
let current_queued_microtasks = [];
let flush_count = 0;
// Handle signal reactivity tree dependencies and consumer
@ -349,15 +351,21 @@ function execute_signal_fn(signal) {
if (current_dependencies !== null) {
let i;
if (dependencies !== null) {
const dep_length = dependencies.length;
// Include any dependencies up until the current_dependencies_index.
const full_dependencies =
current_dependencies_index === 0
? dependencies
: dependencies.slice(0, current_dependencies_index).concat(current_dependencies);
const dep_length = full_dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
const current_dependencies_set = dep_length > 16 ? new Set(full_dependencies) : null;
for (i = current_dependencies_index; i < dep_length; i++) {
const dependency = dependencies[i];
const dependency = full_dependencies[i];
if (
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
!current_dependencies.includes(dependency)
!full_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency, false);
}
@ -573,6 +581,11 @@ function flush_queued_effects(effects) {
function process_microtask() {
is_micro_task_queued = false;
if (current_queued_microtasks.length > 0) {
const tasks = current_queued_microtasks.slice();
current_queued_microtasks = [];
run_all(tasks);
}
if (flush_count > 101) {
return;
}
@ -631,6 +644,18 @@ export function schedule_task(fn) {
current_queued_tasks.push(fn);
}
/**
* @param {() => void} fn
* @returns {void}
*/
export function schedule_microtask(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(process_microtask);
}
current_queued_microtasks.push(fn);
}
/**
* @returns {void}
*/
@ -691,6 +716,9 @@ export function flushSync(fn) {
if (current_queued_pre_and_render_effects.length > 0 || effects.length > 0) {
flushSync();
}
if (is_micro_task_queued) {
process_microtask();
}
if (is_task_queued) {
process_task();
}

@ -21,7 +21,7 @@ import {
managed_effect,
managed_pre_effect,
mark_subtree_inert,
schedule_task,
schedule_microtask,
untrack
} from './runtime.js';
import { raf } from './timing.js';
@ -279,6 +279,9 @@ function create_transition(dom, init, direction, effect) {
// @ts-ignore
payload = payload({ direction: curr_direction });
}
if (payload == null) {
return;
}
const duration = payload.duration ?? 300;
const delay = payload.delay ?? 0;
const css_fn = payload.css;
@ -354,11 +357,15 @@ function create_transition(dom, init, direction, effect) {
cancelled = false;
create_animation();
}
dispatch_event(dom, 'introstart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
if (animation === null) {
transition.x();
} else {
dispatch_event(dom, 'introstart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
}
/** @type {Animation | TickAnimation} */ (animation).play();
}
/** @type {Animation | TickAnimation} */ (animation).play();
},
// out
o() {
@ -368,11 +375,15 @@ function create_transition(dom, init, direction, effect) {
cancelled = false;
create_animation();
}
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
if (animation === null) {
transition.x();
} else {
/** @type {Animation | TickAnimation} */ (animation).play();
dispatch_event(dom, 'outrostart');
if (needs_reverse) {
/** @type {Animation | TickAnimation} */ (animation).reverse();
} else {
/** @type {Animation | TickAnimation} */ (animation).play();
}
}
},
// cancel
@ -671,7 +682,7 @@ function each_item_animate(block, transitions, index, index_is_reactive) {
transition.c();
}
}
schedule_task(() => {
schedule_microtask(() => {
trigger_transitions(transitions, 'key', from);
});
}

@ -20,3 +20,13 @@ export function beforeUpdate() {}
/** @returns {void} */
export function afterUpdate() {}
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
// There's no signals/proxies on the server, so just return the value
return value;
}

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.28';
export const VERSION = '5.0.0-next.29';
export const PUBLIC_VERSION = '5';

@ -495,19 +495,19 @@
"name": "nth-child",
"args": {
"type": "SelectorList",
"start": 476,
"end": 491,
"start": 485,
"end": 486,
"children": [
{
"type": "Selector",
"start": 476,
"end": 491,
"start": 485,
"end": 486,
"children": [
{
"type": "Nth",
"value": "\n n\n ",
"start": 476,
"end": 491
"value": "n",
"start": 485,
"end": 486
}
]
}

@ -15,4 +15,10 @@
::slotted(.content) {
color: red;
}
:is( /*button*/
button, /*p after h1*/
h1 + p
){
color: red;
}
</style>

@ -2,7 +2,7 @@
"css": {
"type": "Style",
"start": 0,
"end": 313,
"end": 386,
"attributes": [],
"children": [
{
@ -225,12 +225,96 @@
},
"start": 266,
"end": 304
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 306,
"end": 359,
"children": [
{
"type": "Selector",
"start": 306,
"end": 359,
"children": [
{
"type": "PseudoClassSelector",
"name": "is",
"args": {
"type": "SelectorList",
"start": 324,
"end": 355,
"children": [
{
"type": "Selector",
"start": 324,
"end": 330,
"children": [
{
"type": "TypeSelector",
"name": "button",
"start": 324,
"end": 330
}
]
},
{
"type": "Selector",
"start": 349,
"end": 355,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 349,
"end": 351
},
{
"type": "Combinator",
"name": "+",
"start": 352,
"end": 353
},
{
"type": "TypeSelector",
"name": "p",
"start": 354,
"end": 355
}
]
}
]
},
"start": 306,
"end": 359
}
]
}
]
},
"block": {
"type": "Block",
"start": 359,
"end": 377,
"children": [
{
"type": "Declaration",
"start": 363,
"end": 373,
"property": "color",
"value": "red"
}
]
},
"start": 306,
"end": 377
}
],
"content": {
"start": 7,
"end": 305,
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n"
"end": 378,
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n"
}
},
"js": [],

@ -1,13 +1,11 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
html: `<button>0 / 0</button>`,
async test({ assert, target, window }) {
async test({ assert, target }) {
const btn = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await btn?.dispatchEvent(clickEvent);
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1 / 1</button>`);
}
});

@ -2,6 +2,7 @@
import { setup } from './utils.js';
let { num } = $state(setup());
let { num: num_frozen } = $state(setup());
</script>
<button on:click={() => num++}>{num}</button>
<button on:click={() => { num++; num_frozen++; }}>{num} / {num_frozen}</button>

@ -0,0 +1,62 @@
import { test } from '../../test';
export default test({
html: `
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
`,
async test({ assert, target }) {
const [b1, b2, b3, b4] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
`
);
b2?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="red">red</button>
<button class="red">red</button>
`
);
b3?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="red">red</button>
`
);
b4?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
`
);
}
});

@ -0,0 +1,24 @@
<script>
let tag = $state('button');
let values = $state({ a: 'red', b: 'red', c: 'red', d: 'red' });
let count = 0;
const factory = (name) => {
count++;
// check that spread effects are isolated from each other
if (count > 8) throw new Error('too many calls');
return {
class: values[name],
onclick: () => {
values[name] = 'blue';
}
}
}
</script>
<button {...factory('a')}>{values.a}</button>
<button {...factory('b')}>{values.b}</button>
<svelte:element this={tag} {...factory('c')}>{values.c}</svelte:element>
<svelte:element this={tag} {...factory('d')}>{values.d}</svelte:element>

@ -0,0 +1,24 @@
import { test } from '../../test';
export default test({
html: `
<div style="color: red;"></div><div class="red"></div><div class="red"></div>
<div style="color: red;"></div><div class="red"></div><div class="red"></div>
<button>toggle</button
`,
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<div class="blue" style="color: blue;"></div><div class="blue"></div><div class="blue"></div>
<div class="blue" style="color: blue;"></div><div class="blue"></div><div class="blue"></div>
<button>toggle</button
`
);
}
});

@ -1,5 +1,6 @@
<script>
let value = $state('red');
let tag = $state('div');
const getValue = () => {
return value;
@ -10,9 +11,19 @@
const getSpread = () => {
return { class: value };
}
const props = {
get class() {
return value;
}
}
</script>
<div class:blue={getClass()} style:color={getValue()}></div>
<div {...getSpread()}></div>
<button on:click={() => value = 'blue'}>toggle</button>
<div {...props}></div>
<svelte:element this={tag} class:blue={getClass()} style:color={getValue()}></svelte:element>
<svelte:element this={tag} {...getSpread()}></svelte:element>
<svelte:element this={tag} {...props}></svelte:element>
<button on:click={() => value = 'blue'}>toggle</button>

@ -0,0 +1,21 @@
import { test } from '../../test';
export default test({
html: `<button class="foo">0</button><button class="foo">0</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
await btn1?.click();
assert.htmlEqual(
target.innerHTML,
`<button class="foo">1</button><button class="foo">1</button>`
);
await btn2?.click();
assert.htmlEqual(
target.innerHTML,
`<button class="foo">2</button><button class="foo">2</button>`
);
}
});

@ -0,0 +1,6 @@
<script>
let { value, ...props } = $props();
</script>
<button {...props} onclick={() => value++}>{value}</button>

@ -0,0 +1,12 @@
<script>
import Button from './button.svelte';
let value = $state(0);
const props = {
class: 'foo'
};
</script>
<Button {...props} bind:value />
<button {...props} onclick={() => value++}>{value}</button>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<p>A<br>B<br>C<br></p>`
});

@ -0,0 +1,9 @@
<script>
let array = $state(['A', 'B', 'C']);
</script>
<p>
{#each array as a}
{a}<br/>
{/each}
</p>

@ -0,0 +1,39 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<p>test costs $1</p><p>test 2 costs $2</p><p>test costs $1</p><p>test 2 costs $2</p><button>add</button><button>change</button><button>reload</button>`,
skip_if_ssr: 'permanent',
skip_if_hydrate: 'permanent',
async test({ assert, target }) {
const [btn1, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test costs $1</p><p>test 2 costs $2000</p><button>add</button><button>change</button><button>reload</button>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test 3 costs $3</p><p>test costs $1</p><p>test 2 costs $2000</p><p>test 3 costs $3</p><button>add</button><button>change</button><button>reload</button>`
);
flushSync(() => {
btn3.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2</p><p>test costs $1</p><p>test 2 costs $2</p><button>add</button><button>change</button><button>reload</button>`
);
}
});

@ -0,0 +1,54 @@
<script>
let data = $state({ items: [] });
function fetchData() {
data = {
items: [{
id: 1,
price: 1,
name: 'test'
}, {
id: 2,
price: 2,
name: 'test 2'
}]
};
}
fetchData();
function copyItems(original) {
return [...original.map((item) => ({ ...item }))];
}
let items = $state();
$effect(() => {
items = copyItems(data.items);
});
</script>
{#each items as item}
<p>{item.name} costs ${item.price}</p>
{/each}
{#each items as item (item.id)}
<p>{item.name} costs ${item.price}</p>
{/each}
<button onclick={() => {
items.push({
id: 3,
price: 3,
name: 'test 3'
})
}}>add</button>
<button onclick={() => {
data.items[1].price = 2000
}}>change</button>
<button onclick={() => {
fetchData();
}}>reload</button>

@ -1,16 +0,0 @@
import { test } from '../../test';
export default test({
html: `<div style="color: red;"></div><div class="red"></div><button>toggle</button`,
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'<div class="blue" style="color: blue;"></div><div class="blue"></div><button>toggle</button>'
);
}
});

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `0 0 <button>0 / 0</button>`);
const [btn] = target.querySelectorAll('button');
btn?.click();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, '0 1 <button>0 / 1</button>');
}
});

@ -0,0 +1,7 @@
<script>
import Sub from './sub.svelte'
let sub = $state();
</script>
<Sub bind:this={sub} />
<button on:click={() => sub.increment()}>{sub?.count1.value} / {sub?.count2.value}</button>

@ -0,0 +1,15 @@
<script>
export const count1 = $state.frozen({value: 0});
export const count2 = $state({value: 0});
export function increment() {
count2.value += 1;
}
</script>
{count1.value}
{count2.value}
<!-- so that count1/2 become sources -->
<svelte:options accessors />

@ -0,0 +1,40 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>show</button><button>animate</button>`,
async test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1>Hello\n!</h1>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn2.click();
});
assert.htmlEqual(target.innerHTML, `<button>show</button><button>animate</button>`);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<button>show</button><button>animate</button><h1 style="opacity: 0;">Hello\n!</h1>`
);
}
});

@ -0,0 +1,16 @@
<script>
import { fade } from 'svelte/transition';
let show = $state(false);
let animate = $state(false);
function maybe(node, animate) {
if (animate) return fade(node);
}
</script>
<button onclick={() => show = !show}>show</button><button onclick={() => animate = !animate}>animate</button>
{#if show}
<h1 transition:maybe={animate}>Hello {name}!</h1>
{/if}

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

@ -0,0 +1,12 @@
<script>
let x = $state({a: 0, b:0});
let count = 0;
</script>
<p>{x.a} - {x.b}</p>
<button onclick={() => {
const a = ++count;
x = {a, b: a};
}}>+</button>

@ -107,3 +107,7 @@ In Svelte 4, it was possible to specify event attributes on HTML elements as a s
```
This is not recommended, and is no longer possible in Svelte 5, where properties like `onclick` replace `on:click` as the mechanism for adding [event handlers](/docs/event-handlers).
### `null` and `undefined` become the empty string
In Svelte 4, `null` and `undefined` were printed as the corresponding string. In 99 out of 100 cases you want this to become the empty string instead, which is also what most other frameworks out there do. Therefore, in Svelte 5, `null` and `undefined` become the empty string.

Loading…
Cancel
Save