pull/16197/head
Rich Harris 7 months ago
commit ef28490c07

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: SSR-safe ID generation with `$props.id()`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ignore typescript abstract methods

@ -45,8 +45,6 @@ If a function is returned from `onMount`, it will be called when the component i
## `onDestroy` ## `onDestroy`
> EXPORT_SNIPPET: svelte#onDestroy
Schedules a callback to run immediately before the component is unmounted. Schedules a callback to run immediately before the component is unmounted.
Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component. Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component.

@ -1,5 +1,23 @@
# svelte # svelte
## 5.20.0
### Minor Changes
- feat: SSR-safe ID generation with `$props.id()` ([#15185](https://github.com/sveltejs/svelte/pull/15185))
### Patch Changes
- fix: take private and public into account for `constant_assignment` of derived state ([#15276](https://github.com/sveltejs/svelte/pull/15276))
- fix: value/checked not correctly set using spread ([#15239](https://github.com/sveltejs/svelte/pull/15239))
- chore: tweak effect self invalidation logic, run transition dispatches without reactive context ([#15275](https://github.com/sveltejs/svelte/pull/15275))
- fix: use `importNode` to clone templates for Firefox ([#15272](https://github.com/sveltejs/svelte/pull/15272))
- fix: recurse into `$derived` for ownership validation ([#15166](https://github.com/sveltejs/svelte/pull/15166))
## 5.19.10 ## 5.19.10
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.19.10", "version": "5.20.0",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -118,6 +118,12 @@ const visitors = {
delete node.implements; delete node.implements;
return context.next(); return context.next();
}, },
MethodDefinition(node, context) {
if (node.abstract) {
return b.empty;
}
return context.next();
},
VariableDeclaration(node, context) { VariableDeclaration(node, context) {
if (node.declare) { if (node.declare) {
return b.empty; return b.empty;

@ -19,7 +19,7 @@ export interface AnalysisState {
component_slots: Set<string>; component_slots: Set<string>;
/** Information about the current expression/directive/block value */ /** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null; expression: ExpressionMetadata | null;
derived_state: string[]; derived_state: { name: string; private: boolean }[];
function_depth: number; function_depth: number;
// legacy stuff // legacy stuff

@ -23,11 +23,6 @@ export function Attribute(node, context) {
if (node.name === 'value' && parent.name === 'option') { if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path); mark_subtree_dynamic(context.path);
} }
// special case <img loading="lazy" />
if (node.name === 'loading' && parent.name === 'img') {
mark_subtree_dynamic(context.path);
}
} }
if (is_event_attribute(node)) { if (is_event_attribute(node)) {

@ -7,7 +7,7 @@ import { get_rune } from '../../scope.js';
* @param {Context} context * @param {Context} context
*/ */
export function ClassBody(node, context) { export function ClassBody(node, context) {
/** @type {string[]} */ /** @type {{name: string, private: boolean}[]} */
const derived_state = []; const derived_state = [];
for (const definition of node.body) { for (const definition of node.body) {
@ -18,7 +18,10 @@ export function ClassBody(node, context) {
) { ) {
const rune = get_rune(definition.value, context.state.scope); const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') { if (rune === '$derived' || rune === '$derived.by') {
derived_state.push(definition.key.name); derived_state.push({
name: definition.key.name,
private: definition.key.type === 'PrivateIdentifier'
});
} }
} }
} }

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AssignmentExpression, Expression, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { AST, Binding } from '#compiler' */ /** @import { AST, Binding } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */ /** @import { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */ /** @import { Scope } from '../../../scope' */
@ -38,16 +38,22 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node); e.snippet_parameter_assignment(node);
} }
} }
if ( if (
argument.type === 'MemberExpression' && argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' && argument.object.type === 'ThisExpression' &&
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
state.derived_state.includes(argument.property.name)) || state.derived_state.some(
(derived) =>
derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name &&
derived.private === (argument.property.type === 'PrivateIdentifier')
)) ||
(argument.property.type === 'Literal' && (argument.property.type === 'Literal' &&
argument.property.value && argument.property.value &&
typeof argument.property.value === 'string' && typeof argument.property.value === 'string' &&
state.derived_state.includes(argument.property.value))) state.derived_state.some(
(derived) =>
derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
)))
) { ) {
e.constant_assignment(node, 'derived state'); e.constant_assignment(node, 'derived state');
} }

@ -190,22 +190,21 @@ export function ClassBody(node, context) {
'method', 'method',
b.id('$.ADD_OWNER'), b.id('$.ADD_OWNER'),
[b.id('owner')], [b.id('owner')],
Array.from(public_state) [
// Only run ownership addition on $state fields. b.stmt(
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, b.call(
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. '$.add_owner_to_class',
.filter(([_, { kind }]) => kind === 'state') b.this,
.map(([name]) => b.id('owner'),
b.stmt( b.array(
b.call( Array.from(public_state).map(([name]) =>
'$.add_owner', b.thunk(b.call('$.get', b.member(b.this, b.private_id(name))))
b.call('$.get', b.member(b.this, b.private_id(name))), )
b.id('owner'), ),
b.literal(false), is_ignored(node, 'ownership_invalid_binding') && b.true
is_ignored(node, 'ownership_invalid_binding') && b.true
)
) )
), )
],
true true
) )
); );

@ -300,11 +300,6 @@ export function RegularElement(node, context) {
build_class_directives(class_directives, node_id, context, is_attributes_reactive); build_class_directives(class_directives, node_id, context, is_attributes_reactive);
build_style_directives(style_directives, node_id, context, is_attributes_reactive); build_style_directives(style_directives, node_id, context, is_attributes_reactive);
// Apply the src and loading attributes for <img> elements after the element is appended to the document
if (node.name === 'img' && (has_spread || lookup.has('loading'))) {
context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id)));
}
if ( if (
is_load_error_element(node.name) && is_load_error_element(node.name) &&
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) (has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))

