merge async

pull/16197/head
Rich Harris 8 months ago
commit dd9bb711d2

@ -1,4 +1,4 @@
Copyright (c) 2016-2025 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -5,7 +5,7 @@
</picture>
</a>
[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
[![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat)
## What is Svelte?

@ -20,12 +20,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(computed5) === 6);
for (let i = 0; i < 1000; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(computed5) === 6);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < 50; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(last) === i + 50);

@ -25,12 +25,12 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
counter = 0;
for (let i = 0; i < iter; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === len + i);

@ -28,13 +28,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === 2 * width);
counter = 0;
for (let i = 0; i < 500; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === (i + 1) * width);

@ -22,13 +22,13 @@ function setup() {
destroy,
run() {
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i);
});
assert($.get(splited[i]) === i + 1);
}
for (let i = 0; i < 10; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(heads[i], i * 2);
});
assert($.get(splited[i]) === i * 2 + 1);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === size);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(current) === i * size);

@ -38,13 +38,13 @@ function setup() {
destroy,
run() {
const constant = count(width);
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(sum) === constant);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
assert($.get(sum) === constant - width + i * width);

@ -25,13 +25,13 @@ function setup() {
return {
destroy,
run() {
$.flush_sync(() => {
$.flush(() => {
$.set(head, 1);
});
assert($.get(current) === 40);
counter = 0;
for (let i = 0; i < 100; i++) {
$.flush_sync(() => {
$.flush(() => {
$.set(head, i);
});
}

@ -51,11 +51,11 @@ function setup() {
*/
run(i) {
res.length = 0;
$.flush_sync(() => {
$.flush(() => {
$.set(B, 1);
$.set(A, 1 + i * 2);
});
$.flush_sync(() => {
$.flush(() => {
$.set(A, 2 + i * 2);
$.set(B, 2);
});

@ -1,5 +1,21 @@
# svelte
## 5.20.4
### Patch Changes
- fix: update types and inline docs for flushSync ([#15348](https://github.com/sveltejs/svelte/pull/15348))
## 5.20.3
### Patch Changes
- fix: allow `@const` inside `#key` ([#15377](https://github.com/sveltejs/svelte/pull/15377))
- fix: remove unnecessary `?? ''` on some expressions ([#15287](https://github.com/sveltejs/svelte/pull/15287))
- fix: correctly override class attributes with class directives ([#15352](https://github.com/sveltejs/svelte/pull/15352))
## 5.20.2
### Patch Changes

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

@ -271,11 +271,9 @@ export function analyze_module(ast, options) {
expression: null,
function_depth: 0,
has_props_rune: false,
instance_scope: /** @type {any} */ (null),
options: /** @type {ValidatedCompileOptions} */ (options),
parent_element: null,
reactive_statement: null,
reactive_statements: new Map()
reactive_statement: null
},
visitors
);
@ -579,7 +577,7 @@ export function analyze_component(root, source, options) {
binding.declaration_kind !== 'import'
) {
binding.kind = 'state';
binding.mutated = binding.updated = true;
binding.mutated = true;
}
}
}
@ -633,9 +631,7 @@ export function analyze_component(root, source, options) {
expression: null,
derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: new Map()
reactive_statement: null
};
walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
@ -697,9 +693,7 @@ export function analyze_component(root, source, options) {
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
instance_scope: instance.scope,
reactive_statement: null,
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
derived_state: [],
@ -780,66 +774,40 @@ export function analyze_component(root, source, options) {
if (!should_ignore_unused) {
warn_unused(analysis.css.ast);
}
}
outer: for (const node of analysis.elements) {
if (node.metadata.scoped) {
// Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (node.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {AST.Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
// The spread method appends the hash to the end of the class attribute on its own
continue outer;
}
for (const node of analysis.elements) {
if (node.metadata.scoped && is_custom_element_node(node)) {
mark_subtree_dynamic(node.metadata.path);
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== 'class') continue;
// The dynamic class method appends the hash to the end of the class attribute on its own
if (attribute.metadata.needs_clsx) continue outer;
let has_class = false;
let has_spread = false;
let has_class_directive = false;
class_attribute = attribute;
}
for (const attribute of node.attributes) {
// The spread method appends the hash to the end of the class attribute on its own
if (attribute.type === 'SpreadAttribute') {
has_spread = true;
break;
}
has_class_directive ||= attribute.type === 'ClassDirective';
has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class';
}
if (class_attribute && class_attribute.value !== true) {
if (is_text_attribute(class_attribute)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`;
} else {
/** @type {AST.Text} */
const css_text = {
type: 'Text',
data: ` ${analysis.css.hash}`,
raw: ` ${analysis.css.hash}`,
start: -1,
end: -1
};
if (Array.isArray(class_attribute.value)) {
class_attribute.value.push(css_text);
} else {
class_attribute.value = [class_attribute.value, css_text];
}
// We need an empty class to generate the set_class() or class="" correctly
if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: '',
raw: '',
start: -1,
end: -1
}
} else {
node.attributes.push(
create_attribute('class', -1, -1, [
{
type: 'Text',
data: analysis.css.hash,
raw: analysis.css.hash,
start: -1,
end: -1
}
])
);
if (is_custom_element_node(node) && node.attributes.length === 1) {
mark_subtree_dynamic(node.metadata.path);
}
}
}
])
);
}
}

@ -1,7 +1,6 @@
import type { Scope } from '../scope.js';
import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler';
import type { LabeledStatement } from 'estree';
export interface AnalysisState {
scope: Scope;
@ -23,9 +22,7 @@ export interface AnalysisState {
function_depth: number;
// legacy stuff
instance_scope: Scope;
reactive_statement: null | ReactiveStatement;
reactive_statements: Map<LabeledStatement, ReactiveStatement>;
}
export type Context<State extends AnalysisState = AnalysisState> = import('zimmerframe').Context<

@ -162,7 +162,7 @@ function get_delegated_event(event_name, handler, context) {
return unhoisted;
}
if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) {
if (binding !== null && binding.initial !== null && !binding.updated) {
const binding_type = binding.initial.type;
if (

@ -5,7 +5,7 @@ import {
is_text_attribute,
object
} from '../../../utils/ast.js';
import { validate_no_const_assignment } from './shared/utils.js';
import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
@ -158,7 +158,7 @@ export function BindDirective(node, context) {
return;
}
validate_no_const_assignment(node, node.expression, context.state.scope, true);
validate_assignment(node, node.expression, context.state);
const assignee = node.expression;
const left = object(assignee);
@ -184,14 +184,6 @@ export function BindDirective(node, context) {
) {
e.bind_invalid_value(node.expression);
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
if (binding?.kind === 'snippet') {
e.snippet_parameter_assignment(node);
}
}
if (node.name === 'group') {

@ -214,14 +214,6 @@ export function CallExpression(node, context) {
break;
}
if (node.callee.type === 'Identifier') {
const binding = context.state.scope.get(node.callee.name);
if (binding !== null) {
binding.is_called = true;
}
}
// `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning
if (rune === '$derived') {
const expression = create_expression_metadata();

@ -25,6 +25,7 @@ export function ConstTag(node, context) {
grand_parent?.type !== 'AwaitBlock' &&
grand_parent?.type !== 'SnippetBlock' &&
grand_parent?.type !== 'SvelteBoundary' &&
grand_parent?.type !== 'KeyBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {

@ -22,7 +22,7 @@ export function ExportSpecifier(node, context) {
});
const binding = context.state.scope.get(local_name);
if (binding) binding.reassigned = binding.updated = true;
if (binding) binding.reassigned = true;
}
} else {
validate_export(node, context.state.scope, local_name);

@ -64,7 +64,7 @@ export function LabeledStatement(node, context) {
}
}
context.state.reactive_statements.set(node, reactive_statement);
context.state.analysis.reactive_statements.set(node, reactive_statement);
if (
node.body.type === 'ExpressionStatement' &&

@ -1,4 +1,4 @@
/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */
/** @import { TaggedTemplateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_pure } from './shared/utils.js';
@ -12,12 +12,5 @@ export function TaggedTemplateExpression(node, context) {
context.state.expression.has_state = true;
}
if (node.tag.type === 'Identifier') {
const binding = context.state.scope.get(node.tag.name);
if (binding !== null) {
binding.is_called = true;
}
}
context.next();
}

@ -10,12 +10,12 @@ import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {AssignmentExpression | UpdateExpression} node
* @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
export function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective');
if (argument.type === 'Identifier') {
const binding = state.scope.get(argument.name);

@ -316,7 +316,7 @@ export function client_component(analysis, options) {
const setter = b.set(key, [
b.stmt(b.call(b.id(name), b.id('$$value'))),
b.stmt(b.call('$.flush_sync'))
b.stmt(b.call('$.flush'))
]);
if (analysis.runes && binding.initial) {

@ -1,4 +1,4 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
@ -20,9 +20,9 @@ import { build_getter } from '../utils.js';
import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_style_directives,
build_set_attributes
build_set_attributes,
build_set_class
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import {
@ -223,13 +223,13 @@ export function RegularElement(node, context) {
build_set_attributes(
attributes,
class_directives,
context,
node,
node_id,
attributes_id,
(node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true,
is_custom_element_node(node) && b.true,
context.state
is_custom_element_node(node) && b.true
);
// If value binding exists, that one takes care of calling $.init_select
@ -270,13 +270,22 @@ export function RegularElement(node, context) {
continue;
}
const name = get_attribute_name(node, attribute);
if (
!is_custom_element &&
!cannot_be_set_statically(attribute.name) &&
(attribute.value === true || is_text_attribute(attribute))
(attribute.value === true || is_text_attribute(attribute)) &&
(name !== 'class' || class_directives.length === 0)
) {
const name = get_attribute_name(node, attribute);
const value = is_text_attribute(attribute) ? attribute.value[0].data : true;
let value = is_text_attribute(attribute) ? attribute.value[0].data : true;
if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) {
if (value === true || value === '') {
value = context.state.analysis.css.hash;
} else {
value += ' ' + context.state.analysis.css.hash;
}
}
if (name !== 'class' || value) {
context.state.template.push(
@ -290,15 +299,22 @@ export function RegularElement(node, context) {
continue;
}
const is = is_custom_element
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
const is =
is_custom_element && name !== 'class'
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
: build_element_attribute_update_assignment(
node,
node_id,
attribute,
attributes,
class_directives,
context
);
if (is) is_attributes_reactive = true;
}
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, node_id, context, is_attributes_reactive);
// style directives must be applied last since they could override class/style attributes
build_style_directives(style_directives, node_id, context, is_attributes_reactive);
if (
@ -368,15 +384,14 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
child_state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, 'textContent'),
build_template_chunk(trimmed, context.visit, child_state).value
)
)
);
const { value } = build_template_chunk(trimmed, context.visit, child_state);
const empty_string = value.type === 'Literal' && value.value === '';
if (!empty_string) {
child_state.init.push(
b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value))
);
}
} else {
/** @type {Expression} */
let arg = context.state.node;
@ -496,6 +511,32 @@ function setup_select_synchronization(value_binding, context) {
);
}
/**
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @return {ObjectExpression}
*/
export function build_class_directives_object(class_directives, context) {
let properties = [];
for (const d of class_directives) {
let expression = /** @type Expression */ (context.visit(d.expression));
if (d.metadata.expression.has_call || d.metadata.expression.is_async) {
expression = get_expression_id(
d.metadata.expression.is_async
? context.state.async_expressions
: context.state.expressions,
expression
);
}
properties.push(b.init(d.name, expression));
}
return b.object(properties);
}
/**
* Serializes an assignment to an element property by adding relevant statements to either only
* the init or the the init and update arrays, depending on whether or not the value is dynamic.
@ -522,6 +563,7 @@ function setup_select_synchronization(value_binding, context) {
* @param {Identifier} node_id
* @param {AST.Attribute} attribute
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @returns {boolean}
*/
@ -530,6 +572,7 @@ function build_element_attribute_update_assignment(
node_id,
attribute,
attributes,
class_directives,
context
) {
const state = context.state;
@ -568,19 +611,15 @@ function build_element_attribute_update_assignment(
let update;
if (name === 'class') {
if (attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
update = b.stmt(
b.call(
is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class',
node_id,
value,
attribute.metadata.needs_clsx && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: undefined
)
return build_set_class(
element,
node_id,
attribute,
value,
has_state,
class_directives,
context,
!is_svg && !is_mathml
);
} else if (name === 'value') {
update = b.stmt(b.call('$.set_value', node_id, value));
@ -644,14 +683,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let { value, has_state } = build_attribute_value(attribute.value, context);
// We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes
if (name === 'class' && attribute.metadata.needs_clsx) {
if (context.state.analysis.css.hash) {
value = b.array([value, b.literal(context.state.analysis.css.hash)]);
}
value = b.call('$.clsx', value);
}
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value));
if (has_state) {

@ -7,11 +7,11 @@ import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import {
build_attribute_value,
build_class_directives,
build_set_attributes,
build_set_class,
build_style_directives
} from './shared/element.js';
import { build_render_statement } from './shared/utils.js';
import { build_render_statement, get_expression_id } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -78,31 +78,52 @@ export function SvelteElement(node, context) {
// Then do attributes
let is_attributes_reactive = false;
if (attributes.length === 0) {
if (context.state.analysis.css.hash) {
inner_context.state.init.push(
b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash)))
);
}
} else {
if (
attributes.length === 1 &&
attributes[0].type === 'Attribute' &&
attributes[0].name.toLowerCase() === 'class'
) {
// special case when there only a class attribute
let { value, has_state } = build_attribute_value(
attributes[0].value,
context,
(value, metadata) =>
metadata.has_call || metadata.is_async
? get_expression_id(
metadata.is_async ? context.state.async_expressions : context.state.expressions,
value
)
: value
);
is_attributes_reactive = build_set_class(
node,
element_id,
attributes[0],
value,
has_state,
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.
is_attributes_reactive = build_set_attributes(
attributes,
class_directives,
inner_context,
node,
element_id,
attributes_id,
b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')),
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')),
context.state
b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-'))
);
}
// class/style directives must be applied last since they could override class/style attributes
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
// style directives must be applied last since they could override class/style attributes
build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);
/** @type {Statement[]} */

@ -1,32 +1,34 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { escape_html } from '../../../../../../escaping.js';
import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter } from '../../utils.js';
import { build_class_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} element_id
* @param {Identifier} attributes_id
* @param {false | Expression} preserve_attribute_case
* @param {false | Expression} is_custom_element
* @param {ComponentClientTransformState} state
*/
export function build_set_attributes(
attributes,
class_directives,
context,
element,
element_id,
attributes_id,
preserve_attribute_case,
is_custom_element,
state
is_custom_element
) {
let is_dynamic = false;
@ -79,6 +81,19 @@ export function build_set_attributes(
}
}
if (class_directives.length) {
values.push(
b.prop(
'init',
b.array([b.id('$.CLASS')]),
build_class_directives_object(class_directives, context)
)
);
is_dynamic ||=
class_directives.find((directive) => directive.metadata.expression.has_state) !== null;
}
const call = b.call(
'$.set_attributes',
element_id,
@ -150,39 +165,6 @@ export function build_style_directives(
}
}
/**
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {AST.ClassDirective[]} class_directives
* @param {Identifier} element_id
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
export function build_class_directives(
class_directives,
element_id,
context,
is_attributes_reactive
) {
const state = context.state;
for (const directive of class_directives) {
const { has_state, has_call, is_async } = directive.metadata.expression;
let value = /** @type {Expression} */ (context.visit(directive.expression));
if (has_call || is_async) {
value = get_expression_id(is_async ? state.async_expressions : state.expressions, value);
}
const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value));
if (is_attributes_reactive || has_state) {
state.update.push(update);
} else {
state.init.push(update);
}
}
}
/**
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
@ -223,3 +205,93 @@ export function get_attribute_name(element, attribute) {
return attribute.name;
}
/**
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Identifier} node_id
* @param {AST.Attribute | null} attribute
* @param {Expression} value
* @param {boolean} has_state
* @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context
* @param {boolean} is_html
* @returns {boolean}
*/
export function build_set_class(
element,
node_id,
attribute,
value,
has_state,
class_directives,
context,
is_html
) {
if (attribute && attribute.metadata.needs_clsx) {
value = b.call('$.clsx', value);
}
/** @type {Identifier | undefined} */
let previous_id;
/** @type {ObjectExpression | Identifier | undefined} */
let prev;
/** @type {ObjectExpression | undefined} */
let next;
if (class_directives.length) {
next = build_class_directives_object(class_directives, context);
has_state ||= class_directives.some((d) => d.metadata.expression.has_state);
if (has_state) {
previous_id = b.id(context.state.scope.generate('classes'));
context.state.init.push(b.declaration('let', [b.declarator(previous_id)]));
prev = previous_id;
} else {
prev = b.object([]);
}
}
/** @type {Expression | undefined} */
let css_hash;
if (element.metadata.scoped && context.state.analysis.css.hash) {
if (value.type === 'Literal' && (value.value === '' || value.value === null)) {
value = b.literal(context.state.analysis.css.hash);
} else if (value.type === 'Literal' && typeof value.value === 'string') {
value = b.literal(escape_html(value.value, true) + ' ' + context.state.analysis.css.hash);
} else {
css_hash = b.literal(context.state.analysis.css.hash);
}
}
if (!css_hash && next) {
css_hash = b.null;
}
/** @type {Expression} */
let set_class = b.call(
'$.set_class',
node_id,
is_html ? b.literal(1) : b.literal(0),
value,
css_hash,
prev,
next
);
if (previous_id) {
set_class = b.assignment('=', previous_id, set_class);
}
const update = b.stmt(set_class);
if (has_state) {
context.state.update.push(update);
return true;
}
context.state.init.push(update);
return false;
}

@ -108,11 +108,15 @@ export function build_template_chunk(
if (node.type === 'Text') {
quasi.value.cooked += node.data;
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
} else if (node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.cooked += node.expression.value + '';
}
} else {
} else if (
node.expression.type !== 'Identifier' ||
node.expression.name !== 'undefined' ||
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
node.metadata.expression
@ -125,31 +129,33 @@ export function build_template_chunk(
// If we have a single expression, then pass that in directly to possibly avoid doing
// extra work in the template_effect (instead we do the work in set_text).
return { value, has_state };
} else {
// add `?? ''` where necessary (TODO optimise more cases)
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
(value.operator === '??' || value.operator === '||')
) {
// `foo ?? null` -=> `foo ?? ''`
// otherwise leave the expression untouched
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} else if (
state.analysis.props_id &&
value.type === 'Identifier' &&
value.name === state.analysis.props_id.name
) {
// do nothing ($props.id() is never null/undefined)
} else {
value = b.logical('??', value, b.literal(''));
}
if (
value.type === 'LogicalExpression' &&
value.right.type === 'Literal' &&
(value.operator === '??' || value.operator === '||')
) {
// `foo ?? null` -=> `foo ?? ''`
// otherwise leave the expression untouched
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
}
const is_defined =
value.type === 'BinaryExpression' ||
(value.type === 'UnaryExpression' && value.operator !== 'void') ||
(value.type === 'LogicalExpression' && value.right.type === 'Literal') ||
(value.type === 'Identifier' && value.name === state.analysis.props_id?.name);
expressions.push(value);
if (!is_defined) {
// add `?? ''` where necessary (TODO optimise more cases)
value = b.logical('??', value, b.literal(''));
}
expressions.push(value);
quasi = b.quasi('', i + 1 === values.length);
quasis.push(quasi);
}
@ -159,7 +165,10 @@ export function build_template_chunk(
quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked));
}
const value = b.template(quasis, expressions);
const value =
expressions.length > 0
? b.template(quasis, expressions)
: b.literal(/** @type {string} */ (quasi.value.cooked));
return { value, has_state };
}

@ -1,4 +1,4 @@
/** @import { Expression, Literal } from 'estree' */
/** @import { Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST, Namespace } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import {
@ -24,6 +24,7 @@ import {
is_content_editable_binding,
is_load_error_element
} from '../../../../../../utils.js';
import { escape_html } from '../../../../../../escaping.js';
const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style'];
@ -86,23 +87,15 @@ export function build_element_attributes(node, context) {
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
if (attribute.name === 'class') {
class_index = attributes.length;
if (attribute.metadata.needs_clsx) {
const clsx_value = b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
);
attributes.push({
...attribute,
value: {
.../** @type {AST.ExpressionTag} */ (attribute.value),
expression: context.state.analysis.css.hash
? b.binary(
'+',
b.binary('+', clsx_value, b.literal(' ')),
b.literal(context.state.analysis.css.hash)
)
: clsx_value
expression: b.call(
'$.clsx',
/** @type {AST.ExpressionTag} */ (attribute.value).expression
)
}
});
} else {
@ -219,8 +212,9 @@ export function build_element_attributes(node, context) {
}
}
if (class_directives.length > 0 && !has_spread) {
const class_attribute = build_class_directives(
if ((node.metadata.scoped || class_directives.length) && !has_spread) {
const class_attribute = build_to_class(
node.metadata.scoped ? context.state.analysis.css.hash : null,
class_directives,
/** @type {AST.Attribute | null} */ (attributes[class_index] ?? null)
);
@ -274,9 +268,14 @@ export function build_element_attributes(node, context) {
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
// pre-escape and inline literal attributes :
if (value.type === 'Literal' && typeof value.value === 'string') {
context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else {
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true)
);
}
}
}
@ -322,7 +321,7 @@ function build_element_spread_attributes(
let styles;
let flags = 0;
if (class_directives.length > 0 || context.state.analysis.css.hash) {
if (class_directives.length) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
@ -331,11 +330,6 @@ function build_element_spread_attributes(
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
if (context.state.analysis.css.hash) {
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
}
classes = b.object(properties);
}
@ -374,55 +368,82 @@ function build_element_spread_attributes(
})
);
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
const css_hash = context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash)
: b.null;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
}
/**
*
* @param {string | null} hash
* @param {AST.ClassDirective[]} class_directives
* @param {AST.Attribute | null} class_attribute
* @returns
*/
function build_class_directives(class_directives, class_attribute) {
const expressions = class_directives.map((directive) =>
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
);
function build_to_class(hash, class_directives, class_attribute) {
if (class_attribute === null) {
class_attribute = create_attribute('class', -1, -1, []);
}
const chunks = get_attribute_chunks(class_attribute.value);
const last = chunks.at(-1);
if (last?.type === 'Text') {
last.data += ' ';
last.raw += ' ';
} else if (last) {
chunks.push({
type: 'Text',
start: -1,
end: -1,
data: ' ',
raw: ' '
});
/** @type {ObjectExpression | undefined} */
let classes;
if (class_directives.length) {
classes = b.object(
class_directives.map((directive) =>
b.prop('init', b.literal(directive.name), directive.expression)
)
);
}
/** @type {Expression} */
let class_name;
if (class_attribute.value === true) {
class_name = b.literal('');
} else if (Array.isArray(class_attribute.value)) {
if (class_attribute.value.length === 0) {
class_name = b.null;
} else {
class_name = class_attribute.value
.map((val) => (val.type === 'Text' ? b.literal(val.data) : val.expression))
.reduce((left, right) => b.binary('+', left, right));
}
} else {
class_name = class_attribute.value.expression;
}
chunks.push({
/** @type {Expression} */
let expression;
if (
hash &&
!classes &&
class_name.type === 'Literal' &&
(class_name.value === null || class_name.value === '' || typeof class_name.value === 'string')
) {
if (class_name.value === null || class_name.value === '') {
expression = b.literal(hash);
} else {
expression = b.literal(escape_html(class_name.value, true) + ' ' + hash);
}
} else {
expression = b.call('$.to_class', class_name, b.literal(hash), classes);
}
class_attribute.value = {
type: 'ExpressionTag',
start: -1,
end: -1,
expression: b.call(
b.member(b.call(b.member(b.array(expressions), 'filter'), b.id('Boolean')), b.id('join')),
b.literal(' ')
),
expression: expression,
metadata: {
expression: create_expression_metadata()
}
});
};
class_attribute.value = chunks;
return class_attribute;
}

@ -1,6 +1,6 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AST, Binding, DeclarationKind } from '#compiler' */
/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { create_expression_metadata } from './nodes.js';
@ -16,6 +16,71 @@ import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
export class Binding {
/** @type {Scope} */
scope;
/** @type {Identifier} */
node;
/** @type {BindingKind} */
kind;
/** @type {DeclarationKind} */
declaration_kind;
/**
* What the value was initialized with.
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
* @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock}
*/
initial;
/** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */
references = [];
/**
* For `legacy_reactive`: its reactive dependencies
* @type {Binding[]}
*/
legacy_dependencies = [];
/**
* Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props()
* @type {string | null}
*/
prop_alias = null;
/**
* Additional metadata, varies per binding type
* @type {null | { inside_rest?: boolean }}
*/
metadata = null;
mutated = false;
reassigned = false;
/**
*
* @param {Scope} scope
* @param {Identifier} node
* @param {BindingKind} kind
* @param {DeclarationKind} declaration_kind
* @param {Binding['initial']} initial
*/
constructor(scope, node, kind, declaration_kind, initial) {
this.scope = scope;
this.node = node;
this.initial = initial;
this.kind = kind;
this.declaration_kind = declaration_kind;
}
get updated() {
return this.mutated || this.reassigned;
}
}
export class Scope {
/** @type {ScopeRoot} */
root;
@ -100,22 +165,7 @@ export class Scope {
e.declaration_duplicate(node, node.name);
}
/** @type {Binding} */
const binding = {
node,
references: [],
legacy_dependencies: [],
initial,
reassigned: false,
mutated: false,
updated: false,
scope: this,
kind,
declaration_kind,
is_called: false,
prop_alias: null,
metadata: null
};
const binding = new Binding(this, node, kind, declaration_kind, initial);
validate_identifier_name(binding, this.function_depth);
@ -707,8 +757,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const binding = left && scope.get(left.name);
if (binding !== null && left !== binding.node) {
binding.updated = true;
if (left === expression) {
binding.reassigned = true;
} else {

@ -1,12 +1,5 @@
import type {
ClassDeclaration,
Expression,
FunctionDeclaration,
Identifier,
ImportDeclaration
} from 'estree';
import type { SourceMap } from 'magic-string';
import type { Scope } from '../phases/scope.js';
import type { Binding } from '../phases/scope.js';
import type { AST, Namespace } from './template.js';
import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js';
@ -246,6 +239,20 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions &
hmr: CompileOptions['hmr'];
};
export type BindingKind =
| 'normal' // A variable that is not in any way special
| 'prop' // A normal prop (possibly reassigned or mutated)
| 'bindable_prop' // A prop one can `bind:` to (possibly reassigned or mutated)
| 'rest_prop' // A rest prop
| 'raw_state' // A state variable
| 'state' // A deeply reactive state variable
| 'derived' // A derived variable
| 'each' // An each block parameter
| '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
export type DeclarationKind =
| 'var'
| 'let'
@ -256,66 +263,6 @@ export type DeclarationKind =
| 'rest_param'
| 'synthetic';
export interface Binding {
node: Identifier;
/**
* - `normal`: A variable that is not in any way special
* - `prop`: A normal prop (possibly reassigned or mutated)
* - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated)
* - `rest_prop`: A rest prop
* - `state`: A state variable
* - `derived`: A derived variable
* - `each`: An each block parameter
* - `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
*/
kind:
| 'normal'
| 'prop'
| 'bindable_prop'
| 'rest_prop'
| 'state'
| 'raw_state'
| 'derived'
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'template'
| 'snippet';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
*/
initial:
| null
| Expression
| FunctionDeclaration
| ClassDeclaration
| ImportDeclaration
| AST.EachBlock
| AST.SnippetBlock;
is_called: boolean;
references: { node: Identifier; path: AST.SvelteNode[] }[];
mutated: boolean;
reassigned: boolean;
/** `true` if mutated _or_ reassigned */
updated: boolean;
scope: Scope;
/** For `legacy_reactive`: its reactive dependencies */
legacy_dependencies: Binding[];
/** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */
prop_alias: string | null;
/** Additional metadata, varies per binding type */
metadata: {
/** `true` if is (inside) a rest parameter */
inside_rest?: boolean;
} | null;
}
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
dependencies: Set<Binding>;
@ -329,5 +276,7 @@ export interface ExpressionMetadata {
export * from './template.js';
export { Binding, Scope } from '../phases/scope.js';
// TODO this chain is a bit weird
export { ReactiveStatement } from '../phases/types.js';

@ -1,7 +1,7 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { flush_sync, untrack } from './internal/client/runtime.js';
import { untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js';
@ -206,15 +206,7 @@ function init_update_callbacks(context) {
return (l.u ??= { a: [], b: [], m: [] });
}
/**
* Synchronously flushes any pending state changes and those that result from it.
* @param {() => void} [fn]
* @returns {void}
*/
export function flushSync(fn) {
flush_sync(fn);
}
export { flushSync } from './internal/client/runtime.js';
export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack } from './internal/client/runtime.js';

@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
import { is_promise } from '../../../shared/utils.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { internal_set, mutable_source, source } from '../../reactivity/sources.js';
import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
@ -105,7 +105,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
// without this, the DOM does not update until two ticks after the promise
// resolves, which is unexpected behaviour (and somewhat irksome to test)
flush_sync();
flushSync();
}
}
}

@ -14,6 +14,10 @@ import {
set_active_reaction
} from '../../runtime.js';
import { clsx } from '../../../shared/attributes.js';
import { set_class } from './class.js';
export const CLASS = Symbol('class');
export const STYLE = Symbol('style');
/**
* The value/checked attribute in the template actually corresponds to the defaultValue property, so we need
@ -254,8 +258,8 @@ export function set_custom_element_data(node, prop, value) {
/**
* Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} element
* @param {Record<string, any> | undefined} prev
* @param {Record<string, any>} next New attributes - this function mutates this object
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [preserve_attribute_case]
* @param {boolean} [is_custom_element]
@ -289,10 +293,8 @@ export function set_attributes(
if (next.class) {
next.class = clsx(next.class);
}
if (css_hash !== undefined) {
next.class = next.class ? next.class + ' ' + css_hash : css_hash;
} else if (css_hash || next[CLASS]) {
next.class = null; /* force call to set_class() */
}
var setters = get_setters(element);
@ -325,7 +327,7 @@ export function set_attributes(
}
var prev_value = current[key];
if (value === prev_value) continue;
if (value === prev_value && key !== 'class') continue;
current[key] = value;
@ -375,6 +377,9 @@ export function set_attributes(
// @ts-ignore
element[`__${event_name}`] = undefined;
}
} else if (key === 'class') {
var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml';
set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]);
} else if (key === 'style' && value != null) {
element.style.cssText = value + '';
} else if (key === 'autofocus') {

@ -1,120 +1,49 @@
import { to_class } from '../../../shared/attributes.js';
import { hydrating } from '../hydration.js';
/**
* @param {SVGElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_svg_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @param {MathMLElement} dom
* @param {string} value
* @param {Element} dom
* @param {boolean | number} is_html
* @param {string | null} value
* @param {string} [hash]
* @returns {void}
* @param {Record<string, boolean>} [prev_classes]
* @param {Record<string, boolean>} [next_classes]
* @returns {Record<string, boolean> | undefined}
*/
export function set_mathml_class(dom, value, hash) {
export function set_class(dom, is_html, value, hash, prev_classes, next_classes) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
if (hydrating && dom.getAttribute('class') === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.getAttribute('class') !== next_class_name)
) {
if (next_class_name === '') {
dom.removeAttribute('class');
} else {
dom.setAttribute('class', next_class_name);
var prev = dom.__className;
if (hydrating || prev !== value) {
var next_class_name = to_class(value, hash, next_classes);
if (!hydrating || next_class_name !== dom.getAttribute('class')) {
// Removing the attribute when the value is only an empty string causes
// performance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish
// and there no hash/directives :
if (next_class_name == null) {
dom.removeAttribute('class');
} else if (is_html) {
dom.className = next_class_name;
} else {
dom.setAttribute('class', next_class_name);
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
dom.__className = value;
} else if (next_classes) {
prev_classes ??= {};
/**
* @param {HTMLElement} dom
* @param {string} value
* @param {string} [hash]
* @returns {void}
*/
export function set_class(dom, value, hash) {
// @ts-expect-error need to add __className to patched prototype
var prev_class_name = dom.__className;
var next_class_name = to_class(value, hash);
for (var key in next_classes) {
var is_present = !!next_classes[key];
if (hydrating && dom.className === next_class_name) {
// In case of hydration don't reset the class as it's already correct.
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
} else if (
prev_class_name !== next_class_name ||
(hydrating && dom.className !== next_class_name)
) {
// Removing the attribute when the value is only an empty string causes
// peformance issues vs simply making the className an empty string. So
// we should only remove the class if the the value is nullish.
if (value == null && !hash) {
dom.removeAttribute('class');
} else {
dom.className = next_class_name;
if (is_present !== !!prev_classes[key]) {
dom.classList.toggle(key, is_present);
}
}
// @ts-expect-error need to add __className to patched prototype
dom.__className = next_class_name;
}
}
/**
* @template V
* @param {V} value
* @param {string} [hash]
* @returns {string | V}
*/
function to_class(value, hash) {
return (value == null ? '' : value) + (hash ? ' ' + hash : '');
}
/**
* @param {Element} dom
* @param {string} class_name
* @param {boolean} value
* @returns {void}
*/
export function toggle_class(dom, class_name, value) {
if (value) {
if (dom.classList.contains(class_name)) return;
dom.classList.add(class_name);
} else {
if (!dom.classList.contains(class_name)) return;
dom.classList.remove(class_name);
}
return next_classes;
}

@ -46,7 +46,7 @@ export function init_operations() {
// @ts-expect-error
element_prototype.__click = undefined;
// @ts-expect-error
element_prototype.__className = '';
element_prototype.__className = undefined;
// @ts-expect-error
element_prototype.__attributes = null;
// @ts-expect-error

@ -1,79 +1,85 @@
import { run_all } from '../../shared/utils.js';
// Fallback for when requestIdleCallback is not available
export const request_idle_callback =
const request_idle_callback =
typeof requestIdleCallback === 'undefined'
? (/** @type {() => void} */ cb) => setTimeout(cb, 1)
: requestIdleCallback;
let is_micro_task_queued = false;
let is_idle_task_queued = false;
/** @type {Array<() => void>} */
let queued_boundary_microtasks = [];
let boundary_micro_tasks = [];
/** @type {Array<() => void>} */
let queued_post_microtasks = [];
let micro_tasks = [];
/** @type {Array<() => void>} */
let queued_idle_tasks = [];
let idle_tasks = [];
export function flush_boundary_micro_tasks() {
const tasks = queued_boundary_microtasks.slice();
queued_boundary_microtasks = [];
function run_boundary_micro_tasks() {
var tasks = boundary_micro_tasks;
boundary_micro_tasks = [];
run_all(tasks);
}
export function flush_post_micro_tasks() {
const tasks = queued_post_microtasks.slice();
queued_post_microtasks = [];
function run_post_micro_tasks() {
var tasks = micro_tasks;
micro_tasks = [];
run_all(tasks);
}
export function flush_idle_tasks() {
if (is_idle_task_queued) {
is_idle_task_queued = false;
const tasks = queued_idle_tasks.slice();
queued_idle_tasks = [];
run_all(tasks);
}
function run_idle_tasks() {
var tasks = idle_tasks;
idle_tasks = [];
run_all(tasks);
}
function flush_all_micro_tasks() {
if (is_micro_task_queued) {
is_micro_task_queued = false;
flush_boundary_micro_tasks();
flush_post_micro_tasks();
}
function run_micro_tasks() {
run_boundary_micro_tasks();
run_post_micro_tasks();
}
/**
* @param {() => void} fn
*/
export function queue_boundary_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(flush_all_micro_tasks);
if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) {
queueMicrotask(run_micro_tasks);
}
queued_boundary_microtasks.push(fn);
boundary_micro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_micro_task(fn) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(flush_all_micro_tasks);
if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) {
queueMicrotask(run_micro_tasks);
}
queued_post_microtasks.push(fn);
micro_tasks.push(fn);
}
/**
* @param {() => void} fn
*/
export function queue_idle_task(fn) {
if (!is_idle_task_queued) {
is_idle_task_queued = true;
request_idle_callback(flush_idle_tasks);
if (idle_tasks.length === 0) {
request_idle_callback(run_idle_tasks);
}
idle_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (boundary_micro_tasks.length > 0 || micro_tasks.length > 0) {
run_micro_tasks();
}
if (idle_tasks.length > 0) {
run_idle_tasks();
}
queued_idle_tasks.push(fn);
}

@ -40,9 +40,11 @@ export {
set_checked,
set_selected,
set_default_checked,
set_default_value
set_default_value,
CLASS,
STYLE
} from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { set_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js';
@ -139,7 +141,7 @@ export {
get,
safe_get,
invalidate_inner_signals,
flush_sync,
flushSync as flush,
tick,
untrack,
exclude_from_object,

@ -18,8 +18,7 @@ import {
update_reaction,
increment_write_version,
set_active_effect,
handle_error,
flush_sync
handle_error
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
@ -223,7 +222,7 @@ function get_derived_parent_effect(derived) {
* @param {Derived} derived
* @returns {T}
*/
export function execute_derived(derived) {
function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;

@ -6,12 +6,10 @@ import {
update_effect,
get,
is_destroying_effect,
is_flushing_effect,
remove_reactions,
schedule_effect,
set_active_reaction,
set_is_destroying_effect,
set_is_flushing_effect,
set_signal_status,
untrack,
untracking
@ -122,17 +120,12 @@ function create_effect(type, fn, sync, push = true) {
}
if (sync) {
var previously_flushing_effect = is_flushing_effect;
try {
set_is_flushing_effect(true);
update_effect(effect);
effect.f |= EFFECT_RAN;
} catch (e) {
destroy_effect(effect);
throw e;
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
} else if (fn !== null) {
schedule_effect(effect);

@ -1,6 +1,6 @@
/** @import { Effect, Source } from '#client' */
import { DIRTY } from '../constants.js';
import { flush_sync } from '../runtime.js';
import { flushSync } from '../runtime.js';
import { internal_set, mark_reactions } from './sources.js';
/** @type {Set<Fork>} */
@ -95,7 +95,7 @@ export class Fork {
*/
run(fn) {
active_fork = this;
flush_sync(fn);
flushSync(fn);
}
increment() {

@ -14,8 +14,6 @@ import {
derived_sources,
set_derived_sources,
check_dirtiness,
set_is_flushing_effect,
is_flushing_effect,
untracking
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
@ -209,22 +207,18 @@ export function internal_set(source, value) {
if (DEV && inspect_effects.size > 0) {
const inspects = Array.from(inspect_effects);
var previously_flushing_effect = is_flushing_effect;
set_is_flushing_effect(true);
try {
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}
for (const effect of inspects) {
// Mark clean inspect-effects as maybe dirty and then check their dirtiness
// instead of just updating the effects - this way we avoid overfiring.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}
} finally {
set_is_flushing_effect(previously_flushing_effect);
}
inspect_effects.clear();
}
}

@ -9,7 +9,6 @@ import {
} from './reactivity/effects.js';
import {
EFFECT,
RENDER_EFFECT,
DIRTY,
MAYBE_DIRTY,
CLEAN,
@ -25,13 +24,10 @@ import {
DISCONNECTED,
BOUNDARY_EFFECT,
REACTION_IS_UPDATING,
EFFECT_ASYNC
EFFECT_ASYNC,
RENDER_EFFECT
} from './constants.js';
import {
flush_idle_tasks,
flush_boundary_micro_tasks,
flush_post_micro_tasks
} from './dom/task.js';
import { flush_tasks } from './dom/task.js';
import { internal_set } from './reactivity/sources.js';
import {
destroy_derived_effects,
@ -56,28 +52,19 @@ import { is_firefox } from './dom/operations.js';
import { active_fork, Fork } from './reactivity/forks.js';
import { log_effect_tree } from './dev/debug.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
// Used for DEV time error handling
/** @param {WeakSet<Error>} value */
const handled_errors = new WeakSet();
export let is_throwing_error = false;
let is_throwing_error = false;
// Used for controlling the flush of effects.
let scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling
let is_micro_task_queued = false;
let is_flushing = false;
/** @type {Effect | null} */
let last_scheduled_effect = null;
export let is_flushing_effect = false;
export let is_destroying_effect = false;
let is_updating_effect = false;
/** @param {boolean} value */
export function set_is_flushing_effect(value) {
is_flushing_effect = value;
}
export let is_destroying_effect = false;
/** @param {boolean} value */
export function set_is_destroying_effect(value) {
@ -89,7 +76,6 @@ export function set_is_destroying_effect(value) {
/** @type {Effect[]} */
let queued_root_effects = [];
let flush_count = 0;
/** @type {Effect[]} Stack of effects, dev only */
let dev_effect_stack = [];
// Handle signal reactivity tree dependencies and reactions
@ -141,7 +127,7 @@ export function set_derived_sources(sources) {
* and until a new dependency is accessed we track this via `skipped_deps`
* @type {null | Value[]}
*/
export let new_deps = null;
let new_deps = null;
let skipped_deps = 0;
@ -432,10 +418,9 @@ export function update_reaction(reaction) {
new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
skip_reaction =
(flags & UNOWNED) !== 0 &&
(!is_flushing_effect || previous_reaction === null || previous_untracking);
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
derived_sources = null;
set_component_context(reaction.ctx);
@ -584,8 +569,10 @@ export function update_effect(effect) {
var previous_effect = active_effect;
var previous_component_context = component_context;
var was_updating_effect = is_updating_effect;
active_effect = effect;
is_updating_effect = true;
if (DEV) {
var previous_component_fn = dev_current_component_function;
@ -627,6 +614,7 @@ export function update_effect(effect) {
} catch (error) {
handle_error(error, effect, previous_effect, previous_component_context || effect.ctx);
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
if (DEV) {
@ -645,79 +633,72 @@ function log_effect_stack() {
}
function infinite_loop_guard() {
if (flush_count > 1000) {
flush_count = 0;
try {
e.effect_update_depth_exceeded();
} catch (error) {
try {
e.effect_update_depth_exceeded();
} catch (error) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
// stack is garbage, ignore. Instead add a console.error message.
define_property(error, 'stack', {
value: ''
});
}
// Try and handle the error so it can be caught at a boundary, that's
// if there's an effect available from when it was last scheduled
if (last_scheduled_effect !== null) {
if (DEV) {
try {
handle_error(error, last_scheduled_effect, null, null);
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
} else {
try {
handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
} catch (e) {
// Only log the effect stack if the error is re-thrown
log_effect_stack();
throw e;
}
throw error;
} else {
handle_error(error, last_scheduled_effect, null, null);
}
} else {
if (DEV) {
log_effect_stack();
}
throw error;
}
}
flush_count++;
}
/**
* @param {Array<Effect>} root_effects
* @returns {void}
*/
function flush_queued_root_effects(root_effects) {
function flush_queued_root_effects() {
if (active_fork === null) {
return;
}
var revert = active_fork.apply();
var length = root_effects.length;
if (length === 0) {
return;
}
infinite_loop_guard();
var previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
try {
for (var i = 0; i < length; i++) {
var effect = root_effects[i];
var flush_count = 0;
if ((effect.f & CLEAN) === 0) {
effect.f ^= CLEAN;
while (queued_root_effects.length > 0) {
if (flush_count++ > 1000) {
infinite_loop_guard();
}
var collected_effects = process_effects(effect);
var root_effects = queued_root_effects;
var length = root_effects.length;
queued_root_effects = [];
for (var i = 0; i < length; i++) {
var root = root_effects[i];
if (active_fork.settled()) {
flush_queued_effects(collected_effects);
if ((root.f & CLEAN) === 0) {
root.f ^= CLEAN;
}
var collected_effects = process_effects(root);
if (active_fork.settled()) {
flush_queued_effects(collected_effects);
}
}
}
} finally {
is_flushing_effect = previously_flushing_effect;
// TODO this doesn't seem quite right — may run into
// interesting cases where there are multiple roots.
// it'll do for now though
@ -726,6 +707,12 @@ function flush_queued_root_effects(root_effects) {
}
revert();
is_flushing = false;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
@ -767,42 +754,17 @@ function flush_queued_effects(effects) {
}
}
function flush_deferred() {
is_micro_task_queued = false;
if (flush_count > 1001) {
return;
}
const previous_queued_root_effects = queued_root_effects;
queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued) {
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
}
}
/**
* @param {Effect} signal
* @returns {void}
*/
export function schedule_effect(signal) {
if (scheduler_mode === FLUSH_MICROTASK) {
if (!is_micro_task_queued) {
is_micro_task_queued = true;
queueMicrotask(flush_deferred);
}
if (!is_flushing) {
is_flushing = true;
queueMicrotask(flush_queued_root_effects);
}
last_scheduled_effect = signal;
var effect = signal;
var effect = (last_scheduled_effect = signal);
while (effect.parent !== null) {
effect = effect.parent;
@ -894,44 +856,30 @@ function process_effects(effect) {
}
/**
* Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given.
* @param {() => any} [fn]
* @returns {any}
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* @template [T=void]
* @param {(() => T) | undefined} [fn]
* @returns {T}
*/
export function flush_sync(fn) {
var previous_scheduler_mode = scheduler_mode;
var previous_queued_root_effects = queued_root_effects;
try {
infinite_loop_guard();
export function flushSync(fn) {
var result;
scheduler_mode = FLUSH_SYNC;
queued_root_effects = [];
is_micro_task_queued = false;
flush_queued_root_effects(previous_queued_root_effects);
var result = fn?.();
flush_boundary_micro_tasks();
flush_post_micro_tasks();
flush_idle_tasks();
if (queued_root_effects.length > 0) {
flush_sync();
}
if (fn) {
is_flushing = true;
flush_queued_root_effects();
result = fn();
}
flush_count = 0;
last_scheduled_effect = null;
if (DEV) {
dev_effect_stack = [];
}
flush_tasks();
return result;
} finally {
scheduler_mode = previous_scheduler_mode;
queued_root_effects = previous_queued_root_effects;
while (queued_root_effects.length > 0) {
is_flushing = true;
flush_queued_root_effects();
flush_tasks();
}
return /** @type {T} */ (result);
}
/**
@ -940,9 +888,9 @@ export function flush_sync(fn) {
*/
export async function tick() {
await Promise.resolve();
// By calling flush_sync we guarantee that any pending state changes are applied after one tick.
// By calling flushSync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future.
flush_sync();
flushSync();
}
/**
@ -1081,7 +1029,7 @@ export function safe_get(signal) {
* @template T
* @param {() => T} fn
*/
export function capture_signals(fn) {
function capture_signals(fn) {
var previous_captured_signals = captured_signals;
captured_signals = new Set();

@ -2,7 +2,7 @@
/** @import { Component, Payload, RenderOutput } from '#server' */
/** @import { Store } from '#shared' */
export { FILENAME, HMR } from '../../constants.js';
import { attr, clsx } from '../shared/attributes.js';
import { attr, clsx, to_class } from '../shared/attributes.js';
import { is_promise, noop } from '../shared/utils.js';
import { subscribe_to_store } from '../../store/utils.js';
import {
@ -10,7 +10,6 @@ import {
ELEMENT_PRESERVE_ATTRIBUTE_CASE,
ELEMENT_IS_NAMESPACED
} from '../../constants.js';
import { escape_html } from '../../escaping.js';
import { DEV } from 'esm-env';
import { current_component, pop, push } from './context.js';
@ -198,12 +197,13 @@ export function css_props(payload, is_html, props, component, dynamic = false) {
/**
* @param {Record<string, unknown>} attrs
* @param {Record<string, string>} [classes]
* @param {string | null} css_hash
* @param {Record<string, boolean>} [classes]
* @param {Record<string, string>} [styles]
* @param {number} [flags]
* @returns {string}
*/
export function spread_attributes(attrs, classes, styles, flags = 0) {
export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) {
if (styles) {
attrs.style = attrs.style
? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles))
@ -214,16 +214,8 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
attrs.class = clsx(attrs.class);
}
if (classes) {
const classlist = attrs.class ? [attrs.class] : [];
for (const key in classes) {
if (classes[key]) {
classlist.push(key);
}
}
attrs.class = classlist.join(' ');
if (css_hash || classes) {
attrs.class = to_class(attrs.class, css_hash, classes);
}
let attr_str = '';
@ -552,7 +544,7 @@ export function props_id(payload) {
return uid;
}
export { attr, clsx };
export { attr, clsx, to_class };
export { html } from './blocks/html.js';

@ -40,3 +40,45 @@ export function clsx(value) {
return value ?? '';
}
}
const whitespace = [...' \t\n\r\f\u00a0\u000b\ufeff'];
/**
* @param {any} value
* @param {string | null} [hash]
* @param {Record<string, boolean>} [directives]
* @returns {string | null}
*/
export function to_class(value, hash, directives) {
var classname = value == null ? '' : '' + value;
if (hash) {
classname = classname ? classname + ' ' + hash : hash;
}
if (directives) {
for (var key in directives) {
if (directives[key]) {
classname = classname ? classname + ' ' + key : key;
} else if (classname.length) {
var len = key.length;
var a = 0;
while ((a = classname.indexOf(key, a)) >= 0) {
var b = a + len;
if (
(a === 0 || whitespace.includes(classname[a - 1])) &&
(b === classname.length || whitespace.includes(classname[b]))
) {
classname = (a === 0 ? '' : classname.substring(0, a)) + classname.substring(b + 1);
} else {
a = b;
}
}
}
}
}
return classname === '' ? null : classname;
}

@ -3,7 +3,7 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j
import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js';
import { active_effect, flush_sync, get, set_signal_status } from '../internal/client/runtime.js';
import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js';
import * as w from '../internal/client/warnings.js';
@ -119,9 +119,9 @@ class Svelte4Component {
recover: options.recover
});
// We don't flush_sync for custom element wrappers or if the user doesn't want it
// We don't flushSync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host || options.sync === false) {
flush_sync();
flushSync();
}
this.#events = props.$$events;

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

@ -1 +1 @@
<p class=" svelte-xyz">Foo</p>
<p class="svelte-xyz">Foo</p>

@ -47,6 +47,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = target.querySelectorAll('div');
assert.ok(divs[0].getAnimations().length > 0);
assert.equal(divs[1].getAnimations().length, 0);

@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);

@ -46,6 +46,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);
assert.equal(divs[4].dy, -120);
@ -66,6 +68,8 @@ export default test({
{ id: 5, name: 'e' }
];
raf.tick(100);
divs = document.querySelectorAll('div');
assert.equal(divs[0].dy, 120);

@ -1,17 +1,17 @@
import { ok, test } from '../../test';
export default test({
html: '<div class=" svelte-x1o6ra"></div>',
html: '<div class="svelte-x1o6ra"></div>',
test({ assert, component, target }) {
const div = target.querySelector('div');
ok(div);
component.testName = null;
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined;
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined + '';
assert.equal(div.className, 'undefined svelte-x1o6ra');
@ -32,10 +32,10 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {};
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = 'testClassName';
assert.equal(div.className, 'testClassName svelte-x1o6ra');

@ -16,10 +16,10 @@ export default test({
assert.equal(div.className, 'testClassName svelte-x1o6ra');
component.testName = null;
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined;
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = undefined + '';
assert.equal(div.className, 'undefined svelte-x1o6ra');
@ -40,9 +40,9 @@ export default test({
assert.equal(div.className, 'true svelte-x1o6ra');
component.testName = {};
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
component.testName = '';
assert.equal(div.className, ' svelte-x1o6ra');
assert.equal(div.className, 'svelte-x1o6ra');
}
});

@ -50,6 +50,8 @@ export default test({
{ id: 1, name: 'a' }
];
raf.tick(0);
divs = target.querySelectorAll('div');
assert.equal(divs[0].style.transform, 'translate(0px, 120px)');
assert.equal(divs[1].style.transform, '');

@ -0,0 +1,43 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// This test counts mutations on hydration
// set_class() should not mutate class on hydration, except if mismatch
export default test({
mode: ['server', 'hydrate'],
server_props: {
browser: false
},
props: {
browser: true
},
html: `
<main id="main" class="browser">
<div class="custom svelte-1cjqok6 foo bar"></div>
<span class="svelte-1cjqok6 foo bar"></span>
<b class="custom foo bar"></b>
<i class="foo bar"></i>
</main>
`,
ssrHtml: `
<main id="main">
<div class="custom svelte-1cjqok6 foo bar"></div>
<span class="svelte-1cjqok6 foo bar"></span>
<b class="custom foo bar"></b>
<i class="foo bar"></i>
</main>
`,
async test({ assert, component, instance }) {
flushSync();
assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']);
component.foo = false;
flushSync();
assert.deepEqual(instance.get_and_clear_mutations(), ['DIV', 'SPAN', 'B', 'I']);
}
});

@ -0,0 +1,47 @@
<script>
let {
classname = 'custom',
foo = true,
bar = true,
browser
} = $props();
let mutations = [];
let observer;
if (browser) {
observer = new MutationObserver(update_mutation_records);
observer.observe(document.querySelector('#main'), { attributes: true, subtree: true });
$effect(() => {
return () => observer.disconnect();
});
}
function update_mutation_records(results) {
for (const r of results) {
mutations.push(r.target.nodeName);
}
}
export function get_and_clear_mutations() {
update_mutation_records(observer.takeRecords());
const result = mutations;
mutations = [];
return result;
}
</script>
<main id="main" class:browser>
<div class={classname} class:foo class:bar></div>
<span class:foo class:bar></span>
<b class={classname} class:foo class:bar></b>
<i class:foo class:bar></i>
</main>
<style>
div,
span {
color: red;
}
</style>

@ -0,0 +1,145 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `
<div class="svelte-tza1s0"></div>
<span></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0 bar"></div>
<span class="foo bar"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0 bar"></span></div>
<div class="svelte-tza1s0"></div>
<span></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="svelte-tza1s0 bar"></div>
<span class="bar"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar"></span></div>
<div class="football svelte-tza1s0 bar"></div>
<span class="football bar"></span>
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 bar"></span></div>
<div class="svelte-tza1s0 bar not-foo"></div>
<span class="bar not-foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar not-foo"></span></div>
`,
test({ assert, target, component }) {
component.foo = true;
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<div class="svelte-tza1s0"></div>
<span></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0 bar"></div>
<span class="foo bar"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0 bar"></span></div>
<div class="svelte-tza1s0 foo"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
<div class="svelte-tza1s0 bar foo"></div>
<span class="bar foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar foo"></span></div>
<div class="football svelte-tza1s0 bar foo"></div>
<span class="football bar foo"></span>
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 bar foo"></span></div>
<div class="svelte-tza1s0 bar foo"></div>
<span class="bar foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 bar foo"></span></div>
`
);
component.bar = false;
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<div class="svelte-tza1s0"></div>
<span></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="svelte-tza1s0 foo"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
<div class="svelte-tza1s0 foo"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
<div class="football svelte-tza1s0 foo"></div>
<span class="football foo"></span>
<div class="svelte-tza1s0"><span class="football svelte-tza1s0 foo"></span></div>
<div class="svelte-tza1s0 foo"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 foo"></span></div>
`
);
component.foo = false;
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<div class="svelte-tza1s0"></div>
<span></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="foo svelte-tza1s0"></div>
<span class="foo"></span>
<div class="svelte-tza1s0"><span class="foo svelte-tza1s0"></span></div>
<div class="svelte-tza1s0"></div>
<span class=""></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="svelte-tza1s0"></div>
<span class=""></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0"></span></div>
<div class="football svelte-tza1s0"></div>
<span class="football"></span>
<div class="svelte-tza1s0"><span class="football svelte-tza1s0"></span></div>
<div class="svelte-tza1s0 not-foo"></div>
<span class="not-foo"></span>
<div class="svelte-tza1s0"><span class="svelte-tza1s0 not-foo"></span></div>
`
);
}
});

@ -0,0 +1,40 @@
<script>
let { foo = false, bar = true } = $props();
</script>
<div></div>
<span></span>
<div><span></span></div>
<div class="foo"></div>
<span class="foo"></span>
<div><span class="foo"></span></div>
<div class="foo" class:bar></div>
<span class="foo" class:bar></span>
<div><span class="foo" class:bar></span></div>
<div class="foo" class:foo></div>
<span class="foo" class:foo></span>
<div><span class="foo" class:foo></span></div>
<div class="foo" class:bar class:foo></div>
<span class="foo" class:bar class:foo></span>
<div><span class="foo" class:bar class:foo></span></div>
<div class="football" class:bar class:foo></div>
<span class="football" class:bar class:foo></span>
<div><span class="football" class:bar class:foo></span></div>
<div class="foo" class:bar class:foo class:not-foo={!foo}></div>
<span class="foo" class:bar class:foo class:not-foo={!foo}></span>
<div><span class="foo" class:bar class:foo class:not-foo={!foo}></span></div>
<style>
div {
color: red;
}
div > span {
font-weight: bold;
}
</style>

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

@ -602,7 +602,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
unsubscribe();
@ -625,7 +625,7 @@ describe('toStore', () => {
assert.deepEqual(log, [0]);
set(count, 1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
store.set(2);
@ -654,11 +654,11 @@ describe('fromStore', () => {
assert.deepEqual(log, [0]);
store.set(1);
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1]);
count.current = 2;
$.flush_sync();
$.flushSync();
assert.deepEqual(log, [0, 1, 2]);
assert.equal(get(store), 2);

@ -408,10 +408,6 @@ declare module 'svelte' {
* @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead
* */
export function afterUpdate(fn: () => void): void;
/**
* Synchronously flushes any pending state changes and those that result from it.
* */
export function flushSync(fn?: (() => void) | undefined): void;
/**
* Create a snippet programmatically
* */
@ -421,6 +417,29 @@ declare module 'svelte' {
}): Snippet<Params>;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.
* */
export function flushSync<T = void>(fn?: (() => T) | undefined): T;
/**
* Returns a promise that resolves once any pending state changes have been applied.
* */
export function tick(): Promise<void>;
/**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency.
*
* ```ts
* $effect(() => {
* // this will run when `data` changes, but not when `time` changes
* save(data, {
* timestamp: untrack(() => time)
* });
* });
* ```
* */
export function untrack<T>(fn: () => T): T;
/**
* Retrieves the context that belongs to the closest parent component with the specified `key`.
* Must be called during component initialisation.
@ -494,24 +513,6 @@ declare module 'svelte' {
export function unmount(component: Record<string, any>, options?: {
outro?: boolean;
} | undefined): Promise<void>;
/**
* Returns a promise that resolves once any pending state changes have been applied.
* */
export function tick(): Promise<void>;
/**
* When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect),
* any state read inside `fn` will not be treated as a dependency.
*
* ```ts
* $effect(() => {
* // this will run when `data` changes, but not when `time` changes
* save(data, {
* timestamp: untrack(() => time)
* });
* });
* ```
* */
export function untrack<T>(fn: () => T): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
@ -622,8 +623,8 @@ declare module 'svelte/animate' {
}
declare module 'svelte/compiler' {
import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { SourceMap } from 'magic-string';
import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
import type { Location } from 'locate-character';
/**
* `compile` converts your `.svelte` source code into a JavaScript module that exports a component

Loading…
Cancel
Save