Merge branch 'main' into revert-15196-corepack-bullshit

pull/15197/head
Ben McCann 2 years ago
commit 11b5259a4a

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure tracking returns true, even if in unowned

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly ssr component in `svelte:head` with `$props.id()` or `css='injected'`

@ -199,3 +199,24 @@ You can, of course, separate the type declaration from the annotation:
> [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components))
Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide.
## `$props.id()`
This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client.
This is useful for linking elements via attributes like `for` and `aria-labelledby`.
```svelte
<script>
const uid = $props.id();
</script>
<form>
<label for="{uid}-firstname">First Name: </label>
<input id="{uid}-firstname" type="text" />
<label for="{uid}-lastname">Last Name: </label>
<input id="{uid}-lastname" type="text" />
</form>
```

@ -45,8 +45,6 @@ If a function is returned from `onMount`, it will be called when the component i
## `onDestroy`
> EXPORT_SNIPPET: svelte#onDestroy
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.

@ -573,7 +573,13 @@ Unrecognised compiler option %keypath%
### props_duplicate
```
Cannot use `$props()` more than once
Cannot use `%rune%()` more than once
```
### props_id_invalid_placement
```
`$props.id()` can only be used at the top level of components as a variable declaration initializer
```
### props_illegal_name

@ -7,7 +7,7 @@
"license": "MIT",
"packageManager": "pnpm@9.4.0",
"engines": {
"pnpm": "^9.0.0"
"pnpm": ">=9.0.0"
},
"repository": {
"type": "git",
@ -44,6 +44,6 @@
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5",
"vitest": "^2.0.5"
"vitest": "^2.1.9"
}
}

