pull/15844/head
Rich Harris 3 months ago
commit a5288ea7aa

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add parent hierarchy to `__svelte_meta` objects

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: add `getAbortSignal()`

@ -1,5 +1,15 @@
# svelte # svelte
## 5.35.0
### Minor Changes
- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266))
### Patch Changes
- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270))
## 5.34.9 ## 5.34.9
### Patch Changes ### Patch Changes

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

@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.AwaitBlock} node * @param {AST.AwaitBlock} node
@ -54,7 +54,7 @@ export function AwaitBlock(node, context) {
} }
context.state.init.push( context.state.init.push(
b.stmt( add_svelte_meta(
b.call( b.call(
'$.await', '$.await',
context.state.node, context.state.node,
@ -64,7 +64,9 @@ export function AwaitBlock(node, context) {
: b.null, : b.null,
then_block, then_block,
catch_block catch_block
) ),
node,
'await'
) )
); );
} }

@ -13,7 +13,7 @@ import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js'; import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.EachBlock} node * @param {AST.EachBlock} node
@ -335,7 +335,7 @@ export function EachBlock(node, context) {
} }
if (has_await) { if (has_await) {
const statements = [b.stmt(b.call('$.each', ...args))]; const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')];
if (dev && node.metadata.keyed) { if (dev && node.metadata.keyed) {
statements.unshift( statements.unshift(
b.stmt( b.stmt(
@ -363,7 +363,7 @@ export function EachBlock(node, context) {
b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function))
); );
} }
context.state.init.push(b.stmt(b.call('$.each', ...args))); context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
} }
} }

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.IfBlock} node * @param {AST.IfBlock} node
@ -69,7 +69,7 @@ export function IfBlock(node, context) {
args.push(b.true); args.push(b.true);
} }
statements.push(b.stmt(b.call('$.if', ...args))); statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
if (has_await) { if (has_await) {
context.state.init.push( context.state.init.push(

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.KeyBlock} node * @param {AST.KeyBlock} node
@ -17,16 +17,22 @@ export function KeyBlock(node, context) {
const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression); const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression);
const body = /** @type {Expression} */ (context.visit(node.fragment)); const body = /** @type {Expression} */ (context.visit(node.fragment));
let call = b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)); let statement = add_svelte_meta(
b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)),
node,
'key'
);
if (has_await) { if (has_await) {
call = b.call( statement = b.stmt(
'$.async', b.call(
context.state.node, '$.async',
b.array([b.thunk(expression, true)]), context.state.node,
b.arrow([context.state.node, b.id('$$key')], b.block([b.stmt(call)])) b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$key')], b.block([statement]))
)
); );
} }
context.state.init.push(b.stmt(call)); context.state.init.push(statement);
} }

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js'; import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression, Memoizer } from './shared/utils.js'; import { add_svelte_meta, build_expression, Memoizer } from './shared/utils.js';
/** /**
* @param {AST.RenderTag} node * @param {AST.RenderTag} node
@ -50,16 +50,22 @@ export function RenderTag(node, context) {
} }
statements.push( statements.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
); );
} else { } else {
statements.push( statements.push(
b.stmt( add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
context.state.node, context.state.node,
...args ...args
) ),
node,
'render'
) )
); );
} }

@ -4,7 +4,7 @@
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_bind_this, Memoizer, validate_binding } from '../shared/utils.js'; import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js'; import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js'; import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js'; import { determine_slot } from '../../../../../utils/slot.js';
@ -490,7 +490,8 @@ export function build_component(node, component_name, context) {
); );
} else { } else {
context.state.template.push_comment(); context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
} }
memoizer.apply(); memoizer.apply();

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ /** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -7,7 +7,7 @@ import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_is_valid_identifier } from '../../../../patterns.js'; import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter } from '../../utils.js'; import { build_getter } from '../../utils.js';
export class Memoizer { export class Memoizer {
@ -427,3 +427,34 @@ export function build_expression(context, expression, metadata, state = context.
return sequence; return sequence;
} }
/**
* Wraps a statement/expression with dev stack tracking in dev mode
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {{ start?: number }} node - AST node for location info
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
*/
export function add_svelte_meta(expression, node, type, additional) {
if (!dev) {
return b.stmt(expression);
}
const location = node.start !== undefined && locator(node.start);
if (!location) {
return b.stmt(expression);
}
return b.stmt(
b.call(
'$.add_svelte_meta',
b.arrow([], expression),
b.literal(type),
b.id(component_name),
b.literal(location.line),
b.literal(location.column),
additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v))))
)
);
}

