UNPKG

svelte

Version:

Cybernetically enhanced web apps

395 lines (348 loc) 11.2 kB
/** @import { Derived, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, PROPS_IS_UPDATED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; import { get, untrack } from '../runtime.js'; import * as e from '../errors.js'; 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'; /** * @param {((value?: number) => number)} fn * @param {1 | -1} [d] * @returns {number} */ export function update_prop(fn, d = 1) { const value = fn(); fn(value + d); return value; } /** * @param {((value?: number) => number)} fn * @param {1 | -1} [d] * @returns {number} */ export function update_pre_prop(fn, d = 1) { const value = fn() + d; 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<string | symbol, unknown>, exclude: Array<string | symbol>, name?: string }>}} */ const rest_props_handler = { get(target, key) { if (target.exclude.includes(key)) return; return target.props[key]; }, set(target, key) { if (DEV) { // TODO should this happen in prod too? e.props_rest_readonly(`${target.name}.${String(key)}`); } return false; }, 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<string, unknown>} props * @param {string[]} exclude * @param {string} [name] * @returns {Record<string, unknown>} */ /*#__NO_SIDE_EFFECTS__*/ export function rest_props(props, exclude, name) { return new Proxy( DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude }, rest_props_handler ); } /** * The proxy handler for legacy $$restProps and $$props * @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number> }>}} */ const legacy_rest_props_handler = { get(target, key) { if (target.exclude.includes(key)) return; get(target.version); return key in target.special ? target.special[key]() : target.props[key]; }, set(target, key, value) { if (!(key in target.special)) { // Handle props that can temporarily get out of sync with the parent /** @type {Record<string, (v?: unknown) => unknown>} */ target.special[key] = prop( { get [key]() { return target.props[key]; } }, /** @type {string} */ (key), PROPS_IS_UPDATED ); } target.special[key](value); update(target.version); // $$props is coarse-grained: when $$props.x is updated, usages of $$props.y etc are also rerun return true; }, getOwnPropertyDescriptor(target, key) { if (target.exclude.includes(key)) return; if (key in target.props) { return { enumerable: true, configurable: true, value: target.props[key] }; } }, deleteProperty(target, key) { // Svelte 4 allowed for deletions on $$restProps if (target.exclude.includes(key)) return true; target.exclude.push(key); update(target.version); return true; }, 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<string, unknown>} props * @param {string[]} exclude * @returns {Record<string, unknown>} */ export function legacy_rest_props(props, exclude) { return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_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<string | symbol, unknown>} T * @type {ProxyHandler<{ props: Array<T | (() => 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]; } }, set(target, key, value) { let i = target.props.length; while (i--) { let p = target.props[i]; if (is_function(p)) p = p(); const desc = get_descriptor(p, key); if (desc && desc.set) { desc.set(value); return true; } } return false; }, 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) { const descriptor = get_descriptor(p, key); if (descriptor && !descriptor.configurable) { // Prevent a "Non-configurability Report Error": The target is an array, it does // not actually contain this property. If it is now described as non-configurable, // the proxy throws a validation error. Setting it to true avoids that. descriptor.configurable = true; } return descriptor; } } }, has(target, key) { // To prevent a false positive `is_entry_props` in the `prop` function if (key === STATE_SYMBOL || key === LEGACY_PROPS) return false; for (let p of target.props) { if (is_function(p)) p = p(); if (p != null && key in p) return true; } return false; }, ownKeys(target) { /** @type {Array<string | symbol>} */ const keys = []; for (let p of target.props) { if (is_function(p)) p = p(); if (!p) continue; for (const key in p) { if (!keys.includes(key)) keys.push(key); } for (const key of Object.getOwnPropertySymbols(p)) { if (!keys.includes(key)) keys.push(key); } } return keys; } }; /** * @param {Array<Record<string, unknown> | (() => Record<string, unknown>)>} props * @returns {any} */ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } /** * @param {Derived} current_value * @returns {boolean} */ function has_destroyed_component_ctx(current_value) { return current_value.ctx?.d ?? false; } /** * 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<string, unknown>} props * @param {string} key * @param {number} flags * @param {V | (() => V)} [fallback] * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} */ 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 fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; var get_fallback = () => { if (fallback_dirty) { fallback_dirty = false; fallback_value = lazy ? untrack(/** @type {() => V} */ (fallback)) : /** @type {V} */ (fallback); } return fallback_value; }; /** @type {((v: V) => void) | undefined} */ var setter; 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; return value; }; } else { getter = () => { 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; }; } // prop is never written to — we only need a getter if (runes && (flags & PROPS_IS_UPDATED) === 0) { return getter; } // 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. // In that case the state proxy (if it exists) should take care of the notification. // If the parent is not in runes mode, we need to notify on mutation, too, that the prop // has changed because the parent will not be able to detect the change otherwise. if (!runes || !mutation || legacy_parent || is_store_sub) { /** @type {Function} */ (setter)(mutation ? getter() : value); } return value; } return getter(); }; } // 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); // Capture the initial value if it's bindable if (bindable) get(d); return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; set(d, new_value); if (fallback_value !== undefined) { fallback_value = new_value; } return value; } // TODO is this still necessary post-#16263? if (has_destroyed_component_ctx(d)) { return d.v; } return get(d); }; }