@ -204,37 +204,18 @@ export function build_component(node, component_name, context, anchor = context.
const expression = /** @type {Expression} */ (context.visit(attribute.expression)); const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && attribute.name !== 'this') { if (dev && attribute.name !== 'this') {
let should_add_owner = true; binding_initializers.push(
b.stmt(
if (attribute.expression.type !== 'SequenceExpression') { b.call(
const left = object(attribute.expression); b.id('$.add_owner_effect'),
expression.type === 'SequenceExpression'
if (left?.type === 'Identifier') { ? expression.expressions[0]
const binding = context.state.scope.get(left.name); : b.thunk(expression),
b.id(component_name),
// Only run ownership addition on $state fields. is_ignored(node, 'ownership_invalid_binding') && b.true
// Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`,
// but that feels so much of an edge case that it doesn't warrant a perf hit for the common case.
if (binding?.kind === 'derived' || binding?.kind === 'raw_state') {
should_add_owner = false;
}
}
}
if (should_add_owner) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.add_owner_effect'),
expression.type === 'SequenceExpression'
? expression.expressions[0]
: b.thunk(expression),
b.id(component_name),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
) )
); )
} );
} }
if (expression.type === 'SequenceExpression') { if (expression.type === 'SequenceExpression') {

@ -6,7 +6,7 @@ import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../context.js'; import { dev_current_component_function } from '../context.js';
import { get_prototype_of } from '../../shared/utils.js'; import { get_prototype_of } from '../../shared/utils.js';
import * as w from '../warnings.js'; import * as w from '../warnings.js';
import { FILENAME } from '../../../constants.js'; import { FILENAME, UNINITIALIZED } from '../../../constants.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */ /** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {}; const boundaries = {};
@ -140,6 +140,25 @@ export function add_owner_effect(get_object, Component, skip_warning = false) {
}); });
} }
/**
* @param {any} _this
* @param {Function} owner
* @param {Array<() => any>} getters
* @param {boolean} skip_warning
*/
export function add_owner_to_class(_this, owner, getters, skip_warning) {
_this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED);
for (let i = 0; i < getters.length; i += 1) {
const current = getters[i]();
// For performance reasons we only re-add the owner if the state has changed
if (current !== _this[ADD_OWNER][i]) {
_this[ADD_OWNER].current[i] = current;
add_owner(current, owner, false, skip_warning);
}
}
}
/** /**
* @param {ProxyMetadata | null} from * @param {ProxyMetadata | null} from
* @param {ProxyMetadata} to * @param {ProxyMetadata} to
@ -196,7 +215,19 @@ function add_owner_to_object(object, owner, seen) {
if (proto === Object.prototype) { if (proto === Object.prototype) {
// recurse until we find a state proxy // recurse until we find a state proxy
for (const key in object) { for (const key in object) {
add_owner_to_object(object[key], owner, seen); if (Object.getOwnPropertyDescriptor(object, key)?.get) {
// Similar to the class case; the getter could update with a new state
let current = UNINITIALIZED;
render_effect(() => {
const next = object[key];
if (current !== next) {
current = next;
add_owner_to_object(next, owner, seen);
}
});
} else {
add_owner_to_object(object[key], owner, seen);
}
} }
} else if (proto === Array.prototype) { } else if (proto === Array.prototype) {
// recurse until we find a state proxy // recurse until we find a state proxy
@ -221,9 +252,10 @@ function has_owner(metadata, component) {
return ( return (
metadata.owners.has(component) || metadata.owners.has(component) ||
// This helps avoid false positives when using HMR, where the component function is replaced // This helps avoid false positives when using HMR, where the component function is replaced
[...metadata.owners].some( (FILENAME in component &&
(owner) => /** @type {any} */ (owner)[FILENAME] === /** @type {any} */ (component)?.[FILENAME] [...metadata.owners].some(
) || (owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
)) ||
(metadata.parent !== null && has_owner(metadata.parent, component)) (metadata.parent !== null && has_owner(metadata.parent, component))
); );
} }

