diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md new file mode 100644 index 0000000000..dd5a28fca8 --- /dev/null +++ b/.changeset/hot-buses-end.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add parent hierarchy to `__svelte_meta` objects diff --git a/.changeset/short-fireants-flow.md b/.changeset/short-fireants-flow.md deleted file mode 100644 index b9955ff577..0000000000 --- a/.changeset/short-fireants-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `getAbortSignal()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ef64091ca3..7589eda728 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # 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 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7d4f7e241a..3c319ca1ea 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.34.9", + "version": "5.35.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 7873cf3ddb..c550c8e17b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_expression } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -54,7 +54,7 @@ export function AwaitBlock(node, context) { } context.state.init.push( - b.stmt( + add_svelte_meta( b.call( '$.await', context.state.node, @@ -64,7 +64,9 @@ export function AwaitBlock(node, context) { : b.null, then_block, catch_block - ) + ), + node, + 'await' ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index d61d9f6ede..789465ac16 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -13,7 +13,7 @@ import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; 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 @@ -335,7 +335,7 @@ export function EachBlock(node, context) { } 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) { statements.unshift( b.stmt( @@ -363,7 +363,7 @@ export function EachBlock(node, context) { 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')); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index f31369a555..3dd36b180b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ 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 @@ -69,7 +69,7 @@ export function IfBlock(node, context) { 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) { context.state.init.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index f211f64d7c..52850090e3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ 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 @@ -17,16 +17,22 @@ export function KeyBlock(node, context) { const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression); 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) { - call = b.call( - '$.async', - context.state.node, - b.array([b.thunk(expression, true)]), - b.arrow([context.state.node, b.id('$$key')], b.block([b.stmt(call)])) + statement = b.stmt( + b.call( + '$.async', + context.state.node, + 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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 1f5a276e04..b7a6e65557 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; 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 @@ -50,16 +50,22 @@ export function RenderTag(node, context) { } 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 { statements.push( - b.stmt( + add_svelte_meta( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, context.state.node, ...args - ) + ), + node, + 'render' ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index b8658162fc..7feeebdbbc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -4,7 +4,7 @@ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; 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_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; @@ -490,7 +490,8 @@ export function build_component(node, component_name, context) { ); } else { 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(); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 4171204708..c87123c72d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -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 { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ 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 { regex_is_valid_identifier } from '../../../../patterns.js'; 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'; export class Memoizer { @@ -427,3 +427,34 @@ export function build_expression(context, expression, metadata, state = context. 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} [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)))) + ) + ); +} diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 9095651ced..5eb25dd6bb 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,9 @@ export let warnings = []; */ export let filename; +/** + * The name of the component that is used in the `export default function ...` statement. + */ export let component_name = ''; /** diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index f326f3a0b7..bbfba20f75 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext } from '#client' */ +/** @import { ComponentContext, DevStackEntry } from '#client' */ import { DEV } from 'esm-env'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -12,6 +12,7 @@ import { } from './runtime.js'; import { effect, teardown } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; +import { FILENAME } from '../../constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -21,6 +22,43 @@ export function set_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} [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: * ```html diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js index f70f893d1e..8dd54e0a2a 100644 --- a/packages/svelte/src/internal/client/dev/elements.js +++ b/packages/svelte/src/internal/client/dev/elements.js @@ -2,6 +2,7 @@ import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { hydrating } from '../dom/hydration.js'; +import { dev_stack } from '../context.js'; /** * @param {any} fn @@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) { function assign_location(element, filename, location) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], column: location[1] } }; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 47df5fc9a5..325224fff2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { component_context, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from '../../context.js'; const PENDING = 0; @@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { /** @type {any} */ var component_function = DEV ? component_context?.function : null; + var dev_original_stack = DEV ? dev_stack : null; /** @type {V | Promise | typeof 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_reaction(effect); // TODO do we need both? 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 { @@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } } finally { 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_active_reaction(null); set_active_effect(null); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index ffa57b2d8b..231a3621b1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -18,7 +18,7 @@ import { import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.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 { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; 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) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a8c04e9d4c..bfbdefd095 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,6 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.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 { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index fdb136d503..d0e772f576 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -38,7 +38,7 @@ import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.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 { flatten } from './async.js'; @@ -348,7 +348,11 @@ export function template_effect(fn, sync = [], async = []) { * @param {number} flags */ 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; } /** diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f3111361c0..3501bcd3c7 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -8,12 +8,11 @@ import { PROPS_IS_UPDATED } from '../../../constants.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 { get, captured_signals, untrack } from '../runtime.js'; -import { safe_equals } from './equality.js'; +import { get, untrack } from '../runtime.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 { capture_store_binding } from './store.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))} */ 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 bindable = (flags & PROPS_IS_BINDABLE) !== 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_dirty = true; - var fallback_used = false; var get_fallback = () => { - fallback_used = true; if (fallback_dirty) { fallback_dirty = false; - if (lazy) { - fallback_value = untrack(/** @type {() => V} */ (fallback)); - } else { - fallback_value = /** @type {V} */ (fallback); - } + + fallback_value = lazy + ? untrack(/** @type {() => V} */ (fallback)) + : /** @type {V} */ (fallback); } return fallback_value; }; - if (prop_value === undefined && fallback !== undefined) { - if (setter && runes) { - e.props_invalid_value(key); - } + /** @type {((v: V) => void) | undefined} */ + var setter; - prop_value = get_fallback(); - if (setter) setter(prop_value); + if (bindable) { + // 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} */ var getter; + if (runes) { getter = () => { var value = /** @type {V} */ (props[key]); if (value === undefined) return get_fallback(); fallback_dirty = true; - fallback_used = false; return value; }; } 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 = () => { - var value = get(derived_getter); - if (value !== undefined) fallback_value = /** @type {V} */ (undefined); + var value = /** @type {V} */ (props[key]); + + 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; }; } - // easy mode — prop is never written to - if ((flags & PROPS_IS_UPDATED) === 0 && runes) { + // prop is never written to — we only need a getter + if (runes && (flags & PROPS_IS_UPDATED) === 0) { return getter; } - // intermediate mode — prop is written to, but the parent component had - // `bind:foo` which means we can just call `$$props.foo = value` directly + // prop is written to, but the parent component had `bind:foo` which + // means we can just call `$$props.foo = value` directly if (setter) { var legacy_parent = props.$$legacy; + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { // 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) { /** @type {Function} */ (setter)(mutation ? getter() : value); } + return value; - } else { - return getter(); } + + return getter(); }; } - // hard mode. this is where it gets ugly — the value in the child should - // synchronize with the parent, but it should also be possible to temporarily - // set the value to something else locally. - var from_child = false; - var was_from_child = false; - - // 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); - } + // Either prop is written to, but there's no binding, which means we + // create a derived that we can write to locally. + // Or we are in legacy mode where we always create a derived to replicate that + // Svelte 4 did not trigger updates when a primitive value was updated to the same value. + var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); - 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) { - // 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) { - const new_value = mutation ? get(current_value) : 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; - } + const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; - if (has_destroyed_component_ctx(current_value)) { - return value; - } + set(d, new_value); - untrack(() => get(current_value)); // force a synchronisation immediately + if (fallback_value !== undefined) { + fallback_value = new_value; } return value; } - if (has_destroyed_component_ctx(current_value)) { - return current_value.v; + // TODO is this still necessary post-#16263? + if (has_destroyed_component_ctx(d)) { + return d.v; } - return get(current_value); + return get(d); }; } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index c24ecdbdc9..75e6f6bec2 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -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'; export interface Signal { @@ -83,6 +89,8 @@ export interface Effect extends Reaction { b: Boundary | null; /** Dev only */ 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 = Value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 40a0f35e9f..8ca7383327 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -20,7 +20,6 @@ import { STATE_SYMBOL, BLOCK_EFFECT, ROOT_EFFECT, - LEGACY_DERIVED_PROP, DISCONNECTED, REACTION_IS_UPDATING, EFFECT_IS_UPDATING, @@ -44,9 +43,11 @@ import { tracing_expressions, get_stack } from './dev/tracing.js'; import { component_context, dev_current_component_function, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from './context.js'; import * as w from './warnings.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; @@ -478,6 +479,9 @@ export function update_effect(effect) { if (DEV) { var previous_component_fn = dev_current_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 { @@ -512,6 +516,7 @@ export function update_effect(effect) { if (DEV) { 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)); for (var signal of captured) { - // Go one level up because derived signals created as part of props in legacy mode - 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); - } + internal_set(signal, signal.v); } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 01baee0467..eb386ce583 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -185,4 +185,13 @@ export type SourceLocation = | [line: number, column: number] | [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'; diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1c0aed6e87..baa2dbd046 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.34.9'; +export const VERSION = '5.35.0'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js index 905c2a6226..84658336e2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js @@ -2,6 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + accessors: false, test({ assert, logs, target }) { assert.deepEqual(logs, ['primitive', 'object']); target.querySelector('button')?.click(); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js new file mode 100644 index 0000000000..eba85d5098 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js @@ -0,0 +1,186 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + compileOptions: { + dev: true + }, + html: ` +

no parent

+ +

if

+

each

+

loading

+

key

+

hi

+

hi

+

hi

+

hi

+

hi

+ `, + + 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(); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte new file mode 100644 index 0000000000..0df6def593 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte @@ -0,0 +1 @@ +

hi

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte new file mode 100644 index 0000000000..b9bd46d8f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte @@ -0,0 +1,50 @@ + + +

no parent

+ + +{#if true} +

if

+{/if} + +{#each [1]} +

each

+{/each} + +{#await Promise.resolve()} +

loading

+{:then} +

await

+{/await} + +{#key key} +

key

+{/key} + + + + + + + + + + + + + +{#if show} + + {#snippet named()} +

hi

+ {/snippet} +
+{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte new file mode 100644 index 0000000000..70ba81a4c1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte @@ -0,0 +1,9 @@ + + +{@render children?.()} + +{#if true} + {@render named?.()} +{/if}