diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md
new file mode 100644
index 0000000000..9dce4d98a8
--- /dev/null
+++ b/.changeset/hip-singers-vanish.md
@@ -0,0 +1,5 @@
+---
+'svelte': minor
+---
+
+feat: SSR-safe ID generation with `$props.id()`
diff --git a/.changeset/long-moles-join.md b/.changeset/long-moles-join.md
deleted file mode 100644
index 92c3d7bf9d..0000000000
--- a/.changeset/long-moles-join.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: ensure tracking returns true, even if in unowned
diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md
index 4b1775bf5a..f300fb239d 100644
--- a/documentation/docs/02-runes/05-$props.md
+++ b/documentation/docs/02-runes/05-$props.md
@@ -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
+
+
+
+```
\ No newline at end of file
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 91633918d2..1de3384dea 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -585,7 +585,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
diff --git a/package.json b/package.json
index 57dc4cdebe..2fe545b361 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index 9ebd37aadf..e112bf6209 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,31 @@
# svelte
+## 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
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 2cd12311bc..fee6825780 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -128,7 +128,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
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 8ea495af99..a4594b2a5c 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
- "version": "5.19.7",
+ "version": "5.19.10",
"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",
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index fbcecba8e4..a1484718cc 100644
--- a/packages/svelte/src/ambient.d.ts
+++ b/packages/svelte/src/ambient.d.ts
@@ -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;
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 0453d1fcb8..aa98fd3b4a 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -297,12 +297,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`);
}
/**
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 73b459958b..7dd0c30d89 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -429,6 +429,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,
@@ -620,7 +621,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,
@@ -691,7 +692,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
};
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index 1e71accb9f..14b14f9c84 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -19,7 +19,7 @@ export interface AnalysisState {
component_slots: Set;
/** Information about the current expression/directive/block value */
expression: ExpressionMetadata | null;
- private_derived_state: string[];
+ derived_state: string[];
function_depth: number;
// legacy stuff
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
index 6b45c9af1b..9ae3c3319d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -56,7 +56,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;
@@ -75,6 +75,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':
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
index d445af0ebf..ed397258f8 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -8,20 +8,20 @@ import { get_rune } from '../../scope.js';
*/
export function ClassBody(node, context) {
/** @type {string[]} */
- const private_derived_state = [];
+ 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(definition.key.name);
}
}
}
- context.next({ ...context.state, private_derived_state });
+ context.next({ ...context.state, derived_state });
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
index e265637c40..2d90c85364 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js
@@ -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);
}
@@ -35,20 +39,17 @@ export function validate_assignment(node, argument, state) {
}
}
- 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.includes(argument.property.name)) ||
+ (argument.property.type === 'Literal' &&
+ argument.property.value &&
+ typeof argument.property.value === 'string' &&
+ state.derived_state.includes(argument.property.value)))
+ ) {
+ e.constant_assignment(node, 'derived state');
}
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
index ed837b2b6f..47aad3a184 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js
@@ -591,6 +591,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)))))
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
index f047fddbdf..817feab2d2 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js
@@ -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'];
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
index 8746143408..e5b45124d0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js
@@ -137,6 +137,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(''));
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index 9aa2b4061b..7d6acdee78 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -246,6 +246,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) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
index 31de811ac7..c4c31d7eb3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js
@@ -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
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index 0be2fa0d73..969a93169e 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -58,6 +58,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` */
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index eab27e6c02..4a0f0cea0e 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -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 {
@@ -402,11 +419,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 +428,10 @@ export function set_attributes(
}
}
+ if (is_hydrating_custom_element) {
+ set_hydrating(true);
+ }
+
return current;
}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index bcbae393ec..3e4f45aba8 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -249,3 +249,23 @@ export function append(anchor, dom) {
anchor.before(/** @type {Node} */ (dom));
}
+
+let 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++;
+}
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index 9035e50e4f..2f32e8c25b 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -97,7 +97,8 @@ export {
mathml_template,
template,
template_with_script,
- text
+ text,
+ props_id
} from './dom/template.js';
export { async_derived, derived, derived_safe_equal } from './reactivity/deriveds.js';
export {
diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js
index c2da6639b8..6fd875c98f 100644
--- a/packages/svelte/src/internal/client/reactivity/deriveds.js
+++ b/packages/svelte/src/internal/client/reactivity/deriveds.js
@@ -12,7 +12,6 @@ import {
import {
active_reaction,
active_effect,
- remove_reactions,
set_signal_status,
skip_reaction,
update_reaction,
diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js
index d157642d5d..5a3b30281f 100644
--- a/packages/svelte/src/internal/client/reactivity/props.js
+++ b/packages/svelte/src/internal/client/reactivity/props.js
@@ -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;
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index b8d9f851cf..39a27000fe 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -209,18 +209,28 @@ export function check_dirtiness(reaction) {
(is_disconnected || is_unowned_connected) &&
(active_effect === null || (active_effect.f & DESTROYED) === 0)
) {
+ 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;
}
}
@@ -423,15 +433,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);
@@ -697,10 +701,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 {
@@ -805,11 +806,11 @@ export function schedule_effect(signal) {
* effects to be flushed.
*
* @param {Effect} effect
- * @param {Effect[]} collected_effects
+ * @param {Effect[]} effects
* @param {Boundary} [boundary]
- * @returns {void}
+ * @returns {Effect[]}
*/
-function process_effects(effect, collected_effects, boundary) {
+function process_effects(effect, effects = [], boundary) {
var current_effect = effect.first;
main_loop: while (current_effect !== null) {
@@ -825,14 +826,14 @@ function process_effects(effect, collected_effects, boundary) {
} else if ((flags & BOUNDARY_EFFECT) !== 0) {
var b = /** @type {Boundary} */ (current_effect.b);
- process_effects(current_effect, collected_effects, b);
+ process_effects(current_effect, effects, b);
if (!b.suspended) {
// no more async work to happen
b.commit();
}
} else if ((flags & EFFECT) !== 0) {
- collected_effects.push(current_effect);
+ effects.push(current_effect);
} else if (is_branch) {
current_effect.f ^= CLEAN;
} else {
@@ -879,6 +880,8 @@ function process_effects(effect, collected_effects, boundary) {
current_effect = sibling;
}
+
+ return effects;
}
/**
@@ -975,7 +978,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);
}
}
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 609b54804b..28e7f013d3 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -28,14 +28,15 @@ 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
- }
+ },
+ uid
};
}
@@ -48,6 +49,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 +85,27 @@ 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} Props
* @param {import('svelte').Component | ComponentType>} component
- * @param {{ props?: Omit; context?: Map }} [options]
+ * @param {{ props?: Omit; context?: Map, uid?: () => string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
/** @type {Payload} */
- const payload = { out: '', css: new Set(), head: { title: '', out: '' } };
+ const payload = {
+ out: '',
+ css: new Set(),
+ head: { title: '', out: '' },
+ uid: options.uid ?? props_id_generator()
+ };
const prev_on_destroy = on_destroy;
on_destroy = [];
@@ -526,6 +538,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 += '';
+ return uid;
+}
+
export { attr, clsx };
export { html } from './blocks/html.js';
diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts
index e6c235147b..8a241deecd 100644
--- a/packages/svelte/src/internal/server/types.d.ts
+++ b/packages/svelte/src/internal/server/types.d.ts
@@ -18,6 +18,8 @@ export interface Payload {
title: string;
out: string;
};
+ /** Function that generates a unique ID */
+ uid: () => string;
}
export interface RenderOutput {
diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js
index e8e1bc224c..d4d106d56d 100644
--- a/packages/svelte/src/utils.js
+++ b/packages/svelte/src/utils.js
@@ -433,6 +433,7 @@ const RUNES = /** @type {const} */ ([
'$state.raw',
'$state.snapshot',
'$props',
+ '$props.id',
'$bindable',
'$derived',
'$derived.by',
diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js
index d3c6e6b321..ada6f9019d 100644
--- a/packages/svelte/src/version.js
+++ b/packages/svelte/src/version.js
@@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
-export const VERSION = '5.19.7';
+export const VERSION = '5.19.10';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
index 2f5b652fac..e1076af2ec 100644
--- a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
+++ b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/_expected.html
@@ -1 +1 @@
-foo
+foofoo
diff --git a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
index be01d05f8e..3f0c988016 100644
--- a/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
+++ b/packages/svelte/tests/hydration/samples/repair-mismatched-a-href/main.svelte
@@ -3,3 +3,4 @@
foo
+foo
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js
index 118a51157e..7f406d8f0d 100644
--- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js
+++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/_config.js
@@ -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, 'test');
+ assert.equal(value2.shadowRoot?.innerHTML, 'test');
}
});
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
index ff94a9484c..4c98245e5b 100644
--- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
@@ -1,2 +1,22 @@
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js
new file mode 100644
index 0000000000..8cd4af0548
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/_config.js
@@ -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,
+ `\n3\n\n1`
+ );
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte
new file mode 100644
index 0000000000..48d4f5fd0b
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-12/main.svelte
@@ -0,0 +1,18 @@
+
+
+ {linked.current}
+ {count}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte
new file mode 100644
index 0000000000..ad8bbd6f01
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte
@@ -0,0 +1,5 @@
+
+
+
{id}
diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js
new file mode 100644
index 0000000000..9d91b98e0f
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js
@@ -0,0 +1,61 @@
+import { flushSync } from 'svelte';
+import { test } from '../../test';
+
+export default test({
+ test({ assert, target, variant }) {
+ if (variant === 'dom') {
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+ `
+ );
+ } else {
+ // `c6` because this runs after the `dom` tests
+ // (slightly brittle but good enough for now)
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+