@ -399,15 +399,18 @@ export function set_attributes(
if (name === 'value' || name === 'checked') { if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve // removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element); let input = /** @type {HTMLInputElement} */ (element);
const use_default = prev === undefined;
if (name === 'value') { if (name === 'value') {
let prev = input.defaultValue; let previous = input.defaultValue;
input.removeAttribute(name); input.removeAttribute(name);
input.defaultValue = prev; input.defaultValue = previous;
// @ts-ignore
input.value = input.__value = use_default ? previous : null;
} else { } else {
let prev = input.defaultChecked; let previous = input.defaultChecked;
input.removeAttribute(name); input.removeAttribute(name);
input.defaultChecked = prev; input.defaultChecked = previous;
input.checked = use_default ? previous : false;
} }
} else { } else {
element.removeAttribute(key); element.removeAttribute(key);
@ -520,28 +523,3 @@ function srcset_url_equal(element, srcset) {
) )
); );
} }
/**
* @param {HTMLImageElement} element
* @returns {void}
*/
export function handle_lazy_img(element) {
// If we're using an image that has a lazy loading attribute, we need to apply
// the loading and src after the img element has been appended to the document.
// Otherwise the lazy behaviour will not work due to our cloneNode heuristic for
// templates.
if (!hydrating && element.loading === 'lazy') {
var src = element.src;
// @ts-expect-error
element[LOADING_ATTR_SYMBOL] = null;
element.loading = 'eager';
element.removeAttribute('src');
requestAnimationFrame(() => {
// @ts-expect-error
if (element[LOADING_ATTR_SYMBOL] !== 'eager') {
element.loading = 'lazy';
}
element.src = src;
});
}
}

