UNPKG

svelte

Version:

Cybernetically enhanced web apps

427 lines (377 loc) 12.1 kB
/** @import { ComponentContext } from '#client' */ /** @import { Derived, Effect, 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, UNINITIALIZED } 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 { active_effect, get, is_destroying_effect, set_active_effect, untrack } from '../runtime.js'; import * as e from '../errors.js'; import { DESTROYED, 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>, parent_effect: Effect }>}} */ 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)) { var previous_effect = active_effect; try { set_active_effect(target.parent_effect); // 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 ); } finally { set_active_effect(previous_effect); } } 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), // TODO this is only necessary because we need to track component // destruction inside `prop`, because of `bind:this`, but it // seems likely that we can simplify `bind:this` instead parent_effect: /** @type {Effect} */ (active_effect) }, 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); } /** * 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 overridden = false; var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => { overridden = false; return getter(); }); // Capture the initial value if it's bindable if (bindable) get(d); var parent_effect = /** @type {Effect} */ (active_effect); 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); overridden = true; if (fallback_value !== undefined) { fallback_value = new_value; } return value; } // 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 (this latter part is necessary // because `bind:this` can read props after the component has // been destroyed. TODO simplify `bind:this` if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) { return d.v; } return get(d); }; }