diff --git a/.changeset/tasty-pears-travel.md b/.changeset/tasty-pears-travel.md new file mode 100644 index 0000000000..96037f3440 --- /dev/null +++ b/.changeset/tasty-pears-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: access last safe value of prop on unmount diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 63fe3223cf..c61ccaa988 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -23,6 +23,8 @@ export interface ClientTransformState extends TransformState { * us to rewrite `this.foo` as `this.#foo.value` */ readonly in_constructor: boolean; + readonly safe_props_ids?: Map; + readonly safe_props_name?: string; readonly transform: Record< string, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js index a10a3da6e3..99eae046b0 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -10,8 +10,24 @@ import { build_component } from './shared/component.js'; */ export function Component(node, context) { if (node.metadata.dynamic) { + let safe_props_ids = new Map(); + + const safe_props_name = context.state.scope.generate('$$safe_props'); + // Handle dynamic references to what seems like static inline components - const component = build_component(node, '$$component', context, b.id('$$anchor')); + const component = build_component( + node, + '$$component', + { + ...context, + state: { + ...context.state, + safe_props_ids, + safe_props_name + } + }, + b.id('$$anchor') + ); context.state.init.push( b.stmt( b.call( @@ -20,7 +36,19 @@ export function Component(node, context) { // TODO use untrack here to not update when binding changes? // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))), - b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component])) + b.arrow( + [b.id('$$anchor'), b.id('$$component')], + b.block([ + b.const( + safe_props_name, + b.call( + '$.safe_props', + b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)]))) + ) + ), + component + ]) + ) ) ) ); 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 ae62909eff..40144859a4 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 @@ -9,9 +9,9 @@ import { build_getter } from '../utils.js'; * @param {Context} context */ export function Identifier(node, context) { - const parent = /** @type {Node} */ (context.path.at(-1)); + let parent = context.path.at(-1); - if (is_reference(node, parent)) { + if (is_reference(node, /** @type {Node} */ (parent))) { if (node.name === '$$props') { return b.id('$$sanitized_props'); } @@ -36,6 +36,36 @@ export function Identifier(node, context) { } } - return build_getter(node, context.state); + const getter = build_getter(node, context.state); + + if ( + // this means we are inside an if or as an attribute of a dynamic component + // and we want to access `$$safe_props` to allow for the component to access them + // after destructuring + context.state.safe_props_name != null && + context.state.safe_props_ids != null && + // the parent can either be a component/svelte component in that case we + // check if this identifier is one of the attributes + (((parent?.type === 'Component' || parent?.type === 'SvelteComponent') && + parent.attributes.some( + (el) => + (el.type === 'Attribute' && + typeof el.value !== 'boolean' && + !Array.isArray(el.value) && + el.value.expression === node) || + (el.type === 'BindDirective' && el.expression === node) + )) || + // or a spread and we check the expression + (parent?.type === 'SpreadAttribute' && parent.expression === node)) && + // we also don't want to transform bindings that are defined withing the if block + // itself (for example an each local variable) + !binding?.references[0].path.some((node) => node.type === 'IfBlock') + ) { + // we store the getter in the safe props id and return an access to `$$safe_props.name` + context.state.safe_props_ids.set(node.name, getter); + return b.member(b.id(context.state.safe_props_name), b.id(node.name)); + } + + return getter; } } 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 d658f9eaf8..2aec6e0156 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 @@ -11,7 +11,29 @@ export function IfBlock(node, context) { context.state.template.push(''); const statements = []; - const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + let safe_props_ids = new Map(); + + const safe_props_id = context.state.scope.generate('$$safe_props'); + + const consequent = /** @type {BlockStatement} */ ( + context.visit(node.consequent, { + ...context.state, + safe_props_ids, + safe_props_name: safe_props_id + }) + ); + + if (consequent.body.length > 0 && safe_props_ids) { + consequent.body.unshift( + b.const( + safe_props_id, + b.call( + '$.safe_props', + b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)]))) + ) + ) + ); + } const consequent_id = context.state.scope.generate('consequent'); statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent))); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index abffad0ff7..f75e9dd93c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -185,7 +185,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: b.literal(chunk.data), has_state: false }; } - let expression = /** @type {Expression} */ (context.visit(chunk.expression)); + let expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state)); return { value: memoize(expression, chunk.metadata.expression), diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d78f6d452e..95b9465113 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -118,7 +118,8 @@ export { legacy_rest_props, spread_props, update_pre_prop, - update_prop + update_prop, + safe_props } from './reactivity/props.js'; export { invalidate_store, diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5a3b30281f..be4b11dec2 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -31,6 +31,7 @@ import { import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; +import { teardown } from './effects.js'; /** * @param {((value?: number) => number)} fn @@ -416,3 +417,39 @@ export function prop(props, key, flags, fallback) { return get(current_value); }; } + +/** + * + * @param {Record} props + */ +export function safe_props(props) { + let unmounting = false; + teardown(() => { + unmounting = true; + }); + const deriveds = new Map(); + /** + * @type {Map} + */ + const olds = new Map(untrack(() => Object.entries(props))); + return new Proxy( + {}, + { + get(_, key) { + if (!deriveds.has(key)) { + deriveds.set( + key, + derived(() => { + if (unmounting) { + return olds.get(key); + } + olds.set(key, props[key]); + return props[key]; + }) + ); + } + return get(deriveds.get(key)); + } + } + ); +}