@ -14,6 +14,7 @@ import { current_each_item } from '../blocks/each.js';
import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js';
import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
/** /**
* @param {Element} element * @param {Element} element
@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js';
* @returns {void} * @returns {void}
*/ */
function dispatch_event(element, type) { function dispatch_event(element, type) {
element.dispatchEvent(new CustomEvent(type)); without_reactive_context(() => {
element.dispatchEvent(new CustomEvent(type));
});
} }
/** /**

@ -13,6 +13,9 @@ export var $window;
/** @type {Document} */ /** @type {Document} */
export var $document; export var $document;
/** @type {boolean} */
export var is_firefox;
/** @type {() => Node | null} */ /** @type {() => Node | null} */
var first_child_getter; var first_child_getter;
/** @type {() => Node | null} */ /** @type {() => Node | null} */
@ -29,6 +32,7 @@ export function init_operations() {
$window = window; $window = window;
$document = document; $document = document;
is_firefox = /Firefox/.test(navigator.userAgent);
var element_prototype = Element.prototype; var element_prototype = Element.prototype;
var node_prototype = Node.prototype; var node_prototype = Node.prototype;

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
import { create_text, get_first_child } from './operations.js'; import { create_text, get_first_child, is_firefox } from './operations.js';
import { create_fragment_from_html } from './reconciler.js'; import { create_fragment_from_html } from './reconciler.js';
import { active_effect } from '../runtime.js'; import { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@ -48,7 +48,7 @@ export function template(content, flags) {
} }
var clone = /** @type {TemplateNode} */ ( var clone = /** @type {TemplateNode} */ (
use_import_node ? document.importNode(node, true) : node.cloneNode(true) use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
); );
if (is_fragment) { if (is_fragment) {

@ -10,6 +10,7 @@ export {
mark_module_start, mark_module_start,
mark_module_end, mark_module_end,
add_owner_effect, add_owner_effect,
add_owner_to_class,
skip_ownership_validation skip_ownership_validation
} from './dev/ownership.js'; } from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js'; export { check_target, legacy_api } from './dev/legacy.js';
@ -35,7 +36,6 @@ export {
set_attributes, set_attributes,
set_custom_element_data, set_custom_element_data,
set_xlink_attribute, set_xlink_attribute,
handle_lazy_img,
set_value, set_value,
set_checked, set_checked,
set_selected, set_selected,

@ -51,6 +51,7 @@ import {
} from './context.js'; } from './context.js';
import { Boundary } from './dom/blocks/boundary.js'; import { Boundary } from './dom/blocks/boundary.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import { is_firefox } from './dom/operations.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
@ -352,7 +353,7 @@ export function handle_error(error, effect, previous_effect, component_context)
current_context = current_context.p; current_context = current_context.p;
} }
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t'; const indent = is_firefox ? ' ' : '\t';
define_property(error, 'message', { define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
}); });
@ -388,22 +389,18 @@ export function handle_error(error, effect, previous_effect, component_context)
/** /**
* @param {Value} signal * @param {Value} signal
* @param {Effect} effect * @param {Effect} effect
* @param {number} [depth] * @param {boolean} [root]
*/ */
function schedule_possible_effect_self_invalidation(signal, effect, depth = 0) { function schedule_possible_effect_self_invalidation(signal, effect, root = true) {
var reactions = signal.reactions; var reactions = signal.reactions;
if (reactions === null) return; if (reactions === null) return;
for (var i = 0; i < reactions.length; i++) { for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i]; var reaction = reactions[i];
if ((reaction.f & DERIVED) !== 0) { if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation( schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
/** @type {Derived} */ (reaction),
effect,
depth + 1
);
} else if (effect === reaction) { } else if (effect === reaction) {
if (depth === 0) { if (root) {
set_signal_status(reaction, DIRTY); set_signal_status(reaction, DIRTY);
} else if ((reaction.f & CLEAN) !== 0) { } else if ((reaction.f & CLEAN) !== 0) {
set_signal_status(reaction, MAYBE_DIRTY); set_signal_status(reaction, MAYBE_DIRTY);
@ -477,6 +474,8 @@ export function update_reaction(reaction) {
if ( if (
is_runes() && is_runes() &&
untracked_writes !== null && untracked_writes !== null &&
!untracking &&
deps !== null &&
(reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0 (reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
) { ) {
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) { for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.19.10'; export const VERSION = '5.20.0';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,33 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ target, assert }) {
// Test for https://github.com/sveltejs/svelte/issues/15237
const [setValues, clearValue] = target.querySelectorAll('button');
const [text1, text2, check1, check2] = target.querySelectorAll('input');
assert.equal(text1.value, '');
assert.equal(text2.value, '');
assert.equal(check1.checked, false);
assert.equal(check2.checked, false);
flushSync(() => {
setValues.click();
});
assert.equal(text1.value, 'message');
assert.equal(text2.value, 'message');
assert.equal(check1.checked, true);
assert.equal(check2.checked, true);
flushSync(() => {
clearValue.click();
});
assert.equal(text1.value, '');
assert.equal(text2.value, '');
assert.equal(check1.checked, false);
assert.equal(check2.checked, false);
}
});

@ -0,0 +1,22 @@
<script>
let value = $state();
let checked = $state(false);
function setValues() {
value = 'message';
checked = true;
}
function clearValues() {
value = null;
checked = null;
}
</script>
<button onclick={setValues}>setValues</button>
<button onclick={clearValues}>clearValues</button>
<input type="text" {value} />
<input type="text" {value} {...{}} />
<input type="checkbox" {checked} />
<input type="checkbox" {checked} {...{}} />

@ -0,0 +1,7 @@
<script>
let { linked3 = $bindable(), linked4 = $bindable() } = $props();
</script>
<p>Binding</p>
<button onclick={() => linked3.count++}>Increment Linked 1 ({linked3.count})</button>
<button onclick={() => linked4.count++}>Increment Linked 2 ({linked4.count})</button>

@ -0,0 +1,13 @@
<script>
import { getContext } from 'svelte';
const linked1 = getContext('linked1');
const linked2 = getContext('linked2');
</script>
<p>Context</p>
<button onclick={() => linked1.linked.current.count++}
>Increment Linked 1 ({linked1.linked.current.count})</button
>
<button onclick={() => linked2.linked.current.count++}
>Increment Linked 2 ({linked2.linked.current.count})</button
>

@ -0,0 +1,34 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
// Tests that ownership is widened with $derived (on class or on its own) that contains $state
export default test({
compileOptions: {
dev: true
},
test({ assert, target, warnings }) {
const [root, counter_context1, counter_context2, counter_binding1, counter_binding2] =
target.querySelectorAll('button');
counter_context1.click();
counter_context2.click();
counter_binding1.click();
counter_binding2.click();
flushSync();
assert.equal(warnings.length, 0);
root.click();
flushSync();
counter_context1.click();
counter_context2.click();
counter_binding1.click();
counter_binding2.click();
flushSync();
assert.equal(warnings.length, 0);
},
warnings: []
});

@ -0,0 +1,46 @@
<script>
import CounterBinding from './CounterBinding.svelte';
import CounterContext from './CounterContext.svelte';
import { setContext } from 'svelte';
let counter = $state({ count: 0 });
class Linked {
#getter;
linked = $derived.by(() => {
const state = $state({ current: $state.snapshot(this.#getter()) });
return state;
});
constructor(fn) {
this.#getter = fn;
}
}
const linked1 = $derived.by(() => {
const state = $state({ current: $state.snapshot(counter) });
return state;
});
const linked2 = new Linked(() => counter);
setContext('linked1', {
get linked() {
return linked1;
}
});
setContext('linked2', linked2);
const linked3 = $derived.by(() => {
const state = $state({ current: $state.snapshot(counter) });
return state;
});
const linked4 = new Linked(() => counter);
</script>
<p>Parent</p>
<button onclick={() => counter.count++}>
Increment Original ({counter.count})
</button>
<CounterContext />
<CounterBinding bind:linked3={linked3.current} bind:linked4={linked4.linked.current} />

@ -22,6 +22,11 @@
class MyClass implements Hello {} class MyClass implements Hello {}
abstract class MyAbstractClass {
abstract x(): void;
y() {}
}
declare const declared_const: number; declare const declared_const: number;
declare function declared_fn(): void; declare function declared_fn(): void;
declare class declared_class { declare class declared_class {

@ -42,12 +42,8 @@ export default function Skip_static_subtree($$anchor, $$props) {
$.reset(select); $.reset(select);
var img = $.sibling(select, 2); var img = $.sibling(select, 2);
var div_2 = $.sibling(img, 2);
var img_1 = $.child(div_2);
$.reset(div_2); $.next(2);
$.template_effect(() => $.set_text(text, $$props.title)); $.template_effect(() => $.set_text(text, $$props.title));
$.handle_lazy_img(img);
$.handle_lazy_img(img_1);
$.append($$anchor, fragment); $.append($$anchor, fragment);
} }

@ -0,0 +1,13 @@
<script>
class Test {
#deps = () => [];
deps = $derived.by(() => {
return [];
});
constructor(f = () => []) {
this.#deps = f;
}
}
</script>
Loading…
Cancel
Save