diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 019e29c9445b..6b38da217ef2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -456,6 +456,7 @@ function read_attribute(parser) { expression, parent: null, metadata: { + contains_call_expression: false, dynamic: false } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1be80cc16986..be66f41d270e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -906,7 +906,10 @@ const common_visitors = { } }, CallExpression(node, context) { - if (context.state.expression?.type === 'ExpressionTag' && !is_known_safe_call(node, context)) { + if ( + context.state.expression?.type === 'ExpressionTag' || + (context.state.expression?.type === 'SpreadAttribute' && !is_known_safe_call(node, context)) + ) { context.state.expression.metadata.contains_call_expression = true; } 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 35e200b06e10..1116afc330c3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -67,7 +67,7 @@ export function serialize_get_binding(node, state) { if (binding.kind === 'prop' && binding.node.name === '$$props') { // Special case for $$props which only exists in the old world - return b.call('$.unwrap', node); + return node; } if ( @@ -88,7 +88,6 @@ export function serialize_get_binding(node, state) { (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || binding.kind === 'derived' || binding.kind === 'prop' || - binding.kind === 'rest_prop' || binding.kind === 'legacy_reactive' ) { return b.call('$.get', node); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 4b053e0566e2..51032cdd5f6e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -7,7 +7,7 @@ export const global_visitors = { Identifier(node, { path, state }) { if (is_reference(node, /** @type {import('estree').Node} */ (path.at(-1)))) { if (node.name === '$$props') { - return b.call('$.get', b.id('$$sanitized_props')); + return b.id('$$sanitized_props'); } return serialize_get_binding(node, state); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index fb43ec654b11..6579d17c01b5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -756,7 +756,20 @@ function serialize_inline_component(node, component_name, context) { } events[attribute.name].push(handler); } else if (attribute.type === 'SpreadAttribute') { - props_and_spreads.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); + const expression = /** @type {import('estree').Expression} */ (context.visit(attribute)); + if (attribute.metadata.dynamic) { + let value = expression; + + if (attribute.metadata.contains_call_expression) { + const id = b.id(context.state.scope.generate('spread_element')); + context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); + value = b.call('$.get', id); + } + + props_and_spreads.push(b.thunk(value)); + } else { + props_and_spreads.push(expression); + } } else if (attribute.type === 'Attribute') { if (attribute.name.startsWith('--')) { custom_css_props.push( @@ -895,7 +908,7 @@ function serialize_inline_component(node, component_name, context) { ? b.object(/** @type {import('estree').Property[]} */ (props_and_spreads[0]) || []) : b.call( '$.spread_props', - b.thunk(b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)))) + ...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)) ); /** @param {import('estree').Identifier} node_id */ let fn = (node_id) => @@ -2764,8 +2777,8 @@ export const template_visitors = { } }, LetDirective(node, { state }) { - // let:x --> const x = $.derived(() => $.unwrap($$slotProps).x); - // let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $.unwrap($$slotProps).x; return { y, z })); + // let:x --> const x = $.derived(() => $$slotProps.x); + // let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $$slotProps.x; return { y, z })); if (node.expression && node.expression.type !== 'Identifier') { const name = state.scope.generate(node.name); const bindings = state.scope.get_bindings(node); @@ -2787,7 +2800,7 @@ export const template_visitors = { b.object_pattern(node.expression.properties) : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine b.array_pattern(node.expression.elements), - b.member(b.call('$.unwrap', b.id('$$slotProps')), b.id(node.name)) + b.member(b.id('$$slotProps'), b.id(node.name)) ), b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node)))) ]) @@ -2798,10 +2811,7 @@ export const template_visitors = { const name = node.expression === null ? node.name : node.expression.name; return b.const( name, - b.call( - '$.derived', - b.thunk(b.member(b.call('$.unwrap', b.id('$$slotProps')), b.id(node.name))) - ) + b.call('$.derived', b.thunk(b.member(b.id('$$slotProps'), b.id(node.name)))) ); } }, @@ -2854,7 +2864,9 @@ export const template_visitors = { for (const attribute of node.attributes) { if (attribute.type === 'SpreadAttribute') { - spreads.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); + spreads.push( + b.thunk(/** @type {import('estree').Expression} */ (context.visit(attribute))) + ); } else if (attribute.type === 'Attribute') { const [, value] = serialize_attribute_value(attribute.value, context); if (attribute.name === 'name') { @@ -2873,7 +2885,7 @@ export const template_visitors = { const props_expression = spreads.length === 0 ? b.object(props) - : b.call('$.spread_props', b.thunk(b.array([b.object(props), ...spreads]))); + : b.call('$.spread_props', b.object(props), ...spreads); const fallback = node.fragment.nodes.length === 0 ? b.literal(null) @@ -2883,8 +2895,8 @@ export const template_visitors = { ); const expression = is_default - ? b.member(b.call('$.unwrap', b.id('$$props')), b.id('children')) - : b.member(b.member(b.call('$.unwrap', b.id('$$props')), b.id('$$slots')), name, true, true); + ? b.member(b.id('$$props'), b.id('children')) + : b.member(b.member(b.id('$$props'), b.id('$$slots')), name, true, true); const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); context.state.init.push(b.stmt(slot)); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 6343b7ae323b..4646dc6d32b2 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -434,6 +434,7 @@ export interface SpreadAttribute extends BaseNode { type: 'SpreadAttribute'; expression: Expression; metadata: { + contains_call_expression: boolean; dynamic: boolean; }; } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 3884aa068579..ef6a23d9781a 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -29,7 +29,6 @@ import { push_destroy_fn, execute_effect, UNINITIALIZED, - derived, untrack, effect, flushSync, @@ -38,8 +37,7 @@ import { managed_effect, push, current_component_context, - pop, - unwrap + pop } from './runtime.js'; import { current_hydration_fragment, @@ -53,12 +51,13 @@ import { get_descriptor, get_descriptors, is_array, + is_function, object_assign, object_keys } from './utils.js'; import { is_promise } from '../common.js'; import { bind_transition, trigger_transitions } from './transitions.js'; -import { observe, proxy } from './proxy/proxy.js'; +import { proxy } from './proxy/proxy.js'; /** @type {Set} */ const all_registerd_events = new Set(); @@ -224,7 +223,7 @@ export function close_frag(anchor, dom) { } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function trusted(fn) { @@ -232,13 +231,13 @@ export function trusted(fn) { const event = /** @type {Event} */ (args[0]); if (event.isTrusted) { // @ts-ignore - unwrap(fn).apply(this, args); + fn.apply(this, args); } }; } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function self(fn) { @@ -247,13 +246,13 @@ export function self(fn) { // @ts-ignore if (event.target === this) { // @ts-ignore - unwrap(fn).apply(this, args); + fn.apply(this, args); } }; } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function stopPropagation(fn) { @@ -261,12 +260,12 @@ export function stopPropagation(fn) { const event = /** @type {Event} */ (args[0]); event.stopPropagation(); // @ts-ignore - return unwrap(fn).apply(this, args); + return fn.apply(this, args); }; } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function once(fn) { @@ -277,12 +276,12 @@ export function once(fn) { } ran = true; // @ts-ignore - return unwrap(fn).apply(this, args); + return fn.apply(this, args); }; } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function stopImmediatePropagation(fn) { @@ -290,12 +289,12 @@ export function stopImmediatePropagation(fn) { const event = /** @type {Event} */ (args[0]); event.stopImmediatePropagation(); // @ts-ignore - return unwrap(fn).apply(this, args); + return fn.apply(this, args); }; } /** - * @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array) => void>} fn + * @param {(event: Event, ...args: Array) => void} fn * @returns {(event: Event, ...args: unknown[]) => void} */ export function preventDefault(fn) { @@ -303,7 +302,7 @@ export function preventDefault(fn) { const event = /** @type {Event} */ (args[0]); event.preventDefault(); // @ts-ignore - return unwrap(fn).apply(this, args); + return fn.apply(this, args); }; } @@ -1185,27 +1184,25 @@ export function bind_property(property, event_name, type, dom, get_value, update } }); } + /** * Makes an `export`ed (non-prop) variable available on the `$$props` object * so that consumers can do `bind:x` on the component. * @template V - * @param {import('./types.js').MaybeSignal>} props + * @param {Record} props * @param {string} prop * @param {V} value * @returns {void} */ export function bind_prop(props, prop, value) { - /** @param {V | null} value */ - const update = (value) => { - const current_props = unwrap(props); - if (get_descriptor(current_props, prop)?.set !== undefined) { - current_props[prop] = value; - } - }; - update(value); - render_effect(() => () => { - update(null); - }); + const desc = get_descriptor(props, prop); + + if (desc && desc.set) { + props[prop] = value; + render_effect(() => () => { + props[prop] = null; + }); + } } /** @@ -2528,71 +2525,98 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { } /** - * @param {import('./types.js').Signal> | Record} props_signal - * @param {string[]} rest - * @returns {Record} + * 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 }>}} */ -export function rest_props(props_signal, rest) { - return derived(() => { - var props = unwrap(props_signal); - - observe(props); - - /** @type {Record} */ - var rest_props = {}; - - for (const key in props) { - if (rest.includes(key)) continue; - - const { enumerable } = /** @type {PropertyDescriptor} */ (get_descriptor(props, key)); +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) { + /** @type {Array} */ + const keys = []; - define_property(rest_props, key, { - get: () => props[key], - enumerable - }); + for (let key in target.props) { + if (!target.exclude.includes(key)) keys.push(key); } - return rest_props; - }); -} + return keys; + } +}; /** - * @param {Record[] | (() => Record[])} props - * @returns {any} + * @param {import('./types.js').Signal> | Record} props + * @param {string[]} rest + * @returns {Record} */ -export function spread_props(props) { - if (typeof props === 'function') { - return derived(() => { - return spread_props(props()); - }); - } - - /** @type {Record} */ - const merged_props = {}; - let key; - for (let i = 0; i < props.length; i++) { - const obj = props[i]; - for (key in obj) { - const desc = /** @type {PropertyDescriptor} */ (get_descriptor(obj, key)); - const getter = desc.get; - if (getter !== undefined) { - define_property(merged_props, key, { - enumerable: true, - configurable: true, - get: getter - }); - } else if (desc.get !== undefined) { - merged_props[key] = obj[key]; - } else { - define_property(merged_props, key, { - enumerable: true, - configurable: true, - value: obj[key] - }); +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() { + return { enumerable: true, configurable: true }; + }, + 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; } - return merged_props; +}; + +/** + * @param {Array | (() => Record)>} props + * @returns {any} + */ +export function spread_props(...props) { + return new Proxy({ props }, spread_props_handler); } /** @@ -2778,11 +2802,10 @@ export function access_props(props) { } /** - * @param {import('./types.js').MaybeSignal>} props + * @param {Record} props * @returns {Record} */ export function sanitize_slots(props) { - props = unwrap(props); const sanitized = { ...props.$$slots }; if (props.children) sanitized.default = props.children; return sanitized; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9a0fe975ae39..5d92c436e858 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -97,32 +97,6 @@ export function set_is_ssr(ssr) { is_ssr = ssr; } -/** - * @param {import('./types.js').MaybeSignal>} props - * @returns {import('./types.js').ComponentContext} - */ -export function create_component_context(props) { - const parent = current_component_context; - return { - // accessors - a: null, - // context - c: null, - // effects - e: null, - // mounted - m: false, - // parent - p: parent, - // props - s: props, - // runes - r: false, - // update_callbacks - u: null - }; -} - /** * @param {null | import('./types.js').ComponentContext} context * @returns {boolean} @@ -1415,18 +1389,17 @@ export function is_store(val) { * - otherwise create a signal that updates whenever the value is updated from the parent, and when it's updated * from within the component itself, call the setter of the parent which will propagate the value change back * @template V - * @param {import('./types.js').MaybeSignal>} props_obj + * @param {Record} props * @param {string} key * @param {number} flags * @param {V | (() => V)} [default_value] * @returns {import('./types.js').Signal | (() => V)} */ -export function prop_source(props_obj, key, flags, default_value) { +export function prop_source(props, key, flags, default_value) { const call_default_value = (flags & PROPS_CALL_DEFAULT_VALUE) !== 0; const immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; const runes = (flags & PROPS_IS_RUNES) !== 0; - const props = is_signal(props_obj) ? get(props_obj) : props_obj; const update_bound_prop = get_descriptor(props, key)?.set; let value = props[key]; const should_set_default_value = value === undefined && default_value !== undefined; @@ -1457,9 +1430,7 @@ export function prop_source(props_obj, key, flags, default_value) { let mount = true; sync_effect(() => { - const props = is_signal(props_obj) ? get(props_obj) : props_obj; - - observe(props_obj); + observe(props); // Before if to ensure signal dependency is registered const propagating_value = props[key]; @@ -1510,12 +1481,13 @@ export function prop_source(props_obj, key, flags, default_value) { /** * If the prop is readonly and has no fallback value, we can use this function, else we need to use `prop_source`. - * @param {import('./types.js').MaybeSignal>} props_obj + * @param {Record} props * @param {string} key * @returns {any} */ -export function prop(props_obj, key) { - return is_signal(props_obj) ? () => get(props_obj)[key] : () => props_obj[key]; +export function prop(props, key) { + // TODO skip this, and rewrite as `$$props.foo` + return () => props[key]; } /** @@ -1595,23 +1567,18 @@ function get_parent_context(component_context) { /** * @this {any} - * @param {import('./types.js').MaybeSignal>} $$props + * @param {Record} $$props * @param {Event} event * @returns {void} */ export function bubble_event($$props, event) { - const events = /** @type {Record} */ (unwrap($$props).$$events)?.[ + var events = /** @type {Record} */ ($$props.$$events)?.[ event.type ]; - const callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; - let fn; - for (fn of callbacks) { + var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; + for (var fn of callbacks) { // Preserve "this" context - if (is_signal(fn)) { - get(fn).call(this, event); - } else { - fn.call(this, event); - } + fn.call(this, event); } } @@ -1756,14 +1723,29 @@ export function onDestroy(fn) { } /** - * @param {import('./types.js').MaybeSignal>} props + * @param {Record} props * @param {any} runes * @returns {void} */ export function push(props, runes = false) { - const context_stack_item = create_component_context(props); - context_stack_item.r = runes; - current_component_context = context_stack_item; + current_component_context = { + // accessors + a: null, + // context + c: null, + // effects + e: null, + // mounted + m: false, + // parent + p: current_component_context, + // props + s: props, + // runes + r: runes, + // update_callbacks + u: null + }; } /** @@ -1819,7 +1801,7 @@ function deep_read(value, visited = new Set()) { } /** - * @param {() => import('./types.js').MaybeSignal<>} get_value + * @param {() => any} get_value * @param {Function} inspect * @returns {void} */ diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 13bfbc16cd7b..55f37c66d763 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -36,7 +36,7 @@ export type Store = { export type ComponentContext = { /** props */ - s: MaybeSignal>; + s: Record; /** accessors */ a: Record | null; /** effectgs */ diff --git a/packages/svelte/src/internal/client/utils.js b/packages/svelte/src/internal/client/utils.js index 952bc9db8afe..627d2b34fb95 100644 --- a/packages/svelte/src/internal/client/utils.js +++ b/packages/svelte/src/internal/client/utils.js @@ -8,3 +8,11 @@ export var object_assign = Object.assign; export var define_property = Object.defineProperty; export var get_descriptor = Object.getOwnPropertyDescriptor; export var get_descriptors = Object.getOwnPropertyDescriptors; + +/** + * @param {any} thing + * @returns {thing is Function} + */ +export function is_function(thing) { + return typeof thing === 'function'; +} diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index f53173bb66a1..5924eb6c0159 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -5,13 +5,10 @@ import { is_ssr, managed_effect, untrack, - is_signal, - get, user_effect, flush_local_render_effects } from '../internal/client/runtime.js'; import { is_array } from '../internal/client/utils.js'; -import { unwrap } from '../internal/index.js'; /** * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. @@ -139,10 +136,9 @@ export function createEventDispatcher() { } return (type, detail, options) => { - const $$events = /** @type {Record} */ ( - unwrap(component_context.s).$$events - ); - const events = $$events?.[/** @type {any} */ (type)]; + const events = /** @type {Record} */ ( + component_context.s.$$events + )?.[/** @type {any} */ (type)]; if (events) { const callbacks = is_array(events) ? events.slice() : [events]; @@ -150,11 +146,7 @@ export function createEventDispatcher() { // in a server (non-DOM) environment? const event = create_custom_event(/** @type {string} */ (type), detail, options); for (const fn of callbacks) { - if (is_signal(fn)) { - get(fn).call(component_context.a, event); - } else { - fn.call(component_context.a, event); - } + fn.call(component_context.a, event); } return !event.defaultPrevented; }