From a1b172c3510e80669998360f726a2e03a7cde51a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 2 Sep 2026 22:35:03 +0200 Subject: [PATCH] fix: ensure last prop value is used on teardown more consistently Adds logic to save the last retrieved prop value, and return it when we see that the component is getting destroyed or already is. This logic lives inside `prop` and `rest_props`. Logic in the compiler was added to determine whether we need to use `$.prop`, which is true if we cannot statically verify that the prop is never used in a position that might be read during/after teardown. Related to #16019 Trimmed down version of #18760 without the other teardown changes --- .changeset/calm-timers-clean.md | 5 + .../phases/3-transform/client/utils.js | 298 +++++++++++++++++- .../3-transform/client/visitors/Identifier.js | 5 +- .../client/visitors/VariableDeclaration.js | 16 +- packages/svelte/src/compiler/phases/scope.js | 2 +- packages/svelte/src/constants.js | 1 + .../src/internal/client/reactivity/props.js | 92 +++++- .../prop-unmount-fixed-value/Child.svelte | 23 ++ .../prop-unmount-fixed-value/_config.js | 38 +++ .../prop-unmount-fixed-value/main.svelte | 16 + .../props-retained-analysis/Component.svelte | 5 + .../_expected/client/Component.svelte.js | 13 + .../_expected/client/index.svelte.js | 60 ++++ .../_expected/server/Component.svelte.js | 7 + .../_expected/server/index.svelte.js | 41 +++ .../props-retained-analysis/index.svelte | 42 +++ 16 files changed, 643 insertions(+), 21 deletions(-) create mode 100644 .changeset/calm-timers-clean.md create mode 100644 packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/prop-unmount-fixed-value/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/Component.svelte create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/Component.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/Component.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/props-retained-analysis/index.svelte 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 @@ + + + + + + + + + +