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. 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 ```js
let todos = $state([ let todos = $state([

@ -1,5 +1,25 @@
# svelte # 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 ## 5.33.18
### Patch Changes ### Patch Changes

@ -926,6 +926,17 @@ export interface HTMLButtonAttributes extends HTMLAttributes<HTMLButtonElement>
value?: string | string[] | number | undefined | null; value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null; popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | 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> { export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> {

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

@ -67,11 +67,20 @@ function build_assignment(operator, left, right, context) {
in_constructor: rune !== '$derived' && rune !== '$derived.by' in_constructor: rune !== '$derived' && rune !== '$derived.by'
}; };
return b.assignment( let value = /** @type {Expression} */ (context.visit(right, child_state));
operator,
b.member(b.this, field.key), if (dev) {
/** @type {Expression} */ (context.visit(right, child_state)) 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 { StateField } from '#compiler' */
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { dev } from '../../../../state.js';
import { get_parent } from '../../../../utils/ast.js';
import { get_name } from '../../../nodes.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 // Replace parts of the class body
for (const definition of node.body) { for (const definition of node.body) {
if (definition.type !== 'PropertyDefinition') { if (definition.type !== 'PropertyDefinition') {
@ -68,17 +74,26 @@ export function ClassBody(node, context) {
} }
if (name[0] === '#') { 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) { } 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 const should_proxy = field.type === '$state' && true; // TODO
body.push( body.push(
b.prop_def( b.prop_def(field.key, call),
field.key,
/** @type {CallExpression} */ (context.visit(field.value, child_state))
),
b.method('get', definition.key, [], [b.return(b.call('$.get', member))]), b.method('get', definition.key, [], [b.return(b.call('$.get', member))]),

@ -1,7 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @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'; import { build_component } from './shared/component.js';
/** /**
@ -9,24 +8,12 @@ import { build_component } from './shared/component.js';
* @param {ComponentContext} context * @param {ComponentContext} context
*/ */
export function Component(node, context) { export function Component(node, context) {
if (node.metadata.dynamic) { const component = build_component(
// Handle dynamic references to what seems like static inline components node,
const component = build_component(node, '$$component', context, b.id('$$anchor')); // if it's not dynamic we will just use the node name, if it is dynamic we will use the node name
context.state.init.push( // only if it's a valid identifier, otherwise we will use a default name
b.stmt( !node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component',
b.call( context
'$.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);
context.state.init.push(component); context.state.init.push(component);
} }

@ -90,6 +90,10 @@ export function VariableDeclaration(node, context) {
should_proxy(initial, context.state.scope) should_proxy(initial, context.state.scope)
) { ) {
initial = b.call('$.proxy', initial); initial = b.call('$.proxy', initial);
if (dev) {
initial = b.call('$.tag_proxy', initial, b.literal(id.name));
}
} }
if (is_prop_source(binding, context.state)) { if (is_prop_source(binding, context.state)) {
@ -128,12 +132,25 @@ export function VariableDeclaration(node, context) {
const binding = /** @type {import('#compiler').Binding} */ ( const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(id.name) 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); 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); value = b.call('$.state', value);
if (dev) {
value = b.call('$.tag', value, b.literal(id.name));
}
} }
return value; return value;
}; };
@ -154,7 +171,11 @@ export function VariableDeclaration(node, context) {
context.state.transform[id.name] = { read: get_value }; context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(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) => { ...paths.map((path) => {
const value = /** @type {Expression} */ (context.visit(path.expression)); const value = /** @type {Expression} */ (context.visit(path.expression));
@ -203,7 +224,10 @@ export function VariableDeclaration(node, context) {
let expression = /** @type {Expression} */ (context.visit(value)); let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression); 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 { } else {
const init = /** @type {CallExpression} */ (declarator.init); const init = /** @type {CallExpression} */ (declarator.init);
@ -216,8 +240,10 @@ export function VariableDeclaration(node, context) {
let expression = /** @type {Expression} */ (context.visit(value)); let expression = /** @type {Expression} */ (context.visit(value));
if (rune === '$derived') expression = b.thunk(expression); if (rune === '$derived') expression = b.thunk(expression);
const call = b.call('$.derived', expression);
declarations.push(b.declarator(id, 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); 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 }; context.state.transform[id.name] = { read: get_value };
const expression = /** @type {Expression} */ (context.visit(b.thunk(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) { for (const path of paths) {
const expression = /** @type {Expression} */ (context.visit(path.expression)); 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 {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
* @param {string} component_name * @param {string} component_name
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {Expression} anchor
* @returns {Statement} * @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>} */ /** @type {Array<Property[] | Expression>} */
const props_and_spreads = []; const props_and_spreads = [];
/** @type {Array<() => void>} */ /** @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 // 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 // will be handled separately through the `$.component` function, and then the component name will
// always be referenced through just the identifier here. // always be referenced through just the identifier here.
node.type === 'SvelteComponent' node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)
? component_name ? component_name
: /** @type {Expression} */ (context.visit(b.member_id(component_name))), : /** @type {Expression} */ (context.visit(b.member_id(component_name))),
node_id, 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; const prev = fn;
fn = (node_id) => { fn = (node_id) => {
return b.call( return b.call(
'$.component', '$.component',
node_id, 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.arrow(
[b.id('$$anchor'), b.id(component_name)], [b.id('$$anchor'), b.id(component_name)],
b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) 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 ASYNC_ERROR = 1;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const PROXY_PATH_SYMBOL = Symbol('proxy path');
export const LEGACY_PROPS = Symbol('legacy props'); export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');

@ -2,52 +2,42 @@
import { UNINITIALIZED } from '../../../constants.js'; import { UNINITIALIZED } from '../../../constants.js';
import { snapshot } from '../../shared/clone.js'; import { snapshot } from '../../shared/clone.js';
import { define_property } from '../../shared/utils.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 { effect_tracking } from '../reactivity/effects.js';
import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.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; export let tracing_expressions = null;
/** /**
* @param { Value } signal * @param {Value} signal
* @param { { read: Error[] } } [entry] * @param {TraceEntry} [entry]
*/ */
function log_entry(signal, entry) { function log_entry(signal, entry) {
const debug = signal.debug; const value = signal.v;
const value = signal.trace_need_increase ? signal.trace_v : signal.v;
if (value === UNINITIALIZED) { if (value === UNINITIALIZED) {
return; 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 type = (signal.f & DERIVED) !== 0 ? '$derived' : '$state';
const current_reaction = /** @type {Reaction} */ (active_reaction); const current_reaction = /** @type {Reaction} */ (active_reaction);
const dirty = signal.wv > current_reaction.wv || current_reaction.wv === 0; 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 // eslint-disable-next-line no-console
console.groupCollapsed( console.groupCollapsed(
`%c${type}`, signal.label ? `%c${type}%c ${signal.label}` : `%c${type}%c`,
dirty ? 'color: CornflowerBlue; font-weight: bold' : 'color: grey; font-weight: bold', style,
dirty ? 'font-weight: normal' : style,
typeof value === 'object' && value !== null && STATE_SYMBOL in value typeof value === 'object' && value !== null && STATE_SYMBOL in value
? snapshot(value, true) ? snapshot(value, true)
: value : value
@ -65,17 +55,15 @@ function log_entry(signal, entry) {
console.log(signal.created); console.log(signal.created);
} }
if (signal.updated) { if (dirty && signal.updated) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(signal.updated); console.log(signal.updated);
} }
const read = entry?.read; if (entry) {
for (var trace of entry.traces) {
if (read && read.length > 0) {
for (var stack of read) {
// eslint-disable-next-line no-console // 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) { export function trace(label, fn) {
var previously_tracing_expressions = tracing_expressions; var previously_tracing_expressions = tracing_expressions;
try { try {
tracing_expressions = { entries: new Map(), reaction: active_reaction }; tracing_expressions = { entries: new Map(), reaction: active_reaction };
@ -97,39 +86,32 @@ export function trace(label, fn) {
var value = fn(); var value = fn();
var time = (performance.now() - start).toFixed(2); var time = (performance.now() - start).toFixed(2);
var prefix = untrack(label);
if (!effect_tracking()) { if (!effect_tracking()) {
// eslint-disable-next-line no-console // 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) { } else if (tracing_expressions.entries.size === 0) {
// eslint-disable-next-line no-console // 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 { } else {
// eslint-disable-next-line no-console // 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; var entries = tracing_expressions.entries;
untrack(() => {
for (const [signal, traces] of entries) {
log_entry(signal, traces);
}
});
tracing_expressions = null; tracing_expressions = null;
for (const [signal, entry] of entries) {
log_entry(signal, entry);
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.groupEnd(); 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; return value;
} finally { } finally {
tracing_expressions = previously_tracing_expressions; tracing_expressions = previously_tracing_expressions;
@ -177,3 +159,34 @@ export function get_stack(label) {
} }
return error; 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) { if (DEV && reactive) {
// For tracing purposes, we need to link the source signal we create with the // For tracing purposes, we need to link the source signal we create with the
// collection + index so that tracing works as intended // 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; var collection_index = typeof i === 'number' ? index : i.v;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
get_collection()[collection_index]; get_collection()[collection_index];

@ -7,7 +7,7 @@ export { add_locations } from './dev/elements.js';
export { hmr } from './dev/hmr.js'; export { hmr } from './dev/hmr.js';
export { create_ownership_validator } from './dev/ownership.js'; export { create_ownership_validator } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.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 { inspect } from './dev/inspect.js';
export { async } from './dom/blocks/async.js'; export { async } from './dom/blocks/async.js';
export { validate_snippet_args } from './dev/validation.js'; export { validate_snippet_args } from './dev/validation.js';

@ -9,12 +9,15 @@ import {
object_prototype object_prototype
} from '../shared/utils.js'; } from '../shared/utils.js';
import { state as source, set } from './reactivity/sources.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 { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.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'; 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 * @template T
* @param {T} value * @param {T} value
@ -61,6 +64,21 @@ export function proxy(value) {
sources.set('length', source(/** @type {any[]} */ (value).length, stack)); 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), { return new Proxy(/** @type {any} */ (value), {
defineProperty(_, prop, descriptor) { defineProperty(_, prop, descriptor) {
if ( if (
@ -76,17 +94,20 @@ export function proxy(value) {
e.state_descriptors_fixed(); e.state_descriptors_fixed();
} }
var s = sources.get(prop); with_parent(() => {
var s = sources.get(prop);
if (s === undefined) { if (s === undefined) {
s = with_parent(() => source(descriptor.value, stack)); s = source(descriptor.value, stack);
sources.set(prop, s); sources.set(prop, s);
} else {
set( if (DEV && typeof prop === 'string') {
s, tag(s, get_label(path, prop));
with_parent(() => proxy(descriptor.value)) }
); } else {
} set(s, descriptor.value, true);
}
});
return true; return true;
}, },
@ -96,11 +117,13 @@ export function proxy(value) {
if (s === undefined) { if (s === undefined) {
if (prop in target) { if (prop in target) {
sources.set( const s = with_parent(() => source(UNINITIALIZED, stack));
prop, sources.set(prop, s);
with_parent(() => source(UNINITIALIZED, stack))
);
update_version(version); update_version(version);
if (DEV) {
tag(s, get_label(path, prop));
}
} }
} else { } else {
// When working with arrays, we need to also ensure we update the length when removing // 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; return value;
} }
if (DEV && prop === PROXY_PATH_SYMBOL) {
return update_path;
}
var s = sources.get(prop); var s = sources.get(prop);
var exists = prop in target; var exists = prop in target;
// create a source, but only if it's an own property and not a prototype property // 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)) { 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); sources.set(prop, s);
} }
@ -178,7 +215,17 @@ export function proxy(value) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { 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); sources.set(prop, s);
} }
@ -207,6 +254,10 @@ export function proxy(value) {
// the value of the original item at that index. // the value of the original item at that index.
other_s = with_parent(() => source(UNINITIALIZED, stack)); other_s = with_parent(() => source(UNINITIALIZED, stack));
sources.set(i + '', other_s); 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. // object property before writing to that property.
if (s === undefined) { if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) { if (!has || get_descriptor(target, prop)?.writable) {
s = with_parent(() => source(undefined, stack)); s = with_parent(() => {
set( var s = source(undefined, stack);
s, set(s, proxy(value));
with_parent(() => proxy(value)) return s;
); });
sources.set(prop, s); sources.set(prop, s);
if (DEV) {
tag(s, get_label(path, prop));
}
} }
} else { } else {
has = s.v !== UNINITIALIZED; has = s.v !== UNINITIALIZED;
set(
s, var p = with_parent(() => proxy(value));
with_parent(() => proxy(value)) set(s, p);
);
} }
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); 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 {Source<number>} signal
* @param {1 | -1} [d] * @param {1 | -1} [d]

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

@ -17,12 +17,21 @@ export interface Value<V = unknown> extends Signal {
rv: number; rv: number;
/** The latest value for this signal */ /** The latest value for this signal */
v: V; 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; created?: Error | null;
/** An error with a stack trace showing when the source was last updated */
updated?: Error | null; updated?: Error | null;
trace_need_increase?: boolean; /**
trace_v?: V; * Whether or not the source was set while running an effect if so, we need to
debug?: null | (() => void); * 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 { export interface Reaction extends Signal {

@ -57,6 +57,7 @@ import {
import * as w from './warnings.js'; import * as w from './warnings.js';
import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js';
import { handle_error, invoke_error_boundary } from './error-handling.js'; import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
/** @type {Effect | null} */ /** @type {Effect | null} */
let last_scheduled_effect = null; let last_scheduled_effect = null;
@ -480,19 +481,13 @@ export function update_effect(effect) {
effect.teardown = typeof teardown === 'function' ? teardown : null; effect.teardown = typeof teardown === 'function' ? teardown : null;
effect.wv = write_version; effect.wv = write_version;
var deps = effect.deps; // 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
// In DEV, we need to handle a case where $inspect.trace() might if (DEV && tracing_mode_flag && (effect.f & DIRTY) !== 0 && effect.deps !== null) {
// incorrectly state a source dependency has not changed when it has. for (var dep of effect.deps) {
// That's beacuse that source was changed by the same effect, causing if (dep.set_during_effect) {
// 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) {
dep.wv = increment_write_version(); dep.wv = increment_write_version();
dep.trace_need_increase = undefined; dep.set_during_effect = false;
dep.trace_v = undefined;
} }
} }
} }
@ -852,28 +847,39 @@ export function get(signal) {
} }
} }
recent_async_deriveds.delete(signal);
if ( if (
tracing_mode_flag && tracing_mode_flag &&
!untracking &&
tracing_expressions !== null && tracing_expressions !== null &&
active_reaction !== null && active_reaction !== null &&
tracing_expressions.reaction === active_reaction tracing_expressions.reaction === active_reaction
) { ) {
// Used when mapping state between special blocks like `each` // Used when mapping state between special blocks like `each`
if (signal.debug) { if (signal.trace) {
signal.debug(); signal.trace();
} else if (signal.created) { } else {
var entry = tracing_expressions.entries.get(signal); var trace = get_stack('TracedAt');
if (entry === undefined) { if (trace) {
entry = { read: [] }; var entry = tracing_expressions.entries.get(signal);
tracing_expressions.entries.set(signal, entry);
}
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)) { 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 { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.js'; import { set, source } from '../internal/client/reactivity/sources.js';
import { render_effect } from '../internal/client/reactivity/effects.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 { get } from '../internal/client/runtime.js';
import { deferred, noop } from '../internal/shared/utils.js'; import { deferred, noop } from '../internal/shared/utils.js';
import { DEV } from 'esm-env';
/** /**
* @template T * @template T
@ -172,8 +174,8 @@ export class Spring {
#damping = source(0.8); #damping = source(0.8);
#precision = source(0.01); #precision = source(0.01);
#current = source(/** @type {T} */ (undefined)); #current;
#target = source(/** @type {T} */ (undefined)); #target;
#last_value = /** @type {T} */ (undefined); #last_value = /** @type {T} */ (undefined);
#last_time = 0; #last_time = 0;
@ -192,11 +194,20 @@ export class Spring {
* @param {SpringOpts} [options] * @param {SpringOpts} [options]
*/ */
constructor(value, 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.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.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1);
if (typeof options.precision === 'number') this.#precision.v = options.precision; 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 { linear } from '../easing/index.js';
import { is_date } from './utils.js'; import { is_date } from './utils.js';
import { set, source } from '../internal/client/reactivity/sources.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 { get, render_effect } from 'svelte/internal/client';
import { DEV } from 'esm-env';
/** /**
* @template T * @template T
@ -175,8 +177,8 @@ export function tweened(value, defaults = {}) {
* @since 5.8.0 * @since 5.8.0
*/ */
export class Tween { export class Tween {
#current = source(/** @type {T} */ (undefined)); #current;
#target = source(/** @type {T} */ (undefined)); #target;
/** @type {TweenedOptions<T>} */ /** @type {TweenedOptions<T>} */
#defaults; #defaults;
@ -189,8 +191,14 @@ export class Tween {
* @param {TweenedOptions<T>} options * @param {TweenedOptions<T>} options
*/ */
constructor(value, options = {}) { constructor(value, options = {}) {
this.#current.v = this.#target.v = value; this.#current = source(value);
this.#target = source(value);
this.#defaults = options; 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 { get, tick, untrack } from '../internal/client/runtime.js';
import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js';
import { source } from '../internal/client/reactivity/sources.js'; import { source } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
import { increment } from './utils.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), * 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} */ /** @type {(() => void) | void} */
let stop; let stop;
if (DEV) {
tag(version, 'createSubscriber version');
}
return () => { return () => {
if (effect_tracking()) { if (effect_tracking()) {
get(version); get(version);

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

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

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

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

@ -1,4 +1,6 @@
import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; 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 { get } from '../internal/client/runtime.js';
import { REPLACE, SvelteURLSearchParams } from './url-search-params.js'; import { REPLACE, SvelteURLSearchParams } from './url-search-params.js';
@ -56,6 +58,17 @@ export class SvelteURL extends URL {
url = new URL(url, base); url = new URL(url, base);
super(url); 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; current_url = this;
this.#searchParams = new SvelteURLSearchParams(url.searchParams); this.#searchParams = new SvelteURLSearchParams(url.searchParams);
current_url = null; 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 { on } from '../../events/index.js';
import { ReactiveValue } from '../reactive-value.js'; import { ReactiveValue } from '../reactive-value.js';
import { get } from '../../internal/client/index.js'; import { get } from '../../internal/client/index.js';
import { set, source } from '../../internal/client/reactivity/sources.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`. * `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) { if (BROWSER) {
this.#update(); this.#update();
} }
if (DEV) {
tag(this.#dpr, 'window.devicePixelRatio');
}
} }
get current() { get current() {

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

@ -193,3 +193,44 @@ if (typeof window !== 'undefined') {
} }
export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html'; 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 { globSync } from 'tinyglobby';
import { createClassComponent } from 'svelte/legacy'; import { createClassComponent } from 'svelte/legacy';
import { proxy } from 'svelte/internal/client'; 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 { render } from 'svelte/server';
import { afterAll, assert, beforeAll } from 'vitest'; import { afterAll, assert, beforeAll } from 'vitest';
import { compile_directory, fragments } from '../helpers.js'; 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 { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
/**
* @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;
}
export default test({ export default test({
compileOptions: { compileOptions: {
@ -34,10 +11,11 @@ export default test({
// initial log, everything is highlighted // initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'iife', highlighted: false }, { log: 'iife' },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 0 }, { log: 0 },
{ log: 'effect', highlighted: false } { log: 'effect' }
]); ]);
logs.length = 0; logs.length = 0;
@ -47,10 +25,11 @@ export default test({
flushSync(); flushSync();
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'iife', highlighted: false }, { log: 'iife' },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 1 }, { log: 1 },
{ log: 'effect', highlighted: false } { log: 'effect' }
]); ]);
} }
}); });

@ -1,29 +1,6 @@
import { flushSync } from 'svelte'; import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
/**
* @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;
}
export default test({ export default test({
compileOptions: { compileOptions: {
@ -34,8 +11,9 @@ export default test({
// initial log, everything is highlighted // initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: false } { log: false }
]); ]);
@ -52,20 +30,26 @@ export default test({
// checked changed, effect reassign state, values should be correct and be correctly highlighted // checked changed, effect reassign state, values should be correct and be correctly highlighted
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: true }, { log: true },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 1 }, { log: 'count', highlighted: false },
{ log: 'effect', highlighted: false }, { log: 2 },
{ log: 'effect' },
{ log: '$state', highlighted: false }, { log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: true }, { log: true },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 2 }, { log: 'count', highlighted: false },
{ log: 'effect', highlighted: false }, { log: 3 },
{ log: 'effect' },
{ log: '$state', highlighted: false }, { log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: true }, { log: true },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 3 } { log: 3 }
]); ]);
} }

@ -1,29 +1,6 @@
import { flushSync } from 'svelte'; import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
import { normalise_trace_logs } from '../../../helpers.js';
/**
* @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;
}
export default test({ export default test({
compileOptions: { compileOptions: {
@ -34,12 +11,15 @@ export default test({
// initial log, everything is highlighted // initial log, everything is highlighted
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$derived', highlighted: true }, { log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 0 }, { log: 0 },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 0 }, { log: 0 },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: false } { log: false }
]); ]);
@ -52,12 +32,15 @@ export default test({
// count changed, derived and state are highlighted, last state is not // count changed, derived and state are highlighted, last state is not
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$derived', highlighted: true }, { log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 2 }, { log: 2 },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 1 }, { log: 1 },
{ log: '$state', highlighted: false }, { log: '$state', highlighted: false },
{ log: 'checked', highlighted: false },
{ log: false } { log: false }
]); ]);
@ -70,12 +53,15 @@ export default test({
// checked changed, last state is highlighted, first two are not // checked changed, last state is highlighted, first two are not
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$derived', highlighted: false }, { log: '$derived', highlighted: false },
{ log: 'double', highlighted: false },
{ log: 2 }, { log: 2 },
{ log: '$state', highlighted: false }, { log: '$state', highlighted: false },
{ log: 'count', highlighted: false },
{ log: 1 }, { log: 1 },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'checked', highlighted: false },
{ log: true } { log: true }
]); ]);
@ -87,10 +73,12 @@ export default test({
// count change and derived it's >=4, checked is not in the dependencies anymore // count change and derived it's >=4, checked is not in the dependencies anymore
assert.deepEqual(normalise_trace_logs(logs), [ assert.deepEqual(normalise_trace_logs(logs), [
{ log: 'effect', highlighted: false }, { log: 'effect' },
{ log: '$derived', highlighted: true }, { log: '$derived', highlighted: true },
{ log: 'double', highlighted: false },
{ log: 4 }, { log: 4 },
{ log: '$state', highlighted: true }, { log: '$state', highlighted: true },
{ log: 'count', highlighted: false },
{ log: 2 } { log: 2 }
]); ]);
} }

Loading…
Cancel
Save