Merge branch 'main' into print

print
Rich Harris 3 months ago
commit 203c321b4b

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: don't set state withing `with_parent` in proxy

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: untrack `$inspect.with` and add check for unsafe mutation

@ -129,12 +129,12 @@ test('Effect', () => {
// effects normally run after a microtask,
// use flushSync to execute all pending effects synchronously
flushSync();
expect(log.value).toEqual([0]);
expect(log).toEqual([0]);
count = 1;
flushSync();
expect(log.value).toEqual([0, 1]);
expect(log).toEqual([0, 1]);
});
cleanup();
@ -148,17 +148,13 @@ test('Effect', () => {
*/
export function logger(getValue) {
/** @type {any[]} */
let log = $state([]);
let log = [];
$effect(() => {
log.push(getValue());
});
return {
get value() {
return log;
}
};
return log;
}
```

@ -125,7 +125,7 @@ Cannot set prototype of `$state` object
### state_unsafe_mutation
```
Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
```
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:

@ -1,5 +1,37 @@
# svelte
## 5.34.7
### Patch Changes
- fix: address css class matching regression ([#16204](https://github.com/sveltejs/svelte/pull/16204))
## 5.34.6
### Patch Changes
- fix: match class and style directives against attribute selector ([#16179](https://github.com/sveltejs/svelte/pull/16179))
## 5.34.5
### Patch Changes
- fix: keep spread non-delegated event handlers up to date ([#16180](https://github.com/sveltejs/svelte/pull/16180))
- fix: remove undefined attributes on hydration ([#16178](https://github.com/sveltejs/svelte/pull/16178))
- fix: ensure sources within nested effects still register correctly ([#16193](https://github.com/sveltejs/svelte/pull/16193))
- fix: avoid shadowing a variable in dynamic components ([#16185](https://github.com/sveltejs/svelte/pull/16185))
## 5.34.4
### Patch Changes
- fix: don't set state withing `with_parent` in proxy ([#16176](https://github.com/sveltejs/svelte/pull/16176))
- fix: use compiler-driven reactivity in legacy mode template expressions ([#16100](https://github.com/sveltejs/svelte/pull/16100))
## 5.34.3
### Patch Changes

@ -82,7 +82,7 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long
## state_unsafe_mutation
> Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
> Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go:

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

@ -63,7 +63,10 @@ function open(parser) {
end: -1,
test: read_expression(parser),
consequent: create_fragment(),
alternate: null
alternate: null,
metadata: {
expression: create_expression_metadata()
}
});
parser.allow_whitespace();
@ -244,7 +247,10 @@ function open(parser) {
error: null,
pending: null,
then: null,
catch: null
catch: null,
metadata: {
expression: create_expression_metadata()
}
});
if (parser.eat('then')) {
@ -326,7 +332,10 @@ function open(parser) {
start,
end: -1,
expression,
fragment: create_fragment()
fragment: create_fragment(),
metadata: {
expression: create_expression_metadata()
}
});
parser.stack.push(block);
@ -466,7 +475,10 @@ function next(parser) {
elseif: true,
test: expression,
consequent: create_fragment(),
alternate: null
alternate: null,
metadata: {
expression: create_expression_metadata()
}
});
parser.stack.push(child);
@ -629,7 +641,10 @@ function special(parser) {
type: 'HtmlTag',
start,
end: parser.index,
expression
expression,
metadata: {
expression: create_expression_metadata()
}
});
return;
@ -704,6 +719,9 @@ function special(parser) {
declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const
end: parser.index - 1
},
metadata: {
expression: create_expression_metadata()
}
});
}
@ -730,6 +748,7 @@ function special(parser) {
end: parser.index,
expression: /** @type {AST.RenderTag['expression']} */ (expression),
metadata: {
expression: create_expression_metadata(),
dynamic: false,
arguments: [],
path: [],

@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
}
case 'ClassSelector': {
if (
!attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
if (!attribute_matches(element, 'class', name, '~=', false)) {
return false;
}
@ -633,14 +628,33 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
if (attribute.type === 'SpreadAttribute') return true;
if (attribute.type === 'BindDirective' && attribute.name === name) return true;
const name_lower = name.toLowerCase();
// match attributes against the corresponding directive but bail out on exact matching
if (attribute.type === 'StyleDirective' && name_lower === 'style') return true;
if (attribute.type === 'ClassDirective' && name_lower === 'class') {
if (operator === '~=') {
if (attribute.name === expected_value) return true;
} else {
return true;
}
}
if (attribute.type !== 'Attribute') continue;
if (attribute.name.toLowerCase() !== name.toLowerCase()) continue;
if (attribute.name.toLowerCase() !== name_lower) continue;
if (attribute.value === true) return operator === null;
if (expected_value === null) return true;
if (is_text_attribute(attribute)) {
return test_attribute(operator, expected_value, case_insensitive, attribute.value[0].data);
const matches = test_attribute(
operator,
expected_value,
case_insensitive,
attribute.value[0].data
);
// continue if we still may match against a class/style directive
if (!matches && (name_lower === 'class' || name_lower === 'style')) continue;
return matches;
}
const chunks = get_attribute_chunks(attribute.value);
@ -649,7 +663,7 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv
/** @type {string[]} */
let prev_values = [];
for (const chunk of chunks) {
const current_possible_values = get_possible_values(chunk, name === 'class');
const current_possible_values = get_possible_values(chunk, name_lower === 'class');
// impossible to find out all combinations
if (!current_possible_values) return true;

@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) {
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}

@ -41,5 +41,8 @@ export function AwaitBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
if (node.pending) context.visit(node.pending);
if (node.then) context.visit(node.then);
if (node.catch) context.visit(node.catch);
}

@ -32,5 +32,8 @@ export function ConstTag(node, context) {
e.const_tag_invalid_placement(node);
}
context.next();
const declaration = node.declaration.declarations[0];
context.visit(declaration.id);
context.visit(declaration.init, { ...context.state, expression: node.metadata.expression });
}

@ -15,5 +15,5 @@ export function HtmlTag(node, context) {
// unfortunately this is necessary in order to fix invalid HTML
mark_subtree_dynamic(context.path);
context.next();
context.next({ ...context.state, expression: node.metadata.expression });
}

@ -90,6 +90,7 @@ export function Identifier(node, context) {
if (binding) {
if (context.state.expression) {
context.state.expression.dependencies.add(binding);
context.state.expression.references.add(binding);
context.state.expression.has_state ||=
binding.kind !== 'static' &&
!binding.is_function() &&

@ -17,5 +17,11 @@ export function IfBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.test, {
...context.state,
expression: node.metadata.expression
});
context.visit(node.consequent);
if (node.alternate) context.visit(node.alternate);
}

@ -16,5 +16,6 @@ export function KeyBlock(node, context) {
mark_subtree_dynamic(context.path);
context.next();
context.visit(node.expression, { ...context.state, expression: node.metadata.expression });
context.visit(node.fragment);
}

@ -15,8 +15,9 @@ export function MemberExpression(node, context) {
}
}
if (context.state.expression && !is_pure(node, context)) {
context.state.expression.has_state = true;
if (context.state.expression) {
context.state.expression.has_member_expression = true;
context.state.expression.has_state ||= !is_pure(node, context);
}
if (!is_safe_identifier(node, context.state.scope)) {

@ -54,7 +54,7 @@ export function RenderTag(node, context) {
mark_subtree_dynamic(context.path);
context.visit(callee);
context.visit(callee, { ...context.state, expression: node.metadata.expression });
for (const arg of expression.arguments) {
const metadata = create_expression_metadata();

@ -21,5 +21,9 @@ export function UpdateExpression(node, context) {
}
}
if (context.state.expression) {
context.state.expression.has_assignment = true;
}
context.next();
}

@ -13,6 +13,16 @@ export function visit_function(node, context) {
scope: context.state.scope
};
if (context.state.expression) {
for (const [name] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding && binding.scope.function_depth < context.state.scope.function_depth) {
context.state.expression.references.add(binding);
}
}
}
context.next({
...context.state,
function_depth: context.state.function_depth + 1,

@ -1,21 +1,14 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
)
)
);
const expression = build_expression(context, node.expression, node.metadata.expression);
context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression))));
context.next();
}

@ -1,10 +1,11 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../types' */
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -14,7 +15,7 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression));
let then_block;
let catch_block;

@ -8,12 +8,6 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context
*/
export function Component(node, context) {
const component = build_component(
node,
// if it's not dynamic we will just use the node name, if it is dynamic we will use the node name
// only if it's a valid identifier, otherwise we will use a default name
!node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component',
context
);
const component = build_component(node, node.name, context);
context.state.init.push(component);
}

@ -1,4 +1,4 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { Pattern } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { dev } from '../../../../state.js';
@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
@ -15,15 +16,8 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
context.state.init.push(
b.const(
declaration.id,
create_derived(
context.state,
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
)
)
);
const init = build_expression(context, declaration.init, node.metadata.expression);
context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init))));
context.state.transform[declaration.id.name] = { read: get_value };
@ -48,13 +42,15 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const init = build_expression(
{ ...context, state: child_state },
declaration.init,
node.metadata.expression
);
const fn = b.arrow(
[],
b.block([
b.const(
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
),
b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])
);

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
@ -12,8 +12,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -24,11 +24,18 @@ export function EachBlock(node, context) {
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
const parent_scope_state = {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
};
const collection = build_expression(
{
...context,
state: parent_scope_state
},
node.expression,
node.metadata.expression
);
if (!each_node_meta.is_controlled) {

@ -1,8 +1,8 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -11,7 +11,7 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const expression = /** @type {Expression} */ (context.visit(node.expression));
const expression = build_expression(context, node.expression, node.metadata.expression);
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -31,6 +32,8 @@ export function IfBlock(node, context) {
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
const test = build_expression(context, node.test, node.metadata.expression);
/** @type {Expression[]} */
const args = [
node.elseif ? b.id('$$anchor') : context.state.node,
@ -38,7 +41,7 @@ export function IfBlock(node, context) {
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -10,7 +11,7 @@ import * as b from '#compiler/builders';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.expression));
const key = build_expression(context, node.expression, node.metadata.expression);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(

@ -331,7 +331,7 @@ export function RegularElement(node, context) {
trimmed.some((node) => node.type === 'ExpressionTag');
if (use_text_content) {
const { value } = build_template_chunk(trimmed, context.visit, child_state);
const { value } = build_template_chunk(trimmed, context, child_state);
const empty_string = value.type === 'Literal' && value.value === '';
if (!empty_string) {

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -19,7 +20,10 @@ export function RenderTag(node, context) {
/** @type {Expression[]} */
let args = [];
for (let i = 0; i < raw_args.length; i++) {
let thunk = b.thunk(/** @type {Expression} */ (context.visit(raw_args[i])));
let thunk = b.thunk(
build_expression(context, /** @type {Expression} */ (raw_args[i]), node.metadata.arguments[i])
);
const { has_call } = node.metadata.arguments[i];
if (has_call) {
@ -31,7 +35,11 @@ export function RenderTag(node, context) {
}
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
let snippet_function = build_expression(
context,
/** @type {Expression} */ (callee),
node.metadata.expression
);
if (node.metadata.dynamic) {
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one

@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js';
export function TitleElement(node, context) {
const { has_state, value } = build_template_chunk(
/** @type {any} */ (node.fragment.nodes),
context.visit,
context.state
context
);
const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value));

@ -52,6 +52,15 @@ export function build_component(node, component_name, context) {
/** @type {ExpressionStatement[]} */
const binding_initializers = [];
const is_component_dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
// The variable name used for the component inside $.component()
const intermediate_name =
node.type === 'Component' && node.metadata.dynamic
? context.state.scope.generate(node.name)
: '$$component';
/**
* If this component has a slot property, it is a named slot within another component. In this case
* the slot scope applies to the component itself, too, and not just its children.
@ -199,7 +208,7 @@ export function build_component(node, component_name, context) {
b.call(
'$$ownership_validator.binding',
b.literal(binding.node.name),
b.id(component_name),
b.id(is_component_dynamic ? intermediate_name : component_name),
b.thunk(expression)
)
)
@ -414,8 +423,8 @@ export function build_component(node, component_name, context) {
// TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components
// will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here.
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)
? component_name
is_component_dynamic
? intermediate_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
props_expression
@ -432,7 +441,7 @@ export function build_component(node, component_name, context) {
const statements = [...snippet_declarations];
if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) {
if (is_component_dynamic) {
const prev = fn;
fn = (node_id) => {
@ -441,11 +450,11 @@ export function build_component(node, component_name, context) {
node_id,
b.thunk(
/** @type {Expression} */ (
context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression)
context.visit(node.type === 'Component' ? b.member_id(component_name) : node.expression)
)
),
b.arrow(
[b.id('$$anchor'), b.id(component_name)],
[b.id('$$anchor'), b.id(intermediate_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))])
)
);

@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js';
import { build_template_chunk, get_expression_id } from './utils.js';
import { build_expression, build_template_chunk, get_expression_id } from './utils.js';
/**
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
@ -121,7 +121,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return { value: b.literal(chunk.data), has_state: false };
}
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
let expression = build_expression(context, chunk.expression, chunk.metadata.expression);
return {
value: memoize(expression, chunk.metadata.expression),
@ -129,7 +129,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
};
}
return build_template_chunk(value, context.visit, context.state, memoize);
return build_template_chunk(value, context, context.state, memoize);
}
/**

@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js';
* @param {boolean} is_element
* @param {ComponentContext} context
*/
export function process_children(nodes, initial, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
export function process_children(nodes, initial, is_element, context) {
const within_bound_contenteditable = context.state.metadata.bound_contenteditable;
let prev = initial;
let skipped = 0;
@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
let id = expression;
if (id.type !== 'Identifier') {
id = b.id(state.scope.generate(name));
state.init.push(b.var(id, expression));
id = b.id(context.state.scope.generate(name));
context.state.init.push(b.var(id, expression));
}
prev = () => id;
@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
function flush_sequence(sequence) {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push_text(sequence);
context.state.template.push_text(sequence);
return;
}
state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]);
const { has_state, value } = build_template_chunk(sequence, visit, state);
const { has_state, value } = build_template_chunk(sequence, context);
// if this is a standalone `{expression}`, make sure we handle the case where
// no text node was created because the expression was empty during SSR
@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
const update = b.stmt(b.call('$.set_text', id, value));
if (has_state && !within_bound_contenteditable) {
state.update.push(update);
context.state.update.push(update);
} else {
state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value)));
}
}
@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
sequence = [];
}
let child_state = state;
let child_state = context.state;
if (is_static_element(node, state)) {
if (is_static_element(node, context.state)) {
skipped += 1;
} else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) {
node.metadata.is_controlled = true;
} else {
const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node');
child_state = { ...state, node: id };
child_state = { ...context.state, node: id };
}
visit(node, child_state);
context.visit(node, child_state);
}
}
@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
// traverse to the last (n - 1) one when hydrating
if (skipped > 1) {
skipped -= 1;
state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped))));
context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped))));
}
}

@ -1,6 +1,6 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, Context } from '../../types' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { create_derived } from '../../utils.js';
import { build_getter, create_derived } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
@ -31,15 +31,15 @@ export function get_expression_id(expressions, value) {
/**
* @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentContext} context
* @param {ComponentClientTransformState} state
* @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean }}
*/
export function build_template_chunk(
values,
visit,
state,
context,
state = context.state,
memoize = (value, metadata) =>
metadata.has_call ? get_expression_id(state.expressions, value) : value
) {
@ -66,7 +66,7 @@ export function build_template_chunk(
state.scope.get('undefined')
) {
let value = memoize(
/** @type {Expression} */ (visit(node.expression, state)),
build_expression(context, node.expression, node.metadata.expression, state),
node.metadata.expression
);
@ -360,3 +360,48 @@ export function validate_mutation(node, context, expression) {
loc && b.literal(loc.column)
);
}
/**
*
* @param {ComponentContext} context
* @param {Expression} expression
* @param {ExpressionMetadata} metadata
*/
export function build_expression(context, expression, metadata, state = context.state) {
const value = /** @type {Expression} */ (context.visit(expression, state));
if (context.state.analysis.runes) {
return value;
}
if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) {
return value;
}
// Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here
const sequence = b.sequence([]);
for (const binding of metadata.references) {
if (binding.kind === 'normal' && binding.declaration_kind !== 'import') {
continue;
}
var getter = build_getter({ ...binding.node }, state);
if (
binding.kind === 'bindable_prop' ||
binding.kind === 'template' ||
binding.declaration_kind === 'import' ||
binding.node.name === '$$props' ||
binding.node.name === '$$restProps'
) {
getter = b.call('$.deep_read_state', getter);
}
sequence.expressions.push(getter);
}
sequence.expressions.push(b.call('$.untrack', b.thunk(value)));
return sequence;
}

@ -62,8 +62,11 @@ export function create_attribute(name, start, end, value) {
export function create_expression_metadata() {
return {
dependencies: new Set(),
references: new Set(),
has_state: false,
has_call: false
has_call: false,
has_member_expression: false,
has_assignment: false
};
}

@ -279,12 +279,18 @@ export type DeclarationKind =
| 'synthetic';
export interface ExpressionMetadata {
/** All the bindings that are referenced inside this expression */
/** All the bindings that are referenced eagerly (not inside functions) in this expression */
dependencies: Set<Binding>;
/** All the bindings that are referenced inside this expression, including inside functions */
references: Set<Binding>;
/** True if the expression references state directly, or _might_ (via member/call expressions) */
has_state: boolean;
/** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */
has_call: boolean;
/** True if the expression includes a member expression */
has_member_expression: boolean;
/** True if the expression includes an assignment or an update */
has_assignment: boolean;
}
export interface StateField {

@ -137,6 +137,10 @@ export namespace AST {
export interface HtmlTag extends BaseNode {
type: 'HtmlTag';
expression: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** An HTML comment */
@ -153,6 +157,10 @@ export namespace AST {
declaration: VariableDeclaration & {
declarations: [VariableDeclarator & { id: Pattern; init: Expression }];
};
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** A `{@debug ...}` tag */
@ -167,6 +175,7 @@ export namespace AST {
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
/** @internal */
metadata: {
expression: ExpressionMetadata;
dynamic: boolean;
arguments: ExpressionMetadata[];
path: SvelteNode[];
@ -449,6 +458,10 @@ export namespace AST {
test: Expression;
consequent: Fragment;
alternate: Fragment | null;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
/** An `{#await ...}` block */
@ -463,12 +476,20 @@ export namespace AST {
pending: Fragment | null;
then: Fragment | null;
catch: Fragment | null;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
export interface KeyBlock extends BaseNode {
type: 'KeyBlock';
expression: Expression;
fragment: Fragment;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
export interface SnippetBlock extends BaseNode {

@ -26,3 +26,8 @@ export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
export const DOCUMENT_FRAGMENT_NODE = 11;

@ -1,4 +1,5 @@
/** @import { SourceLocation } from '#client' */
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js';
@ -12,7 +13,7 @@ export function add_locations(fn, filename, locations) {
return (/** @type {any[]} */ ...args) => {
const dom = fn(...args);
var node = hydrating ? dom : dom.nodeType === 11 ? dom.firstChild : dom;
var node = hydrating ? dom : dom.nodeType === DOCUMENT_FRAGMENT_NODE ? dom.firstChild : dom;
assign_locations(node, filename, locations);
return dom;
@ -45,13 +46,13 @@ function assign_locations(node, filename, locations) {
var depth = 0;
while (node && i < locations.length) {
if (hydrating && node.nodeType === 8) {
if (hydrating && node.nodeType === COMMENT_NODE) {
var comment = /** @type {Comment} */ (node);
if (comment.data === HYDRATION_START || comment.data === HYDRATION_START_ELSE) depth += 1;
else if (comment.data[0] === HYDRATION_END) depth -= 1;
}
if (depth === 0 && node.nodeType === 1) {
if (depth === 0 && node.nodeType === ELEMENT_NODE) {
assign_location(/** @type {Element} */ (node), filename, locations[i++]);
}

@ -1,6 +1,7 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { inspect_effect, validate_effect } from '../reactivity/effects.js';
import { untrack } from '../runtime.js';
/**
* @param {() => any[]} get_value
@ -28,7 +29,10 @@ export function inspect(get_value, inspector = console.log) {
}
if (value !== UNINITIALIZED) {
inspector(initial ? 'init' : 'update', ...snapshot(value, true));
var snap = snapshot(value, true);
untrack(() => {
inspector(initial ? 'init' : 'update', ...snap);
});
}
initial = false;

@ -34,7 +34,7 @@ import {
} from '../../reactivity/effects.js';
import { source, mutable_source, internal_set } from '../../reactivity/sources.js';
import { array_from, is_array } from '../../../shared/utils.js';
import { INERT } from '#client/constants';
import { COMMENT_NODE, INERT } from '#client/constants';
import { queue_micro_task } from '../task.js';
import { active_effect, get } from '../../runtime.js';
import { DEV } from 'esm-env';
@ -183,7 +183,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
for (var i = 0; i < length; i++) {
if (
hydrate_node.nodeType === 8 &&
hydrate_node.nodeType === COMMENT_NODE &&
/** @type {Comment} */ (hydrate_node).data === HYDRATION_END
) {
// The server rendered fewer items than expected,

@ -10,6 +10,7 @@ import { DEV } from 'esm-env';
import { dev_current_component_function } from '../../context.js';
import { get_first_child, get_next_sibling } from '../operations.js';
import { active_effect } from '../../runtime.js';
import { COMMENT_NODE } from '#client/constants';
/**
* @param {Element} element
@ -67,7 +68,10 @@ export function html(node, get_value, svg = false, mathml = false, skip_warning
var next = hydrate_next();
var last = next;
while (next !== null && (next.nodeType !== 8 || /** @type {Comment} */ (next).data !== '')) {
while (
next !== null &&
(next.nodeType !== COMMENT_NODE || /** @type {Comment} */ (next).data !== '')
) {
last = next;
next = /** @type {TemplateNode} */ (get_next_sibling(next));
}

@ -1,7 +1,7 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { EFFECT_TRANSPARENT } from '#client/constants';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
@ -102,7 +102,7 @@ export function createRawSnippet(fn) {
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 1)) {
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== ELEMENT_NODE)) {
w.invalid_raw_snippet_render();
}

@ -20,7 +20,7 @@ import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js';
import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT } from '#client/constants';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js';
import { is_raw_text_element } from '../../../../utils.js';
@ -51,7 +51,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
/** @type {null | Element} */
var element = null;
if (hydrating && hydrate_node.nodeType === 1) {
if (hydrating && hydrate_node.nodeType === ELEMENT_NODE) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
}

@ -2,7 +2,7 @@
import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js';
import { create_text, get_first_child, get_next_sibling } from '../operations.js';
import { block } from '../../reactivity/effects.js';
import { HEAD_EFFECT } from '#client/constants';
import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants';
import { HYDRATION_START } from '../../../../constants.js';
/**
@ -37,7 +37,8 @@ export function head(render_fn) {
while (
head_anchor !== null &&
(head_anchor.nodeType !== 8 || /** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
(head_anchor.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (head_anchor).data !== HYDRATION_START)
) {
head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor));
}

@ -345,7 +345,11 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
}
var prev_value = current[key];
if (value === prev_value) continue;
// Skip if value is unchanged, unless it's `undefined` and the element still has the attribute
if (value === prev_value && !(value === undefined && element.hasAttribute(key))) {
continue;
}
current[key] = value;
@ -483,8 +487,8 @@ export function attribute_effect(
block(() => {
var next = fn(...deriveds.map(get));
set_attributes(element, prev, next, css_hash, skip_warning);
/** @type {Record<string | symbol, any>} */
var current = set_attributes(element, prev, next, css_hash, skip_warning);
if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value, false);
@ -501,9 +505,11 @@ export function attribute_effect(
if (effects[symbol]) destroy_effect(effects[symbol]);
effects[symbol] = branch(() => attach(element, () => n));
}
current[symbol] = n;
}
prev = next;
prev = current;
});
if (is_select) {

@ -1,5 +1,6 @@
/** @import { TemplateNode } from '#client' */
import { COMMENT_NODE } from '#client/constants';
import {
HYDRATION_END,
HYDRATION_ERROR,
@ -87,7 +88,7 @@ export function remove_nodes() {
var node = hydrate_node;
while (true) {
if (node.nodeType === 8) {
if (node.nodeType === COMMENT_NODE) {
var data = /** @type {Comment} */ (node).data;
if (data === HYDRATION_END) {
@ -109,7 +110,7 @@ export function remove_nodes() {
* @param {TemplateNode} node
*/
export function read_hydration_instruction(node) {
if (!node || node.nodeType !== 8) {
if (!node || node.nodeType !== COMMENT_NODE) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}

@ -3,6 +3,7 @@ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { DEV } from 'esm-env';
import { init_array_prototype_warnings } from '../dev/equality.js';
import { get_descriptor, is_extensible } from '../../shared/utils.js';
import { TEXT_NODE } from '#client/constants';
// export these for reference in the compiled code, making global name deduplication unnecessary
/** @type {Window} */
@ -113,7 +114,7 @@ export function child(node, is_text) {
// Child can be null if we have an element with a single child, like `<p>{text}</p>`, where `text` is empty
if (child === null) {
child = hydrate_node.appendChild(create_text());
} else if (is_text && child.nodeType !== 3) {
} else if (is_text && child.nodeType !== TEXT_NODE) {
var text = create_text();
child?.before(text);
set_hydrate_node(text);
@ -143,7 +144,7 @@ export function first_child(fragment, is_text) {
// if an {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && hydrate_node?.nodeType !== 3) {
if (is_text && hydrate_node?.nodeType !== TEXT_NODE) {
var text = create_text();
hydrate_node?.before(text);
@ -174,11 +175,9 @@ export function sibling(node, count = 1, is_text = false) {
return next_sibling;
}
var type = next_sibling?.nodeType;
// if a sibling {expression} is empty during SSR, there might be no
// text node to hydrate — we must therefore create one
if (is_text && type !== 3) {
if (is_text && next_sibling?.nodeType !== TEXT_NODE) {
var text = create_text();
// If the next sibling is `null` and we're handling text then it's because
// the SSR content was empty for the text, so we need to generate a new text

@ -20,6 +20,7 @@ import {
TEMPLATE_USE_MATHML,
TEMPLATE_USE_SVG
} from '../../../constants.js';
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, TEXT_NODE } from '#client/constants';
/**
* @param {TemplateNode} start
@ -264,7 +265,7 @@ function run_scripts(node) {
// scripts were SSR'd, in which case they will run
if (hydrating) return node;
const is_fragment = node.nodeType === 11;
const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE;
const scripts =
/** @type {HTMLElement} */ (node).tagName === 'SCRIPT'
? [/** @type {HTMLScriptElement} */ (node)]
@ -305,7 +306,7 @@ export function text(value = '') {
var node = hydrate_node;
if (node.nodeType !== 3) {
if (node.nodeType !== TEXT_NODE) {
// if an {expression} is empty during SSR, we need to insert an empty text node
node.before((node = create_text()));
set_hydrate_node(node);
@ -360,7 +361,7 @@ export function props_id() {
if (
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.nodeType === COMMENT_NODE &&
hydrate_node.textContent?.startsWith(`#`)
) {
const id = hydrate_node.textContent.substring(1);

@ -328,12 +328,12 @@ export function state_prototype_fixed() {
}
/**
* Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* Updating state inside `$derived(...)`, `$inspect(...)` or a template expression is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never}
*/
export function state_unsafe_mutation() {
if (DEV) {
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
const error = new Error(`state_unsafe_mutation\nUpdating state inside \`$derived(...)\`, \`$inspect(...)\` or a template expression is forbidden. If the value should not be reactive, declare it without \`$state\`\nhttps://svelte.dev/e/state_unsafe_mutation`);
error.name = 'Svelte error';

@ -44,6 +44,7 @@ export function proxy(value) {
var reaction = active_reaction;
/**
* Executes the proxy in the context of the reaction it was originally created in, if any
* @template T
* @param {() => T} fn
*/

@ -135,10 +135,12 @@ export function mutate(source, value) {
export function set(source, value, should_proxy = false) {
if (
active_reaction !== null &&
!untracking &&
// since we are untracking the function inside `$inspect.with` we need to add this check
// to ensure we error if state is set inside an inspect effect
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 &&
!reaction_sources?.includes(source)
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction)
) {
e.state_unsafe_mutation();
}

@ -30,6 +30,7 @@ import * as w from './warnings.js';
import * as e from './errors.js';
import { assign_nodes } from './dom/template.js';
import { is_passive_event } from '../../utils.js';
import { COMMENT_NODE } from './constants.js';
/**
* This is normally true block effects should run their intro transitions
@ -107,7 +108,7 @@ export function hydrate(component, options) {
var anchor = /** @type {TemplateNode} */ (get_first_child(target));
while (
anchor &&
(anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
(anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (anchor).data !== HYDRATION_START)
) {
anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor));
}
@ -124,7 +125,7 @@ export function hydrate(component, options) {
if (
hydrate_node === null ||
hydrate_node.nodeType !== 8 ||
hydrate_node.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (hydrate_node).data !== HYDRATION_END
) {
w.hydration_mismatch();

@ -84,8 +84,8 @@ export function set_active_effect(effect) {
/**
* When sources are created within a reaction, reading and writing
* them should not cause a re-run
* @type {null | Source[]}
* them within that reaction should not cause a re-run
* @type {null | [active_reaction: Reaction, sources: Source[]]}
*/
export let reaction_sources = null;
@ -93,9 +93,9 @@ export let reaction_sources = null;
export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (reaction_sources === null) {
reaction_sources = [value];
reaction_sources = [active_reaction, [value]];
} else {
reaction_sources.push(value);
reaction_sources[1].push(value);
}
}
}
@ -234,7 +234,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
if (reaction_sources?.includes(signal)) continue;
if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue;
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
@ -724,7 +724,7 @@ export function get(signal) {
// Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) {
if (!reaction_sources?.includes(signal)) {
if (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) {
var deps = active_reaction.deps;
if (signal.rv < read_version) {
signal.rv = read_version;

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

@ -0,0 +1,2 @@
span[class].svelte-xyz { color: green }
div[style].svelte-xyz { color: green }

@ -0,0 +1,7 @@
<span class:foo={true}></span>
<div style:--foo="bar"></div>
<style>
span[class] { color: green }
div[style] { color: green }
</style>

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector ".forth"\nhttps://svelte.dev/e/css_unused_selector',
start: {
line: 8,
column: 2,
character: 190
},
end: {
line: 8,
column: 8,
character: 196
}
}
]
});

@ -0,0 +1,5 @@
.zero.first.svelte-xyz { color: green }
.second.svelte-xyz { color: green }
.third.svelte-xyz { color: green }
/* (unused) .forth { color: red }*/

@ -0,0 +1,9 @@
<div class="zero" class:first={true}></div>
<div class:second={true} class:third={true}></div>
<style>
.zero.first { color: green }
.second { color: green }
.third { color: green }
.forth { color: red }
</style>

@ -1,3 +1,4 @@
import { COMMENT_NODE, ELEMENT_NODE, TEXT_NODE } from '#client/constants';
import { assert } from 'vitest';
/**
@ -35,7 +36,7 @@ function clean_children(node, opts) {
});
for (let child of [...node.childNodes]) {
if (child.nodeType === 3) {
if (child.nodeType === TEXT_NODE) {
let text = /** @type {Text} */ (child);
if (
@ -49,7 +50,7 @@ function clean_children(node, opts) {
text.data = text.data.replace(/[^\S]+/g, ' ');
if (previous && previous.nodeType === 3) {
if (previous && previous.nodeType === TEXT_NODE) {
const prev = /** @type {Text} */ (previous);
prev.data += text.data;
@ -62,22 +63,22 @@ function clean_children(node, opts) {
}
}
if (child.nodeType === 8 && !opts.preserveComments) {
if (child.nodeType === COMMENT_NODE && !opts.preserveComments) {
// comment
child.remove();
continue;
}
// add newlines for better readability and potentially recurse into children
if (child.nodeType === 1 || child.nodeType === 8) {
if (previous?.nodeType === 3) {
if (child.nodeType === ELEMENT_NODE || child.nodeType === COMMENT_NODE) {
if (previous?.nodeType === TEXT_NODE) {
const prev = /** @type {Text} */ (previous);
prev.data = prev.data.replace(/^[^\S]+$/, '\n');
} else if (previous?.nodeType === 1 || previous?.nodeType === 8) {
} else if (previous?.nodeType === ELEMENT_NODE || previous?.nodeType === COMMENT_NODE) {
node.insertBefore(document.createTextNode('\n'), child);
}
if (child.nodeType === 1) {
if (child.nodeType === ELEMENT_NODE) {
has_element_children = true;
clean_children(/** @type {Element} */ (child), opts);
}
@ -87,12 +88,12 @@ function clean_children(node, opts) {
}
// collapse whitespace
if (node.firstChild && node.firstChild.nodeType === 3) {
if (node.firstChild && node.firstChild.nodeType === TEXT_NODE) {
const text = /** @type {Text} */ (node.firstChild);
text.data = text.data.trimStart();
}
if (node.lastChild && node.lastChild.nodeType === 3) {
if (node.lastChild && node.lastChild.nodeType === TEXT_NODE) {
const text = /** @type {Text} */ (node.lastChild);
text.data = text.data.trimEnd();
}

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
}
});

@ -0,0 +1,9 @@
<script>
const { browser } = $props();
const attributes = {
"data-test": browser ? undefined : ""
};
</script>
<div {...attributes}></div>

@ -1,5 +1,8 @@
/** @import { assert } from 'vitest' */
/** @import { CompileOptions, Warning } from '#compiler' */
import { ELEMENT_NODE } from '#client/constants';
/**
* @param {any} a
* @param {any} b
@ -102,7 +105,7 @@ function normalize_children(node) {
}
for (let child of [...node.childNodes]) {
if (child.nodeType === 1 /* Element */) {
if (child.nodeType === ELEMENT_NODE) {
normalize_children(child);
}
}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
}
});

@ -0,0 +1,45 @@
<script>
let a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0, i = 0;
function inc() {
a++;
b++;
c++;
d++;
e++;
f++;
g++;
h++;
i++;
}
</script>
{#if a = 0}{/if}
{#each [b = 0] as x}{x,''}{/each}
{#key c = 0}{/key}
{#await d = 0}{/await}
{#snippet snip()}{/snippet}
{@render (e = 0, snip)()}
{@html f = 0, ''}
<div {@attach !!(g = 0)}></div>
{#key 1}
{@const x = (h = 0)}
{x, ''}
{/key}
{#if 1}
{@const x = (i = 0)}
{x, ''}
{/if}
<button on:click={inc}>inc</button>
[{a},{b},{c},{d},{e},{f},{g},{h},{i}]

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button data-foo="true">inc</button> 12 - 12`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button data-foo="true">inc</button> 13 - 12`);
}
});

@ -0,0 +1,36 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
</script>
{#if fn(false)}{:else if fn(true)}{/if}
{#each fn([]) as x}{x, ''}{/each}
{#key fn(1)}{/key}
{#await fn(Promise.resolve())}{/await}
{#snippet snip()}{/snippet}
{@render fn(snip)()}
{@html fn('')}
<div {@attach fn(() => {})}></div>
{#key 1}
{@const x = fn('')}
{x}
{/key}
<button data-foo={fn(true)} on:click={() => count1++}>{fn('inc')}</button>
{count1} - {count2}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 10 - 10`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 11 - 10`);
}
});

@ -0,0 +1,46 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
const obj = {
get true() { return fn(true) },
get false() { return fn(false) },
get array() { return fn([]) },
get string() { return fn('') },
get promise() { return fn(Promise.resolve()) },
get snippet() { return fn(snip) },
get attachment() { return fn(() => {}) },
}
</script>
{#if obj.false}{:else if obj.true}{/if}
{#each obj.array as x}{x, ''}{/each}
{#key obj.string}{/key}
{#await obj.promise}{/await}
{#snippet snip()}{/snippet}
{@render obj.snippet()}
{@html obj.string}
<div {@attach obj.attachment}></div>
{#key 1}
{@const x = obj.string}
{x}
{/key}
<button on:click={() => count1++}>inc</button>
{count1} - {count2}

@ -5,7 +5,7 @@
export let index;
export let n;
function logRender () {
function logRender (n) {
order.push(`${index}: render ${n}`);
return index;
}
@ -24,5 +24,5 @@
</script>
<li>
{logRender()}
{logRender(n)}
</li>

@ -5,7 +5,7 @@
export let n = 0;
function logRender () {
function logRender (n) {
order.push(`parent: render ${n}`);
return 'parent';
}
@ -23,7 +23,7 @@
})
</script>
{logRender()}
{logRender(n)}
<ul>
{#each [1,2,3] as index}
<Item {index} {n} />

@ -1,3 +1,4 @@
import { COMMENT_NODE } from '#client/constants';
import { ok, test } from '../../test';
export default test({
@ -41,7 +42,7 @@ export default test({
// get all childNodes of template3 except comments
let childNodes = [];
for (const node of template3.content.childNodes) {
if (node.nodeType !== 8) {
if (node.nodeType !== COMMENT_NODE) {
childNodes.push(/** @type {Element} */ (node));
}
}

@ -0,0 +1,52 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
/**
* Ensure that sorting an array inside an $effect works correctly
* and re-runs when the array changes (e.g., when items are added).
*/
test({ assert, target }) {
const button = target.querySelector('button');
// initial render — array should be sorted
assert.htmlEqual(
target.innerHTML,
`
<button>add item</button>
<p>0</p>
<p>50</p>
<p>100</p>
`
);
// add first item (20); effect should re-run and sort the array
flushSync(() => button?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>add item</button>
<p>0</p>
<p>20</p>
<p>50</p>
<p>100</p>
`
);
// add second item (80); effect should re-run and sort the array
flushSync(() => button?.click());
assert.htmlEqual(
target.innerHTML,
`
<button>add item</button>
<p>0</p>
<p>20</p>
<p>50</p>
<p>80</p>
<p>100</p>
`
);
}
});

@ -0,0 +1,21 @@
<script>
let arr = $state([100, 0, 50]);
let nextValues = [20, 80];
let valueIndex = 0;
$effect(() => {
arr.sort((a, b) => a - b);
});
function addItem() {
if (valueIndex < nextValues.length) {
arr.push(nextValues[valueIndex]);
valueIndex++;
}
}
</script>
<button onclick={addItem}>add item</button>
{#each arr as x}
<p>{x}</p>
{/each}

@ -0,0 +1,5 @@
<script>
const { children } = $props()
</script>
{@render children()}

@ -0,0 +1,8 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, 'test');
}
});

@ -0,0 +1,9 @@
<script>
import A from './A.svelte';
const B = $derived(A);
</script>
<B>
<B>test</B>
</B>

@ -0,0 +1,18 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const [change, increment] = target.querySelectorAll('button');
increment.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>change handlers</button><button>1 / 1</button>');
change.click();
flushSync();
increment.click();
flushSync();
assert.htmlEqual(target.innerHTML, '<button>change handlers</button><button>3 / 3</button>');
}
});

@ -0,0 +1,27 @@
<script>
let delegated = $state(0);
let non_delegated = $state(0);
let attrs = $state({
onclick: () => {
delegated += 1;
},
onclickcapture: () => {
non_delegated += 1;
}
});
</script>
<button
onclick={() =>
(attrs = {
onclick: () => {
delegated += 2;
},
onclickcapture: () => {
non_delegated += 2;
}
})}
>
change handlers
</button>
<button {...attrs}>{delegated} / {non_delegated}</button>

@ -0,0 +1,9 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: 'state_unsafe_mutation'
});

@ -0,0 +1,10 @@
<script>
let a = $state(0);
let b = $state(0);
$inspect(a).with((...args)=>{
console.log(...args);
b++;
});
</script>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
async test({ assert, target, logs }) {
const [a, b] = target.querySelectorAll('button');
assert.deepEqual(logs, ['init', 0]);
flushSync(() => {
b?.click();
});
assert.deepEqual(logs, ['init', 0]);
flushSync(() => {
a?.click();
});
assert.deepEqual(logs, ['init', 0, 'update', 1]);
}
});

@ -0,0 +1,13 @@
<script>
let a = $state(0);
let b = $state(0);
$inspect(a).with((...args)=>{
console.log(...args);
b;
});
</script>
<button onclick={()=>a++}></button>
<button onclick={()=>b++}></button>

@ -1021,6 +1021,41 @@ describe('signals', () => {
};
});
test('nested effects depend on state of upper effects', () => {
const logs: number[] = [];
user_effect(() => {
const raw = state(0);
const proxied = proxy({ current: 0 });
// We need those separate, else one working and rerunning the effect
// could mask the other one not rerunning
user_effect(() => {
logs.push($.get(raw));
});
user_effect(() => {
logs.push(proxied.current);
});
// Important so that the updating effect is not running
// together with the reading effects
flushSync();
user_effect(() => {
$.untrack(() => {
set(raw, $.get(raw) + 1);
proxied.current += 1;
});
});
});
return () => {
flushSync();
assert.deepEqual(logs, [0, 0, 1, 1]);
};
});
test('proxy version state does not trigger self-dependency guard', () => {
return () => {
const s = proxy({ a: { b: 1 } });

@ -8,11 +8,13 @@ export default function Purity($$anchor) {
var fragment = root();
var p = $.first_child(fragment);
p.textContent = '0';
p.textContent = (
$.untrack(() => Math.max(0, Math.min(0, 100)))
);
var p_1 = $.sibling(p, 2);
p_1.textContent = location.href;
p_1.textContent = ($.untrack(() => location.href));
var node = $.sibling(p_1, 2);

Loading…
Cancel
Save