@ -16,6 +16,9 @@ export let warnings = [];
*/ */
export let filename; export let filename;
/**
* The name of the component that is used in the `export default function ...` statement.
*/
export let component_name = '<unknown>'; export let component_name = '<unknown>';
/** /**

@ -1,4 +1,4 @@
/** @import { ComponentContext } from '#client' */ /** @import { ComponentContext, DevStackEntry } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { lifecycle_outside_component } from '../shared/errors.js'; import { lifecycle_outside_component } from '../shared/errors.js';
@ -12,6 +12,7 @@ import {
} from './runtime.js'; } from './runtime.js';
import { effect, teardown } from './reactivity/effects.js'; import { effect, teardown } from './reactivity/effects.js';
import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
/** @type {ComponentContext | null} */ /** @type {ComponentContext | null} */
export let component_context = null; export let component_context = null;
@ -21,6 +22,43 @@ export function set_component_context(context) {
component_context = context; component_context = context;
} }
/** @type {DevStackEntry | null} */
export let dev_stack = null;
/** @param {DevStackEntry | null} stack */
export function set_dev_stack(stack) {
dev_stack = stack;
}
/**
* Execute a callback with a new dev stack entry
* @param {() => any} callback - Function to execute
* @param {DevStackEntry['type']} type - Type of block/component
* @param {any} component - Component function
* @param {number} line - Line number
* @param {number} column - Column number
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
* @returns {any}
*/
export function add_svelte_meta(callback, type, component, line, column, additional) {
const parent = dev_stack;
dev_stack = {
type,
file: component[FILENAME],
line,
column,
parent,
...additional
};
try {
return callback();
} finally {
dev_stack = parent;
}
}
/** /**
* The current component function. Different from current component context: * The current component function. Different from current component context:
* ```html * ```html

@ -2,6 +2,7 @@
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js'; import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
/** /**
* @param {any} fn * @param {any} fn
@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) { function assign_location(element, filename, location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] } loc: { file: filename, line: location[0], column: location[1] }
}; };

@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { import {
component_context, component_context,
dev_stack,
is_runes, is_runes,
set_component_context, set_component_context,
set_dev_current_component_function set_dev_current_component_function,
set_dev_stack
} from '../../context.js'; } from '../../context.js';
const PENDING = 0; const PENDING = 0;
@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */ /** @type {any} */
var component_function = DEV ? component_context?.function : null; var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */ /** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED; var input = UNINITIALIZED;
@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_active_effect(effect); set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both? set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context); set_component_context(active_component_context);
if (DEV) set_dev_current_component_function(component_function); if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
} }
try { try {
@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
} }
} finally { } finally {
if (restore) { if (restore) {
if (DEV) set_dev_current_component_function(null); if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
set_component_context(null); set_component_context(null);
set_active_reaction(null); set_active_reaction(null);
set_active_effect(null); set_active_effect(null);

@ -18,7 +18,7 @@ import {
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js'; import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js'; import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js'; import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (DEV && location) { if (DEV && location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { element.__svelte_meta = {
parent: dev_stack,
loc: { loc: {
file: filename, file: filename,
line: location[0], line: location[0],

@ -1,6 +1,6 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js'; export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js'; export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js'; export { add_locations } from './dev/elements.js';

@ -38,7 +38,7 @@ import * as e from '../errors.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js'; import { get_next_sibling } from '../dom/operations.js';
import { component_context, dev_current_component_function } from '../context.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js';
import { Batch } from './batch.js'; import { Batch } from './batch.js';
import { flatten } from './async.js'; import { flatten } from './async.js';
@ -348,7 +348,11 @@ export function template_effect(fn, sync = [], async = []) {
* @param {number} flags * @param {number} flags
*/ */
export function block(fn, flags = 0) { export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
} }
/** /**

@ -8,12 +8,11 @@ import {
PROPS_IS_UPDATED PROPS_IS_UPDATED
} from '../../../constants.js'; } from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js'; import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js'; import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js'; import { derived, derived_safe_equal } from './deriveds.js';
import { get, captured_signals, untrack } from '../runtime.js'; import { get, untrack } from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js'; import * as e from '../errors.js';
import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js'; import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js'; import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js'; import { legacy_mode_flag } from '../../flags/index.js';
@ -260,89 +259,92 @@ function has_destroyed_component_ctx(current_value) {
* @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))}
*/ */
export function prop(props, key, flags, fallback) { export function prop(props, key, flags, fallback) {
var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0; var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0;
var bindable = (flags & PROPS_IS_BINDABLE) !== 0; var bindable = (flags & PROPS_IS_BINDABLE) !== 0;
var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0; var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0;
var is_store_sub = false;
var prop_value;
if (bindable) {
[prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
prop_value = /** @type {V} */ (props[key]);
}
// Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
var setter =
(bindable &&
(get_descriptor(props, key)?.set ??
(is_entry_props && key in props && ((v) => (props[key] = v))))) ||
undefined;
var fallback_value = /** @type {V} */ (fallback); var fallback_value = /** @type {V} */ (fallback);
var fallback_dirty = true; var fallback_dirty = true;
var fallback_used = false;
var get_fallback = () => { var get_fallback = () => {
fallback_used = true;
if (fallback_dirty) { if (fallback_dirty) {
fallback_dirty = false; fallback_dirty = false;
if (lazy) {
fallback_value = untrack(/** @type {() => V} */ (fallback)); fallback_value = lazy
} else { ? untrack(/** @type {() => V} */ (fallback))
fallback_value = /** @type {V} */ (fallback); : /** @type {V} */ (fallback);
}
} }
return fallback_value; return fallback_value;
}; };
if (prop_value === undefined && fallback !== undefined) { /** @type {((v: V) => void) | undefined} */
if (setter && runes) { var setter;
e.props_invalid_value(key);
}
prop_value = get_fallback(); if (bindable) {
if (setter) setter(prop_value); // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})`
// or `createClassComponent(Component, props)`
var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props;
setter =
get_descriptor(props, key)?.set ??
(is_entry_props && key in props ? (v) => (props[key] = v) : undefined);
}
var initial_value;
var is_store_sub = false;
if (bindable) {
[initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key]));
} else {
initial_value = /** @type {V} */ (props[key]);
}
if (initial_value === undefined && fallback !== undefined) {
initial_value = get_fallback();
if (setter) {
if (runes) e.props_invalid_value(key);
setter(initial_value);
}
} }
/** @type {() => V} */ /** @type {() => V} */
var getter; var getter;
if (runes) { if (runes) {
getter = () => { getter = () => {
var value = /** @type {V} */ (props[key]); var value = /** @type {V} */ (props[key]);
if (value === undefined) return get_fallback(); if (value === undefined) return get_fallback();
fallback_dirty = true; fallback_dirty = true;
fallback_used = false;
return value; return value;
}; };
} else { } else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => { getter = () => {
var value = get(derived_getter); var value = /** @type {V} */ (props[key]);
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
if (value !== undefined) {
// in legacy mode, we don't revert to the fallback value
// if the prop goes from defined to undefined. The easiest
// way to model this is to make the fallback undefined
// as soon as the prop has a value
fallback_value = /** @type {V} */ (undefined);
}
return value === undefined ? fallback_value : value; return value === undefined ? fallback_value : value;
}; };
} }
// easy mode — prop is never written to // prop is never written to — we only need a getter
if ((flags & PROPS_IS_UPDATED) === 0 && runes) { if (runes && (flags & PROPS_IS_UPDATED) === 0) {
return getter; return getter;
} }
// intermediate mode — prop is written to, but the parent component had // prop is written to, but the parent component had `bind:foo` which
// `bind:foo` which means we can just call `$$props.foo = value` directly // means we can just call `$$props.foo = value` directly
if (setter) { if (setter) {
var legacy_parent = props.$$legacy; var legacy_parent = props.$$legacy;
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) { if (arguments.length > 0) {
// We don't want to notify if the value was mutated and the parent is in runes mode. // We don't want to notify if the value was mutated and the parent is in runes mode.
@ -352,82 +354,41 @@ export function prop(props, key, flags, fallback) {
if (!runes || !mutation || legacy_parent || is_store_sub) { if (!runes || !mutation || legacy_parent || is_store_sub) {
/** @type {Function} */ (setter)(mutation ? getter() : value); /** @type {Function} */ (setter)(mutation ? getter() : value);
} }
return value; return value;
} else {
return getter();
} }
return getter();
}; };
} }
// hard mode. this is where it gets ugly — the value in the child should // Either prop is written to, but there's no binding, which means we
// synchronize with the parent, but it should also be possible to temporarily // create a derived that we can write to locally.
// set the value to something else locally. // Or we are in legacy mode where we always create a derived to replicate that
var from_child = false; // Svelte 4 did not trigger updates when a primitive value was updated to the same value.
var was_from_child = false; var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
var current_value = derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
was_from_child = false;
return (inner_current_value.v = parent_value);
});
// Ensure we eagerly capture the initial value if it's bindable
if (bindable) {
get(current_value);
}
if (!immutable) current_value.equals = safe_equals; // Capture the initial value if it's bindable
if (bindable) get(d);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
// legacy nonsense — need to ensure the source is invalidated when necessary
// also needed for when handling inspect logic so we can inspect the correct source signal
if (captured_signals !== null) {
// set this so that we don't reset to the parent value if `d`
// is invalidated because of `invalidate_inner_signals` (rather
// than because the parent or child value changed)
from_child = was_from_child;
// invoke getters so that signals are picked up by `invalidate_inner_signals`
getter();
get(inner_current_value);
}
if (arguments.length > 0) { if (arguments.length > 0) {
const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
if (!current_value.equals(new_value)) {
from_child = true;
set(inner_current_value, new_value);
// To ensure the fallback value is consistent when used with proxies, we
// update the local fallback_value, but only if the fallback is actively used
if (fallback_used && fallback_value !== undefined) {
fallback_value = new_value;
}
if (has_destroyed_component_ctx(current_value)) { set(d, new_value);
return value;
}
untrack(() => get(current_value)); // force a synchronisation immediately if (fallback_value !== undefined) {
fallback_value = new_value;
} }
return value; return value;
} }
if (has_destroyed_component_ctx(current_value)) { // TODO is this still necessary post-#16263?
return current_value.v; if (has_destroyed_component_ctx(d)) {
return d.v;
} }
return get(current_value); return get(d);
}; };
} }

