diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md
index 4b1775bf5a61..f300fb239d77 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/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md
index a3dbe04b0029..2b97ca796fed 100644
--- a/documentation/docs/06-runtime/03-lifecycle-hooks.md
+++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md
@@ -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.
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 2fef3bd45d50..a4ecbb31d569 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -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
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index e112bf620991..ff3b08fe7a3f 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -1,5 +1,23 @@
# 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
diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md
index 0aa6fbed90d8..795c0b007dca 100644
--- a/packages/svelte/messages/compile-errors/script.md
+++ b/packages/svelte/messages/compile-errors/script.md
@@ -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
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index a4594b2a5c7f..bea1efd7b425 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.10",
+ "version": "5.20.0",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts
index fbcecba8e47c..a1484718cc77 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 53a6ac6849ec..93eeee539cc3 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -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`);
}
/**
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index ad9db24e1e65..846abcf7dfc9 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -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,
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 14b14f9c84c1..70796a0d59b5 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;
- derived_state: string[];
+ derived_state: { name: string; private: boolean }[];
function_depth: number;
// legacy stuff
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
index 41144fc74c5c..42e449896928 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -23,11 +23,6 @@ export function Attribute(node, context) {
if (node.name === 'value' && parent.name === 'option') {
mark_subtree_dynamic(context.path);
}
-
- // special case
- if (node.name === 'loading' && parent.name === 'img') {
- mark_subtree_dynamic(context.path);
- }
}
if (is_event_attribute(node)) {
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 0a6b3f3ee520..ce520cc98055 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js
@@ -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':
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 ed397258f804..0463e4da8563 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js
@@ -7,7 +7,7 @@ import { get_rune } from '../../scope.js';
* @param {Context} context
*/
export function ClassBody(node, context) {
- /** @type {string[]} */
+ /** @type {{name: string, private: boolean}[]} */
const derived_state = [];
for (const definition of node.body) {
@@ -18,7 +18,10 @@ export function ClassBody(node, context) {
) {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
- derived_state.push(definition.key.name);
+ derived_state.push({
+ name: definition.key.name,
+ private: definition.key.type === 'PrivateIdentifier'
+ });
}
}
}
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 5fe2a8f24ecc..1507123e1342 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
@@ -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,16 +38,22 @@ export function validate_assignment(node, argument, state) {
e.snippet_parameter_assignment(node);
}
}
-
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
(((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.value &&
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');
}
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 f4a6c9a4147b..2e6307a4b7a6 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
@@ -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)))))
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
index 7b3a9a4d0e29..ed800e5226ce 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js
@@ -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
)
);
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
index c7e218d52143..98036aa9b609 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js
@@ -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 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'))
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 afb90bbec7f9..31e712cdcc4d 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/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
index 15e4f68e9e49..2bae4486dc58 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js
@@ -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') {
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 00634f229eeb..9214a13c94ca 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
@@ -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(''));
}
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 982b75e12f53..df3d831d3cc3 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
@@ -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) {
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 31de811ac76f..c4c31d7eb304 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 fe32dbba3e4a..abe2b115de02 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -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` */
diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js
index 2a2527803af9..62119b36dbd6 100644
--- a/packages/svelte/src/internal/client/dev/ownership.js
+++ b/packages/svelte/src/internal/client/dev/ownership.js
@@ -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>} */
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))
);
}
diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js
index 4a0f0cea0e00..2dba2d797a4a 100644
--- a/packages/svelte/src/internal/client/dom/elements/attributes.js
+++ b/packages/svelte/src/internal/client/dom/elements/attributes.js
@@ -399,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);
@@ -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;
- });
- }
-}
diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js
index b3c16cdd080f..fbc1da95df95 100644
--- a/packages/svelte/src/internal/client/dom/elements/transitions.js
+++ b/packages/svelte/src/internal/client/dom/elements/transitions.js
@@ -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));
+ });
}
/**
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 627bf917eee1..83565d17ae68 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -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;
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index bcbae393ecff..6ff3b0fa19a0 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -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,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 3f3b29b029dd..d78f6d452e84 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -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 {
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index 8b0f84268c5a..8a9ca9065b47 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -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;
@@ -333,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`
});
@@ -369,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);
@@ -457,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++) {
@@ -679,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 {
@@ -783,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;
@@ -797,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;
}
}
@@ -847,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;
}
/**
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 89b3c33df887..c4e5d318dcd0 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 e6c235147b5f..8a241deecd18 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 e8e1bc224ce4..d4d106d56deb 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 ada6f9019d7e..b24607615685 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.10';
+export const VERSION = '5.20.0';
export const PUBLIC_VERSION = '5';
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js
new file mode 100644
index 000000000000..ab941255037e
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js
@@ -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);
+ }
+});
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte
new file mode 100644
index 000000000000..4bb4365ee270
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
new file mode 100644
index 000000000000..d6da559fb176
--- /dev/null
+++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte
@@ -0,0 +1,7 @@
+
+
+
+ `
+ );
+ } else {
+ // `c6` because this runs after the `dom` tests
+ // (slightly brittle but good enough for now)
+ assert.htmlEqual(
+ target.innerHTML,
+ `
+
+
+
+
+
+
+
+{#if show}
+
+{/if}
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
index 9b203b97e82d..46d376aca2f9 100644
--- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js
@@ -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);
}
\ No newline at end of file
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json
new file mode 100644
index 000000000000..fe51488c7066
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json
@@ -0,0 +1 @@
+[]
diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte
new file mode 100644
index 000000000000..92f356492042
--- /dev/null
+++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte
@@ -0,0 +1,13 @@
+
\ No newline at end of file
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index eb3e93e4b5df..77d78477ee93 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -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;
pFad - Phonifier reborn
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.