pull/16197/head
Rich Harris 3 months ago
commit 6e1c89f448

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect

@ -20,7 +20,7 @@ Unlike other frameworks you may have encountered, there is no API for interactin
If `$state` is used with an array or a simple object, the result is a deeply reactive _state proxy_. [Proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) allow Svelte to run code when you read or write properties, including via methods like `array.push(...)`, triggering granular updates.
State is proxified recursively until Svelte finds something other than an array or simple object (like a class). In a case like this...
State is proxified recursively until Svelte finds something other than an array or simple object (like a class or an object created with `Object.create`). In a case like this...
```js
let todos = $state([

@ -1,5 +1,25 @@
# svelte
## 5.34.0
### Minor Changes
- feat: add source name logging to `$inspect.trace` ([#16060](https://github.com/sveltejs/svelte/pull/16060))
### Patch Changes
- fix: add `command` and `commandfor` to `HTMLButtonAttributes` ([#16117](https://github.com/sveltejs/svelte/pull/16117))
- fix: better `$inspect.trace()` output ([#16131](https://github.com/sveltejs/svelte/pull/16131))
- fix: properly hydrate dynamic css props components and remove element removal ([#16118](https://github.com/sveltejs/svelte/pull/16118))
## 5.33.19
### Patch Changes
- fix: reset `is_flushing` if `flushSync` is called and there's no scheduled effect ([#16119](https://github.com/sveltejs/svelte/pull/16119))
## 5.33.18
### Patch Changes

@ -926,6 +926,17 @@ export interface HTMLButtonAttributes extends HTMLAttributes<HTMLButtonElement>
value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null;
command?:
| 'show-modal'
| 'close'
| 'request-close'
| 'show-popover'
| 'hide-popover'
| 'toggle-popover'
| (string & {})
| undefined
| null;
commandfor?: string | undefined | null;
}
export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> {

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

@ -67,11 +67,20 @@ function build_assignment(operator, left, right, context) {
in_constructor: rune !== '$derived' && rune !== '$derived.by'
};
return b.assignment(
operator,
b.member(b.this, field.key),
/** @type {Expression} */ (context.visit(right, child_state))
);
let value = /** @type {Expression} */ (context.visit(right, child_state));
if (dev) {
const declaration = context.path.findLast(
(parent) => parent.type === 'ClassDeclaration' || parent.type === 'ClassExpression'
);
value = b.call(
'$.tag',
value,
b.literal(`${declaration?.id?.name ?? '[class]'}.${name}`)
);
}
return b.assignment(operator, b.member(b.this, field.key), value);
}
}

@ -1,7 +1,9 @@
/** @import { CallExpression, ClassBody, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { CallExpression, ClassBody, ClassDeclaration, ClassExpression, MethodDefinition, PropertyDefinition, StaticBlock } from 'estree' */
/** @import { StateField } from '#compiler' */
/** @import { Context } from '../types' */
import * as b from '#compiler/builders';
import { dev } from '../../../../state.js';
import { get_parent } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.js';
/**
@ -50,6 +52,10 @@ export function ClassBody(node, context) {
}
}
const declaration = /** @type {ClassDeclaration | ClassExpression} */ (
get_parent(context.path, -1)
);
// Replace parts of the class body
for (const definition of node.body) {
if (definition.type !== 'PropertyDefinition') {
@ -68,17 +74,26 @@ export function ClassBody(node, context) {
}
if (name[0] === '#') {
body.push(/** @type {PropertyDefinition} */ (context.visit(definition, child_state)));
let value = definition.value
? /** @type {CallExpression} */ (context.visit(definition.value, child_state))
: undefined;
if (dev) {
value = b.call('$.tag', value, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
}
body.push(b.prop_def(definition.key, value));
} else if (field.node === definition) {
const member = b.member(b.this, field.key);
let call = /** @type {CallExpression} */ (context.visit(field.value, child_state));
if (dev) {
call = b.call('$.tag', call, b.literal(`${declaration.id?.name ?? '[class]'}.${name}`));
}
const member = b.member(b.this, field.key);
const should_proxy = field.type === '$state' && true; // TODO
body.push(
b.prop_def(
field.key,
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.prop_def(field.key, call),
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),

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

@ -90,6 +90,10 @@ export function VariableDeclaration(node, context) {
should_proxy(initial, context.state.scope)
) {
initial = b.call('$.proxy', initial);
if (dev) {
initial = b.call('$.tag_proxy', initial, b.literal(id.name));
}
}
if (is_prop_source(binding, context.state)) {
@ -128,12 +132,25 @@ export function VariableDeclaration(node, context) {
const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name)
);
if (rune === '$state' && should_proxy(value, context.state.scope)) {
const is_state = is_state_source(binding, context.state.analysis);
const is_proxy = should_proxy(value, context.state.scope);
if (rune === '$state' && is_proxy) {
value = b.call('$.proxy', value);
if (dev && !is_state) {
value = b.call('$.tag_proxy', value, b.literal(id.name));
}
}
if (is_state_source(binding, context.state.analysis)) {
if (is_state) {
value = b.call('$.state', value);
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
}
}
return value;
};
@ -154,7 +171,11 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
return b.declarator(id, b.call('$.derived', expression));
const call = b.call('$.derived', expression);
return b.declarator(
id,
dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call
);
}),
...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression));
@ -203,7 +224,10 @@ export function VariableDeclaration(node, context) {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
declarations.push(b.declarator(declarator.id, b.call('$.derived', expression)));
let call = b.call('$.derived', expression);
if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name));
declarations.push(b.declarator(declarator.id, call));
}
} else {
const init = /** @type {CallExpression} */ (declarator.init);
@ -216,8 +240,10 @@ export function VariableDeclaration(node, context) {
let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression);
declarations.push(b.declarator(id, b.call('$.derived', expression)));
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
}
const { inserts, paths } = extract_paths(declarator.id, rhs);
@ -227,12 +253,23 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(value)));
declarations.push(b.declarator(id, b.call('$.derived', expression)));
const call = b.call('$.derived', expression);
declarations.push(
b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call)
);
}
for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression));
declarations.push(b.declarator(path.node, b.call('$.derived', b.thunk(expression))));
const call = b.call('$.derived', b.thunk(expression));
declarations.push(
b.declarator(
path.node,
dev
? b.call('$.tag', call, b.literal(/** @type {Identifier} */ (path.node).name))
: call
)
);
}
}

@ -14,10 +14,13 @@ import { create_derived } from '../../utils.js';
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {string} component_name
* @param {ComponentContext} context
* @param {Expression} anchor
* @returns {Statement}
*/
export function build_component(node, component_name, context, anchor = context.state.node) {
export function build_component(node, component_name, context) {
/**
* @type {Expression}
*/
const anchor = context.state.node;
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Array<() => void>} */
@ -435,7 +438,7 @@ export function build_component(node, component_name, context, anchor = 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 === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)
? component_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id,
@ -458,14 +461,18 @@ export function build_component(node, component_name, context, anchor = context.
)
];
if (node.type === 'SvelteComponent') {
if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) {
const prev = fn;
fn = (node_id) => {
return b.call(
'$.component',
node_id,
b.thunk(/** @type {Expression} */ (context.visit(node.expression))),
b.thunk(
/** @type {Expression} */ (
context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression)
)
),
b.arrow(
[b.id('$$anchor'), b.id(component_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))])

@ -30,6 +30,7 @@ export const EFFECT_ASYNC = 1 << 25;
export const ASYNC_ERROR = 1;
export const STATE_SYMBOL = Symbol('$state');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol('');

@ -2,52 +2,42 @@
import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.js';
import { DERIVED, STATE_SYMBOL } from '#client/constants';
import { DERIVED, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js';
/** @type { any } */
/**
* @typedef {{
* traces: Error[];
* }} TraceEntry
*/
/** @type {{ reaction: Reaction | null, entries: Map<Value, TraceEntry> } | null} */
export let tracing_expressions = null;
/**
* @param { Value } signal
* @param { { read: Error[] } } [entry]
* @param {Value} signal
* @param {TraceEntry} [entry]
*/
function log_entry(signal, entry) {
const debug = signal.debug;
const value = signal.trace_need_increase ? signal.trace_v : signal.v;
const value = signal.v;
if (value === UNINITIALIZED) {
return;
}
if (debug) {
var previous_captured_signals = captured_signals;
var captured = new Set();
set_captured_signals(captured);
try {
untrack(() => {
debug();
});
} finally {
set_captured_signals(previous_captured_signals);
}
if (captured.size > 0) {
for (const dep of captured) {
log_entry(dep);
}
return;
}
}
const type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0;
const style = dirty
? 'color: CornflowerBlue; font-weight: bold'
: 'color: grey; font-weight: normal';
// eslint-disable-next-line no-console
console.groupCollapsed(
`%c${type}`,
dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold',
signal.label ? `%c${type}%c ${signal.label}` : `%c${type}%c`,
style,
dirty ? 'font-weight: normal' : style,
typeof value === 'object' && value !== null && STATE_SYMBOL in value
? snapshot(value, true)
: value
@ -65,17 +55,15 @@ function log_entry(signal, entry) {
console.log(signal.created);
}
if (signal.updated) {
if (dirty && signal.updated) {
// eslint-disable-next-line no-console
console.log(signal.updated);
}
const read = entry?.read;
if (read && read.length > 0) {
for (var stack of read) {
if (entry) {
for (var trace of entry.traces) {
// eslint-disable-next-line no-console
console.log(stack);
console.log(trace);
}
}
@ -90,6 +78,7 @@ function log_entry(signal, entry) {
*/
export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions;
try {
tracing_expressions = { entries: new Map(), reaction: active_reaction };
@ -97,39 +86,32 @@ export function trace(label, fn) {
var value = fn();
var time = (performance.now() - start).toFixed(2);
var prefix = untrack(label);
if (!effect_tracking()) {
// eslint-disable-next-line no-console
console.log(`${label()} %cran outside of an effect (${time}ms)`, 'color: grey');
console.log(`${prefix} %cran outside of an effect (${time}ms)`, 'color: grey');
} else if (tracing_expressions.entries.size === 0) {
// eslint-disable-next-line no-console
console.log(`${label()} %cno reactive dependencies (${time}ms)`, 'color: grey');
console.log(`${prefix} %cno reactive dependencies (${time}ms)`, 'color: grey');
} else {
// eslint-disable-next-line no-console
console.group(`${label()} %c(${time}ms)`, 'color: grey');
console.group(`${prefix} %c(${time}ms)`, 'color: grey');
var entries = tracing_expressions.entries;
untrack(() => {
for (const [signal, traces] of entries) {
log_entry(signal, traces);
}
});
tracing_expressions = null;
for (const [signal, entry] of entries) {
log_entry(signal, entry);
}
// eslint-disable-next-line no-console
console.groupEnd();
}
if (previously_tracing_expressions !== null && tracing_expressions !== null) {
for (const [signal, entry] of tracing_expressions.entries) {
var prev_entry = previously_tracing_expressions.get(signal);
if (prev_entry === undefined) {
previously_tracing_expressions.set(signal, entry);
} else {
prev_entry.read.push(...entry.read);
}
}
}
return value;
} finally {
tracing_expressions = previously_tracing_expressions;
@ -177,3 +159,34 @@ export function get_stack(label) {
}
return error;
}
/**
* @param {Value} source
* @param {string} label
*/
export function tag(source, label) {
source.label = label;
tag_proxy(source.v, label);
return source;
}
/**
* @param {unknown} value
* @param {string} label
*/
export function tag_proxy(value, label) {
// @ts-expect-error
value?.[PROXY_PATH_SYMBOL]?.(label);
return value;
}
/**
* @param {unknown} value
*/
export function label(value) {
if (typeof value === 'symbol') return `Symbol(${value.description})`;
if (typeof value === 'function') return '<function>';
if (typeof value === 'object' && value) return '<object>';
return String(value);
}

@ -26,8 +26,4 @@ export function css_props(element, get_styles) {
}
}
});
teardown(() => {
element.remove();
});
}

@ -642,7 +642,7 @@ function create_item(
if (DEV && reactive) {
// For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended
/** @type {Value} */ (v).debug = () => {
/** @type {Value} */ (v).trace = () => {
var collection_index = typeof i === 'number' ? index : i.v;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[collection_index];

@ -7,7 +7,7 @@ export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js';
export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
export { trace } from './dev/tracing.js';
export { trace, tag, tag_proxy } from './dev/tracing.js';
export { inspect } from './dev/inspect.js';
export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js';

@ -9,12 +9,15 @@ import {
object_prototype
} from '../shared/utils.js';
import { state as source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from '#client/constants';
import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants';
import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js';
import { get_stack } from './dev/tracing.js';
import { get_stack, tag } from './dev/tracing.js';
import { tracing_mode_flag } from '../flags/index.js';
// TODO move all regexes into shared module?
const regex_is_valid_identifier = /^[a-zA-Z_$][a-zA-Z_$0-9]*$/;
/**
* @template T
* @param {T} value
@ -61,6 +64,21 @@ export function proxy(value) {
sources.set('length', source(/** @type {any[]} */ (value).length, stack));
}
/** Used in dev for $inspect.trace() */
var path = '';
/** @param {string} new_path */
function update_path(new_path) {
path = new_path;
tag(version, `${path} version`);
// rename all child sources and child proxies
for (const [prop, source] of sources) {
tag(source, get_label(path, prop));
}
}
return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) {
if (
@ -76,17 +94,20 @@ export function proxy(value) {
e.state_descriptors_fixed();
}
var s = sources.get(prop);
with_parent(() => {
var s = sources.get(prop);
if (s === undefined) {
s = with_parent(() => source(descriptor.value, stack));
sources.set(prop, s);
} else {
set(
s,
with_parent(() => proxy(descriptor.value))
);
}
if (s === undefined) {
s = source(descriptor.value, stack);
sources.set(prop, s);
if (DEV && typeof prop === 'string') {
tag(s, get_label(path, prop));
}
} else {
set(s, descriptor.value, true);
}
});
return true;
},
@ -96,11 +117,13 @@ export function proxy(value) {
if (s === undefined) {
if (prop in target) {
sources.set(
prop,
with_parent(() => source(UNINITIALIZED, stack))
);
const s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(prop, s);
update_version(version);
if (DEV) {
tag(s, get_label(path, prop));
}
}
} else {
// When working with arrays, we need to also ensure we update the length when removing
@ -125,12 +148,26 @@ export function proxy(value) {
return value;
}
if (DEV && prop === PROXY_PATH_SYMBOL) {
return update_path;
}
var s = sources.get(prop);
var exists = prop in target;
// create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack));
s = with_parent(() => {
var p = proxy(exists ? target[prop] : UNINITIALIZED);
var s = source(p, stack);
if (DEV) {
tag(s, get_label(path, prop));
}
return s;
});
sources.set(prop, s);
}
@ -178,7 +215,17 @@ export function proxy(value) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) {
if (s === undefined) {
s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack));
s = with_parent(() => {
var p = has ? proxy(target[prop]) : UNINITIALIZED;
var s = source(p, stack);
if (DEV) {
tag(s, get_label(path, prop));
}
return s;
});
sources.set(prop, s);
}
@ -207,6 +254,10 @@ export function proxy(value) {
// the value of the original item at that index.
other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s);
if (DEV) {
tag(other_s, get_label(path, i));
}
}
}
}
@ -217,19 +268,23 @@ export function proxy(value) {
// object property before writing to that property.
if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack));
set(
s,
with_parent(() => proxy(value))
);
s = with_parent(() => {
var s = source(undefined, stack);
set(s, proxy(value));
return s;
});
sources.set(prop, s);
if (DEV) {
tag(s, get_label(path, prop));
}
}
} else {
has = s.v !== UNINITIALIZED;
set(
s,
with_parent(() => proxy(value))
);
var p = with_parent(() => proxy(value));
set(s, p);
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
@ -282,6 +337,16 @@ export function proxy(value) {
});
}
/**
* @param {string} path
* @param {string | symbol} prop
*/
function get_label(path, prop) {
if (typeof prop === 'symbol') return `${path}[Symbol(${prop.description ?? ''})]`;
if (regex_is_valid_identifier.test(prop)) return `${path}.${prop}`;
return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`;
}
/**
* @param {Source<number>} signal
* @param {1 | -1} [d]

@ -32,13 +32,15 @@ import {
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
import { get_stack } from '../dev/tracing.js';
import { get_stack, tag_proxy } from '../dev/tracing.js';
import { component_context, is_runes } from '../context.js';
import { Batch } from './batch.js';
import { proxy } from '../proxy.js';
import { execute_derived } from './deriveds.js';
export let inspect_effects = new Set();
/** @type {Map<Source, any>} */
export const old_values = new Map();
/** Internal representation of `$effect.pending()` */
@ -71,7 +73,9 @@ export function source(v, stack) {
if (DEV && tracing_mode_flag) {
signal.created = stack ?? get_stack('CreatedAt');
signal.debug = null;
signal.updated = null;
signal.set_during_effect = false;
signal.trace = null;
}
return signal;
@ -146,6 +150,10 @@ export function set(source, value, should_proxy = false) {
let new_value = should_proxy ? proxy(value) : value;
if (DEV) {
tag_proxy(new_value, /** @type {string} */ (source.label));
}
return internal_set(source, new_value);
}
@ -174,9 +182,9 @@ export function internal_set(source, value) {
if (DEV && tracing_mode_flag) {
source.updated = get_stack('UpdatedAt');
if (active_effect != null) {
source.trace_need_increase = true;
source.trace_v ??= old_value;
if (active_effect !== null) {
source.set_during_effect = true;
}
}

@ -17,12 +17,21 @@ export interface Value<V = unknown> extends Signal {
rv: number;
/** The latest value for this signal */
v: V;
/** Dev only */
// dev-only
/** A label (e.g. the `foo` in `let foo = $state(...)`) used for `$inspect.trace()` */
label?: string;
/** An error with a stack trace showing when the source was created */
created?: Error | null;
/** An error with a stack trace showing when the source was last updated */
updated?: Error | null;
trace_need_increase?: boolean;
trace_v?: V;
debug?: null | (() => void);
/**
* Whether or not the source was set while running an effect if so, we need to
* increment the write version so that it shows up as dirty when the effect re-runs
*/
set_during_effect?: boolean;
/** A function that retrieves the underlying source, used for each block item signals */
trace?: null | (() => void);
}
export interface Reaction extends Signal {

@ -57,6 +57,7 @@ import {
import * as w from './warnings.js';
import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
/** @type {Effect | null} */
let last_scheduled_effect = null;
@ -480,19 +481,13 @@ export function update_effect(effect) {
effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.wv = write_version;
var deps = effect.deps;
// In DEV, we need to handle a case where $inspect.trace() might
// incorrectly state a source dependency has not changed when it has.
// That's beacuse that source was changed by the same effect, causing
// the versions to match. We can avoid this by incrementing the version
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && deps !== null) {
for (let i = 0; i < deps.length; i++) {
var dep = deps[i];
if (dep.trace_need_increase) {
// In DEV, increment versions of any sources that were written to during the effect,
// so that they are correctly marked as dirty when the effect re-runs
if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
for (var dep of effect.deps) {
if (dep.set_during_effect) {
dep.wv = increment_write_version();
dep.trace_need_increase = undefined;
dep.trace_v = undefined;
dep.set_during_effect = false;
}
}
}
@ -852,28 +847,39 @@ export function get(signal) {
}
}
recent_async_deriveds.delete(signal);
if (
tracing_mode_flag &&
!untracking &&
tracing_expressions !== null &&
active_reaction !== null &&
tracing_expressions.reaction === active_reaction
) {
// Used when mapping state between special blocks like `each`
if (signal.debug) {
signal.debug();
} else if (signal.created) {
var entry = tracing_expressions.entries.get(signal);
if (entry === undefined) {
entry = { read: [] };
tracing_expressions.entries.set(signal, entry);
}
if (signal.trace) {
signal.trace();
} else {
var trace = get_stack('TracedAt');
if (trace) {
var entry = tracing_expressions.entries.get(signal);
entry.read.push(get_stack('TracedAt'));
if (entry === undefined) {
entry = { traces: [] };
tracing_expressions.entries.set(signal, entry);
}
var last = entry.traces[entry.traces.length - 1];
// traces can be duplicated, e.g. by `snapshot` invoking both
// both `getOwnPropertyDescriptor` and `get` traps at once
if (trace.stack !== last?.stack) {
entry.traces.push(trace);
}
}
}
}
recent_async_deriveds.delete(signal);
}
if (is_destroying_effect && old_values.has(signal)) {

@ -7,8 +7,10 @@ import { raf } from '../internal/client/timing.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
import { render_effect } from '../internal/client/reactivity/effects.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { deferred, noop } from '../internal/shared/utils.js';
import { DEV } from 'esm-env';
/**
* @template T
@ -172,8 +174,8 @@ export class Spring {
#damping = source(0.8);
#precision = source(0.01);
#current = source(/** @type {T} */ (undefined));
#target = source(/** @type {T} */ (undefined));
#current;
#target;
#last_value = /** @type {T} */ (undefined);
#last_time = 0;
@ -192,11 +194,20 @@ export class Spring {
* @param {SpringOpts} [options]
*/
constructor(value, options = {}) {
this.#current.v = this.#target.v = value;
this.#current = DEV ? tag(source(value), 'Spring.current') : source(value);
this.#target = DEV ? tag(source(value), 'Spring.target') : source(value);
if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1);
if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);
if (typeof options.precision === 'number') this.#precision.v = options.precision;
if (DEV) {
tag(this.#stiffness, 'Spring.stiffness');
tag(this.#damping, 'Spring.damping');
tag(this.#precision, 'Spring.precision');
tag(this.#current, 'Spring.current');
tag(this.#target, 'Spring.target');
}
}
/**

@ -7,7 +7,9 @@ import { loop } from '../internal/client/loop.js';
import { linear } from '../easing/index.js';
import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get, render_effect } from 'svelte/internal/client';
import { DEV } from 'esm-env';
/**
* @template T
@ -175,8 +177,8 @@ export function tweened(value, defaults = {}) {
* @since 5.8.0
*/
export class Tween {
#current = source(/** @type {T} */ (undefined));
#target = source(/** @type {T} */ (undefined));
#current;
#target;
/** @type {TweenedOptions<T>} */
#defaults;
@ -189,8 +191,14 @@ export class Tween {
* @param {TweenedOptions<T>} options
*/
constructor(value, options = {}) {
this.#current.v = this.#target.v = value;
this.#current = source(value);
this.#target = source(value);
this.#defaults = options;
if (DEV) {
tag(this.#current, 'Tween.current');
tag(this.#target, 'Tween.target');
}
}
/**

@ -1,7 +1,9 @@
import { get, tick, untrack } from '../internal/client/runtime.js';
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { increment } from './utils.js';
import { DEV } from 'esm-env';
/**
* Returns a `subscribe` function that, if called in an effect (including expressions in the template),
@ -51,6 +53,10 @@ export function createSubscriber(start) {
/** @type {(() => void) | void} */
let stop;
if (DEV) {
tag(version, 'createSubscriber version');
}
return () => {
if (effect_tracking()) {
get(version);

@ -1,7 +1,9 @@
/** @import { Source } from '#client' */
import { derived } from '../internal/client/index.js';
import { source, set } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { active_reaction, get, set_active_reaction } from '../internal/client/runtime.js';
import { DEV } from 'esm-env';
var inited = false;
@ -49,6 +51,11 @@ export class SvelteDate extends Date {
constructor(...params) {
// @ts-ignore
super(...params);
if (DEV) {
tag(this.#time, 'SvelteDate.#time');
}
if (!inited) this.#init();
}

@ -1,6 +1,7 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { set, source } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
@ -62,8 +63,13 @@ export class SvelteMap extends Map {
constructor(value) {
super();
// If the value is invalid then the native exception will fire here
if (DEV) value = new Map(value);
if (DEV) {
// If the value is invalid then the native exception will fire here
value = new Map(value);
tag(this.#version, 'SvelteMap version');
tag(this.#size, 'SvelteMap.size');
}
if (value) {
for (var [key, v] of value) {
@ -82,6 +88,11 @@ export class SvelteMap extends Map {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
} else {
// We should always track the version in case
@ -113,6 +124,11 @@ export class SvelteMap extends Map {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
} else {
// We should always track the version in case
@ -138,7 +154,13 @@ export class SvelteMap extends Map {
var version = this.#version;
if (s === undefined) {
sources.set(key, source(0));
s = source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
set(this.#size, super.size);
increment(version);
} else if (prev_res !== value) {
@ -197,12 +219,18 @@ export class SvelteMap extends Map {
if (this.#size.v !== sources.size) {
for (var key of super.keys()) {
if (!sources.has(key)) {
sources.set(key, source(0));
var s = source(0);
if (DEV) {
tag(s, `SvelteMap get(${label(key)})`);
}
sources.set(key, s);
}
}
}
for (var [, s] of this.#sources) {
for ([, s] of this.#sources) {
get(s);
}
}

@ -1,6 +1,7 @@
/** @import { Source } from '#client' */
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { label, tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
@ -56,8 +57,13 @@ export class SvelteSet extends Set {
constructor(value) {
super();
// If the value is invalid then the native exception will fire here
if (DEV) value = new Set(value);
if (DEV) {
// If the value is invalid then the native exception will fire here
value = new Set(value);
tag(this.#version, 'SvelteSet version');
tag(this.#size, 'SvelteSet.size');
}
if (value) {
for (var element of value) {
@ -111,6 +117,11 @@ export class SvelteSet extends Set {
}
s = source(true);
if (DEV) {
tag(s, `SvelteSet has(${label(value)})`);
}
sources.set(value, s);
}

@ -1,4 +1,6 @@
import { DEV } from 'esm-env';
import { source } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { get_current_url } from './url.js';
import { increment } from './utils.js';
@ -32,7 +34,7 @@ export const REPLACE = Symbol();
* ```
*/
export class SvelteURLSearchParams extends URLSearchParams {
#version = source(0);
#version = DEV ? tag(source(0), 'SvelteURLSearchParams version') : source(0);
#url = get_current_url();
#updating = false;

@ -1,4 +1,6 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { get } from '../internal/client/runtime.js';
import { REPLACE, SvelteURLSearchParams } from './url-search-params.js';
@ -56,6 +58,17 @@ export class SvelteURL extends URL {
url = new URL(url, base);
super(url);
if (DEV) {
tag(this.#protocol, 'SvelteURL.protocol');
tag(this.#username, 'SvelteURL.username');
tag(this.#password, 'SvelteURL.password');
tag(this.#hostname, 'SvelteURL.hostname');
tag(this.#port, 'SvelteURL.port');
tag(this.#pathname, 'SvelteURL.pathname');
tag(this.#hash, 'SvelteURL.hash');
tag(this.#search, 'SvelteURL.search');
}
current_url = this;
this.#searchParams = new SvelteURLSearchParams(url.searchParams);
current_url = null;

@ -1,8 +1,9 @@
import { BROWSER } from 'esm-env';
import { BROWSER, DEV } from 'esm-env';
import { on } from '../../events/index.js';
import { ReactiveValue } from '../reactive-value.js';
import { get } from '../../internal/client/index.js';
import { set, source } from '../../internal/client/reactivity/sources.js';
import { tag } from '../../internal/client/dev/tracing.js';
/**
* `scrollX.current` is a reactive view of `window.scrollX`. On the server it is `undefined`.
@ -147,6 +148,10 @@ export const devicePixelRatio = /* @__PURE__ */ new (class DevicePixelRatio {
if (BROWSER) {
this.#update();
}
if (DEV) {
tag(this.#dpr, 'window.devicePixelRatio');
}
}
get current() {

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

@ -193,3 +193,44 @@ if (typeof window !== 'undefined') {
}
export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html';
/**
* @param {any[]} logs
*/
export function normalise_trace_logs(logs) {
let normalised = [];
logs = logs.slice();
while (logs.length > 0) {
const log = logs.shift();
if (log instanceof Error) {
continue;
}
if (typeof log === 'string' && log.includes('%c')) {
const split = log.split('%c');
const first = /** @type {string} */ (split.shift()).trim();
if (first) normalised.push({ log: first });
while (split.length > 0) {
const log = /** @type {string} */ (split.shift()).trim();
const highlighted = logs.shift() === 'color: CornflowerBlue; font-weight: bold';
// omit timings, as they will differ between runs
if (/\(.+ms\)/.test(log)) continue;
normalised.push({
log,
highlighted
});
}
} else {
normalised.push({ log });
}
}
return normalised;
}

@ -0,0 +1,7 @@
<div>a</div>
<style>
div{
color: var(--prop);
}
</style>

@ -0,0 +1,7 @@
<div>b</div>
<style>
div{
color: var(--prop);
}
</style>

@ -0,0 +1,16 @@
import { test } from '../../assert';
import { flushSync } from 'svelte';
export default test({
warnings: [],
async test({ assert, target }) {
const btn = target.querySelector('button');
let div = /** @type {HTMLElement} */ (target.querySelector('div'));
assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)');
flushSync(() => {
btn?.click();
});
div = /** @type {HTMLElement} */ (target.querySelector('div'));
assert.equal(getComputedStyle(div).color, 'rgb(255, 0, 0)');
}
});

@ -0,0 +1,11 @@
<script>
import A from "./B.svelte";
import B from "./A.svelte";
let value = $state(0);
let Comp = $derived(value % 2 === 0 ? A : B);
</script>
<button onclick={()=>value++}>click</button>
<Comp --prop="red"/>

@ -3,7 +3,7 @@ import { setImmediate } from 'node:timers/promises';
import { globSync } from 'tinyglobby';
import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client';
import { flushSync, hydrate, mount, unmount } from 'svelte';
import { flushSync, hydrate, mount, unmount, untrack } from 'svelte';
import { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory, fragments } from '../helpers.js';

@ -0,0 +1,8 @@
<script>
let { entry } = $props();
$effect(() => {
$inspect.trace('effect');
entry;
});
</script>

@ -0,0 +1,40 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
dev: true
},
test({ assert, target, logs }) {
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect' },
{ log: '$state', highlighted: true },
{ log: 'array', highlighted: false },
{ log: [{ id: 1, hi: true }] },
// this _doesn't_ appear in the browser, but it does appear during tests
// and i cannot for the life of me figure out why. this does at least
// test that we don't log `array[0].id` etc
{ log: '$state', highlighted: true },
{ log: 'array[0]', highlighted: false },
{ log: { id: 1, hi: true } }
]);
logs.length = 0;
const button = target.querySelector('button');
button?.click();
flushSync();
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect' },
{ log: '$state', highlighted: true },
{ log: 'array', highlighted: false },
{ log: [{ id: 1, hi: false }] },
{ log: '$state', highlighted: false },
{ log: 'array[0]', highlighted: false },
{ log: { id: 1, hi: false } }
]);
}
});

@ -0,0 +1,11 @@
<script>
import Entry from './Entry.svelte';
let array = $state([{ id: 1, hi: true }]);
</script>
<button onclick={() => array = [{ id: 1, hi: false}]}>update</button>
{#each array as entry (entry.id)}
<Entry {entry} />
{/each}

@ -1,29 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
/**
* @param {any[]} logs
*/
function normalise_trace_logs(logs) {
let normalised = [];
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
if (typeof log === 'string' && log.includes('%c')) {
const split = log.split('%c');
normalised.push({
log: (split[0].length !== 0 ? split[0] : split[1]).trim(),
highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold'
});
i++;
} else if (log instanceof Error) {
continue;
} else {
normalised.push({ log });
}
}
return normalised;
}
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -34,10 +11,11 @@ export default test({
// initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'iife', highlighted: false },
{ log: 'iife' },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 0 },
{ log: 'effect', highlighted: false }
{ log: 'effect' }
]);
logs.length = 0;
@ -47,10 +25,11 @@ export default test({
flushSync();
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'iife', highlighted: false },
{ log: 'iife' },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 1 },
{ log: 'effect', highlighted: false }
{ log: 'effect' }
]);
}
});

@ -1,29 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
/**
* @param {any[]} logs
*/
function normalise_trace_logs(logs) {
let normalised = [];
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
if (typeof log === 'string' && log.includes('%c')) {
const split = log.split('%c');
normalised.push({
log: (split[0].length !== 0 ? split[0] : split[1]).trim(),
highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold'
});
i++;
} else if (log instanceof Error) {
continue;
} else {
normalised.push({ log });
}
}
return normalised;
}
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -34,8 +11,9 @@ export default test({
// initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: false }
]);
@ -52,20 +30,26 @@ export default test({
// checked changed, effect reassign state, values should be correct and be correctly highlighted
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: true },
{ log: '$state', highlighted: true },
{ log: 1 },
{ log: 'effect', highlighted: false },
{ log: 'count', highlighted: false },
{ log: 2 },
{ log: 'effect' },
{ log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: true },
{ log: '$state', highlighted: true },
{ log: 2 },
{ log: 'effect', highlighted: false },
{ log: 'count', highlighted: false },
{ log: 3 },
{ log: 'effect' },
{ log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: true },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 3 }
]);
}

@ -1,29 +1,6 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
/**
* @param {any[]} logs
*/
function normalise_trace_logs(logs) {
let normalised = [];
for (let i = 0; i < logs.length; i++) {
const log = logs[i];
if (typeof log === 'string' && log.includes('%c')) {
const split = log.split('%c');
normalised.push({
log: (split[0].length !== 0 ? split[0] : split[1]).trim(),
highlighted: logs[i + 1] === 'color: CornflowerBlue; font-weight: bold'
});
i++;
} else if (log instanceof Error) {
continue;
} else {
normalised.push({ log });
}
}
return normalised;
}
import { normalise_trace_logs } from '../../../helpers.js';
export default test({
compileOptions: {
@ -34,12 +11,15 @@ export default test({
// initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 0 },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 0 },
{ log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: false }
]);
@ -52,12 +32,15 @@ export default test({
// count changed, derived and state are highlighted, last state is not
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 2 },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 1 },
{ log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: false }
]);
@ -70,12 +53,15 @@ export default test({
// checked changed, last state is highlighted, first two are not
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$derived', highlighted: false },
{ log: 'double', highlighted: false },
{ log: 2 },
{ log: '$state', highlighted: false },
{ log: 'count', highlighted: false },
{ log: 1 },
{ log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: true }
]);
@ -87,10 +73,12 @@ export default test({
// count change and derived it's >=4, checked is not in the dependencies anymore
assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false },
{ log: 'effect' },
{ log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 4 },
{ log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 2 }
]);
}

Loading…
Cancel
Save