@ -1,4 +1,10 @@
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; import type {
ComponentContext,
DevStackEntry,
Equals,
TemplateNode,
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary'; import type { Boundary } from '../dom/blocks/boundary';
export interface Signal { export interface Signal {
@ -83,6 +89,8 @@ export interface Effect extends Reaction {
b: Boundary | null; b: Boundary | null;
/** Dev only */ /** Dev only */
component_function?: any; component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
dev_stack?: DevStackEntry | null;
} }
export type Source<V = unknown> = Value<V>; export type Source<V = unknown> = Value<V>;

@ -20,7 +20,6 @@ import {
STATE_SYMBOL, STATE_SYMBOL,
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP,
DISCONNECTED, DISCONNECTED,
REACTION_IS_UPDATING, REACTION_IS_UPDATING,
EFFECT_IS_UPDATING, EFFECT_IS_UPDATING,
@ -44,9 +43,11 @@ import { tracing_expressions, get_stack } from './dev/tracing.js';
import { import {
component_context, component_context,
dev_current_component_function, dev_current_component_function,
dev_stack,
is_runes, is_runes,
set_component_context, set_component_context,
set_dev_current_component_function set_dev_current_component_function,
set_dev_stack
} from './context.js'; } from './context.js';
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';
@ -478,6 +479,9 @@ export function update_effect(effect) {
if (DEV) { if (DEV) {
var previous_component_fn = dev_current_component_function; var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function); set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
} }
try { try {
@ -512,6 +516,7 @@ export function update_effect(effect) {
if (DEV) { if (DEV) {
set_dev_current_component_function(previous_component_fn); set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
} }
} }
} }
@ -978,17 +983,7 @@ export function invalidate_inner_signals(fn) {
var captured = capture_signals(() => untrack(fn)); var captured = capture_signals(() => untrack(fn));
for (var signal of captured) { for (var signal of captured) {
// Go one level up because derived signals created as part of props in legacy mode internal_set(signal, signal.v);
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
for (const dep of /** @type {Derived} */ (signal).deps || []) {
if ((dep.f & DERIVED) === 0) {
// Use internal_set instead of set here and below to avoid mutation validation
internal_set(dep, dep.v);
}
}
} else {
internal_set(signal, signal.v);
}
} }
} }

