chore: use proxy instead of signal in createRoot (#9799)

* use proxy instead of signal in createRoot

* DRY

* remove for now

* lint

* chore: use proxies instead of signals for spread/rest props (#9801)

* use proxies instead of signals for spread/rest

* fix some spread attribute stuff

* remove is_signal calls

* simplify some more

* more

* remove some unnecessary unwrapping

* another

* simplify

* simplify

* simplify

* remove another MaybeSignal

* more

* remove more unwraps

* code-golf, docs

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>

* add missing jsdoc annotation

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
pull/9814/head
Rich Harris 2 years ago committed by GitHub
parent 3c2e656187
commit 01a2117330
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -456,6 +456,7 @@ function read_attribute(parser) {
expression, expression,
parent: null, parent: null,
metadata: { metadata: {
contains_call_expression: false,
dynamic: false dynamic: false
} }
}; };

@ -906,7 +906,10 @@ const common_visitors = {
} }
}, },
CallExpression(node, context) { 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; context.state.expression.metadata.contains_call_expression = true;
} }

@ -67,7 +67,7 @@ export function serialize_get_binding(node, state) {
if (binding.kind === 'prop' && binding.node.name === '$$props') { if (binding.kind === 'prop' && binding.node.name === '$$props') {
// Special case for $$props which only exists in the old world // Special case for $$props which only exists in the old world
return b.call('$.unwrap', node); return node;
} }
if ( if (
@ -88,7 +88,6 @@ export function serialize_get_binding(node, state) {
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' || binding.kind === 'derived' ||
binding.kind === 'prop' || binding.kind === 'prop' ||
binding.kind === 'rest_prop' ||
binding.kind === 'legacy_reactive' binding.kind === 'legacy_reactive'
) { ) {
return b.call('$.get', node); return b.call('$.get', node);

@ -7,7 +7,7 @@ export const global_visitors = {
Identifier(node, { path, state }) { Identifier(node, { path, state }) {
if (is_reference(node, /** @type {import('estree').Node} */ (path.at(-1)))) { if (is_reference(node, /** @type {import('estree').Node} */ (path.at(-1)))) {
if (node.name === '$$props') { if (node.name === '$$props') {
return b.call('$.get', b.id('$$sanitized_props')); return b.id('$$sanitized_props');
} }
return serialize_get_binding(node, state); return serialize_get_binding(node, state);
} }

@ -756,7 +756,20 @@ function serialize_inline_component(node, component_name, context) {
} }
events[attribute.name].push(handler); events[attribute.name].push(handler);
} else if (attribute.type === 'SpreadAttribute') { } 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') { } else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) { if (attribute.name.startsWith('--')) {
custom_css_props.push( 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.object(/** @type {import('estree').Property[]} */ (props_and_spreads[0]) || [])
: b.call( : b.call(
'$.spread_props', '$.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 */ /** @param {import('estree').Identifier} node_id */
let fn = (node_id) => let fn = (node_id) =>
@ -2764,8 +2777,8 @@ export const template_visitors = {
} }
}, },
LetDirective(node, { state }) { LetDirective(node, { state }) {
// let:x --> const x = $.derived(() => $.unwrap($$slotProps).x); // let:x --> const x = $.derived(() => $$slotProps.x);
// let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $.unwrap($$slotProps).x; return { y, z })); // let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $$slotProps.x; return { y, z }));
if (node.expression && node.expression.type !== 'Identifier') { if (node.expression && node.expression.type !== 'Identifier') {
const name = state.scope.generate(node.name); const name = state.scope.generate(node.name);
const bindings = state.scope.get_bindings(node); const bindings = state.scope.get_bindings(node);
@ -2787,7 +2800,7 @@ export const template_visitors = {
b.object_pattern(node.expression.properties) 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 : // @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.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)))) 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; const name = node.expression === null ? node.name : node.expression.name;
return b.const( return b.const(
name, name,
b.call( b.call('$.derived', b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
'$.derived',
b.thunk(b.member(b.call('$.unwrap', b.id('$$slotProps')), b.id(node.name)))
)
); );
} }
}, },
@ -2854,7 +2864,9 @@ export const template_visitors = {
for (const attribute of node.attributes) { for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') { 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') { } else if (attribute.type === 'Attribute') {
const [, value] = serialize_attribute_value(attribute.value, context); const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.name === 'name') { if (attribute.name === 'name') {
@ -2873,7 +2885,7 @@ export const template_visitors = {
const props_expression = const props_expression =
spreads.length === 0 spreads.length === 0
? b.object(props) ? 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 = const fallback =
node.fragment.nodes.length === 0 node.fragment.nodes.length === 0
? b.literal(null) ? b.literal(null)
@ -2883,8 +2895,8 @@ export const template_visitors = {
); );
const expression = is_default const expression = is_default
? b.member(b.call('$.unwrap', b.id('$$props')), b.id('children')) ? b.member(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.member(b.id('$$props'), b.id('$$slots')), name, true, true);
const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback); const slot = b.call('$.slot', context.state.node, expression, props_expression, fallback);
context.state.init.push(b.stmt(slot)); context.state.init.push(b.stmt(slot));

@ -434,6 +434,7 @@ export interface SpreadAttribute extends BaseNode {
type: 'SpreadAttribute'; type: 'SpreadAttribute';
expression: Expression; expression: Expression;
metadata: { metadata: {
contains_call_expression: boolean;
dynamic: boolean; dynamic: boolean;
}; };
} }

@ -243,7 +243,7 @@ function reconcile_indexed_array(
flags, flags,
apply_transitions apply_transitions
) { ) {
var is_proxied_array = STATE_SYMBOL in array; var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i;
var a_blocks = each_block.v; var a_blocks = each_block.v;
var active_transitions = each_block.s; var active_transitions = each_block.s;
@ -351,7 +351,7 @@ function reconcile_tracked_array(
) { ) {
var a_blocks = each_block.v; var a_blocks = each_block.v;
const is_computed_key = keys !== null; const is_computed_key = keys !== null;
var is_proxied_array = STATE_SYMBOL in array; var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i;
var active_transitions = each_block.s; var active_transitions = each_block.s;
if (is_proxied_array) { if (is_proxied_array) {

@ -6,7 +6,8 @@ import {
increment, increment,
source, source,
updating_derived, updating_derived,
UNINITIALIZED UNINITIALIZED,
mutable_source
} from '../runtime.js'; } from '../runtime.js';
import { import {
define_property, define_property,
@ -17,7 +18,7 @@ import {
} from '../utils.js'; } from '../utils.js';
import { READONLY_SYMBOL } from './readonly.js'; import { READONLY_SYMBOL } from './readonly.js';
/** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean }} Metadata */ /** @typedef {{ s: Map<string | symbol, import('../types.js').SourceSignal<any>>; v: import('../types.js').SourceSignal<number>; a: boolean, i: boolean }} Metadata */
/** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */ /** @typedef {Record<string | symbol, any> & { [STATE_SYMBOL]: Metadata }} StateObject */
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
@ -30,15 +31,16 @@ const is_frozen = Object.isFrozen;
/** /**
* @template {StateObject} T * @template {StateObject} T
* @param {T} value * @param {T} value
* @param {boolean} [immutable]
* @returns {T} * @returns {T}
*/ */
export function proxy(value) { export function proxy(value, immutable = true) {
if (typeof value === 'object' && value != null && !is_frozen(value) && !(STATE_SYMBOL in value)) { if (typeof value === 'object' && value != null && !is_frozen(value) && !(STATE_SYMBOL in value)) {
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
// TODO handle Map and Set as well // TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) { if (prototype === object_prototype || prototype === array_prototype) {
define_property(value, STATE_SYMBOL, { value: init(value), writable: false }); define_property(value, STATE_SYMBOL, { value: init(value, immutable), writable: false });
// @ts-expect-error not sure how to fix this // @ts-expect-error not sure how to fix this
return new Proxy(value, handler); return new Proxy(value, handler);
@ -100,13 +102,15 @@ export function unstate(value) {
/** /**
* @param {StateObject} value * @param {StateObject} value
* @param {boolean} immutable
* @returns {Metadata} * @returns {Metadata}
*/ */
function init(value) { function init(value, immutable) {
return { return {
s: new Map(), s: new Map(),
v: source(0), v: source(0),
a: is_array(value) a: is_array(value),
i: immutable
}; };
} }
@ -117,7 +121,7 @@ const handler = {
const metadata = target[STATE_SYMBOL]; const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop); const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(descriptor.value)); if (s !== undefined) set(s, proxy(descriptor.value, metadata.i));
} }
return Reflect.defineProperty(target, prop, descriptor); return Reflect.defineProperty(target, prop, descriptor);
@ -147,7 +151,7 @@ const handler = {
(effect_active() || updating_derived) && (effect_active() || updating_derived) &&
(!(prop in target) || get_descriptor(target, prop)?.writable) (!(prop in target) || get_descriptor(target, prop)?.writable)
) { ) {
s = source(proxy(target[prop])); s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i));
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
@ -179,7 +183,9 @@ const handler = {
let s = metadata.s.get(prop); let s = metadata.s.get(prop);
if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) { if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) {
if (s === undefined) { if (s === undefined) {
s = source(has ? proxy(target[prop]) : UNINITIALIZED); s = (metadata.i ? source : mutable_source)(
has ? proxy(target[prop], metadata.i) : UNINITIALIZED
);
metadata.s.set(prop, s); metadata.s.set(prop, s);
} }
const value = get(s); const value = get(s);
@ -197,7 +203,7 @@ const handler = {
} }
const metadata = target[STATE_SYMBOL]; const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop); const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(value)); if (s !== undefined) set(s, proxy(value, metadata.i));
const is_array = metadata.a; const is_array = metadata.a;
const not_has = !(prop in target); const not_has = !(prop in target);
@ -234,6 +240,12 @@ const handler = {
} }
}; };
/** @param {any} object */
export function observe(object) {
const metadata = object[STATE_SYMBOL];
if (metadata) get(metadata.v);
}
if (DEV) { if (DEV) {
handler.setPrototypeOf = () => { handler.setPrototypeOf = () => {
throw new Error('Cannot set prototype of $state object'); throw new Error('Cannot set prototype of $state object');

@ -27,22 +27,17 @@ import {
get, get,
is_signal, is_signal,
push_destroy_fn, push_destroy_fn,
set,
execute_effect, execute_effect,
UNINITIALIZED, UNINITIALIZED,
derived,
untrack, untrack,
effect, effect,
flushSync, flushSync,
safe_not_equal, safe_not_equal,
current_block, current_block,
source,
managed_effect, managed_effect,
push, push,
current_component_context, current_component_context,
pop, pop
unwrap,
mutable_source
} from './runtime.js'; } from './runtime.js';
import { import {
current_hydration_fragment, current_hydration_fragment,
@ -56,12 +51,13 @@ import {
get_descriptor, get_descriptor,
get_descriptors, get_descriptors,
is_array, is_array,
is_function,
object_assign, object_assign,
object_entries,
object_keys object_keys
} from './utils.js'; } from './utils.js';
import { is_promise } from '../common.js'; import { is_promise } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js'; import { bind_transition, trigger_transitions } from './transitions.js';
import { proxy } from './proxy/proxy.js';
/** @type {Set<string>} */ /** @type {Set<string>} */
const all_registerd_events = new Set(); const all_registerd_events = new Set();
@ -227,7 +223,7 @@ export function close_frag(anchor, dom) {
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function trusted(fn) { export function trusted(fn) {
@ -235,13 +231,13 @@ export function trusted(fn) {
const event = /** @type {Event} */ (args[0]); const event = /** @type {Event} */ (args[0]);
if (event.isTrusted) { if (event.isTrusted) {
// @ts-ignore // @ts-ignore
unwrap(fn).apply(this, args); fn.apply(this, args);
} }
}; };
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function self(fn) { export function self(fn) {
@ -250,13 +246,13 @@ export function self(fn) {
// @ts-ignore // @ts-ignore
if (event.target === this) { if (event.target === this) {
// @ts-ignore // @ts-ignore
unwrap(fn).apply(this, args); fn.apply(this, args);
} }
}; };
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function stopPropagation(fn) { export function stopPropagation(fn) {
@ -264,12 +260,12 @@ export function stopPropagation(fn) {
const event = /** @type {Event} */ (args[0]); const event = /** @type {Event} */ (args[0]);
event.stopPropagation(); event.stopPropagation();
// @ts-ignore // @ts-ignore
return unwrap(fn).apply(this, args); return fn.apply(this, args);
}; };
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function once(fn) { export function once(fn) {
@ -280,12 +276,12 @@ export function once(fn) {
} }
ran = true; ran = true;
// @ts-ignore // @ts-ignore
return unwrap(fn).apply(this, args); return fn.apply(this, args);
}; };
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function stopImmediatePropagation(fn) { export function stopImmediatePropagation(fn) {
@ -293,12 +289,12 @@ export function stopImmediatePropagation(fn) {
const event = /** @type {Event} */ (args[0]); const event = /** @type {Event} */ (args[0]);
event.stopImmediatePropagation(); event.stopImmediatePropagation();
// @ts-ignore // @ts-ignore
return unwrap(fn).apply(this, args); return fn.apply(this, args);
}; };
} }
/** /**
* @param {import('./types.js').MaybeSignal<(event: Event, ...args: Array<unknown>) => void>} fn * @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void} * @returns {(event: Event, ...args: unknown[]) => void}
*/ */
export function preventDefault(fn) { export function preventDefault(fn) {
@ -306,7 +302,7 @@ export function preventDefault(fn) {
const event = /** @type {Event} */ (args[0]); const event = /** @type {Event} */ (args[0]);
event.preventDefault(); event.preventDefault();
// @ts-ignore // @ts-ignore
return unwrap(fn).apply(this, args); return fn.apply(this, args);
}; };
} }
@ -1188,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 * Makes an `export`ed (non-prop) variable available on the `$$props` object
* so that consumers can do `bind:x` on the component. * so that consumers can do `bind:x` on the component.
* @template V * @template V
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props * @param {Record<string, unknown>} props
* @param {string} prop * @param {string} prop
* @param {V} value * @param {V} value
* @returns {void} * @returns {void}
*/ */
export function bind_prop(props, prop, value) { export function bind_prop(props, prop, value) {
/** @param {V | null} value */ const desc = get_descriptor(props, prop);
const update = (value) => {
const current_props = unwrap(props); if (desc && desc.set) {
if (get_descriptor(current_props, prop)?.set !== undefined) { props[prop] = value;
current_props[prop] = value; render_effect(() => () => {
} props[prop] = null;
}; });
update(value); }
render_effect(() => () => {
update(null);
});
} }
/** /**
@ -2531,68 +2525,98 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
} }
/** /**
* @param {import('./types.js').Signal<Record<string, unknown>> | Record<string, unknown>} props_signal * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`).
* @param {string[]} rest * Is passed the full `$$props` object and excludes the named props.
* @returns {Record<string, unknown>} * @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol> }>}}
*/ */
export function rest_props(props_signal, rest) { const rest_props_handler = {
return derived(() => { get(target, key) {
var props = unwrap(props_signal); if (target.exclude.includes(key)) return;
return target.props[key];
/** @type {Record<string, unknown>} */ },
var rest_props = {}; getOwnPropertyDescriptor(target, key) {
if (target.exclude.includes(key)) return;
for (var key in props) { if (key in target.props) {
if (rest.includes(key)) continue; return {
enumerable: true,
const { get, value, enumerable } = /** @type {PropertyDescriptor} */ ( configurable: true,
get_descriptor(props, key) value: target.props[key]
); };
}
},
has(target, key) {
if (target.exclude.includes(key)) return false;
return key in target.props;
},
ownKeys(target) {
/** @type {Array<string | symbol>} */
const keys = [];
define_property(rest_props, key, get ? { get, enumerable } : { value, enumerable }); for (let key in target.props) {
if (!target.exclude.includes(key)) keys.push(key);
} }
return rest_props; return keys;
}); }
} };
/** /**
* @param {Record<string, unknown>[] | (() => Record<string, unknown>[])} props * @param {import('./types.js').Signal<Record<string, unknown>> | Record<string, unknown>} props
* @returns {any} * @param {string[]} rest
* @returns {Record<string, unknown>}
*/ */
export function spread_props(props) { export function rest_props(props, rest) {
if (typeof props === 'function') { return new Proxy({ props, exclude: rest }, rest_props_handler);
return derived(() => { }
return spread_props(props());
}); /**
} * The proxy handler for spread props. Handles the incoming array of props
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
/** @type {Record<string, unknown>} */ * them so that the whole thing is passed to the component as the `$$props` argument.
const merged_props = {}; * @template {Record<string | symbol, unknown>} T
let key; * @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
for (let i = 0; i < props.length; i++) { */
const obj = props[i]; const spread_props_handler = {
for (key in obj) { get(target, key) {
const desc = /** @type {PropertyDescriptor} */ (get_descriptor(obj, key)); let i = target.props.length;
const getter = desc.get; while (i--) {
if (getter !== undefined) { let p = target.props[i];
define_property(merged_props, key, { if (is_function(p)) p = p();
enumerable: true, if (typeof p === 'object' && p !== null && key in p) return p[key];
configurable: true, }
get: getter },
}); getOwnPropertyDescriptor() {
} else if (desc.get !== undefined) { return { enumerable: true, configurable: true };
merged_props[key] = obj[key]; },
} else { has(target, key) {
define_property(merged_props, key, { for (let p of target.props) {
enumerable: true, if (is_function(p)) p = p();
configurable: true, if (key in p) return true;
value: obj[key] }
});
return false;
},
ownKeys(target) {
/** @type {Array<string | symbol>} */
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<string, unknown> | (() => Record<string, unknown>)>} props
* @returns {any}
*/
export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
} }
/** /**
@ -2616,73 +2640,14 @@ export function spread_props(props) {
* @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} * @returns {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }}
*/ */
export function createRoot(component, options) { export function createRoot(component, options) {
// The following definitions aren't duplicative. We need _sources to update single props and const props = proxy(/** @type {any} */ (options.props) || {}, false);
// _props in case the component uses $$props / $$restProps / const { x, ...rest } = $props().
/** @type {any} */
const _props = {};
/** @type {any} */
const _sources = {};
/**
* @param {string} name
* @param {any} value
*/
function add_prop(name, value) {
const prop = source(value);
_sources[name] = prop;
define_property(_props, name, {
get() {
return get(prop);
},
enumerable: true
});
}
for (const prop in options.props || {}) { let [accessors, $destroy] = mount(component, { ...options, props });
add_prop(
prop,
// @ts-expect-error TS doesn't understand this properly
options.props[prop]
);
}
// The proxy ensures that we can add new signals on the fly when a prop signal is accessed from within the component
// but no corresponding prop value was set from the outside. The whole things becomes a _propsSignal
// so that adding new props is reflected in the component if it uses $$props or $$restProps.
const props_proxy = new Proxy(_props, {
/**
* @param {any} target
* @param {any} property
*/
get: (target, property) => {
if (typeof property !== 'string') return target[property];
if (!(property in _sources)) {
add_prop(property, undefined);
}
return _props[property];
}
});
// We're resetting the same proxy instance for updates, therefore bypass equality checks
const props_source = mutable_source(props_proxy);
let [accessors, $destroy] = mount(component, {
...options,
// @ts-expect-error We hide the "the props object could be a signal" fact from the public typings
props: props_source
});
const result = const result =
/** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({ /** @type {Exports & { $destroy: () => void; $set: (props: Partial<Props>) => void; }} */ ({
$set: (props) => { $set: (next) => {
for (const [prop, value] of object_entries(props)) { object_assign(props, next);
if (prop in _sources) {
set(_sources[prop], value);
} else {
add_prop(prop, value);
set(props_source, props_proxy);
}
}
}, },
$destroy $destroy
}); });
@ -2837,11 +2802,10 @@ export function access_props(props) {
} }
/** /**
* @param {import('./types.js').MaybeSignal<Record<string, any>>} props * @param {Record<string, any>} props
* @returns {Record<string, any>} * @returns {Record<string, any>}
*/ */
export function sanitize_slots(props) { export function sanitize_slots(props) {
props = unwrap(props);
const sanitized = { ...props.$$slots }; const sanitized = { ...props.$$slots };
if (props.children) sanitized.default = props.children; if (props.children) sanitized.default = props.children;
return sanitized; return sanitized;

@ -4,6 +4,7 @@ import { EMPTY_FUNC, run_all } from '../common.js';
import { get_descriptor, get_descriptors, is_array } from './utils.js'; import { get_descriptor, get_descriptors, is_array } from './utils.js';
import { PROPS_CALL_DEFAULT_VALUE, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES } from '../../constants.js'; import { PROPS_CALL_DEFAULT_VALUE, PROPS_IS_IMMUTABLE, PROPS_IS_RUNES } from '../../constants.js';
import { readonly } from './proxy/readonly.js'; import { readonly } from './proxy/readonly.js';
import { observe } from './proxy/proxy.js';
export const SOURCE = 1; export const SOURCE = 1;
export const DERIVED = 1 << 1; export const DERIVED = 1 << 1;
@ -96,32 +97,6 @@ export function set_is_ssr(ssr) {
is_ssr = ssr; is_ssr = ssr;
} }
/**
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} 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 * @param {null | import('./types.js').ComponentContext} context
* @returns {boolean} * @returns {boolean}
@ -1414,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 * - 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 * from within the component itself, call the setter of the parent which will propagate the value change back
* @template V * @template V
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props_obj * @param {Record<string, unknown>} props
* @param {string} key * @param {string} key
* @param {number} flags * @param {number} flags
* @param {V | (() => V)} [default_value] * @param {V | (() => V)} [default_value]
* @returns {import('./types.js').Signal<V> | (() => V)} * @returns {import('./types.js').Signal<V> | (() => 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 call_default_value = (flags & PROPS_CALL_DEFAULT_VALUE) !== 0;
const immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; const immutable = (flags & PROPS_IS_IMMUTABLE) !== 0;
const runes = (flags & PROPS_IS_RUNES) !== 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; const update_bound_prop = get_descriptor(props, key)?.set;
let value = props[key]; let value = props[key];
const should_set_default_value = value === undefined && default_value !== undefined; const should_set_default_value = value === undefined && default_value !== undefined;
@ -1456,7 +1430,8 @@ export function prop_source(props_obj, key, flags, default_value) {
let mount = true; let mount = true;
sync_effect(() => { sync_effect(() => {
const props = is_signal(props_obj) ? get(props_obj) : props_obj; observe(props);
// Before if to ensure signal dependency is registered // Before if to ensure signal dependency is registered
const propagating_value = props[key]; const propagating_value = props[key];
if (mount) { if (mount) {
@ -1506,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`. * 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<Record<string, unknown>>} props_obj * @param {Record<string, unknown>} props
* @param {string} key * @param {string} key
* @returns {any} * @returns {any}
*/ */
export function prop(props_obj, key) { export function prop(props, key) {
return is_signal(props_obj) ? () => get(props_obj)[key] : () => props_obj[key]; // TODO skip this, and rewrite as `$$props.foo`
return () => props[key];
} }
/** /**
@ -1591,23 +1567,18 @@ function get_parent_context(component_context) {
/** /**
* @this {any} * @this {any}
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} $$props * @param {Record<string, unknown>} $$props
* @param {Event} event * @param {Event} event
* @returns {void} * @returns {void}
*/ */
export function bubble_event($$props, event) { export function bubble_event($$props, event) {
const events = /** @type {Record<string, Function[] | Function>} */ (unwrap($$props).$$events)?.[ var events = /** @type {Record<string, Function[] | Function>} */ ($$props.$$events)?.[
event.type event.type
]; ];
const callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; var callbacks = is_array(events) ? events.slice() : events == null ? [] : [events];
let fn; for (var fn of callbacks) {
for (fn of callbacks) {
// Preserve "this" context // Preserve "this" context
if (is_signal(fn)) { fn.call(this, event);
get(fn).call(this, event);
} else {
fn.call(this, event);
}
} }
} }
@ -1752,14 +1723,29 @@ export function onDestroy(fn) {
} }
/** /**
* @param {import('./types.js').MaybeSignal<Record<string, unknown>>} props * @param {Record<string, unknown>} props
* @param {any} runes * @param {any} runes
* @returns {void} * @returns {void}
*/ */
export function push(props, runes = false) { export function push(props, runes = false) {
const context_stack_item = create_component_context(props); current_component_context = {
context_stack_item.r = runes; // accessors
current_component_context = context_stack_item; 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
};
} }
/** /**
@ -1815,7 +1801,7 @@ function deep_read(value, visited = new Set()) {
} }
/** /**
* @param {() => import('./types.js').MaybeSignal<>} get_value * @param {() => any} get_value
* @param {Function} inspect * @param {Function} inspect
* @returns {void} * @returns {void}
*/ */

@ -36,7 +36,7 @@ export type Store<V> = {
export type ComponentContext = { export type ComponentContext = {
/** props */ /** props */
s: MaybeSignal<Record<string, unknown>>; s: Record<string, unknown>;
/** accessors */ /** accessors */
a: Record<string, any> | null; a: Record<string, any> | null;
/** effectgs */ /** effectgs */

@ -8,3 +8,11 @@ export var object_assign = Object.assign;
export var define_property = Object.defineProperty; export var define_property = Object.defineProperty;
export var get_descriptor = Object.getOwnPropertyDescriptor; export var get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors; export var get_descriptors = Object.getOwnPropertyDescriptors;
/**
* @param {any} thing
* @returns {thing is Function}
*/
export function is_function(thing) {
return typeof thing === 'function';
}

@ -5,13 +5,10 @@ import {
is_ssr, is_ssr,
managed_effect, managed_effect,
untrack, untrack,
is_signal,
get,
user_effect, user_effect,
flush_local_render_effects flush_local_render_effects
} from '../internal/client/runtime.js'; } from '../internal/client/runtime.js';
import { is_array } from '../internal/client/utils.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. * 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) => { return (type, detail, options) => {
const $$events = /** @type {Record<string, Function | Function[]>} */ ( const events = /** @type {Record<string, Function | Function[]>} */ (
unwrap(component_context.s).$$events component_context.s.$$events
); )?.[/** @type {any} */ (type)];
const events = $$events?.[/** @type {any} */ (type)];
if (events) { if (events) {
const callbacks = is_array(events) ? events.slice() : [events]; const callbacks = is_array(events) ? events.slice() : [events];
@ -150,11 +146,7 @@ export function createEventDispatcher() {
// in a server (non-DOM) environment? // in a server (non-DOM) environment?
const event = create_custom_event(/** @type {string} */ (type), detail, options); const event = create_custom_event(/** @type {string} */ (type), detail, options);
for (const fn of callbacks) { for (const fn of callbacks) {
if (is_signal(fn)) { fn.call(component_context.a, event);
get(fn).call(component_context.a, event);
} else {
fn.call(component_context.a, event);
}
} }
return !event.defaultPrevented; return !event.defaultPrevented;
} }

@ -10,6 +10,6 @@ export default test({
}); });
await Promise.resolve(); await Promise.resolve();
assert.htmlEqual(target.innerHTML, '{"visible":true,"foo":"bar"} {"foo":"bar"}'); assert.htmlEqual(target.innerHTML, '{"foo":"bar","visible":true} {"foo":"bar"}');
} }
}); });

@ -1,5 +1,5 @@
<script> <script>
const { foo, default1 = 1, default2 = 2, default3 = 3, ...others } = $props(); const { foo, default1 = 1, default2 = 2, default3 = 3, ...others } = $props();
</script> </script>
{foo} {default1} {default2} {default3} {others.bar} {foo} {default1} {default2} {default3} {others.bar}
Loading…
Cancel
Save