@ -1,5 +1,49 @@
# 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
### Patch Changes
- fix: when re-connecting unowned deriveds, remove their unowned flag ([#15255](https://github.com/sveltejs/svelte/pull/15255))
- fix: allow mutation of private derived state ([#15228](https://github.com/sveltejs/svelte/pull/15228))
## 5.19.9
### Patch Changes
- fix: ensure unowned derived dependencies are not duplicated when reactions are skipped ([#15232](https://github.com/sveltejs/svelte/pull/15232))
- fix: hydrate `href` that is part of spread attributes ([#15226](https://github.com/sveltejs/svelte/pull/15226))
## 5.19.8
### Patch Changes
- fix: properly set `value` property of custom elements ([#15206](https://github.com/sveltejs/svelte/pull/15206))
- fix: ensure custom element updates don't run in hydration mode ([#15217](https://github.com/sveltejs/svelte/pull/15217))
- fix: ensure tracking returns true, even if in unowned ([#15214](https://github.com/sveltejs/svelte/pull/15214))
## 5.19.7
### Patch Changes

@ -120,7 +120,11 @@ This turned out to be buggy and unpredictable, particularly when working with de
## props_duplicate
> Cannot use `$props()` more than once
> Cannot use `%rune%()` more than once
## props_id_invalid_placement
> `$props.id()` can only be used at the top level of components as a variable declaration initializer
## props_illegal_name

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.19.7",
"version": "5.20.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -143,7 +143,7 @@
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
"vitest": "^2.1.9"
},
"dependencies": {
"@ampproject/remapping": "^2.3.0",

@ -339,6 +339,15 @@ declare namespace $effect {
declare function $props(): any;
declare namespace $props {
/**
* Generates an ID that is unique to the current component instance. When hydrating a server-rendered component,
* the value will be consistent between server and client.
*
* This is useful for linking elements via attributes like `for` and `aria-labelledby`.
* @since 5.20.0
*/
export function id(): string;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;

@ -279,12 +279,22 @@ export function module_illegal_default_export(node) {
}
/**
* Cannot use `$props()` more than once
* Cannot use `%rune%()` more than once
* @param {null | number | NodeLike} node
* @param {string} rune
* @returns {never}
*/
export function props_duplicate(node, rune) {
e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
}
/**
* `$props.id()` can only be used at the top level of components as a variable declaration initializer
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_duplicate(node) {
e(node, 'props_duplicate', `Cannot use \`$props()\` more than once\nhttps://svelte.dev/e/props_duplicate`);
export function props_id_invalid_placement(node) {
e(node, 'props_id_invalid_placement', `\`$props.id()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`);
}
/**

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

@ -416,6 +416,7 @@ export function analyze_component(root, source, options) {
immutable: runes || options.immutable,
exports: [],
uses_props: false,
props_id: null,
uses_rest_props: false,
uses_slots: false,
uses_component_bindings: false,
@ -605,7 +606,7 @@ export function analyze_component(root, source, options) {
has_props_rune: false,
component_slots: new Set(),
expression: null,
private_derived_state: [],
derived_state: [],
function_depth: scope.function_depth,
instance_scope: instance.scope,
reactive_statement: null,
@ -676,7 +677,7 @@ export function analyze_component(root, source, options) {
reactive_statements: analysis.reactive_statements,
component_slots: new Set(),
expression: null,
private_derived_state: [],
derived_state: [],
function_depth: scope.function_depth
};

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

@ -23,11 +23,6 @@ export function Attribute(node, context) {
if (node.name === 'value' && parent.name === 'option') {
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)) {

@ -55,7 +55,7 @@ export function CallExpression(node, context) {
case '$props':
if (context.state.has_props_rune) {
e.props_duplicate(node);
e.props_duplicate(node, rune);
}
context.state.has_props_rune = true;
@ -74,6 +74,32 @@ export function CallExpression(node, context) {
break;
case '$props.id': {
const grand_parent = get_parent(context.path, -2);
if (context.state.analysis.props_id) {
e.props_duplicate(node, rune);
}
if (
parent.type !== 'VariableDeclarator' ||
parent.id.type !== 'Identifier' ||
context.state.ast_type !== 'instance' ||
context.state.scope !== context.state.analysis.instance.scope ||
grand_parent.type !== 'VariableDeclaration'
) {
e.props_id_invalid_placement(node);
}
if (node.arguments.length > 0) {
e.rune_invalid_arguments(node, rune);
}
context.state.analysis.props_id = parent.id;
break;
}
case '$state':
case '$state.raw':
case '$derived':

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

@ -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 { AnalysisState, Context } from '../../types' */
/** @import { Scope } from '../../../scope' */
@ -25,6 +25,10 @@ export function validate_assignment(node, argument, state) {
e.constant_assignment(node, 'derived state');
}
if (binding?.node === state.analysis.props_id) {
e.constant_assignment(node, '$props.id()');
}
if (binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}
@ -34,21 +38,24 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
let object = /** @type {Expression | Super} */ (argument);
/** @type {Expression | PrivateIdentifier | null} */
let property = null;
while (object.type === 'MemberExpression') {
property = object.property;
object = object.object;
}
if (object.type === 'ThisExpression' && property?.type === 'PrivateIdentifier') {
if (state.private_derived_state.includes(property.name)) {
e.constant_assignment(node, 'derived state');
}
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
(((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') &&
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.value &&
typeof argument.property.value === 'string' &&
state.derived_state.some(
(derived) =>
derived.name === /** @type {Literal} */ (argument.property).value && !derived.private
)))
) {
e.constant_assignment(node, 'derived state');
}
}

@ -562,6 +562,11 @@ export function client_component(analysis, options) {
component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
}
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id')));
}
if (state.events.size > 0) {
body.push(
b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name)))))

@ -190,22 +190,21 @@ export function ClassBody(node, context) {
'method',
b.id('$.ADD_OWNER'),
[b.id('owner')],
Array.from(public_state)
// Only run ownership addition on $state fields.
// 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.
.filter(([_, { kind }]) => kind === 'state')
.map(([name]) =>
b.stmt(
b.call(
'$.add_owner',
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
)
[
b.stmt(
b.call(
'$.add_owner_to_class',
b.this,
b.id('owner'),
b.array(
Array.from(public_state).map(([name]) =>
b.thunk(b.call('$.get', b.member(b.this, b.private_id(name))))
)
),
is_ignored(node, 'ownership_invalid_binding') && b.true
)
),
)
],
true
)
);

@ -300,11 +300,6 @@ export function RegularElement(node, context) {
build_class_directives(class_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 (
is_load_error_element(node.name) &&
(has_spread || has_use || lookup.has('onload') || lookup.has('onerror'))

@ -42,6 +42,11 @@ export function VariableDeclaration(node, context) {
continue;
}
if (rune === '$props.id') {
// skip
continue;
}
if (rune === '$props') {
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];

@ -180,37 +180,18 @@ export function build_component(node, component_name, context, anchor = context.
const expression = /** @type {Expression} */ (context.visit(attribute.expression));
if (dev && attribute.name !== 'this') {
let should_add_owner = true;
if (attribute.expression.type !== 'SequenceExpression') {
const left = object(attribute.expression);
if (left?.type === 'Identifier') {
const binding = context.state.scope.get(left.name);
// Only run ownership addition on $state fields.
// 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
)
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') {

@ -129,6 +129,12 @@ export function build_template_chunk(
if (value.right.value === null) {
value = { ...value, right: b.literal('') };
}
} else if (
state.analysis.props_id &&
value.type === 'Identifier' &&
value.name === state.analysis.props_id.name
) {
// do nothing ($props.id() is never null/undefined)
} else {
value = b.logical('??', value, b.literal(''));
}

@ -244,6 +244,13 @@ export function server_component(analysis, options) {
.../** @type {Statement[]} */ (template.body)
]);
if (analysis.props_id) {
// need to be placed on first line of the component for hydration
component_block.body.unshift(
b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload')))
);
}
let should_inject_context = dev || analysis.needs_context;
if (should_inject_context) {

@ -24,6 +24,11 @@ export function VariableDeclaration(node, context) {
continue;
}
if (rune === '$props.id') {
// skip
continue;
}
if (rune === '$props') {
let has_rest = false;
// remove $bindable() from props declaration
@ -156,6 +161,10 @@ export function VariableDeclaration(node, context) {
}
}
if (declarations.length === 0) {
return b.empty;
}
return {
...node,
declarations

@ -44,6 +44,8 @@ export interface ComponentAnalysis extends Analysis {
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */
uses_props: boolean;
/** The component ID variable name, if any */
props_id: Identifier | null;
/** Whether the component uses `$$restProps` */
uses_rest_props: boolean;
/** Whether the component uses `$$slots` */

@ -6,7 +6,7 @@ import { render_effect, user_pre_effect } from '../reactivity/effects.js';
import { dev_current_component_function } from '../context.js';
import { get_prototype_of } from '../../shared/utils.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 }>>} */
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} to
@ -196,7 +215,19 @@ function add_owner_to_object(object, owner, seen) {
if (proto === Object.prototype) {
// recurse until we find a state proxy
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) {
// recurse until we find a state proxy
@ -221,9 +252,10 @@ function has_owner(metadata, component) {
return (
metadata.owners.has(component) ||
// This helps avoid false positives when using HMR, where the component function is replaced
[...metadata.owners].some(
(owner) => /** @type {any} */ (owner)[FILENAME] === /** @type {any} */ (component)?.[FILENAME]
) ||
(FILENAME in component &&
[...metadata.owners].some(
(owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME]
)) ||
(metadata.parent !== null && has_owner(metadata.parent, component))
);
}

@ -1,5 +1,5 @@
import { DEV } from 'esm-env';
import { hydrating } from '../hydration.js';
import { hydrating, set_hydrating } from '../hydration.js';
import { get_descriptors, get_prototype_of } from '../../../shared/utils.js';
import { create_event, delegate } from './events.js';
import { add_form_reset_listener, autofocus } from './misc.js';
@ -213,6 +213,12 @@ export function set_custom_element_data(node, prop, value) {
// or effect
var previous_reaction = active_reaction;
var previous_effect = active_effect;
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let was_hydrating = hydrating;
if (hydrating) {
set_hydrating(false);
}
set_active_reaction(null);
set_active_effect(null);
@ -239,6 +245,9 @@ export function set_custom_element_data(node, prop, value) {
} finally {
set_active_reaction(previous_reaction);
set_active_effect(previous_effect);
if (was_hydrating) {
set_hydrating(true);
}
}
}
@ -262,6 +271,13 @@ export function set_attributes(
is_custom_element = false,
skip_warning = false
) {
// If we're hydrating but the custom element is from Svelte, and it already scaffolded,
// then it might run block logic in hydration mode, which we have to prevent.
let is_hydrating_custom_element = hydrating && is_custom_element;
if (is_hydrating_custom_element) {
set_hydrating(false);
}
var current = prev || {};
var is_option_element = element.tagName === 'OPTION';
@ -363,9 +379,10 @@ export function set_attributes(
element.style.cssText = value + '';
} else if (key === 'autofocus') {
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
} else if (key === '__value' || (key === 'value' && value != null)) {
// @ts-ignore
element.value = element[key] = element.__value = value;
} else if (!is_custom_element && (key === '__value' || (key === 'value' && value != null))) {
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
element.value = element.__value = value;
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
@ -382,15 +399,18 @@ export function set_attributes(
if (name === 'value' || name === 'checked') {
// removing value/checked also removes defaultValue/defaultChecked — preserve
let input = /** @type {HTMLInputElement} */ (element);
const use_default = prev === undefined;
if (name === 'value') {
let prev = input.defaultValue;
let previous = input.defaultValue;
input.removeAttribute(name);
input.defaultValue = prev;
input.defaultValue = previous;
// @ts-ignore
input.value = input.__value = use_default ? previous : null;
} else {
let prev = input.defaultChecked;
let previous = input.defaultChecked;
input.removeAttribute(name);
input.defaultChecked = prev;
input.defaultChecked = previous;
input.checked = use_default ? previous : false;
}
} else {
element.removeAttribute(key);
@ -402,11 +422,7 @@ export function set_attributes(
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
} else {
set_attribute(element, name, value);
}
set_attribute(element, name, value);
}
}
if (key === 'style' && '__styles' in element) {
@ -415,6 +431,10 @@ export function set_attributes(
}
}
if (is_hydrating_custom_element) {
set_hydrating(true);
}
return current;
}
@ -503,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 { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js';
import { queue_micro_task } from '../task.js';
import { without_reactive_context } from './bindings/shared.js';
/**
* @param {Element} element
@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js';
* @returns {void}
*/
function dispatch_event(element, type) {
element.dispatchEvent(new CustomEvent(type));
without_reactive_context(() => {
element.dispatchEvent(new CustomEvent(type));
});
}
/**

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

@ -1,6 +1,6 @@
/** @import { Effect, TemplateNode } from '#client' */
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 { active_effect } from '../runtime.js';
import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
@ -48,7 +48,7 @@ export function template(content, flags) {
}
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) {
@ -249,3 +249,27 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
let uid = 1;
export function reset_props_id() {
uid = 1;
}
/**
* Create (or hydrate) an unique UID for the component instance.
*/
export function props_id() {
if (
hydrating &&
hydrate_node &&
hydrate_node.nodeType === 8 &&
hydrate_node.textContent?.startsWith('#s')
) {
const id = hydrate_node.textContent.substring(1);
hydrate_next();
return id;
}
return 'c' + uid++;
}

@ -10,6 +10,7 @@ export {
mark_module_start,
mark_module_end,
add_owner_effect,
add_owner_to_class,
skip_ownership_validation
} from './dev/ownership.js';
export { check_target, legacy_api } from './dev/legacy.js';
@ -34,7 +35,6 @@ export {
set_attributes,
set_custom_element_data,
set_xlink_attribute,
handle_lazy_img,
set_value,
set_checked,
set_selected,
@ -96,7 +96,8 @@ export {
mathml_template,
template,
template_with_script,
text
text,
props_id
} from './dom/template.js';
export { derived, derived_safe_equal } from './reactivity/deriveds.js';
export {

@ -1,18 +1,9 @@
/** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env';
import {
CLEAN,
DERIVED,
DESTROYED,
DIRTY,
EFFECT_HAS_DERIVED,
MAYBE_DIRTY,
UNOWNED
} from '../constants.js';
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
active_reaction,
active_effect,
remove_reactions,
set_signal_status,
skip_reaction,
update_reaction,

@ -10,7 +10,15 @@ import {
import { get_descriptor, is_function } from '../../shared/utils.js';
import { mutable_source, set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import { active_effect, get, captured_signals, set_active_effect, untrack } from '../runtime.js';
import {
active_effect,
get,
captured_signals,
set_active_effect,
untrack,
active_reaction,
set_active_reaction
} from '../runtime.js';
import { safe_equals } from './equality.js';
import * as e from '../errors.js';
import {
@ -241,26 +249,6 @@ export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
}
/**
* @template T
* @param {() => T} fn
* @returns {T}
*/
function with_parent_branch(fn) {
var effect = active_effect;
var previous_effect = active_effect;
while (effect !== null && (effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0) {
effect = effect.parent;
}
try {
set_active_effect(effect);
return fn();
} finally {
set_active_effect(previous_effect);
}
}
/**
* 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.
@ -335,8 +323,8 @@ export function prop(props, key, flags, fallback) {
} else {
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
// Replicate that behavior through using a derived
var derived_getter = with_parent_branch(() =>
(immutable ? derived : derived_safe_equal)(() => /** @type {V} */ (props[key]))
var derived_getter = (immutable ? derived : derived_safe_equal)(
() => /** @type {V} */ (props[key])
);
derived_getter.f |= LEGACY_DERIVED_PROP;
getter = () => {
@ -380,21 +368,19 @@ export function prop(props, key, flags, fallback) {
// The derived returns the current value. The underlying mutable
// source is written to from various places to persist this value.
var inner_current_value = mutable_source(prop_value);
var current_value = with_parent_branch(() =>
derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
var current_value = derived(() => {
var parent_value = getter();
var child_value = get(inner_current_value);
if (from_child) {
from_child = false;
was_from_child = true;
return child_value;
}
was_from_child = false;
return (inner_current_value.v = parent_value);
})
);
was_from_child = false;
return (inner_current_value.v = parent_value);
});
if (!immutable) current_value.equals = safe_equals;

@ -39,6 +39,7 @@ import {
set_component_context,
set_dev_current_component_function
} from './context.js';
import { is_firefox } from './dom/operations.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
@ -184,18 +185,28 @@ export function check_dirtiness(reaction) {
// If we are working with a disconnected or an unowned signal that is now connected (due to an active effect)
// then we need to re-connect the reaction to the dependency
if (is_disconnected || is_unowned_connected) {
var derived = /** @type {Derived} */ (reaction);
var parent = derived.parent;
for (i = 0; i < length; i++) {
dependency = dependencies[i];
// We always re-add all reactions (even duplicates) if the derived was
// previously disconnected
if (is_disconnected || !dependency?.reactions?.includes(reaction)) {
(dependency.reactions ??= []).push(reaction);
// previously disconnected, however we don't if it was unowned as we
// de-duplicate dependencies in that case
if (is_disconnected || !dependency?.reactions?.includes(derived)) {
(dependency.reactions ??= []).push(derived);
}
}
if (is_disconnected) {
reaction.f ^= DISCONNECTED;
derived.f ^= DISCONNECTED;
}
// If the unowned derived is now fully connected to the graph again (it's unowned and reconnected, has a parent
// and the parent is not unowned), then we can mark it as connected again, removing the need for the unowned
// flag
if (is_unowned_connected && parent !== null && (parent.f & UNOWNED) === 0) {
derived.f ^= UNOWNED;
}
}
@ -323,7 +334,7 @@ export function handle_error(error, effect, previous_effect, component_context)
current_context = current_context.p;
}
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
const indent = is_firefox ? ' ' : '\t';
define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
@ -359,22 +370,18 @@ export function handle_error(error, effect, previous_effect, component_context)
/**
* @param {Value} signal
* @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;
if (reactions === null) return;
for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i];
if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(
/** @type {Derived} */ (reaction),
effect,
depth + 1
);
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
} else if (effect === reaction) {
if (depth === 0) {
if (root) {
set_signal_status(reaction, DIRTY);
} else if ((reaction.f & CLEAN) !== 0) {
set_signal_status(reaction, MAYBE_DIRTY);
@ -404,15 +411,9 @@ export function update_reaction(reaction) {
skipped_deps = 0;
untracked_writes = null;
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
// prettier-ignore
skip_reaction =
(flags & UNOWNED) !== 0 &&
(!is_flushing_effect ||
// If we were previously not in a reactive context and we're reading an unowned derived
// that was created inside another reaction, then we don't fully know the real owner and thus
// we need to skip adding any reactions for this unowned
((previous_reaction === null || previous_untracking) &&
/** @type {Derived} */ (reaction).parent !== null));
(!is_flushing_effect || previous_reaction === null || previous_untracking);
derived_sources = null;
set_component_context(reaction.ctx);
@ -453,6 +454,8 @@ export function update_reaction(reaction) {
if (
is_runes() &&
untracked_writes !== null &&
!untracking &&
deps !== null &&
(reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0
) {
for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) {
@ -675,10 +678,7 @@ function flush_queued_root_effects(root_effects) {
effect.f ^= CLEAN;
}
/** @type {Effect[]} */
var collected_effects = [];
process_effects(effect, collected_effects);
var collected_effects = process_effects(effect);
flush_queued_effects(collected_effects);
}
} finally {
@ -779,13 +779,14 @@ export function schedule_effect(signal) {
* effects to be flushed.
*
* @param {Effect} effect
* @param {Effect[]} collected_effects
* @returns {void}
* @returns {Effect[]}
*/
function process_effects(effect, collected_effects) {
var current_effect = effect.first;
function process_effects(effect) {
/** @type {Effect[]} */
var effects = [];
var current_effect = effect.first;
main_loop: while (current_effect !== null) {
var flags = current_effect.f;
var is_branch = (flags & BRANCH_EFFECT) !== 0;
@ -793,34 +794,32 @@ function process_effects(effect, collected_effects) {
var sibling = current_effect.next;
if (!is_skippable_branch && (flags & INERT) === 0) {
if ((flags & RENDER_EFFECT) !== 0) {
if (is_branch) {
current_effect.f ^= CLEAN;
} else {
// Ensure we set the effect to be the active reaction
// to ensure that unowned deriveds are correctly tracked
// because we're flushing the current effect
var previous_active_reaction = active_reaction;
try {
active_reaction = current_effect;
if (check_dirtiness(current_effect)) {
update_effect(current_effect);
}
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
} finally {
active_reaction = previous_active_reaction;
if ((flags & EFFECT) !== 0) {
effects.push(current_effect);
} else if (is_branch) {
current_effect.f ^= CLEAN;
} else {
// Ensure we set the effect to be the active reaction
// to ensure that unowned deriveds are correctly tracked
// because we're flushing the current effect
var previous_active_reaction = active_reaction;
try {
active_reaction = current_effect;
if (check_dirtiness(current_effect)) {
update_effect(current_effect);
}
} catch (error) {
handle_error(error, current_effect, null, current_effect.ctx);
} finally {
active_reaction = previous_active_reaction;
}
}
var child = current_effect.first;
var child = current_effect.first;
if (child !== null) {
current_effect = child;
continue;
}
} else if ((flags & EFFECT) !== 0) {
effects.push(current_effect);
if (child !== null) {
current_effect = child;
continue;
}
}
@ -843,13 +842,7 @@ function process_effects(effect, collected_effects) {
current_effect = sibling;
}
// We might be dealing with many effects here, far more than can be spread into
// an array push call (callstack overflow). So let's deal with each effect in a loop.
for (var i = 0; i < effects.length; i++) {
child = effects[i];
collected_effects.push(child);
process_effects(child, collected_effects);
}
return effects;
}
/**
@ -933,7 +926,10 @@ export function get(signal) {
skipped_deps++;
} else if (new_deps === null) {
new_deps = [signal];
} else {
} else if (!skip_reaction || !new_deps.includes(signal)) {
// Normally we can push duplicated dependencies to `new_deps`, but if we're inside
// an unowned derived because skip_reaction is true, then we need to ensure that
// we don't have duplicates
new_deps.push(signal);
}
}

@ -28,14 +28,17 @@ const INVALID_ATTR_NAME_CHAR_REGEX =
* @param {Payload} to_copy
* @returns {Payload}
*/
export function copy_payload({ out, css, head }) {
export function copy_payload({ out, css, head, uid }) {
return {
out,
css: new Set(css),
head: {
title: head.title,
out: head.out
}
out: head.out,
css: new Set(head.css),
uid: head.uid
},
uid
};
}
@ -48,6 +51,7 @@ export function copy_payload({ out, css, head }) {
export function assign_payload(p1, p2) {
p1.out = p2.out;
p1.head = p2.head;
p1.uid = p2.uid;
}
/**
@ -83,17 +87,28 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop)
*/
export let on_destroy = [];
function props_id_generator() {
let uid = 1;
return () => 's' + uid++;
}
/**
* Only available on the server and when compiling with the `server` option.
* Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app.
* @template {Record<string, any>} Props
* @param {import('svelte').Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any> }} [options]
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>, uid?: () => string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
const uid = options.uid ?? props_id_generator();
/** @type {Payload} */
const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
const payload = {
out: '',
css: new Set(),
head: { title: '', out: '', css: new Set(), uid },
uid
};
const prev_on_destroy = on_destroy;
on_destroy = [];
@ -526,6 +541,17 @@ export function once(get_value) {
};
}
/**
* Create an unique ID
* @param {Payload} payload
* @returns {string}
*/
export function props_id(payload) {
const uid = payload.uid();
payload.out += '<!--#' + uid + '-->';
return uid;
}
export { attr, clsx };
export { html } from './blocks/html.js';

@ -17,7 +17,11 @@ export interface Payload {
head: {
title: string;
out: string;
uid: () => string;
css: Set<{ hash: string; code: string }>;
};
/** Function that generates a unique ID */
uid: () => string;
}
export interface RenderOutput {

@ -433,6 +433,7 @@ const RUNES = /** @type {const} */ ([
'$state.raw',
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
'$derived',
'$derived.by',

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

@ -1 +1 @@
<!--[--><a href="/foo">foo</a><!--]-->
<!--[--><a href="/foo">foo</a> <a href="/foo">foo</a><!--]-->

@ -3,3 +3,4 @@
</script>
<a href={browser ? '/foo' : '/bar'}>foo</a>
<a {...{href: browser ? '/foo' : '/bar'}}>foo</a>

@ -11,6 +11,7 @@ import { setup_html_equal } from '../html_equal.js';
import { raf } from '../animation-helpers.js';
import type { CompileOptions } from '#compiler';
import { suite_with_variants, type BaseTest } from '../suite.js';
import { reset_props_id } from '../../src/internal/client/dom/template.js';
type Assert = typeof import('vitest').assert & {
htmlEqual(a: string, b: string, description?: string): void;
@ -345,6 +346,7 @@ async function run_test_variant(
if (runes) {
props = proxy({ ...(config.props || {}) });
reset_props_id();
if (manual_hydrate) {
hydrate_fn = () => {
instance = hydrate(mod.default, {

@ -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} {...{}} />

@ -1,19 +1,24 @@
import { test } from '../../test';
export default test({
mode: ['client', 'server'],
mode: ['client'],
async test({ assert, target }) {
const my_element = /** @type HTMLElement & { object: { test: true }; } */ (
target.querySelector('my-element')
);
const my_link = /** @type HTMLAnchorElement & { object: { test: true }; } */ (
target.querySelector('a')
);
assert.equal(my_element.getAttribute('string'), 'test');
assert.equal(my_element.hasAttribute('object'), false);
assert.deepEqual(my_element.object, { test: true });
const my_link = /** @type HTMLAnchorElement & { object: { test: true }; } */ (
target.querySelector('a')
);
assert.equal(my_link.getAttribute('string'), 'test');
assert.equal(my_link.hasAttribute('object'), false);
assert.deepEqual(my_link.object, { test: true });
const [value1, value2] = target.querySelectorAll('value-element');
assert.equal(value1.shadowRoot?.innerHTML, '<span>test</span>');
assert.equal(value2.shadowRoot?.innerHTML, '<span>test</span>');
}
});

@ -1,2 +1,22 @@
<script module>
customElements.define('value-element', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
set value(v) {
if (this.__value !== v) {
this.__value = v;
this.shadowRoot.innerHTML = `<span>${v}</span>`;
}
}
});
</script>
<my-element string="test" object={{ test: true }}></my-element>
<a is="my-link" string="test" object={{ test: true }}></a>
<value-element value="test"></value-element>
<value-element {...{value: "test"}}></value-element>

@ -0,0 +1,25 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
let [btn1, btn2] = target.querySelectorAll('button');
btn1?.click();
flushSync();
btn2?.click();
flushSync();
btn1?.click();
flushSync();
btn1?.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`<button>linked.current</button>\n3\n<button>count</button>\n1`
);
}
});

@ -0,0 +1,18 @@
<script>
import { untrack } from 'svelte';
let count = $state(0);
let state = $state({current: count});
let linked = $derived.by(() => {
count;
untrack(() => state.current = count);
return untrack(() => state);
});
linked.current++;
</script>
<button onclick={() => linked.current++}>linked.current</button> {linked.current}
<button onclick={() => count++}>count</button> {count}

@ -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} />

@ -0,0 +1,5 @@
<script>
let id = $props.id();
</script>
<p>{id}</p>

@ -0,0 +1,59 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, variant }) {
if (variant === 'dom') {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>c1</h1>
<p>c2</p>
<p>c3</p>
<p>c4</p>
`
);
} else {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>s1</h1>
<p>s2</p>
<p>s3</p>
<p>s4</p>
`
);
}
let button = target.querySelector('button');
flushSync(() => button?.click());
if (variant === 'dom') {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>c1</h1>
<p>c2</p>
<p>c3</p>
<p>c4</p>
<p>c5</p>
`
);
} else {
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<h1>s1</h1>
<p>s2</p>
<p>s3</p>
<p>s4</p>
<p>c1</p>
`
);
}
}
});

@ -0,0 +1,19 @@
<script>
import Child from './Child.svelte';
let id = $props.id();
let show = $state(false);
</script>
<button onclick={() => show = !show}>toggle</button>
<h1>{id}</h1>
<Child />
<Child />
<Child />
{#if show}
<Child />
{/if}

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

@ -0,0 +1,5 @@
<script>
let id = $props.id();
</script>
<meta name="id" content={id} />

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,8 @@
<script>
import HeadNested from './HeadNested.svelte';
</script>
<svelte:head>
<HeadNested />
</svelte:head>

@ -224,20 +224,75 @@ describe('signals', () => {
};
});
test('effects correctly handle unowned derived values that do not change', () => {
const log: number[] = [];
test('https://perf.js.hyoo.ru/#!bench=9h2as6_u0mfnn #2', () => {
let res: number[] = [];
let count = state(0);
const read = () => {
const x = derived(() => ({ count: $.get(count) }));
return $.get(x);
const numbers = Array.from({ length: 2 }, (_, i) => i);
const fib = (n: number): number => (n < 2 ? 1 : fib(n - 1) + fib(n - 2));
const hard = (n: number, l: string) => n + fib(16);
const A = state(0);
const B = state(0);
return () => {
const C = derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
const E = derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E'));
const F = derived(() => hard($.get(D)[0]! && $.get(B), 'F'));
const G = derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F));
const destroy = effect_root(() => {
effect(() => {
res.push(hard($.get(G), 'H'));
});
effect(() => {
res.push($.get(G));
});
effect(() => {
res.push(hard($.get(F), 'J'));
});
});
flushSync();
let i = 2;
while (--i) {
res.length = 0;
set(B, 1);
set(A, 1 + i * 2);
flushSync();
set(A, 2 + i * 2);
set(B, 2);
flushSync();
assert.equal(res.length, 4);
assert.deepEqual(res, [3198, 1601, 3195, 1598]);
}
destroy();
assert(A.reactions === null);
assert(B.reactions === null);
};
const derivedCount = derived(() => read().count);
user_effect(() => {
log.push($.get(derivedCount));
});
});
test('effects correctly handle unowned derived values that do not change', () => {
const log: number[] = [];
return () => {
let count = state(0);
const read = () => {
const x = derived(() => ({ count: $.get(count) }));
return $.get(x);
};
const derivedCount = derived(() => read().count);
const destroy = effect_root(() => {
user_effect(() => {
log.push($.get(derivedCount));
});
});
flushSync(() => set(count, 1));
// Ensure we're not leaking consumers
assert.deepEqual(count.reactions?.length, 1);
@ -248,6 +303,8 @@ describe('signals', () => {
// Ensure we're not leaking consumers
assert.deepEqual(count.reactions?.length, 1);
assert.deepEqual(log, [0, 1, 2, 3]);
destroy();
};
});
@ -343,25 +400,69 @@ describe('signals', () => {
};
});
let some_state = state({});
let some_deps = derived(() => {
return [$.get(some_state)];
});
test('two effects with an unowned derived that has some dependencies', () => {
const log: Array<Array<any>> = [];
render_effect(() => {
log.push($.get(some_deps));
});
return () => {
let some_state = state({});
let some_deps = derived(() => {
return [$.get(some_state)];
});
let destroy2: any;
render_effect(() => {
log.push($.get(some_deps));
});
const destroy = effect_root(() => {
render_effect(() => {
$.untrack(() => {
log.push($.get(some_deps));
});
});
return () => {
destroy2 = effect_root(() => {
render_effect(() => {
log.push($.get(some_deps));
});
render_effect(() => {
log.push($.get(some_deps));
});
});
});
set(some_state, {});
flushSync();
assert.deepEqual(log, [[{}], [{}]]);
assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
destroy2();
set(some_state, {});
flushSync();
assert.deepEqual(log, [[{}], [{}], [{}], [{}], [{}]]);
log.length = 0;
const destroy3 = effect_root(() => {
render_effect(() => {
$.untrack(() => {
log.push($.get(some_deps));
});
log.push($.get(some_deps));
});
});
set(some_state, {});
flushSync();
assert.deepEqual(log, [[{}], [{}], [{}], [{}]]);
destroy3();
assert(some_state.reactions === null);
destroy();
assert(some_state.reactions === null);
};
});
@ -818,6 +919,28 @@ describe('signals', () => {
};
});
test('unowned deriveds dependencies are correctly de-duped', () => {
return () => {
let a = state(0);
let b = state(true);
let c = derived(() => $.get(a));
let d = derived(() => ($.get(b) ? 1 : $.get(a) + $.get(c) + $.get(a)));
$.get(d);
assert.equal(d.deps?.length, 1);
$.get(d);
set(a, 1);
set(b, false);
$.get(d);
assert.equal(d.deps?.length, 3);
};
});
test('unowned deriveds correctly update', () => {
return () => {
const arr1 = proxy<{ a: number }[]>([]);

@ -42,12 +42,8 @@ export default function Skip_static_subtree($$anchor, $$props) {
$.reset(select);
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));
$.handle_lazy_img(img);
$.handle_lazy_img(img_1);
$.append($$anchor, fragment);
}

@ -0,0 +1,9 @@
<script>
class Test{
#der = $derived({test: 0});
set test(v){
this.#der.test = 45;
}
}
</script>

@ -0,0 +1,14 @@
[
{
"code": "constant_assignment",
"message": "Cannot assign to derived state",
"start": {
"column": 3,
"line": 6
},
"end": {
"column": 29,
"line": 6
}
}
]

@ -0,0 +1,9 @@
<script>
class Test{
der = $derived({ test: 0 });
set test(v){
this["der"] = { test: 45 };
}
}
</script>

@ -0,0 +1,14 @@
[
{
"code": "constant_assignment",
"message": "Cannot assign to derived state",
"start": {
"column": 3,
"line": 6
},
"end": {
"column": 27,
"line": 6
}
}
]

@ -0,0 +1,9 @@
<script>
class Test{
#der = $derived({ test: 0 });
set test(v){
this.#der = { test: 45 };
}
}
</script>

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

@ -0,0 +1,14 @@
[
{
"code": "constant_assignment",
"message": "Cannot assign to derived state",
"start": {
"column": 3,
"line": 6
},
"end": {
"column": 26,
"line": 6
}
}
]

@ -0,0 +1,9 @@
<script>
class Test{
der = $derived({ test: 0 });
set test(v){
this.der = { test: 45 };
}
}
</script>

@ -2995,6 +2995,15 @@ declare namespace $effect {
declare function $props(): any;
declare namespace $props {
/**
* Generates an ID that is unique to the current component instance. When hydrating a server-rendered component,
* the value will be consistent between server and client.
*
* This is useful for linking elements via attributes like `for` and `aria-labelledby`.
* @since 5.20.0
*/
export function id(): string;
// prevent intellisense from being unhelpful
/** @deprecated */
export const apply: never;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save