@ -185,4 +185,13 @@ export type SourceLocation =
| [line: number, column: number] | [line: number, column: number]
| [line: number, column: number, SourceLocation[]]; | [line: number, column: number, SourceLocation[]];
export interface DevStackEntry {
file: string;
type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
line: number;
column: number;
parent: DevStackEntry | null;
componentTag?: string;
}
export * from './reactivity/types'; export * from './reactivity/types';

@ -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.34.9'; export const VERSION = '5.35.0';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -2,6 +2,7 @@ import { flushSync } from 'svelte';
import { test } from '../../test'; import { test } from '../../test';
export default test({ export default test({
accessors: false,
test({ assert, logs, target }) { test({ assert, logs, target }) {
assert.deepEqual(logs, ['primitive', 'object']); assert.deepEqual(logs, ['primitive', 'object']);
target.querySelector('button')?.click(); target.querySelector('button')?.click();

@ -0,0 +1,186 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
compileOptions: {
dev: true
},
html: `
<p>no parent</p>
<button>toggle</button>
<p>if</p>
<p>each</p>
<p>loading</p>
<p>key</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
`,
async test({ target, assert }) {
function check() {
const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] =
target.querySelectorAll('p');
// @ts-expect-error
assert.deepEqual(main.__svelte_meta.parent, null);
// @ts-expect-error
assert.deepEqual(if_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'if',
line: 12,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(each.__svelte_meta.parent, {
file: 'main.svelte',
type: 'each',
line: 16,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(await_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'await',
line: 20,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(key.__svelte_meta.parent, {
file: 'main.svelte',
type: 'key',
line: 26,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child1.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 30,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child2.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 33,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 32,
column: 0,
parent: null
}
}
});
// @ts-expect-error
assert.deepEqual(child3.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 38,
column: 2,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 37,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 36,
column: 0,
parent: null
}
}
}
}
});
// @ts-expect-error
assert.deepEqual(child4.__svelte_meta.parent, {
file: 'passthrough.svelte',
type: 'render',
line: 8,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'if',
line: 7,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 43,
column: 1,
parent: {
file: 'main.svelte',
type: 'if',
line: 42,
column: 0,
parent: null
}
}
}
});
// @ts-expect-error
assert.deepEqual(dynamic.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'x.y',
line: 50,
column: 0,
parent: null
});
}
await tick();
check();
// Test that stack is kept when re-rendering
const button = target.querySelector('button');
button?.click();
await tick();
button?.click();
await tick();
check();
}
});

@ -0,0 +1,50 @@
<script>
import Child from "./child.svelte";
import Passthrough from "./passthrough.svelte";
let x = { y: Child }
let key = 'test';
let show = $state(true);
</script>
<p>no parent</p>
<button onclick={() => show = !show}>toggle</button>
{#if true}
<p>if</p>
{/if}
{#each [1]}
<p>each</p>
{/each}
{#await Promise.resolve()}
<p>loading</p>
{:then}
<p>await</p>
{/await}
{#key key}
<p>key</p>
{/key}
<Child />
<Passthrough>
<Child />
</Passthrough>
<Passthrough>
<Passthrough>
<Child />
</Passthrough>
</Passthrough>
{#if show}
<Passthrough>
{#snippet named()}
<p>hi</p>
{/snippet}
</Passthrough>
{/if}
<x.y />

@ -0,0 +1,9 @@
<script>
let { children, named } = $props();
</script>
{@render children?.()}
{#if true}
{@render named?.()}
{/if}
Loading…
Cancel
Save