diff --git a/.changeset/calm-timers-clean.md b/.changeset/calm-timers-clean.md new file mode 100644 index 0000000000..8e02d9471b --- /dev/null +++ b/.changeset/calm-timers-clean.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure last prop value is used on teardown more consistently diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 9cdfa5cae1..7f80a6da1b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -5,14 +5,16 @@ /** @import { ExpressionMetadata } from '../../nodes.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; -import { is_simple_expression, save } from '../../../utils/ast.js'; +import { is_event_attribute, is_simple_expression, save } from '../../../utils/ast.js'; import { PROPS_IS_LAZY_INITIAL, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES, PROPS_IS_UPDATED, - PROPS_IS_BINDABLE + PROPS_IS_BINDABLE, + PROPS_IS_RETAINED } from '../../../../constants.js'; +import { get_rune } from '../../scope.js'; /** * @param {Binding} binding @@ -44,6 +46,291 @@ export function build_getter(node, state) { return node; } +/** + * @param {Binding} binding + * @param {ClientTransformState} state + */ +export function prop_binding_may_be_in_teardown(binding, state) { + return ((binding.metadata ??= {}).prop_read_may_be_in_teardown ??= binding.references.some( + (reference) => reference_may_be_in_teardown(reference, state, new Set()) + )); +} + +/** + * @param {Binding['references'][number]} reference + * @param {ClientTransformState} state + * @param {Set} checked + */ +function reference_may_be_in_teardown(reference, state, checked) { + const { path } = reference; + + for (let i = path.length - 1; i >= 0; i -= 1) { + const fn = path[i]; + + if ( + fn.type !== 'ArrowFunctionExpression' && + fn.type !== 'FunctionExpression' && + fn.type !== 'FunctionDeclaration' + ) { + continue; + } + + const parent = path[i - 1]; + + if (parent?.type === 'CallExpression') { + // An IIFE executes in the same context as the function containing it. + if (parent.callee === fn) continue; + + if (parent.arguments.includes(/** @type {Expression} */ (fn))) { + const scope = get_scope(path, i - 1, state); + if (is_save_svelte_import_call(parent, scope)) return false; + + const rune = get_rune(parent, scope); + + if (is_effect_rune(rune)) return false; + if (rune === '$derived.by') { + return derived_may_be_read_in_teardown(parent, path, i - 1, state, checked); + } + } + } + + if (is_safe_element_event_handler(path, i)) return false; + + const function_binding = get_function_binding(fn, path, i, state); + return function_binding === null + ? true + : function_may_be_called_in_teardown(function_binding, state, checked); + } + + for (let i = path.length - 1; i >= 0; i -= 1) { + const node = path[i]; + if (node.type !== 'CallExpression') continue; + + const rune = get_rune(node, get_scope(path, i, state)); + if (rune === '$derived' || rune === '$derived.by') { + return derived_may_be_read_in_teardown(node, path, i, state, checked); + } + } + + return false; +} + +/** + * @param {Binding} binding + * @param {ClientTransformState} state + * @param {Set} checked + */ +function function_may_be_called_in_teardown(binding, state, checked) { + if (checked.has(binding)) return false; + checked.add(binding); + + for (const reference of binding.references) { + if (reference.node === binding.node) continue; + + const parent = reference.path.at(-1); + + if (parent?.type !== 'CallExpression') { + if (is_safe_element_event_handler(reference.path, reference.path.length)) continue; + return true; + } + + if (parent.callee === reference.node) { + if (reference_may_be_in_teardown(reference, state, checked)) return true; + continue; + } + + if (!parent.arguments.includes(reference.node)) return true; + + const scope = get_scope(reference.path, reference.path.length - 1, state); + if (is_save_svelte_import_call(parent, scope)) continue; + + const rune = get_rune(parent, scope); + + if (is_effect_rune(rune)) continue; + if ( + rune === '$derived.by' && + !derived_may_be_read_in_teardown( + parent, + reference.path, + reference.path.length - 1, + state, + checked + ) + ) { + continue; + } + + return true; + } + + return false; +} + +/** + * @param {import('estree').CallExpression} call + * @param {import('#compiler').AST.SvelteNode[]} path + * @param {number} index + * @param {ClientTransformState} state + * @param {Set} checked + */ +function derived_may_be_read_in_teardown(call, path, index, state, checked) { + const parent = path[index - 1]; + if (parent?.type !== 'VariableDeclarator' || parent.init !== call) return true; + if (parent.id.type !== 'Identifier') return true; + + const binding = get_scope(path, index - 1, state).get(parent.id.name); + if (binding === null || checked.has(binding)) return false; + + checked.add(binding); + + for (const reference of binding.references) { + if (reference.node === binding.node) continue; + if (reference_may_be_in_teardown(reference, state, checked)) return true; + } + + return false; +} + +/** + * @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression | import('estree').ArrowFunctionExpression} fn + * @param {import('#compiler').AST.SvelteNode[]} path + * @param {number} index + * @param {ClientTransformState} state + * @returns {Binding | null} + */ +function get_function_binding(fn, path, index, state) { + if (fn.type === 'FunctionDeclaration' && fn.id !== null) { + return get_scope(path, index - 1, state).get(fn.id.name); + } + + const parent = path[index - 1]; + if ( + parent?.type === 'VariableDeclarator' && + parent.init === fn && + parent.id.type === 'Identifier' + ) { + return get_scope(path, index - 1, state).get(parent.id.name); + } + + return null; +} + +/** + * @param {import('#compiler').AST.SvelteNode[]} path + * @param {number} index + * @param {ClientTransformState} state + */ +function get_scope(path, index, state) { + for (let i = index; i >= 0; i -= 1) { + const scope = state.scopes.get(path[i]); + if (scope !== undefined) return scope; + } + + return state.scope; +} + +/** @param {string | null} rune */ +function is_effect_rune(rune) { + return rune === '$effect' || rune === '$effect.pre' || rune === '$effect.root'; +} + +/** + * @param {import('estree').CallExpression} call + * @param {Scope} scope + */ +function is_save_svelte_import_call(call, scope) { + if (call.callee.type === 'Identifier') { + return ( + is_svelte_import(scope.get(call.callee.name), call.callee.name, 'untrack') || + is_svelte_import(scope.get(call.callee.name), call.callee.name, 'hydratable') + ); + } + + if ( + call.callee.type === 'MemberExpression' && + call.callee.object.type === 'Identifier' && + ((!call.callee.computed && + call.callee.property.type === 'Identifier' && + (call.callee.property.name === 'untrack' || call.callee.property.name === 'hydratable')) || + (call.callee.computed && + call.callee.property.type === 'Literal' && + (call.callee.property.value === 'untrack' || call.callee.property.value === 'hydratable'))) + ) { + return is_svelte_import(scope.get(call.callee.object.name), call.callee.object.name, null); + } + + return false; +} + +/** + * @param {Binding | null} binding + * @param {string} local_name + * @param {string | null} imported_name + */ +function is_svelte_import(binding, local_name, imported_name) { + if (binding?.initial?.type !== 'ImportDeclaration' || binding.initial.source.value !== 'svelte') { + return false; + } + + return binding.initial.specifiers.some((specifier) => { + if (specifier.local.name !== local_name) return false; + if (imported_name === null) return specifier.type === 'ImportNamespaceSpecifier'; + + return ( + specifier.type === 'ImportSpecifier' && + specifier.imported.type === 'Identifier' && + specifier.imported.name === imported_name + ); + }); +} + +/** + * @param {import('#compiler').AST.SvelteNode[]} path + * @param {number} index + */ +function is_safe_element_event_handler(path, index) { + for (let i = index - 1; i >= 0; i -= 1) { + const node = path[i]; + + if ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionExpression' || + node.type === 'FunctionDeclaration' || + node.type === 'CallExpression' || + node.type === 'NewExpression' || + node.type === 'SpreadAttribute' + ) { + return false; + } + + if (node.type === 'Attribute') { + if (!is_event_attribute(node)) return false; + + const element = path[i - 1]; + return ( + (element?.type === 'RegularElement' || element?.type === 'SvelteElement') && + is_safe_event_name(node.name.slice(2)) + ); + } + + if (node.type === 'OnDirective') { + const element = path[i - 1]; + return ( + (element?.type === 'RegularElement' || element?.type === 'SvelteElement') && + is_safe_event_name(node.name) + ); + } + } + + return false; +} + +/** @param {string} name */ +function is_safe_event_name(name) { + if (name.endsWith('capture')) name = name.slice(0, -7); + return name !== 'blur' && name !== 'focusout'; +} + /** * @param {Binding} binding * @param {ComponentClientTransformState} state @@ -69,6 +356,10 @@ export function get_prop_source(binding, state, name, initial) { flags |= PROPS_IS_RUNES; } + if (state.analysis.runes && prop_binding_may_be_in_teardown(binding, state)) { + flags |= PROPS_IS_RETAINED; + } + if ( state.analysis.accessors || (state.analysis.immutable @@ -123,7 +414,8 @@ export function is_prop_source(binding, state) { binding.initial || // Until legacy mode is gone, we also need to use the prop source when only mutated is true, // because the parent could be a legacy component which needs coarse-grained reactivity - binding.updated) + binding.updated || + prop_binding_may_be_in_teardown(binding, state)) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js index b43ec7891e..5a359a2d50 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js @@ -2,7 +2,7 @@ /** @import { Context } from '../types' */ import is_reference from 'is-reference'; import * as b from '#compiler/builders'; -import { build_getter } from '../utils.js'; +import { build_getter, prop_binding_may_be_in_teardown } from '../utils.js'; /** * @param {Identifier} node @@ -22,7 +22,8 @@ export function Identifier(node, context) { context.state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write binding !== null && node !== binding.node && - binding.kind === 'rest_prop' + binding.kind === 'rest_prop' && + !prop_binding_may_be_in_teardown(binding, context.state) ) { const grand_parent = context.path.at(-2); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 246feaccf6..4ec3128eaf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -11,6 +11,7 @@ import { get_prop_source, is_prop_source, is_state_source, + prop_binding_may_be_in_teardown, should_proxy } from '../utils.js'; import { get_value } from './shared/declarations.js'; @@ -55,6 +56,7 @@ export function VariableDeclaration(node, context) { } if (declarator.id.type === 'Identifier') { + const binding = /** @type {Binding} */ (context.state.scope.get(declarator.id.name)); const exclude_id = context.state.scope.root.unique('rest_excludes'); context.state.hoisted.push( b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name))))) @@ -68,6 +70,11 @@ export function VariableDeclaration(node, context) { args.push(b.literal(declarator.id.name)); } + if (prop_binding_may_be_in_teardown(binding, context.state)) { + if (!dev) args.push(b.void0); + args.push(b.true); + } + declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args))); } else { assert.equal(declarator.id.type, 'ObjectPattern'); @@ -106,6 +113,8 @@ export function VariableDeclaration(node, context) { } } else { // RestElement + const rest_id = /** @type {Identifier} */ (property.argument); + const binding = /** @type {Binding} */ (context.state.scope.get(rest_id.name)); const exclude_id = context.state.scope.root.unique('rest_excludes'); context.state.hoisted.push( b.var(exclude_id, b.new('Set', b.array(seen.map((name) => b.literal(name))))) @@ -116,7 +125,12 @@ export function VariableDeclaration(node, context) { if (dev) { // include rest name, so we can provide informative error messages - args.push(b.literal(/** @type {Identifier} */ (property.argument).name)); + args.push(b.literal(rest_id.name)); + } + + if (prop_binding_may_be_in_teardown(binding, context.state)) { + if (!dev) args.push(b.void0); + args.push(b.true); } declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index e3560753b4..bc0e00f72e 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -134,7 +134,7 @@ export class Binding { /** * Additional metadata, varies per binding type - * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean; exclude_props?: string[] }} + * @type {null | { inside_rest?: boolean; is_template_declaration?: boolean; exclude_props?: string[]; prop_read_may_be_in_teardown?: boolean }} */ metadata = null; diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 1e721b7d30..c840e6ad59 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -10,6 +10,7 @@ export const PROPS_IS_RUNES = 1 << 1; export const PROPS_IS_UPDATED = 1 << 2; export const PROPS_IS_BINDABLE = 1 << 3; export const PROPS_IS_LAZY_INITIAL = 1 << 4; +export const PROPS_IS_RETAINED = 1 << 5; export const TRANSITION_IN = 1; export const TRANSITION_OUT = 1 << 1; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 274d780b1e..f1ac5dae17 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -4,8 +4,10 @@ import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, + PROPS_IS_RETAINED, PROPS_IS_RUNES, - PROPS_IS_UPDATED + PROPS_IS_UPDATED, + UNINITIALIZED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; @@ -18,7 +20,7 @@ import { untrack } from '../runtime.js'; import * as e from '../errors.js'; -import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; +import { DESTROYED, DESTROYING, INERT, 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'; @@ -46,15 +48,34 @@ export function update_pre_prop(fn, d = 1) { return value; } +/** @typedef {{ props: Record, exclude: Set, name?: string, retained: null | { effect: Effect, values: Map } }} RestPropsTarget */ + +/** + * @param {RestPropsTarget} target + * @param {string | symbol} key + */ +function get_rest_prop(target, key) { + var value = target.props[key]; + + if (target.retained === null) return value; + + if ((target.retained.effect.f & (INERT | DESTROYING | DESTROYED)) !== 0) { + return target.retained.values.has(key) ? target.retained.values.get(key) : value; + } + + target.retained.values.set(key, value); + return value; +} + /** * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`). * Is passed the full `$$props` object and excludes the named props. - * @type {ProxyHandler<{ props: Record, exclude: Set, name?: string }>}} + * @type {ProxyHandler} */ const rest_props_handler = { get(target, key) { if (target.exclude.has(key)) return; - return target.props[key]; + return get_rest_prop(target, key); }, set(target, key) { if (DEV) { @@ -66,17 +87,29 @@ const rest_props_handler = { }, getOwnPropertyDescriptor(target, key) { if (target.exclude.has(key)) return; - if (key in target.props) { + + var exists = + key in target.props || + (target.retained !== null && + (target.retained.effect.f & (INERT | DESTROYING | DESTROYED)) !== 0 && + target.retained.values.has(key)); + + if (exists) { return { enumerable: true, configurable: true, - value: target.props[key] + value: get_rest_prop(target, key) }; } }, has(target, key) { if (target.exclude.has(key)) return false; - return key in target.props; + return ( + key in target.props || + (target.retained !== null && + (target.retained.effect.f & (INERT | DESTROYING | DESTROYED)) !== 0 && + target.retained.values.has(key)) + ); }, ownKeys(target) { return Reflect.ownKeys(target.props).filter((key) => !target.exclude.has(key)); @@ -84,14 +117,28 @@ const rest_props_handler = { }; /** - * @param {Record} props - * @param {Set} exclude + * @param {Record} props + * @param {Set} exclude * @param {string} [name] + * @param {boolean} [retain] * @returns {Record} */ /*#__NO_SIDE_EFFECTS__*/ -export function rest_props(props, exclude, name) { - return new Proxy(DEV ? { props, exclude, name } : { props, exclude }, rest_props_handler); +export function rest_props(props, exclude, name, retain = false) { + /** @type {RestPropsTarget['retained']} */ + var retained = null; + + if (retain) { + retained = { + effect: /** @type {Effect} */ (active_effect), + values: new Map() + }; + } + + return new Proxy( + DEV ? { props, exclude, name, retained } : { props, exclude, retained }, + rest_props_handler + ); } /** @@ -277,6 +324,7 @@ export function prop(props, key, flags, fallback) { 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 retained = (flags & PROPS_IS_RETAINED) !== 0; var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; @@ -357,6 +405,24 @@ export function prop(props, key, flags, fallback) { }; } + var parent_effect = /** @type {Effect} */ (active_effect); + + if (retained) { + var get_value = getter; + var retained_value = /** @type {V | typeof UNINITIALIZED} */ (UNINITIALIZED); + + getter = () => { + if ( + (parent_effect.f & (INERT | DESTROYING | DESTROYED)) !== 0 && + retained_value !== UNINITIALIZED + ) { + return retained_value; + } + + return (retained_value = get_value()); + }; + } + // prop is never written to — we only need a getter if (runes && (flags & PROPS_IS_UPDATED) === 0) { return getter; @@ -403,8 +469,6 @@ export function prop(props, key, flags, fallback) { // Capture the initial value if it's bindable if (bindable) get(d); - var parent_effect = /** @type {Effect} */ (active_effect); - return /** @type {() => V} */ ( function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { @@ -423,7 +487,7 @@ export function prop(props, key, flags, fallback) { // special case — avoid recalculating the derived if we're in a // teardown function and the prop was overridden locally, or the // component was already destroyed (people could access props in a timeout) - if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) { + if ((parent_effect.f & (INERT | DESTROYING | DESTROYED)) !== 0 && overridden) { return d.v; } diff --git a/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/Child.svelte b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/Child.svelte new file mode 100644 index 0000000000..990a44c9b2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/Child.svelte @@ -0,0 +1,23 @@ + + +
+ +
diff --git a/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/_config.js b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/_config.js new file mode 100644 index 0000000000..9420c82d5f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/_config.js @@ -0,0 +1,38 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { vi } from 'vitest'; + +export default test({ + before_test() { + vi.useFakeTimers(); + }, + after_test() { + vi.useRealTimers(); + }, + test({ assert, logs, raf, target }) { + const toggle = target.querySelector('button'); + + flushSync(() => toggle?.click()); + target.querySelectorAll('button')[1]?.click(); + flushSync(() => toggle?.click()); + + raf.tick(100); + raf.tick(200); + vi.advanceTimersByTime(1000); + + assert.deepEqual(logs, [ + 'PARENT: set value to', + true, + 'CHILD: mount value is', + true, + 'PARENT: set value to', + false, + 'CHILD: outro value is', + true, + 'CHILD: unmount value is', + true, + 'CHILD: timeout value is', + true + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/main.svelte new file mode 100644 index 0000000000..838ce7a04a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/main.svelte @@ -0,0 +1,16 @@ + + + + +{#if value} + +{/if} diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/Component.svelte b/packages/svelte/tests/snapshot/samples/props-retained-analysis/Component.svelte new file mode 100644 index 0000000000..853e335e3e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/Component.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/Component.svelte.js b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/Component.svelte.js new file mode 100644 index 0000000000..023519aad2 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/Component.svelte.js @@ -0,0 +1,13 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; + +var rest_excludes = new Set(['$$slots', '$$events', '$$legacy']); +var root = $.from_html(``); + +export default function Component($$anchor, $$props) { + let props = $.rest_props($$props, rest_excludes); + var button = root(); + + $.attribute_effect(button, () => ({ ...props })); + $.append($$anchor, button); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/index.svelte.js new file mode 100644 index 0000000000..7b6cab107b --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/index.svelte.js @@ -0,0 +1,60 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; +import { untrack as read_untracked } from 'svelte'; +import * as svelte from 'svelte'; +import Component from './Component.svelte'; + +var root = $.from_html(` `, 1); + +export default function Props_retained_analysis($$anchor, $$props) { + $.push($$props, true); + + let unsafe_blur = $.prop($$props, 'unsafe_blur', 35), + unsafe_focusout = $.prop($$props, 'unsafe_focusout', 35), + unsafe_component_event = $.prop($$props, 'unsafe_component_event', 35), + unsafe_spread_event = $.prop($$props, 'unsafe_spread_event', 35), + unsafe_local_untrack = $.prop($$props, 'unsafe_local_untrack', 35), + unsafe_mixed_event = $.prop($$props, 'unsafe_mixed_event', 35); + + read_untracked(() => $$props.safe_untrack); + svelte.untrack(() => $$props.safe_namespace_untrack); + + const handle_click = () => $$props.safe_named_event; + const handle_blur = () => unsafe_blur(); + const handle_focusout = () => unsafe_focusout(); + const handle_component_event = () => unsafe_component_event(); + const handle_mixed_event = () => unsafe_mixed_event(); + + function untrack(fn) { + fn(); + } + + untrack(() => unsafe_local_untrack()); + + var fragment = root(); + var button = $.first_child(fragment); + var button_1 = $.sibling(button, 2); + var button_2 = $.sibling(button_1, 2); + var button_3 = $.sibling(button_2, 2); + var button_4 = $.sibling(button_3, 2); + + $.attribute_effect(button_4, () => ({ ...{ onclick: () => unsafe_spread_event() } })); + + var node = $.sibling(button_4, 2); + + Component(node, { onclick: handle_component_event }); + + var button_5 = $.sibling(node, 2); + var node_1 = $.sibling(button_5, 2); + + Component(node_1, { onclick: handle_mixed_event }); + $.delegated('click', button, () => $$props.safe_inline_event); + $.delegated('click', button_1, handle_click); + $.event('blur', button_2, handle_blur); + $.delegated('focusout', button_3, handle_focusout); + $.delegated('click', button_5, handle_mixed_event); + $.append($$anchor, fragment); + $.pop(); +} + +$.delegate(['click', 'focusout']); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/Component.svelte.js b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/Component.svelte.js new file mode 100644 index 0000000000..03cc2ac06a --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/Component.svelte.js @@ -0,0 +1,7 @@ +import * as $ from 'svelte/internal/server'; + +export default function Component($$renderer, $$props) { + let { $$slots, $$events, ...props } = $$props; + + $$renderer.push(`component`); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/index.svelte.js new file mode 100644 index 0000000000..b450e1fd1d --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/index.svelte.js @@ -0,0 +1,41 @@ +import * as $ from 'svelte/internal/server'; +import { untrack as read_untracked } from 'svelte'; +import * as svelte from 'svelte'; +import Component from './Component.svelte'; + +export default function Props_retained_analysis($$renderer, $$props) { + $$renderer.component(($$renderer) => { + let { + safe_untrack, + safe_namespace_untrack, + safe_inline_event, + safe_named_event, + unsafe_blur, + unsafe_focusout, + unsafe_component_event, + unsafe_spread_event, + unsafe_local_untrack, + unsafe_mixed_event + } = $$props; + + read_untracked(() => safe_untrack); + svelte.untrack(() => safe_namespace_untrack); + + const handle_click = () => safe_named_event; + const handle_blur = () => unsafe_blur; + const handle_focusout = () => unsafe_focusout; + const handle_component_event = () => unsafe_component_event; + const handle_mixed_event = () => unsafe_mixed_event; + + function untrack(fn) { + fn(); + } + + untrack(() => unsafe_local_untrack); + $$renderer.push(` unsafe_spread_event } })}>spread `); + Component($$renderer, { onclick: handle_component_event }); + $$renderer.push(` `); + Component($$renderer, { onclick: handle_mixed_event }); + $$renderer.push(``); + }); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/props-retained-analysis/index.svelte b/packages/svelte/tests/snapshot/samples/props-retained-analysis/index.svelte new file mode 100644 index 0000000000..b2403d39e3 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/props-retained-analysis/index.svelte @@ -0,0 +1,42 @@ + + + + + + + + + +