From e36a8d06ea4ade519f3208faa2e3d3c81cb8ee2c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 12 Mar 2024 12:53:19 -0400 Subject: [PATCH] move props code (#10774) * move props code * oops --------- Co-authored-by: Rich Harris --- .../src/internal/client/reactivity/props.js | 219 +++++++++++++++++ packages/svelte/src/internal/client/render.js | 231 +----------------- 2 files changed, 223 insertions(+), 227 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 99e5048627..b3559ae99f 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,3 +1,16 @@ +import { DEV } from 'esm-env'; +import { + PROPS_IS_IMMUTABLE, + PROPS_IS_LAZY_INITIAL, + PROPS_IS_RUNES, + PROPS_IS_UPDATED +} from '../../../constants.js'; +import { get_descriptor, is_function } from '../utils.js'; +import { mutable_source, set } from './sources.js'; +import { derived } from './deriveds.js'; +import { get, inspect_fn, is_signals_recorded } from '../runtime.js'; +import { safe_equals, safe_not_equal } from './equality.js'; + /** * @param {((value?: number) => number)} fn * @param {1 | -1} [d] @@ -19,3 +32,209 @@ export function update_pre_prop(fn, d = 1) { fn(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: Array }>}} + */ +const rest_props_handler = { + get(target, key) { + if (target.exclude.includes(key)) return; + return target.props[key]; + }, + getOwnPropertyDescriptor(target, key) { + if (target.exclude.includes(key)) return; + if (key in target.props) { + return { + enumerable: true, + configurable: true, + value: target.props[key] + }; + } + }, + has(target, key) { + if (target.exclude.includes(key)) return false; + return key in target.props; + }, + ownKeys(target) { + return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key)); + } +}; + +/** + * @param {Record} props + * @param {string[]} rest + * @returns {Record} + */ +export function rest_props(props, rest) { + return new Proxy({ props, exclude: rest }, rest_props_handler); +} + +/** + * The proxy handler for spread props. Handles the incoming array of props + * that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps + * them so that the whole thing is passed to the component as the `$$props` argument. + * @template {Record} T + * @type {ProxyHandler<{ props: Array T)> }>}} + */ +const spread_props_handler = { + get(target, key) { + let i = target.props.length; + while (i--) { + let p = target.props[i]; + if (is_function(p)) p = p(); + if (typeof p === 'object' && p !== null && key in p) return p[key]; + } + }, + getOwnPropertyDescriptor(target, key) { + let i = target.props.length; + while (i--) { + let p = target.props[i]; + if (is_function(p)) p = p(); + if (typeof p === 'object' && p !== null && key in p) return get_descriptor(p, key); + } + }, + has(target, key) { + for (let p of target.props) { + if (is_function(p)) p = p(); + if (key in p) return true; + } + + return false; + }, + ownKeys(target) { + /** @type {Array} */ + const keys = []; + + for (let p of target.props) { + if (is_function(p)) p = p(); + for (const key in p) { + if (!keys.includes(key)) keys.push(key); + } + } + + return keys; + } +}; + +/** + * @param {Array | (() => Record)>} props + * @returns {any} + */ +export function spread_props(...props) { + return new Proxy({ props }, spread_props_handler); +} + +/** + * This function is responsible for synchronizing a possibly bound prop with the inner component state. + * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. + * @template V + * @param {Record} props + * @param {string} key + * @param {number} flags + * @param {V | (() => V)} [initial] + * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} + */ +export function prop(props, key, flags, initial) { + var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; + var runes = (flags & PROPS_IS_RUNES) !== 0; + var prop_value = /** @type {V} */ (props[key]); + var setter = get_descriptor(props, key)?.set; + + if (prop_value === undefined && initial !== undefined) { + if (setter && runes) { + // TODO consolidate all these random runtime errors + throw new Error( + 'ERR_SVELTE_BINDING_FALLBACK' + + (DEV + ? `: Cannot pass undefined to bind:${key} because the property contains a fallback value. Pass a different value than undefined to ${key}.` + : '') + ); + } + + // @ts-expect-error would need a cumbersome method overload to type this + if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial(); + + prop_value = /** @type {V} */ (initial); + + if (setter) setter(prop_value); + } + + var getter = () => { + var value = /** @type {V} */ (props[key]); + if (value !== undefined) initial = undefined; + return value === undefined ? /** @type {V} */ (initial) : value; + }; + + // easy mode — prop is never written to + if ((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 + if (setter) { + return function (/** @type {V} */ value) { + if (arguments.length === 1) { + /** @type {Function} */ (setter)(value); + return value; + } else { + 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); + }); + + if (!immutable) current_value.equals = safe_equals; + + return function (/** @type {V} */ value, mutation = false) { + var current = get(current_value); + + // 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 (is_signals_recorded || (DEV && inspect_fn)) { + // set this so that we don't reset to the parent value if `d` + // is invalidated because of `invalidate_inner_signals` (rather + // than because the parent or child value changed) + from_child = was_from_child; + // invoke getters so that signals are picked up by `invalidate_inner_signals` + getter(); + get(inner_current_value); + } + + if (arguments.length > 0) { + if (mutation || (immutable ? value !== current : safe_not_equal(value, current))) { + from_child = true; + set(inner_current_value, mutation ? current : value); + get(current_value); // force a synchronisation immediately + } + + return value; + } + + return current; + }; +} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 0a2da60f02..75c37326ad 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -12,11 +12,7 @@ import { PassiveDelegatedEvents, DelegatedEvents, AttributeAliases, - namespace_svg, - PROPS_IS_IMMUTABLE, - PROPS_IS_RUNES, - PROPS_IS_UPDATED, - PROPS_IS_LAZY_INITIAL + namespace_svg } from '../../constants.js'; import { remove } from './reconciler.js'; import { @@ -27,15 +23,11 @@ import { pop, current_component_context, get, - is_signals_recorded, - inspect_fn, deep_read_state } from './runtime.js'; -import { derived } from './reactivity/deriveds.js'; import { render_effect, effect, - managed_effect, pre_effect, user_effect, destroy_effect @@ -47,20 +39,11 @@ import { hydrating, set_current_hydration_fragment } from './hydration.js'; -import { - array_from, - define_property, - get_descriptor, - get_descriptors, - is_array, - is_function, - object_assign -} from './utils.js'; +import { array_from, define_property, get_descriptors, is_array, object_assign } from './utils.js'; import { run } from '../common.js'; import { bind_transition } from './transitions.js'; -import { mutable_source, source, set } from './reactivity/sources.js'; -import { safe_equals, safe_not_equal } from './reactivity/equality.js'; -import { ROOT_BLOCK, STATE_SYMBOL } from './constants.js'; +import { source, set } from './reactivity/sources.js'; +import { ROOT_BLOCK } from './constants.js'; /** @type {Set} */ const all_registered_events = new Set(); @@ -938,99 +921,6 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { } } -/** - * 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: Array }>}} - */ -const rest_props_handler = { - get(target, key) { - if (target.exclude.includes(key)) return; - return target.props[key]; - }, - getOwnPropertyDescriptor(target, key) { - if (target.exclude.includes(key)) return; - if (key in target.props) { - return { - enumerable: true, - configurable: true, - value: target.props[key] - }; - } - }, - has(target, key) { - if (target.exclude.includes(key)) return false; - return key in target.props; - }, - ownKeys(target) { - return Reflect.ownKeys(target.props).filter((key) => !target.exclude.includes(key)); - } -}; - -/** - * @param {Record} props - * @param {string[]} rest - * @returns {Record} - */ -export function rest_props(props, rest) { - return new Proxy({ props, exclude: rest }, rest_props_handler); -} - -/** - * The proxy handler for spread props. Handles the incoming array of props - * that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps - * them so that the whole thing is passed to the component as the `$$props` argument. - * @template {Record} T - * @type {ProxyHandler<{ props: Array T)> }>}} - */ -const spread_props_handler = { - get(target, key) { - let i = target.props.length; - while (i--) { - let p = target.props[i]; - if (is_function(p)) p = p(); - if (typeof p === 'object' && p !== null && key in p) return p[key]; - } - }, - getOwnPropertyDescriptor(target, key) { - let i = target.props.length; - while (i--) { - let p = target.props[i]; - if (is_function(p)) p = p(); - if (typeof p === 'object' && p !== null && key in p) return get_descriptor(p, key); - } - }, - has(target, key) { - for (let p of target.props) { - if (is_function(p)) p = p(); - if (key in p) return true; - } - - return false; - }, - ownKeys(target) { - /** @type {Array} */ - const keys = []; - - for (let p of target.props) { - if (is_function(p)) p = p(); - for (const key in p) { - if (!keys.includes(key)) keys.push(key); - } - } - - return keys; - } -}; - -/** - * @param {Array | (() => Record)>} props - * @returns {any} - */ -export function spread_props(...props) { - return new Proxy({ props }, spread_props_handler); -} - // TODO 5.0 remove this /** * @deprecated Use `mount` or `hydrate` instead @@ -1312,119 +1202,6 @@ function get_root_for_style(node) { return /** @type {Document} */ (node.ownerDocument); } -/** - * This function is responsible for synchronizing a possibly bound prop with the inner component state. - * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. - * @template V - * @param {Record} props - * @param {string} key - * @param {number} flags - * @param {V | (() => V)} [initial] - * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} - */ -export function prop(props, key, flags, initial) { - var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; - var runes = (flags & PROPS_IS_RUNES) !== 0; - var prop_value = /** @type {V} */ (props[key]); - var setter = get_descriptor(props, key)?.set; - - if (prop_value === undefined && initial !== undefined) { - if (setter && runes) { - // TODO consolidate all these random runtime errors - throw new Error( - 'ERR_SVELTE_BINDING_FALLBACK' + - (DEV - ? `: Cannot pass undefined to bind:${key} because the property contains a fallback value. Pass a different value than undefined to ${key}.` - : '') - ); - } - - // @ts-expect-error would need a cumbersome method overload to type this - if ((flags & PROPS_IS_LAZY_INITIAL) !== 0) initial = initial(); - - prop_value = /** @type {V} */ (initial); - - if (setter) setter(prop_value); - } - - var getter = () => { - var value = /** @type {V} */ (props[key]); - if (value !== undefined) initial = undefined; - return value === undefined ? /** @type {V} */ (initial) : value; - }; - - // easy mode — prop is never written to - if ((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 - if (setter) { - return function (/** @type {V} */ value) { - if (arguments.length === 1) { - /** @type {Function} */ (setter)(value); - return value; - } else { - 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); - }); - - if (!immutable) current_value.equals = safe_equals; - - return function (/** @type {V} */ value, mutation = false) { - var current = get(current_value); - - // 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 (is_signals_recorded || (DEV && inspect_fn)) { - // set this so that we don't reset to the parent value if `d` - // is invalidated because of `invalidate_inner_signals` (rather - // than because the parent or child value changed) - from_child = was_from_child; - // invoke getters so that signals are picked up by `invalidate_inner_signals` - getter(); - get(inner_current_value); - } - - if (arguments.length > 0) { - if (mutation || (immutable ? value !== current : safe_not_equal(value, current))) { - from_child = true; - set(inner_current_value, mutation ? current : value); - get(current_value); // force a synchronisation immediately - } - - return value; - } - - return current; - }; -} - /** * Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects */