From 4eb432e941d4d8ebf1206ea86433b0c778b71737 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 17:27:04 -0400 Subject: [PATCH 01/30] chore: remove event hoisting (#17030) * chore: get rid of hoisted event handlers * remove unused stuff * simplify * wow we can delete so much more code. this makes me so happy * even more! --- .../phases/2-analyze/visitors/Attribute.js | 188 +----------------- .../2-analyze/visitors/shared/function.js | 7 - .../phases/3-transform/client/utils.js | 126 +----------- .../client/visitors/FunctionDeclaration.js | 11 - .../client/visitors/VariableDeclaration.js | 18 -- .../client/visitors/shared/events.js | 30 +-- .../client/visitors/shared/function.js | 13 -- .../src/compiler/phases/3-transform/utils.js | 19 -- packages/svelte/src/compiler/phases/nodes.js | 2 +- .../svelte/src/compiler/phases/types.d.ts | 26 --- .../svelte/src/compiler/types/template.d.ts | 11 +- .../svelte/src/compiler/utils/builders.js | 9 +- .../client/dom/elements/attributes.js | 4 +- .../internal/client/dom/elements/events.js | 9 +- packages/svelte/src/utils.js | 2 +- .../samples/inspect-new-property/_config.js | 4 +- .../samples/inspect-recursive/_config.js | 4 +- .../_expected/client/index.svelte.js | 11 +- .../_expected/client/index.svelte.js | 14 +- .../_expected/client/index.svelte.js | 3 +- .../_expected/client/index.svelte.js | 17 +- 21 files changed, 45 insertions(+), 483 deletions(-) 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 b13f3f89b6..2b7d636606 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,12 +1,7 @@ -/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ -/** @import { AST, DelegatedEvent } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ -import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js'; -import { - get_attribute_chunks, - get_attribute_expression, - is_event_attribute -} from '../../../utils/ast.js'; +import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js'; +import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; /** @@ -64,181 +59,8 @@ export function Attribute(node, context) { context.state.analysis.uses_event_attributes = true; } - const expression = get_attribute_expression(node); - const delegated_event = get_delegated_event(node.name.slice(2), expression, context); - - if (delegated_event !== null) { - if (delegated_event.hoisted) { - delegated_event.function.metadata.hoisted = true; - } - - node.metadata.delegated = delegated_event; - } - } - } -} - -/** @type {DelegatedEvent} */ -const unhoisted = { hoisted: false }; - -/** - * Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so - * @param {string} event_name - * @param {Expression | null} handler - * @param {Context} context - * @returns {null | DelegatedEvent} - */ -function get_delegated_event(event_name, handler, context) { - // Handle delegated event handlers. Bail out if not a delegated event. - if (!handler || !is_delegated(event_name)) { - return null; - } - - // If we are not working with a RegularElement, then bail out. - const element = context.path.at(-1); - if (element?.type !== 'RegularElement') { - return null; - } - - /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */ - let target_function = null; - let binding = null; - - if (element.metadata.has_spread) { - // event attribute becomes part of the dynamic spread array - return unhoisted; - } - - if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') { - target_function = handler; - } else if (handler.type === 'Identifier') { - binding = context.state.scope.get(handler.name); - - if (context.state.analysis.module.scope.references.has(handler.name)) { - // If a binding with the same name is referenced in the module scope (even if not declared there), bail out - return unhoisted; - } - - if (binding != null) { - for (const { path } of binding.references) { - const parent = path.at(-1); - if (parent === undefined) return unhoisted; - - const grandparent = path.at(-2); - - /** @type {AST.RegularElement | null} */ - let element = null; - /** @type {string | null} */ - let event_name = null; - if (parent.type === 'OnDirective') { - element = /** @type {AST.RegularElement} */ (grandparent); - event_name = parent.name; - } else if ( - parent.type === 'ExpressionTag' && - grandparent?.type === 'Attribute' && - is_event_attribute(grandparent) - ) { - element = /** @type {AST.RegularElement} */ (path.at(-3)); - const attribute = /** @type {AST.Attribute} */ (grandparent); - event_name = get_attribute_event_name(attribute.name); - } - - if (element && event_name) { - if ( - element.type !== 'RegularElement' || - element.metadata.has_spread || - !is_delegated(event_name) - ) { - return unhoisted; - } - } else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') { - return unhoisted; - } - } + node.metadata.delegated = + parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2)); } - - // If the binding is exported, bail out - if (context.state.analysis.exports.find((node) => node.name === handler.name)) { - return unhoisted; - } - - if (binding?.is_function()) { - target_function = binding.initial; - } - } - - // If we can't find a function, or the function has multiple parameters, bail out - if (target_function == null || target_function.params.length > 1) { - return unhoisted; - } - - const visited_references = new Set(); - const scope = target_function.metadata.scope; - for (const [reference] of scope.references) { - // Bail out if the arguments keyword is used or $host is referenced - if (reference === 'arguments' || reference === '$host') return unhoisted; - // Bail out if references a store subscription - if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted; - - const binding = scope.get(reference); - const local_binding = context.state.scope.get(reference); - - // if the function access a snippet that can't be hoisted we bail out - if ( - local_binding !== null && - local_binding.initial?.type === 'SnippetBlock' && - !local_binding.initial.metadata.can_hoist - ) { - return unhoisted; - } - - // If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function). - if ( - local_binding !== null && - binding !== null && - local_binding.node !== binding.node && - scope.declarations.get(reference) !== binding - ) { - return unhoisted; - } - - // If we have multiple references to the same store using $ prefix, bail out. - if ( - binding !== null && - binding.kind === 'store_sub' && - visited_references.has(reference.slice(1)) - ) { - return unhoisted; - } - - // If we reference the index within an each block, then bail out. - if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted; - - if ( - binding !== null && - // Bail out if the binding is a rest param - (binding.declaration_kind === 'rest_param' || - // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, - (((!context.state.analysis.runes && binding.kind === 'each') || - // or any normal not reactive bindings that are mutated. - binding.kind === 'normal') && - binding.updated)) - ) { - return unhoisted; - } - visited_references.add(reference); - } - - return { hoisted: true, function: target_function }; -} - -/** - * @param {string} event_name - */ -function get_attribute_event_name(event_name) { - event_name = event_name.slice(2); - if (is_capture_event(event_name)) { - event_name = event_name.slice(0, -7); } - return event_name; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index 1776167850..4d93cd44e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -6,13 +6,6 @@ * @param {Context} context */ export function visit_function(node, context) { - // TODO retire this in favour of a more general solution based on bindings - node.metadata = { - hoisted: false, - hoisted_params: [], - scope: context.state.scope - }; - if (context.state.expression) { for (const [name] of context.state.scope.references) { const binding = context.state.scope.get(name); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 41ed277898..f21fb43fc1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,6 +1,6 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { Binding } from '#compiler' */ -/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ +/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; @@ -12,9 +12,6 @@ import { PROPS_IS_UPDATED, PROPS_IS_BINDABLE } from '../../../../constants.js'; -import { dev } from '../../../state.js'; -import { walk } from 'zimmerframe'; -import { validate_mutation } from './visitors/shared/utils.js'; /** * @param {Binding} binding @@ -46,125 +43,6 @@ export function build_getter(node, state) { return node; } -/** - * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node - * @param {ComponentContext} context - * @returns {Pattern[]} - */ -function get_hoisted_params(node, context) { - const scope = context.state.scope; - - /** @type {Identifier[]} */ - const params = []; - - /** - * We only want to push if it's not already present to avoid name clashing - * @param {Identifier} id - */ - function push_unique(id) { - if (!params.find((param) => param.name === id.name)) { - params.push(id); - } - } - - for (const [reference] of scope.references) { - let binding = scope.get(reference); - - if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) { - if (binding.kind === 'store_sub') { - // We need both the subscription for getting the value and the store for updating - push_unique(b.id(binding.node.name)); - binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); - } - - let expression = context.state.transform[reference]?.read(b.id(binding.node.name)); - - if ( - // If it's a destructured derived binding, then we can extract the derived signal reference and use that. - // TODO this code is bad, we need to kill it - expression != null && - typeof expression !== 'function' && - expression.type === 'MemberExpression' && - expression.object.type === 'CallExpression' && - expression.object.callee.type === 'Identifier' && - expression.object.callee.name === '$.get' && - expression.object.arguments[0].type === 'Identifier' - ) { - push_unique(b.id(expression.object.arguments[0].name)); - } else if ( - // If we are referencing a simple $$props value, then we need to reference the object property instead - (binding.kind === 'prop' || binding.kind === 'bindable_prop') && - !is_prop_source(binding, context.state) - ) { - push_unique(b.id('$$props')); - } else if ( - // imports don't need to be hoisted - binding.declaration_kind !== 'import' - ) { - // create a copy to remove start/end tags which would mess up source maps - push_unique(b.id(binding.node.name)); - // rest props are often accessed through the $$props object for optimization reasons, - // but we can't know if the delegated event handler will use it, so we need to add both as params - if (binding.kind === 'rest_prop' && context.state.analysis.runes) { - push_unique(b.id('$$props')); - } - } - } - } - - if (dev) { - // this is a little hacky, but necessary for ownership validation - // to work inside hoisted event handlers - - /** - * @param {AssignmentExpression | UpdateExpression} node - * @param {{ next: () => void, stop: () => void }} context - */ - function visit(node, { next, stop }) { - if (validate_mutation(node, /** @type {any} */ (context), node) !== node) { - params.push(b.id('$$ownership_validator')); - stop(); - } else { - next(); - } - } - - walk(/** @type {Node} */ (node), null, { - AssignmentExpression: visit, - UpdateExpression: visit - }); - } - - return params; -} - -/** - * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node - * @param {ComponentContext} context - * @returns {Pattern[]} - */ -export function build_hoisted_params(node, context) { - const hoisted_params = get_hoisted_params(node, context); - node.metadata.hoisted_params = hoisted_params; - - /** @type {Pattern[]} */ - const params = []; - - if (node.params.length === 0) { - if (hoisted_params.length > 0) { - // For the event object - params.push(b.id(context.state.scope.generate('_'))); - } - } else { - for (const param of node.params) { - params.push(/** @type {Pattern} */ (context.visit(param))); - } - } - - params.push(...hoisted_params); - return params; -} - /** * @param {Binding} binding * @param {ComponentClientTransformState} state diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js index cd299a710b..17327c21d6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js @@ -1,7 +1,5 @@ /** @import { FunctionDeclaration } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { build_hoisted_params } from '../utils.js'; -import * as b from '#compiler/builders'; /** * @param {FunctionDeclaration} node @@ -10,14 +8,5 @@ import * as b from '#compiler/builders'; export function FunctionDeclaration(node, context) { const state = { ...context.state, in_constructor: false, in_derived: false }; - if (node.metadata?.hoisted === true) { - const params = build_hoisted_params(node, context); - const body = context.visit(node.body, state); - - context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body })); - - return b.empty; - } - context.next(state); } 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 2fc3a8ed80..0f87baa433 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 @@ -7,7 +7,6 @@ import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; -import { is_hoisted_function } from '../../utils.js'; import { get_value } from './shared/declarations.js'; /** @@ -32,13 +31,6 @@ export function VariableDeclaration(node, context) { rune === '$state.snapshot' || rune === '$host' ) { - if (init != null && is_hoisted_function(init)) { - context.state.hoisted.push( - b.const(declarator.id, /** @type {Expression} */ (context.visit(init))) - ); - - continue; - } declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } @@ -295,16 +287,6 @@ export function VariableDeclaration(node, context) { const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { - const init = declarator.init; - - if (init != null && is_hoisted_function(init)) { - context.state.hoisted.push( - b.const(declarator.id, /** @type {Expression} */ (context.visit(init))) - ); - - continue; - } - declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index d252bd5474..d4d6721960 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -26,40 +26,12 @@ export function visit_event_attribute(node, context) { let handler = build_event_handler(tag.expression, tag.metadata.expression, context); if (node.metadata.delegated) { - let delegated_assignment; - if (!context.state.events.has(event_name)) { context.state.events.add(event_name); } - // Hoist function if we can, otherwise we leave the function as is - if (node.metadata.delegated.hoisted) { - if (node.metadata.delegated.function === tag.expression) { - const func_name = context.state.scope.root.unique('on_' + event_name); - context.state.hoisted.push(b.var(func_name, handler)); - handler = func_name; - } - - const hoisted_params = /** @type {Expression[]} */ ( - node.metadata.delegated.function.metadata.hoisted_params - ); - - // When we hoist a function we assign an array with the function and all - // hoisted closure params. - if (hoisted_params) { - const args = [handler, ...hoisted_params]; - delegated_assignment = b.array(args); - } else { - delegated_assignment = handler; - } - } else { - delegated_assignment = handler; - } - context.state.init.push( - b.stmt( - b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment) - ) + b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler)) ); } else { const statement = b.stmt( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js index 691ac0b01e..3677b30814 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js @@ -1,14 +1,11 @@ /** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */ /** @import { ComponentContext } from '../../types' */ -import { build_hoisted_params } from '../../utils.js'; /** * @param {ArrowFunctionExpression | FunctionExpression} node * @param {ComponentContext} context */ export const visit_function = (node, context) => { - const metadata = node.metadata; - let state = { ...context.state, in_constructor: false, in_derived: false }; if (node.type === 'FunctionExpression') { @@ -16,15 +13,5 @@ export const visit_function = (node, context) => { state.in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor'; } - if (metadata?.hoisted === true) { - const params = build_hoisted_params(node, context); - - return /** @type {FunctionExpression} */ ({ - ...node, - params, - body: context.visit(node.body, state) - }); - } - context.next(state); }; diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index dfc2ab1de1..f61b59f3bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -1,36 +1,17 @@ -/** @import { Context } from 'zimmerframe' */ /** @import { TransformState } from './types.js' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */ /** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */ import { regex_ends_with_whitespaces, regex_not_whitespace, - regex_starts_with_newline, regex_starts_with_whitespaces } from '../patterns.js'; -import * as b from '#compiler/builders'; import * as e from '../../errors.js'; import { walk } from 'zimmerframe'; import { extract_identifiers } from '../../utils/ast.js'; import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js'; import is_reference from 'is-reference'; import { set_scope } from '../scope.js'; -import { dev } from '../../state.js'; - -/** - * @param {Node} node - * @returns {boolean} - */ -export function is_hoisted_function(node) { - if ( - node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression' || - node.type === 'FunctionDeclaration' - ) { - return node.metadata?.hoisted === true; - } - return false; -} /** * Match Svelte 4 behaviour by sorting ConstTag nodes in topological order diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index f4127db359..13188681d2 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -59,7 +59,7 @@ export function create_attribute(name, start, end, value) { name, value, metadata: { - delegated: null, + delegated: false, needs_clsx: false } }; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 4e287fd199..074012e03f 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -109,29 +109,3 @@ export interface ComponentAnalysis extends Analysis { */ snippets: Set; } - -declare module 'estree' { - interface ArrowFunctionExpression { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } - - interface FunctionExpression { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } - - interface FunctionDeclaration { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } -} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 42048c3525..f38706d075 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -5,8 +5,6 @@ import type { VariableDeclaration, VariableDeclarator, Expression, - FunctionDeclaration, - FunctionExpression, Identifier, MemberExpression, Node, @@ -27,13 +25,6 @@ import type { _CSS } from './css'; */ export type Namespace = 'html' | 'svg' | 'mathml'; -export type DelegatedEvent = - | { - hoisted: true; - function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration; - } - | { hoisted: false }; - export namespace AST { export interface BaseNode { type: string; @@ -531,7 +522,7 @@ export namespace AST { /** @internal */ metadata: { /** May be set if this is an event attribute */ - delegated: null | DelegatedEvent; + delegated: boolean; /** May be `true` if this is a `class` attribute that needs `clsx` */ needs_clsx: boolean; }; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index f21b0dc8b4..1a2d5cab5c 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -42,8 +42,7 @@ export function arrow(params, body, async = false) { body, expression: body.type !== 'BlockStatement', generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } @@ -237,8 +236,7 @@ export function function_declaration(id, params, body, async = false) { params, body, generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } @@ -595,8 +593,7 @@ function function_builder(id, params, body, async = false) { params, body, generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5f63359c9..55d5d7860a 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -7,7 +7,7 @@ import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; -import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; +import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { active_effect, active_reaction, @@ -378,7 +378,7 @@ function set_attributes( const opts = {}; const event_handle_key = '$$' + key; let event_name = key.slice(2); - var delegated = is_delegated(event_name); + var delegated = can_delegate_event(event_name); if (is_capture_event(event_name)) { event_name = event_name.slice(0, -7); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 15544d7426..4c64c8364a 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,5 +1,5 @@ import { teardown } from '../../reactivity/effects.js'; -import { define_property, is_array } from '../../../shared/utils.js'; +import { define_property } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; @@ -258,12 +258,7 @@ export function handle_event_propagation(event) { // -> the target could not have been disabled because it emits the event in the first place event.target === current_target) ) { - if (is_array(delegated)) { - var [fn, ...data] = delegated; - fn.apply(current_target, [event, ...data]); - } else { - delegated.call(current_target, event); - } + delegated.call(current_target, event); } } catch (error) { if (throw_error) { diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index a54a421418..d63d4ff801 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -137,7 +137,7 @@ const DELEGATED_EVENTS = [ * Returns `true` if `event_name` is a delegated event * @param {string} event_name */ -export function is_delegated(event_name) { +export function can_delegate_event(event_name) { return DELEGATED_EVENTS.includes(event_name); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js index 8134044b16..43d217977e 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js @@ -15,9 +15,9 @@ export default test({ {}, [], { x: 'hello' }, - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', ['hello'], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js index 9d95956e7d..8bf67159f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -15,9 +15,9 @@ export default test({ assert.deepEqual(normalise_inspect_logs(logs), [ [], [{}], - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', [{}, {}], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 9bb45ebf78..52820c1652 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -1,19 +1,20 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -function increment(_, counter) { - counter.count += 1; -} - var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { let counter = $.proxy({ count: 0 }); const promise = $.derived(() => Promise.resolve(counter)); + + function increment() { + counter.count += 1; + } + var fragment = root(); var button = $.first_child(fragment); - button.__click = [increment, counter]; + button.__click = increment; var text = $.child(button); diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js index 0d95d8d335..ae28419b95 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -2,12 +2,6 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var on_click = (e) => { - const index = Number(e.currentTarget.dataset.index); - - console.log(index); -}; - var root_1 = $.from_html(``); export default function Delegated_locally_declared_shadowed($$anchor) { @@ -18,7 +12,13 @@ export default function Delegated_locally_declared_shadowed($$anchor) { var button = root_1(); $.set_attribute(button, 'data-index', index); - button.__click = [on_click]; + + button.__click = (e) => { + const index = Number(e.currentTarget.dataset.index); + + console.log(index); + }; + $.append($$anchor, button); }); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index b46acee82e..7025c788be 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -1,7 +1,6 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var on_click = (_, count) => $.update(count); var root = $.from_html(`

`, 1); export default function Nullish_coallescence_omittance($$anchor) { @@ -18,7 +17,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var button = $.sibling(b, 2); - button.__click = [on_click, count]; + button.__click = () => $.update(count); var text = $.child(button); diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index c446b3d3ef..30691231f4 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -1,18 +1,19 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -function reset(_, str, tpl) { - $.set(str, ''); - $.set(str, ``); - $.set(tpl, ''); - $.set(tpl, ``); -} - var root = $.from_html(` `, 1); export default function State_proxy_literal($$anchor) { let str = $.state(''); let tpl = $.state(``); + + function reset() { + $.set(str, ''); + $.set(str, ``); + $.set(tpl, ''); + $.set(tpl, ``); + } + var fragment = root(); var input = $.first_child(fragment); @@ -24,7 +25,7 @@ export default function State_proxy_literal($$anchor) { var button = $.sibling(input_1, 2); - button.__click = [reset, str, tpl]; + button.__click = reset; $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value)); $.append($$anchor, fragment); From b7f39b464a00eda8eeb233dbf52d03a1fe0a740b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:16:29 +0200 Subject: [PATCH 02/30] fix: always allow `setContext` before first await in component (#17031) The previous check was flawed because EFFECT_RAN would be set by the time it is checked, since a promise in a parent component will cause a delay of the inner component being instantiated. Instead we have a new field on the component context checking if the component was already popped (if se we are indeed too late). Don't love it to have a field just for this but I don't see another way to reliably check it. Fixes #16629 --- .changeset/itchy-hats-study.md | 5 +++++ packages/svelte/src/internal/client/constants.js | 1 + packages/svelte/src/internal/client/context.js | 9 ++++++++- packages/svelte/src/internal/client/error-handling.js | 4 ++-- packages/svelte/src/internal/client/types.d.ts | 4 +++- .../async-context-throws-after-await/_config.js | 11 +++++++++++ .../async-context-throws-after-await/main.svelte | 7 +++++++ .../samples/async-set-context/Inner.svelte | 7 +++++++ .../samples/async-set-context/Outer.svelte | 9 +++++++++ .../samples/async-set-context/_config.js | 11 +++++++++++ .../samples/async-set-context/main.svelte | 7 +++++++ 11 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .changeset/itchy-hats-study.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte diff --git a/.changeset/itchy-hats-study.md b/.changeset/itchy-hats-study.md new file mode 100644 index 0000000000..e92ec5affd --- /dev/null +++ b/.changeset/itchy-hats-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always allow `setContext` before first await in component diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 24dc9e4fb8..c2f7861b78 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -13,6 +13,7 @@ export const INERT = 1 << 13; export const DESTROYED = 1 << 14; // Flags exclusive to effects +/** Set once an effect that should run synchronously has run */ export const EFFECT_RAN = 1 << 15; /** * 'Transparent' effects do not create a transition boundary. diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 751a35321a..ffdb342adb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -128,7 +128,11 @@ export function setContext(key, context) { if (async_mode_flag) { var flags = /** @type {Effect} */ (active_effect).f; - var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; + var valid = + !active_reaction && + (flags & BRANCH_EFFECT) !== 0 && + // pop() runs synchronously, so this indicates we're setting context after an await + !(/** @type {ComponentContext} */ (component_context).i); if (!valid) { e.set_context_after_init(); @@ -173,6 +177,7 @@ export function getAllContexts() { export function push(props, runes = false, fn) { component_context = { p: component_context, + i: false, c: null, e: null, s: props, @@ -208,6 +213,8 @@ export function pop(component) { context.x = component; } + context.i = true; + component_context = context.p; if (DEV) { diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 6c83a453d5..dcbbf14e20 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -29,7 +29,7 @@ export function handle_error(error) { // if the error occurred while creating this subtree, we let it // bubble up until it hits a boundary that can handle it if ((effect.f & BOUNDARY_EFFECT) === 0) { - if (!effect.parent && error instanceof Error) { + if (DEV && !effect.parent && error instanceof Error) { apply_adjustments(error); } @@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) { effect = effect.parent; } - if (error instanceof Error) { + if (DEV && error instanceof Error) { apply_adjustments(error); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index d24218c4d3..deb3e82986 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -1,6 +1,6 @@ import type { Store } from '#shared'; import { STATE_SYMBOL } from './constants.js'; -import type { Effect, Source, Value, Reaction } from './reactivity/types.js'; +import type { Effect, Source, Value } from './reactivity/types.js'; type EventCallback = (event: Event) => boolean; export type EventCallbackMap = Record; @@ -16,6 +16,8 @@ export type ComponentContext = { c: null | Map; /** deferred effects */ e: null | Array<() => void | (() => void)>; + /** True if initialized, i.e. pop() ran */ + i: boolean; /** * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` * @deprecated remove in 6.0 diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js new file mode 100644 index 0000000000..be73968a88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test() { + // else runtime_error is checked too soon + await tick(); + }, + runtime_error: 'set_context_after_init' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte new file mode 100644 index 0000000000..8e770c214b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte new file mode 100644 index 0000000000..2c7fd5d43d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte @@ -0,0 +1,7 @@ + + +

{greeting}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte new file mode 100644 index 0000000000..9a493c5b75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js new file mode 100644 index 0000000000..041f67a39e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'async-server'], + ssrHtml: `

hi

`, + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hi

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte new file mode 100644 index 0000000000..01b46bda93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -0,0 +1,7 @@ + + + From d8137b78a5ab747d0b51fd531288b113b974d830 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 11:29:10 -0400 Subject: [PATCH 03/30] fix: less confusing names for inspect errors (#17026) * fix: less confusing names for inspect errors * fix * Update packages/svelte/src/internal/client/dev/inspect.js * fix --- .changeset/swift-taxes-shake.md | 5 +++++ .../svelte/src/internal/client/dev/inspect.js | 11 ++++++++++- .../svelte/src/internal/client/dev/tracing.js | 3 +-- packages/svelte/src/internal/client/proxy.js | 2 +- .../src/internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/sources.js | 4 ++-- packages/svelte/src/internal/client/runtime.js | 4 ++-- packages/svelte/tests/helpers.js | 18 ++++++++++++++---- .../async-reactivity-loss-for-await/_config.js | 2 +- .../samples/async-reactivity-loss/_config.js | 2 +- .../samples/effect-loop-infinite/_config.js | 2 +- 11 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 .changeset/swift-taxes-shake.md diff --git a/.changeset/swift-taxes-shake.md b/.changeset/swift-taxes-shake.md new file mode 100644 index 0000000000..73b1529d87 --- /dev/null +++ b/.changeset/swift-taxes-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: less confusing names for inspect errors diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index 09150d6ee4..34ba508984 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) { inspector(...snap); if (!initial) { + const stack = get_stack('$inspect(...)'); // eslint-disable-next-line no-console - console.log(get_stack('UpdatedAt')); + + if (stack) { + // eslint-disable-next-line no-console + console.groupCollapsed('stack trace'); + // eslint-disable-next-line no-console + console.log(stack); + // eslint-disable-next-line no-console + console.groupEnd(); + } } } else { inspector(initial ? 'init' : 'update', ...snap); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 98be92d4b2..4688637f5d 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -179,8 +179,7 @@ export function get_stack(label) { }); define_property(error, 'name', { - // 'Error' suffix is required for stack traces to be rendered properly - value: `${label}Error` + value: label }); return /** @type {Error & { stack: string }} */ (error); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 9baacacd0d..49cef451b3 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -53,7 +53,7 @@ export function proxy(value) { var is_proxied_array = is_array(value); var version = source(0); - var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + var stack = DEV && tracing_mode_flag ? get_stack('created at') : null; var parent_version = update_version; /** diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b6a50acc4d..1eb640ad26 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -86,7 +86,7 @@ export function derived(fn) { }; if (DEV && tracing_mode_flag) { - signal.created = get_stack('CreatedAt'); + signal.created = get_stack('created at'); } return signal; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9534e718a5..b480d4155a 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -76,7 +76,7 @@ export function source(v, stack) { }; if (DEV && tracing_mode_flag) { - signal.created = stack ?? get_stack('CreatedAt'); + signal.created = stack ?? get_stack('created at'); signal.updated = null; signal.set_during_effect = false; signal.trace = null; @@ -186,7 +186,7 @@ export function internal_set(source, value) { if (DEV) { if (tracing_mode_flag || active_effect !== null) { - const error = get_stack('UpdatedAt'); + const error = get_stack('updated at'); if (error !== null) { source.updated ??= new Map(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2e6f05b4b1..49396d6feb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -609,7 +609,7 @@ export function get(signal) { if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); - var trace = get_stack('TracedAt'); + var trace = get_stack('traced at'); // eslint-disable-next-line no-console if (trace) console.warn(trace); } @@ -628,7 +628,7 @@ export function get(signal) { if (signal.trace) { signal.trace(); } else { - trace = get_stack('TracedAt'); + trace = get_stack('traced at'); if (trace) { var entry = tracing_expressions.entries.get(signal); diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index bf708878a3..d0ec8b6e44 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; * @param {any[]} logs */ export function normalise_inspect_logs(logs) { - return logs.map((log) => { + /** @type {string[]} */ + const normalised = []; + + for (const log of logs) { + if (log === 'stack trace') { + // ignore `console.group('stack trace')` in default `$inspect(...)` output + continue; + } + if (log instanceof Error) { const last_line = log.stack ?.trim() @@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) { const match = last_line && /(at .+) /.exec(last_line); - return match && match[1]; + if (match) normalised.push(match[1]); + } else { + normalised.push(log); } + } - return log; - }); + return normalised; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index bde65a499f..2bcb129b12 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -17,7 +17,7 @@ export default test({ 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 16318a3b44..747648e83f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -20,7 +20,7 @@ export default test({ 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js index 400495050c..57f60c2b44 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -14,7 +14,7 @@ export default test({ try { flushSync(() => button.click()); } catch (e) { - assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError + assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at' assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); } } From 875a04170ec58e81234a28aea742131391233ae9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 11:35:04 -0400 Subject: [PATCH 04/30] Version Packages (#17029) * Version Packages * minor not patch * Apply suggestions from code review --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/itchy-hats-study.md | 5 ----- .changeset/small-geckos-camp.md | 5 ----- .changeset/swift-taxes-shake.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/itchy-hats-study.md delete mode 100644 .changeset/small-geckos-camp.md delete mode 100644 .changeset/swift-taxes-shake.md diff --git a/.changeset/itchy-hats-study.md b/.changeset/itchy-hats-study.md deleted file mode 100644 index e92ec5affd..0000000000 --- a/.changeset/itchy-hats-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always allow `setContext` before first await in component diff --git a/.changeset/small-geckos-camp.md b/.changeset/small-geckos-camp.md deleted file mode 100644 index 622cbbbfa0..0000000000 --- a/.changeset/small-geckos-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -feat: experimental `fork` API diff --git a/.changeset/swift-taxes-shake.md b/.changeset/swift-taxes-shake.md deleted file mode 100644 index 73b1529d87..0000000000 --- a/.changeset/swift-taxes-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: less confusing names for inspect errors diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d63548d3e1..c59d4dbc6e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.42.0 + +### Minor Changes + +- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004)) + +### Patch Changes + +- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031)) + +- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026)) + ## 5.41.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1d50920d4f..c3b7254bd5 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.41.4", + "version": "5.42.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f5f47c6056..abdb12e088 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.41.4'; +export const VERSION = '5.42.0'; export const PUBLIC_VERSION = '5'; From 657ec89caa93d39d2cf8d8cdbe27b0041615409f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 14:12:22 -0400 Subject: [PATCH 05/30] fix: ignore fork `discard()` after `commit()` (#17034) * fix: ignore fork `discard()` after `commit()` * fix message --- .changeset/twenty-onions-attack.md | 5 +++++ .../98-reference/.generated/client-errors.md | 2 +- .../svelte/messages/client-errors/errors.md | 2 +- packages/svelte/src/internal/client/errors.js | 4 ++-- .../src/internal/client/reactivity/batch.js | 22 +++++++++++++------ 5 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 .changeset/twenty-onions-attack.md diff --git a/.changeset/twenty-onions-attack.md b/.changeset/twenty-onions-attack.md new file mode 100644 index 0000000000..c23c8b590b --- /dev/null +++ b/.changeset/twenty-onions-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore fork `discard()` after `commit()` diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 74a0674dba..3f1cb8f76b 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -149,7 +149,7 @@ This restriction only applies when using the `experimental.async` option, which ### fork_discarded ``` -Cannot commit a fork that was already committed or discarded +Cannot commit a fork that was already discarded ``` ### fork_timing diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index b5fe51539d..ae7d811b2e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -114,7 +114,7 @@ This restriction only applies when using the `experimental.async` option, which ## fork_discarded -> Cannot commit a fork that was already committed or discarded +> Cannot commit a fork that was already discarded ## fork_timing diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 2a433ed8f9..8a5fde4f3b 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -262,12 +262,12 @@ export function flush_sync_in_effect() { } /** - * Cannot commit a fork that was already committed or discarded + * Cannot commit a fork that was already discarded * @returns {never} */ export function fork_discarded() { if (DEV) { - const error = new Error(`fork_discarded\nCannot commit a fork that was already committed or discarded\nhttps://svelte.dev/e/fork_discarded`); + const error = new Error(`fork_discarded\nCannot commit a fork that was already discarded\nhttps://svelte.dev/e/fork_discarded`); error.name = 'Svelte error'; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fdeb111a4d..ab83050cd0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -913,28 +913,36 @@ export function fork(fn) { e.fork_timing(); } - const batch = Batch.ensure(); + var batch = Batch.ensure(); batch.is_fork = true; - const settled = batch.settled(); + var committed = false; + var settled = batch.settled(); flushSync(fn); // revert state changes - for (const [source, value] of batch.previous) { + for (var [source, value] of batch.previous) { source.v = value; } return { commit: async () => { + if (committed) { + await settled; + return; + } + if (!batches.has(batch)) { e.fork_discarded(); } + committed = true; + batch.is_fork = false; // apply changes - for (const [source, value] of batch.current) { + for (var [source, value] of batch.current) { source.v = value; } @@ -945,9 +953,9 @@ export function fork(fn) { // TODO maybe there's a better implementation? flushSync(() => { /** @type {Set} */ - const eager_effects = new Set(); + var eager_effects = new Set(); - for (const source of batch.current.keys()) { + for (var source of batch.current.keys()) { mark_eager_effects(source, eager_effects); } @@ -959,7 +967,7 @@ export function fork(fn) { await settled; }, discard: () => { - if (batches.has(batch)) { + if (!committed && batches.has(batch)) { batches.delete(batch); batch.discard(); } From e33f774877be45cf68fb96f84b4670a9deb52159 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 14:48:43 -0400 Subject: [PATCH 06/30] Version Packages (#17035) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/twenty-onions-attack.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/twenty-onions-attack.md diff --git a/.changeset/twenty-onions-attack.md b/.changeset/twenty-onions-attack.md deleted file mode 100644 index c23c8b590b..0000000000 --- a/.changeset/twenty-onions-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore fork `discard()` after `commit()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c59d4dbc6e..51e6317491 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.42.1 + +### Patch Changes + +- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034)) + ## 5.42.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c3b7254bd5..1f8746e72d 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.42.0", + "version": "5.42.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index abdb12e088..8d50b983ce 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.42.0'; +export const VERSION = '5.42.1'; export const PUBLIC_VERSION = '5'; From 8368a4beb98fd9ed07fc34500ddb689916fbf8ad Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 25 Oct 2025 23:46:58 +0200 Subject: [PATCH 07/30] fix: better error message for global variable assignments (#17036) --- .changeset/slick-teeth-exist.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/utils.js | 5 ++++- .../samples/global-variable-assignment/_config.js | 6 ++++++ .../samples/global-variable-assignment/foo.svelte.js | 1 + .../samples/global-variable-assignment/main.svelte | 3 +++ 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/slick-teeth-exist.md create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte diff --git a/.changeset/slick-teeth-exist.md b/.changeset/slick-teeth-exist.md new file mode 100644 index 0000000000..aeeb0f41b9 --- /dev/null +++ b/.changeset/slick-teeth-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better error message for global variable assignments 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 d7b682da08..cc4376a0c2 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 @@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) { const binding = context.state.scope.get(argument.name); if (context.state.analysis.runes) { - if (binding?.node === context.state.analysis.props_id) { + if ( + context.state.analysis.props_id != null && + binding?.node === context.state.analysis.props_id + ) { e.constant_assignment(node, '$props.id()'); } diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js new file mode 100644 index 0000000000..37f4b2814c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + error: 'x is not defined', + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js new file mode 100644 index 0000000000..198b8f89e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js @@ -0,0 +1 @@ +x = 1; diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte new file mode 100644 index 0000000000..0ac6956b1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte @@ -0,0 +1,3 @@ + From b01647455c495c4b99b84f9ecd7ce664e593b4f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:36:01 -0400 Subject: [PATCH 08/30] chore: create_expression_metadata -> ExpressionMetadata (#17039) * chore: create_expression_metadata -> ExpressionMetadata * err whatever --- .../compiler/phases/1-parse/state/element.js | 17 ++++---- .../src/compiler/phases/1-parse/state/tag.js | 18 ++++---- .../src/compiler/phases/2-analyze/types.d.ts | 3 +- .../2-analyze/visitors/CallExpression.js | 4 +- .../phases/2-analyze/visitors/RenderTag.js | 4 +- .../client/visitors/shared/element.js | 3 +- .../client/visitors/shared/events.js | 3 +- .../client/visitors/shared/utils.js | 3 +- .../server/visitors/shared/element.js | 12 ++---- .../server/visitors/shared/utils.js | 5 ++- packages/svelte/src/compiler/phases/nodes.js | 41 ++++++++++++------- packages/svelte/src/compiler/phases/scope.js | 4 +- packages/svelte/src/compiler/types/index.d.ts | 17 -------- .../svelte/src/compiler/types/template.d.ts | 3 +- 14 files changed, 67 insertions(+), 70 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index b0db9ce178..7b0950ae82 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -9,11 +9,10 @@ import { decode_character_references } from '../utils/html.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_fragment } from '../utils/create.js'; -import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { list } from '../../../utils/string.js'; -import { regex_whitespace } from '../../patterns.js'; const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; @@ -297,7 +296,7 @@ export default function element(parser) { element.tag = get_attribute_expression(definition); } - element.metadata.expression = create_expression_metadata(); + element.metadata.expression = new ExpressionMetadata(); } if (is_top_level_script_or_style) { @@ -508,7 +507,7 @@ function read_attribute(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -528,7 +527,7 @@ function read_attribute(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -568,7 +567,7 @@ function read_attribute(parser) { name }, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -628,7 +627,7 @@ function read_attribute(parser) { modifiers: /** @type {Array<'important'>} */ (modifiers), value, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; } @@ -658,7 +657,7 @@ function read_attribute(parser) { name: directive_name, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -824,7 +823,7 @@ function read_sequence(parser, done, location) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index ba091ef7ec..4ff948e165 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -3,7 +3,7 @@ /** @import { Parser } from '../index.js' */ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; import { parse_expression_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; @@ -42,7 +42,7 @@ export default function tag(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); } @@ -65,7 +65,7 @@ function open(parser) { consequent: create_fragment(), alternate: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -249,7 +249,7 @@ function open(parser) { then: null, catch: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -334,7 +334,7 @@ function open(parser) { expression, fragment: create_fragment(), metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -477,7 +477,7 @@ function next(parser) { consequent: create_fragment(), alternate: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -643,7 +643,7 @@ function special(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -721,7 +721,7 @@ function special(parser) { end: parser.index - 1 }, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); } @@ -748,7 +748,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { - expression: create_expression_metadata(), + expression: new ExpressionMetadata(), dynamic: false, arguments: [], path: [], 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 bad6c7d613..9d24f9dbac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,6 +1,7 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler'; +import type { AST, StateField, ValidatedCompileOptions } from '#compiler'; +import type { ExpressionMetadata } from '../nodes.js'; export interface AnalysisState { scope: Scope; 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 4b66abe1d1..52eba8c735 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -7,7 +7,7 @@ import { get_parent } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; import { dev, locate_node, source } from '../../../state.js'; import * as b from '#compiler/builders'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; /** * @param {CallExpression} node @@ -243,7 +243,7 @@ export function CallExpression(node, context) { // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning if (rune === '$derived') { - const expression = create_expression_metadata(); + const expression = new ExpressionMetadata(); context.next({ ...context.state, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 1230ef6b04..d0c994b7a4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -5,7 +5,7 @@ import * as e from '../../../errors.js'; import { validate_opening_tag } from './shared/utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { is_resolved_snippet } from './shared/snippets.js'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; /** * @param {AST.RenderTag} node @@ -57,7 +57,7 @@ export function RenderTag(node, context) { context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { - const metadata = create_expression_metadata(); + const metadata = new ExpressionMetadata(); node.metadata.arguments.push(metadata); context.visit(arg, { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 4b32dab82a..dd390c99da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,5 +1,5 @@ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types' */ import { escape_html } from '../../../../../../escaping.js'; import { normalize_attribute } from '../../../../../../utils.js'; @@ -8,6 +8,7 @@ import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; import { build_expression, build_template_chunk, Memoizer } from './utils.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * @param {Array} attributes diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index d4d6721960..3ab1506eb3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -1,9 +1,10 @@ /** @import { Expression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types' */ import { is_capture_event, is_passive_event } from '../../../../../../utils.js'; import { dev, locator } from '../../../../../state.js'; import * as b from '#compiler/builders'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * @param {AST.Attribute} node 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 a42063b2e2..46d2f2b777 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 @@ -1,5 +1,5 @@ /** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; @@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { build_getter } from '../../utils.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * A utility for extracting complex expressions (such as call expressions) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index cfb87b0ce7..b7607f3fb8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,13 +1,9 @@ /** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; -import { - create_attribute, - create_expression_metadata, - is_custom_element_node -} from '../../../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js'; import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '#compiler/builders'; import { @@ -160,7 +156,7 @@ export function build_element_attributes(node, context, transform) { build_attribute_value(value_attribute.value, context, transform) ), metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } } ]) @@ -174,7 +170,7 @@ export function build_element_attributes(node, context, transform) { end: -1, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } } ]) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 92653ed73c..09a854670c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -1,5 +1,5 @@ -/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */ import { escape_html } from '../../../../../../escaping.js'; @@ -13,6 +13,7 @@ import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; import { has_await_expression } from '../../../../../utils/ast.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ export const block_open = b.literal(BLOCK_OPEN); diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 13188681d2..bca9c29a65 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -1,5 +1,5 @@ /** @import { Expression, PrivateIdentifier } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST, Binding } from '#compiler' */ /** * All nodes that can appear elsewhere than the top level, have attributes and can contain children @@ -64,20 +64,33 @@ export function create_attribute(name, start, end, value) { } }; } +export class ExpressionMetadata { + /** True if the expression references state directly, or _might_ (via member/call expressions) */ + has_state = false; -/** - * @returns {ExpressionMetadata} - */ -export function create_expression_metadata() { - return { - dependencies: new Set(), - references: new Set(), - has_state: false, - has_call: false, - has_member_expression: false, - has_assignment: false, - has_await: false - }; + /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ + has_call = false; + + /** True if the expression contains `await` */ + has_await = false; + + /** True if the expression includes a member expression */ + has_member_expression = false; + + /** True if the expression includes an assignment or an update */ + has_assignment = false; + + /** + * All the bindings that are referenced eagerly (not inside functions) in this expression + * @type {Set} + */ + dependencies = new Set(); + + /** + * True if the expression references state directly, or _might_ (via member/call expressions) + * @type {Set} + */ + references = new Set(); } /** diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f7d3dac0f7..7dbdf47967 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -3,7 +3,7 @@ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; -import { create_expression_metadata } from './nodes.js'; +import { ExpressionMetadata } from './nodes.js'; import * as b from '#compiler/builders'; import * as e from '../errors.js'; import { @@ -1201,7 +1201,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { if (node.fallback) visit(node.fallback, { scope }); node.metadata = { - expression: create_expression_metadata(), + expression: new ExpressionMetadata(), keyed: false, contains_group_binding: false, index: scope.root.unique('$$index'), diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 9bd4b91d58..fce3f62c5c 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -294,23 +294,6 @@ export type DeclarationKind = | 'using' | 'await using'; -export interface ExpressionMetadata { - /** All the bindings that are referenced eagerly (not inside functions) in this expression */ - dependencies: Set; - /** All the bindings that are referenced inside this expression, including inside functions */ - references: Set; - /** True if the expression references state directly, or _might_ (via member/call expressions) */ - has_state: boolean; - /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ - has_call: boolean; - /** True if the expression contains `await` */ - has_await: boolean; - /** True if the expression includes a member expression */ - has_member_expression: boolean; - /** True if the expression includes an assignment or an update */ - has_assignment: boolean; -} - export interface StateField { type: StateCreationRuneName; node: PropertyDefinition | AssignmentExpression; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f38706d075..fa7484e523 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -1,4 +1,4 @@ -import type { Binding, ExpressionMetadata } from '#compiler'; +import type { Binding } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, @@ -17,6 +17,7 @@ import type { } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; +import type { ExpressionMetadata } from '../phases/nodes'; /** * - `html` — the default, for e.g. `
` or `` From 9096680c2eef27b7a682c2c71f302a12e5e6991b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:41:13 -0400 Subject: [PATCH 09/30] chore: use ESTree namespace imports (#17040) --- .../src/compiler/phases/2-analyze/index.js | 12 +++++----- .../3-transform/server/transform-server.js | 24 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index b4c704c34d..4c05fd6148 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,4 +1,4 @@ -/** @import { Expression, Node, Program } from 'estree' */ +/** @import * as ESTree from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ @@ -206,7 +206,7 @@ const visitors = { * @returns {Js} */ function js(script, root, allow_reactive_declarations, parent) { - /** @type {Program} */ + /** @type {ESTree.Program} */ const ast = script?.content ?? { type: 'Program', sourceType: 'module', @@ -289,7 +289,7 @@ export function analyze_module(source, options) { }); walk( - /** @type {Node} */ (ast), + /** @type {ESTree.Node} */ (ast), { scope, scopes, @@ -347,7 +347,7 @@ export function analyze_component(root, source, options) { const store_name = name.slice(1); const declaration = instance.scope.get(store_name); - const init = /** @type {Node | undefined} */ (declaration?.initial); + const init = /** @type {ESTree.Node | undefined} */ (declaration?.initial); // If we're not in legacy mode through the compiler option, assume the user // is referencing a rune and not a global store. @@ -407,7 +407,7 @@ export function analyze_component(root, source, options) { /** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) && /** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) && // const state = $state(0) is valid - get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null + get_rune(/** @type {ESTree.Node} */ (path.at(-1)), module.scope) === null ) { e.store_invalid_subscription(node); } @@ -636,7 +636,7 @@ export function analyze_component(root, source, options) { // @ts-expect-error _: set_scope, Identifier(node, context) { - const parent = /** @type {Expression} */ (context.path.at(-1)); + const parent = /** @type {ESTree.Expression} */ (context.path.at(-1)); if (is_reference(node, parent)) { const binding = context.state.scope.get(node.name); 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 b22b95f5aa..1c9764a759 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 @@ -1,4 +1,4 @@ -/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */ +/** @import * as ESTree from 'estree' */ /** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */ @@ -86,7 +86,7 @@ const template_visitors = { /** * @param {ComponentAnalysis} analysis * @param {ValidatedCompileOptions} options - * @returns {Program} + * @returns {ESTree.Program} */ export function server_component(analysis, options) { /** @type {ComponentServerTransformState} */ @@ -106,11 +106,11 @@ export function server_component(analysis, options) { skip_hydration_boundaries: false }; - const module = /** @type {Program} */ ( + const module = /** @type {ESTree.Program} */ ( walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) ); - const instance = /** @type {Program} */ ( + const instance = /** @type {ESTree.Program} */ ( walk( /** @type {AST.SvelteNode} */ (analysis.instance.ast), { ...state, scopes: analysis.instance.scopes }, @@ -131,7 +131,7 @@ export function server_component(analysis, options) { ) ); - const template = /** @type {Program} */ ( + const template = /** @type {ESTree.Program} */ ( walk( /** @type {AST.SvelteNode} */ (analysis.template.ast), { ...state, scopes: analysis.template.scopes }, @@ -140,7 +140,7 @@ export function server_component(analysis, options) { ) ); - /** @type {VariableDeclarator[]} */ + /** @type {ESTree.VariableDeclarator[]} */ const legacy_reactive_declarations = []; for (const [node] of analysis.reactive_statements) { @@ -192,7 +192,7 @@ export function server_component(analysis, options) { b.function_declaration( b.id('$$render_inner'), [b.id('$$renderer')], - b.block(/** @type {Statement[]} */ (rest)) + b.block(/** @type {ESTree.Statement[]} */ (rest)) ), b.do_while( b.unary('!', b.id('$$settled')), @@ -219,7 +219,7 @@ export function server_component(analysis, options) { // Propagate values of bound props upwards if they're undefined in the parent and have a value. // Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script. - /** @type {Property[]} */ + /** @type {ESTree.Property[]} */ const props = []; for (const [name, binding] of analysis.instance.scope.declarations) { @@ -239,8 +239,8 @@ export function server_component(analysis, options) { } let component_block = b.block([ - .../** @type {Statement[]} */ (instance.body), - .../** @type {Statement[]} */ (template.body) + .../** @type {ESTree.Statement[]} */ (instance.body), + .../** @type {ESTree.Statement[]} */ (template.body) ]); if (analysis.instance.has_await) { @@ -395,7 +395,7 @@ export function server_component(analysis, options) { /** * @param {Analysis} analysis * @param {ValidatedModuleCompileOptions} options - * @returns {Program} + * @returns {ESTree.Program} */ export function server_module(analysis, options) { /** @type {ServerTransformState} */ @@ -411,7 +411,7 @@ export function server_module(analysis, options) { state_fields: new Map() }; - const module = /** @type {Program} */ ( + const module = /** @type {ESTree.Program} */ ( walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) ); From fc181cf6110519f693536611f30c2c3cc9618fb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:54:01 -0400 Subject: [PATCH 10/30] chore: move `$effect` visitors (#17041) --- .../3-transform/client/visitors/CallExpression.js | 11 +++++++++++ .../client/visitors/ExpressionStatement.js | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index ae60f3be40..c9e5f49b96 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -62,6 +62,17 @@ export function CallExpression(node, context) { is_ignored(node, 'state_snapshot_uncloneable') && b.true ); + case '$effect': + case '$effect.pre': { + const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; + const func = /** @type {Expression} */ (context.visit(node.arguments[0])); + + const expr = b.call(callee, /** @type {Expression} */ (func)); + expr.callee.loc = node.callee.loc; // ensure correct mapping + + return expr; + } + case '$effect.root': return b.call( '$.effect_root', diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js index 859842ebc3..96a378747f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js @@ -11,16 +11,6 @@ export function ExpressionStatement(node, context) { if (node.expression.type === 'CallExpression') { const rune = get_rune(node.expression, context.state.scope); - if (rune === '$effect' || rune === '$effect.pre') { - const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; - const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0])); - - const expr = b.call(callee, /** @type {Expression} */ (func)); - expr.callee.loc = node.expression.callee.loc; // ensure correct mapping - - return b.stmt(expr); - } - if (rune === '$inspect.trace') { return b.empty; } From b5e23a6d13d2dfceb2826fe64ea83bf2cd8253fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 21:04:13 -0400 Subject: [PATCH 11/30] chore: tweak memoizer logic (#17042) --- .changeset/slimy-shirts-lose.md | 5 ++ .../client/visitors/RegularElement.js | 46 +++++++++++-------- .../3-transform/client/visitors/RenderTag.js | 2 +- .../client/visitors/SlotElement.js | 2 +- .../client/visitors/shared/component.js | 6 +-- .../client/visitors/shared/element.js | 14 ++---- .../client/visitors/shared/utils.js | 18 ++++++-- 7 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 .changeset/slimy-shirts-lose.md diff --git a/.changeset/slimy-shirts-lose.md b/.changeset/slimy-shirts-lose.md new file mode 100644 index 0000000000..084fb07ea7 --- /dev/null +++ b/.changeset/slimy-shirts-lose.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: tweak memoizer logic 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 ab119e8f80..3998770a71 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 @@ -11,7 +11,7 @@ import { import { is_ignored } from '../../../../state.js'; import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_attribute, is_custom_element_node } from '../../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../nodes.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { build_getter } from '../utils.js'; import { @@ -267,10 +267,7 @@ export function RegularElement(node, context) { const { value, has_state } = build_attribute_value( attribute.value, context, - (value, metadata) => - metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata.has_await) - : value + (value, metadata) => context.state.memoizer.add(value, metadata) ); const update = build_element_attribute_update(node, node_id, name, value, attributes); @@ -487,11 +484,25 @@ function setup_select_synchronization(value_binding, context) { ); } +/** + * @param {ExpressionMetadata} target + * @param {ExpressionMetadata} source + */ +function merge_metadata(target, source) { + target.has_assignment ||= source.has_assignment; + target.has_await ||= source.has_await; + target.has_call ||= source.has_call; + target.has_member_expression ||= source.has_member_expression; + target.has_state ||= source.has_state; + + for (const r of source.references) target.references.add(r); + for (const b of source.dependencies) target.dependencies.add(b); +} + /** * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @param {Memoizer} memoizer - * @return {ObjectExpression | Identifier} */ export function build_class_directives_object( class_directives, @@ -499,26 +510,25 @@ export function build_class_directives_object( memoizer = context.state.memoizer ) { let properties = []; - let has_call_or_state = false; - let has_await = false; + + const metadata = new ExpressionMetadata(); for (const d of class_directives) { + merge_metadata(metadata, d.metadata.expression); + const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = b.object(properties); - return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; + return memoizer.add(directives, metadata); } /** * @param {AST.StyleDirective[]} style_directives * @param {ComponentContext} context * @param {Memoizer} memoizer - * @return {ObjectExpression | ArrayExpression | Identifier}} */ export function build_style_directives_object( style_directives, @@ -528,10 +538,11 @@ export function build_style_directives_object( const normal = b.object([]); const important = b.object([]); - let has_call_or_state = false; - let has_await = false; + const metadata = new ExpressionMetadata(); for (const d of style_directives) { + merge_metadata(metadata, d.metadata.expression); + const expression = d.value === true ? build_getter(b.id(d.name), context.state) @@ -539,14 +550,11 @@ export function build_style_directives_object( const object = d.modifiers.includes('important') ? important : normal; object.properties.push(b.init(d.name, expression)); - - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = important.properties.length ? b.array([normal, important]) : normal; - return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; + return memoizer.add(directives, metadata); } /** @@ -675,7 +683,7 @@ function build_element_special_value_attribute( element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value + state.memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index b7a6e65557..b3619e8669 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -26,7 +26,7 @@ export function RenderTag(node, context) { let expression = build_expression(context, arg, metadata); if (metadata.has_await || metadata.has_call) { - expression = b.call('$.get', memoizer.add(expression, metadata.has_await)); + expression = b.call('$.get', memoizer.add(expression, metadata)); } args.push(b.thunk(expression)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index b87a13253b..f6db21212b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -35,7 +35,7 @@ export function SlotElement(node, context) { context, (value, metadata) => metadata.has_call || metadata.has_await - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata)) : value ); 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 5ca941fd70..688191fd20 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 @@ -134,7 +134,7 @@ export function build_component(node, component_name, context) { props_and_spreads.push( b.thunk( attribute.metadata.expression.has_await || attribute.metadata.expression.has_call - ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await)) + ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression)) : expression ) ); @@ -149,7 +149,7 @@ export function build_component(node, component_name, context) { build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block return metadata.has_call || metadata.has_await - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata)) : value; }).value ) @@ -185,7 +185,7 @@ export function build_component(node, component_name, context) { }); return should_wrap_in_derived - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata, true)) : value; } ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index dd390c99da..29baf2cad5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -36,7 +36,7 @@ export function build_attribute_effect( for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { value } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value + memoizer.add(value, metadata) ); if ( @@ -53,9 +53,7 @@ export function build_attribute_effect( } else { let value = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) { - value = memoizer.add(value, attribute.metadata.expression.has_await); - } + value = memoizer.add(value, attribute.metadata.expression); values.push(b.spread(value)); } @@ -156,9 +154,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c value = b.call('$.clsx', value); } - return metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata.has_await) - : value; + return context.state.memoizer.add(value, metadata); }); /** @type {Identifier | undefined} */ @@ -167,7 +163,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ObjectExpression | Identifier | undefined} */ + /** @type {Expression | undefined} */ let next; if (class_directives.length) { @@ -228,7 +224,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c */ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value + context.state.memoizer.add(value, metadata) ); /** @type {Identifier | undefined} */ 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 46d2f2b777..691b78199e 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 @@ -24,12 +24,21 @@ export class Memoizer { /** * @param {Expression} expression - * @param {boolean} has_await + * @param {ExpressionMetadata} metadata + * @param {boolean} memoize_if_state */ - add(expression, has_await) { + add(expression, metadata, memoize_if_state = false) { + const should_memoize = + metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state); + + if (!should_memoize) { + // no memoization required + return expression; + } + const id = b.id('#'); // filled in later - (has_await ? this.#async : this.#sync).push({ id, expression }); + (metadata.has_await ? this.#async : this.#sync).push({ id, expression }); return id; } @@ -73,8 +82,7 @@ export function build_template_chunk( values, context, state = context.state, - memoize = (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value + memoize = (value, metadata) => state.memoizer.add(value, metadata) ) { /** @type {Expression[]} */ const expressions = []; From da00abe1162a8e56455e92b79020c4e33290e10e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:13:05 -0400 Subject: [PATCH 12/30] Version Packages (#17037) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/slick-teeth-exist.md | 5 ----- .changeset/slimy-shirts-lose.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/slick-teeth-exist.md delete mode 100644 .changeset/slimy-shirts-lose.md diff --git a/.changeset/slick-teeth-exist.md b/.changeset/slick-teeth-exist.md deleted file mode 100644 index aeeb0f41b9..0000000000 --- a/.changeset/slick-teeth-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better error message for global variable assignments diff --git a/.changeset/slimy-shirts-lose.md b/.changeset/slimy-shirts-lose.md deleted file mode 100644 index 084fb07ea7..0000000000 --- a/.changeset/slimy-shirts-lose.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: tweak memoizer logic diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 51e6317491..1d58806694 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.42.2 + +### Patch Changes + +- fix: better error message for global variable assignments ([#17036](https://github.com/sveltejs/svelte/pull/17036)) + +- chore: tweak memoizer logic ([#17042](https://github.com/sveltejs/svelte/pull/17042)) + ## 5.42.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1f8746e72d..9fb7189d07 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.42.1", + "version": "5.42.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8d50b983ce..605e1d9cdc 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.42.1'; +export const VERSION = '5.42.2'; export const PUBLIC_VERSION = '5'; From cc0143c904ec48dcce1eac2600b5a88ca5df0d17 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 02:39:43 +0100 Subject: [PATCH 13/30] fix: handle `` rendered asynchronously (#17052) * fix: handle `` rendered asynchronously * fix tests --- .changeset/khaki-emus-rest.md | 5 ++++ .../3-transform/client/visitors/SvelteHead.js | 3 ++ .../3-transform/server/visitors/SvelteHead.js | 11 ++++++- .../internal/client/dom/blocks/svelte-head.js | 29 +++++++------------ packages/svelte/src/internal/client/render.js | 10 +------ packages/svelte/src/internal/server/index.js | 7 +++-- packages/svelte/tests/hydration/test.ts | 6 +++- .../runtime-runes/samples/async-head/A.svelte | 7 +++++ .../runtime-runes/samples/async-head/B.svelte | 8 +++++ .../samples/async-head/_config.js | 23 +++++++++++++++ .../samples/async-head/main.svelte | 11 +++++++ 11 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 .changeset/khaki-emus-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/main.svelte diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md new file mode 100644 index 0000000000..5364ff60df --- /dev/null +++ b/.changeset/khaki-emus-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle `` rendered asynchronously diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js index 0701c37c48..3a45389dd7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -13,6 +15,7 @@ export function SvelteHead(node, context) { b.stmt( b.call( '$.head', + b.literal(hash(filename)), b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js index a519057cb6..177ec62416 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -11,6 +13,13 @@ export function SvelteHead(node, context) { const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); context.state.template.push( - b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block))) + b.stmt( + b.call( + '$.head', + b.literal(hash(filename)), + b.id('$$renderer'), + b.arrow([b.id('$$renderer')], block) + ) + ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 66d3371836..13926ccc4b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; -import { HYDRATION_START } from '../../../../constants.js'; - -/** - * @type {Node | undefined} - */ -let head_anchor; - -export function reset_head_anchor() { - head_anchor = undefined; -} /** + * @param {string} hash * @param {(anchor: Node) => void} render_fn * @returns {void} */ -export function head(render_fn) { +export function head(hash, render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. let previous_hydrate_node = null; @@ -30,15 +21,13 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_node = hydrate_node; - // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. - if (head_anchor === undefined) { - head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); - } + var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); + // There might be multiple head blocks in our app, and they could have been + // rendered in an arbitrary order — find one corresponding to this component while ( head_anchor !== null && - (head_anchor.nodeType !== COMMENT_NODE || - /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) + (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash) ) { head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); } @@ -48,7 +37,10 @@ export function head(render_fn) { if (head_anchor === null) { set_hydrating(false); } else { - head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor))); + var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor.remove(); // in case this component is repeated + + set_hydrate_node(start); } } @@ -61,7 +53,6 @@ export function head(render_fn) { } finally { if (was_hydrating) { set_hydrating(true); - head_anchor = hydrate_node; // so that next head block starts from the correct node set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b1165a6e7a..416627a157 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -12,20 +12,13 @@ import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants import { active_effect } from './runtime.js'; import { push, pop, component_context } from './context.js'; import { component_root } from './reactivity/effects.js'; -import { - hydrate_next, - hydrate_node, - hydrating, - set_hydrate_node, - set_hydrating -} from './dom/hydration.js'; +import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; import { all_registered_events, handle_event_propagation, root_event_handles } from './dom/elements/events.js'; -import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; @@ -152,7 +145,6 @@ export function hydrate(component, options) { } finally { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); - reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 74a90a8600..c0dbdbda14 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -64,15 +64,16 @@ export function render(component, options = {}) { } /** + * @param {string} hash * @param {Renderer} renderer * @param {(renderer: Renderer) => Promise | void} fn * @returns {void} */ -export function head(renderer, fn) { +export function head(hash, renderer, fn) { renderer.head((renderer) => { - renderer.push(BLOCK_OPEN); + renderer.push(``); renderer.child(fn); - renderer.push(BLOCK_CLOSE); + renderer.push(EMPTY_COMMENT); }); } diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d072..ba13d2c611 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -132,7 +132,11 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); const normalize = (string: string) => - string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); + string + .trim() + .replaceAll('\r\n', '\n') + .replaceAll('/>', '>') + .replace(//g, ''); const expected = read(`${cwd}/_expected.html`) ?? rendered.html; assert.equal(normalize(target.innerHTML), normalize(expected)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte new file mode 100644 index 0000000000..d821bb6fa0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte new file mode 100644 index 0000000000..d725d5f03b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js new file mode 100644 index 0000000000..6fdf41b434 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, window }) { + await tick(); + + const head = window.document.head; + + // we don't care about the order, but we want to ensure that the + // elements didn't clobber each other + for (let n of ['1', '2', '3']) { + const a = head.querySelector(`meta[name="a-${n}"]`); + assert.equal(a?.getAttribute('content'), n); + + const b1 = head.querySelector(`meta[name="b-${n}-1"]`); + assert.equal(b1?.getAttribute('content'), `${n}-1`); + + const b2 = head.querySelector(`meta[name="b-${n}-2"]`); + assert.equal(b2?.getAttribute('content'), `${n}-2`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte new file mode 100644 index 0000000000..7f23489373 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + + + + + + + From d2f453f8b099ee46eb5835fc2af2952bda0e2fe6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:40:13 +0100 Subject: [PATCH 14/30] fix: don't restore batch in `#await` (#17051) #16977 had one slight regression which might contribute to #16990: The batch from earlier was restored, but that doesn't make sense in this situations since this has nothing to do with our new async logic of batches suspending until pending work is done. As a result you could end up with a batch being created, and then the restore then instead reverting to an earlier batch that was already done, which means a ghost-batch ends up in the set of batches, subsequently triggering time traveling when it shouldn't. This may help with #16990 No test because basically impossible to do so --- .changeset/shaky-jars-cut.md | 5 +++++ packages/svelte/src/internal/client/dom/blocks/await.js | 8 ++++++-- packages/svelte/src/internal/client/reactivity/async.js | 5 ++--- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md new file mode 100644 index 0000000000..b74b00fa1c --- /dev/null +++ b/.changeset/shaky-jars-cut.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't restore batch in `#await` diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index bac01e4c33..87d64df23e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -12,7 +12,7 @@ import { import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { is_runes } from '../../context.js'; -import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; import { capture, unset_context } from '../../reactivity/async.js'; @@ -69,7 +69,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { if (destroyed) return; resolved = true; - restore(); + // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic + // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. + restore(false); + // Make sure we have a batch, since the branch manager expects one to exist + Batch.ensure(); if (hydrating) { // `restore()` could set `hydrating` to `true`, which we very much diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fb836df989..bdd7eed940 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -33,7 +33,6 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; -import { create_text } from '../dom/operations.js'; /** * @@ -102,11 +101,11 @@ export function capture() { var previous_dev_stack = dev_stack; } - return function restore() { + return function restore(activate_batch = true) { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); - previous_batch?.activate(); + if (activate_batch) previous_batch?.activate(); if (was_hydrating) { set_hydrating(true); From 1b2f7b068e01f0407f4013a292ec9f3f3381233e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:43:59 +0100 Subject: [PATCH 15/30] Version Packages (#17053) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/khaki-emus-rest.md | 5 ----- .changeset/shaky-jars-cut.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/khaki-emus-rest.md delete mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md deleted file mode 100644 index 5364ff60df..0000000000 --- a/.changeset/khaki-emus-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle `` rendered asynchronously diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md deleted file mode 100644 index b74b00fa1c..0000000000 --- a/.changeset/shaky-jars-cut.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't restore batch in `#await` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1d58806694..8dabe54b33 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.42.3 + +### Patch Changes + +- fix: handle `` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052)) + +- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051)) + ## 5.42.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9fb7189d07..1ee6c50121 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.42.2", + "version": "5.42.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 605e1d9cdc..999aacc998 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.42.2'; +export const VERSION = '5.42.3'; export const PUBLIC_VERSION = '5'; From 90a8a039889ffdf0566dff6b2aa1b55d6a176b98 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 10:48:35 +0100 Subject: [PATCH 16/30] fix: settle batch after DOM updates (#17054) --- .changeset/loud-chairs-tease.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 4 +-- .../async-settled-after-dom/_config.js | 27 +++++++++++++++++++ .../async-settled-after-dom/main.svelte | 20 ++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .changeset/loud-chairs-tease.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte diff --git a/.changeset/loud-chairs-tease.md b/.changeset/loud-chairs-tease.md new file mode 100644 index 0000000000..e08d111cdd --- /dev/null +++ b/.changeset/loud-chairs-tease.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: settle batch after DOM updates diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ab83050cd0..bbc05bb1ff 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -196,6 +196,8 @@ export class Batch { flush_queued_effects(target.effects); previous_batch = null; + + this.#deferred?.resolve(); } batch_values = null; @@ -432,8 +434,6 @@ export class Batch { this.committed = true; batches.delete(this); - - this.#deferred?.resolve(); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js new file mode 100644 index 0000000000..2c9816f3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js @@ -0,0 +1,27 @@ +import { settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + async test({ assert, target }) { + const [shift, update] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + + update.click(); + const promise = settled(); + + await tick(); + shift.click(); + await promise; + + assert.htmlEqual( + target.innerHTML, + '

goodbye

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte new file mode 100644 index 0000000000..0db9f80118 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

{await push(text)}

+ + {#snippet pending()}{/snippet} +
From 1126ef318650792dcca883eeb72dbe58633b37cc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 17:50:29 +0100 Subject: [PATCH 17/30] feat: out of order rendering (#17038) * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * revert * note to self * unused * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * deprecate * update tests * lint * lint * WIP * WIP * fix * WIP * unused * deopt to ensure state is ready * fix * DRY * reduce diff * reduce diff * reduce diff * handle blocked attributes * WIP * pre-transform * tidy up * fix * WIP * WIP * fix: handle `` rendered asynchronously * fix tests * fix * delay resolve * Revert "fix" This reverts commit 2e56cd75753abc1fdfabbada9d27829d9eaa496a. * add error * simplify/fix hydration restoration * fix * use $state.eager mechanism for $effect.pending - way simpler and more robust * disable these warnings for now, too many false positives * fix * changeset was already merged * changeset * oops * lint * docs + tidy * prettier * robustify: logic inside memoizer and outside could get out of sync, introducing bugs. use equality comparison instead * oops * uncomment * use finally * use is_async --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/bitter-rings-help.md | 5 + .../src/compiler/phases/2-analyze/index.js | 203 +++++++++++++++++- .../2-analyze/visitors/AwaitExpression.js | 12 +- .../2-analyze/visitors/BindDirective.js | 1 + .../phases/2-analyze/visitors/SnippetBlock.js | 7 +- .../3-transform/client/transform-client.js | 47 ++-- .../phases/3-transform/client/types.d.ts | 3 - .../client/visitors/BindDirective.js | 25 ++- .../client/visitors/CallExpression.js | 4 +- .../3-transform/client/visitors/EachBlock.js | 10 +- .../client/visitors/ExpressionStatement.js | 2 +- .../3-transform/client/visitors/HtmlTag.js | 10 +- .../3-transform/client/visitors/IfBlock.js | 10 +- .../client/visitors/ImportDeclaration.js | 16 -- .../3-transform/client/visitors/KeyBlock.js | 9 +- .../3-transform/client/visitors/Program.js | 18 +- .../3-transform/client/visitors/RenderTag.js | 10 +- .../client/visitors/SlotElement.js | 4 +- .../client/visitors/SvelteElement.js | 9 +- .../client/visitors/VariableDeclaration.js | 15 +- .../client/visitors/shared/component.js | 36 ++-- .../client/visitors/shared/element.js | 1 + .../client/visitors/shared/utils.js | 16 +- .../3-transform/server/transform-server.js | 18 +- .../3-transform/server/visitors/AwaitBlock.js | 8 +- .../server/visitors/CallExpression.js | 9 +- .../3-transform/server/visitors/EachBlock.js | 17 +- .../3-transform/server/visitors/HtmlTag.js | 8 +- .../3-transform/server/visitors/IfBlock.js | 16 +- .../3-transform/server/visitors/KeyBlock.js | 8 +- .../3-transform/server/visitors/Program.js | 25 +++ .../server/visitors/RegularElement.js | 21 +- .../3-transform/server/visitors/RenderTag.js | 38 +++- .../server/visitors/SlotElement.js | 11 +- .../server/visitors/SvelteElement.js | 28 ++- .../server/visitors/shared/component.js | 14 +- .../server/visitors/shared/utils.js | 87 +++++++- .../3-transform/shared/transform-async.js | 102 +++++++++ .../compiler/phases/3-transform/types.d.ts | 3 + packages/svelte/src/compiler/phases/nodes.js | 24 +++ packages/svelte/src/compiler/phases/scope.js | 35 ++- .../svelte/src/compiler/phases/types.d.ts | 27 ++- .../svelte/src/compiler/types/template.d.ts | 2 + packages/svelte/src/compiler/utils/ast.js | 2 +- .../svelte/src/compiler/utils/builders.js | 19 +- .../src/internal/client/dom/blocks/async.js | 13 +- .../internal/client/dom/blocks/boundary.js | 13 +- .../client/dom/elements/attributes.js | 4 +- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/async.js | 151 +++++++++---- .../src/internal/client/reactivity/batch.js | 13 -- .../src/internal/client/reactivity/effects.js | 5 +- .../svelte/src/internal/client/runtime.js | 23 +- .../svelte/src/internal/server/renderer.js | 59 ++++- .../svelte/tests/runtime-legacy/shared.ts | 5 + .../async-if-after-await-in-script/_config.js | 16 ++ .../main.svelte | 10 + .../_config.js | 3 + .../samples/async-reactivity-loss/_config.js | 3 + .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 61 +++--- .../_expected/server/index.svelte.js | 58 ++--- 69 files changed, 1097 insertions(+), 352 deletions(-) create mode 100644 .changeset/bitter-rings-help.md delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/main.svelte diff --git a/.changeset/bitter-rings-help.md b/.changeset/bitter-rings-help.md new file mode 100644 index 0000000000..f71b9f96b2 --- /dev/null +++ b/.changeset/bitter-rings-help.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: out-of-order rendering diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 4c05fd6148..ed71b898ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -6,7 +6,12 @@ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; +import { + extract_identifiers, + has_await_expression, + object, + unwrap_pattern +} from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; @@ -543,7 +548,13 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - pickled_awaits: new Set() + pickled_awaits: new Set(), + instance_body: { + sync: [], + async: [], + declarations: [], + hoisted: [] + } }; if (!runes) { @@ -676,6 +687,194 @@ export function analyze_component(root, source, options) { } } + /** + * @param {ESTree.Node} expression + * @param {Scope} scope + * @param {Set} touched + * @param {Set} seen + */ + const touch = (expression, scope, touched, seen = new Set()) => { + if (seen.has(expression)) return; + seen.add(expression); + + walk( + expression, + { scope }, + { + ImportDeclaration(node) {}, + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + touched.add(binding); + + for (const assignment of binding.assignments) { + touch(assignment.value, assignment.scope, touched, seen); + } + } + } + } + } + ); + }; + + /** + * @param {ESTree.Node} node + * @param {Set} seen + * @param {Set} reads + * @param {Set} writes + */ + const trace_references = (node, reads, writes, seen = new Set()) => { + if (seen.has(node)) return; + seen.add(node); + + /** + * @param {ESTree.Pattern} node + * @param {Scope} scope + */ + function update(node, scope) { + for (const pattern of unwrap_pattern(node)) { + const node = object(pattern); + if (!node) return; + + const binding = scope.get(node.name); + if (!binding) return; + + writes.add(binding); + } + } + + walk( + node, + { scope: instance.scope }, + { + _(node, context) { + const scope = scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, + AssignmentExpression(node, context) { + update(node.left, context.state.scope); + }, + UpdateExpression(node, context) { + update( + /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), + context.state.scope + ); + }, + CallExpression(node, context) { + // for now, assume everything touched by the callee ends up mutating the object + // TODO optimise this better + + // special case — no need to peek inside effects as they only run once async work has completed + const rune = get_rune(node, context.state.scope); + if (rune === '$effect') return; + + /** @type {Set} */ + const touched = new Set(); + touch(node, context.state.scope, touched); + + for (const b of touched) { + writes.add(b); + } + }, + // don't look inside functions until they are called + ArrowFunctionExpression(_, context) {}, + FunctionDeclaration(_, context) {}, + FunctionExpression(_, context) {}, + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + reads.add(binding); + } + } + } + } + ); + }; + + let awaited = false; + + // TODO this should probably be attached to the scope? + var promises = b.id('$$promises'); + + /** + * @param {ESTree.Identifier} id + * @param {ESTree.Expression} blocker + */ + function push_declaration(id, blocker) { + analysis.instance_body.declarations.push(id); + + const binding = /** @type {Binding} */ (instance.scope.get(id.name)); + binding.blocker = blocker; + } + + for (let node of instance.ast.body) { + if (node.type === 'ImportDeclaration') { + analysis.instance_body.hoisted.push(node); + continue; + } + + if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { + // these can't exist inside ` + +{#if condition} +

yep

+{:else} +

nope

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index 2bcb129b12..ce7cd6bd49 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate + skip: true, + compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 747648e83f..ad333a573a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate this + skip: true, + compileOptions: { dev: true }, diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js index cf667e1624..6f1c40988d 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_each_fallback_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([])], (node, $$collection) => { $.each( node, 16, diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index c579fda929..7249fd6e4f 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js index a1535d6886..4045ad4bf4 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Async_each_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([first, second, third])], (node, $$collection) => { $.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => { $.next(); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index e87b50e2a4..43fe9414eb 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js index e385f5d234..d86001e273 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_alternate_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(false)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(false)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index df4ad80899..1e7330429a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js index 356e8e9607..5cdb6978d9 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(true)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(true)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 1d935f9be8..1ca24cf81a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index 7a97850175..e4df43c6c2 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -5,48 +5,47 @@ import * as $ from 'svelte/internal/client'; export default function Async_in_derived($$anchor, $$props) { $.push($$props, true); - $.async_body($$anchor, async ($$anchor) => { - let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var yes1, yes2, no1, no2; - let no1 = $.derived(async () => { - return await 1; - }); + var $$promises = $.run([ + async () => yes1 = await $.async_derived(() => 1), + async () => yes2 = await $.async_derived(async () => foo(await 1)), - let no2 = $.derived(() => async () => { + () => no1 = $.derived(async () => { return await 1; - }); + }), - if ($.aborted()) return; - - var fragment = $.comment(); - var node = $.first_child(fragment); + () => no2 = $.derived(() => async () => { + return await 1; + }) + ]); - { - var consequent = ($$anchor) => { - $.async_body($$anchor, async ($$anchor) => { - const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var fragment = $.comment(); + var node = $.first_child(fragment); - const no1 = $.derived(() => (async () => { - return await 1; - })()); + { + var consequent = ($$anchor) => { + $.async_body($$anchor, async ($$anchor) => { + const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); - const no2 = $.derived(() => (async () => { - return await 1; - })()); + const no1 = $.derived(() => (async () => { + return await 1; + })()); - if ($.aborted()) return; - }); - }; + const no2 = $.derived(() => (async () => { + return await 1; + })()); - $.if(node, ($$render) => { - if (true) $$render(consequent); + if ($.aborted()) return; }); - } + }; - $.append($$anchor, fragment); - }); + $.if(node, ($$render) => { + if (true) $$render(consequent); + }); + } + $.append($$anchor, fragment); $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js index 69eca5a383..bece6402c6 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -3,38 +3,40 @@ import * as $ from 'svelte/internal/server'; export default function Async_in_derived($$renderer, $$props) { $$renderer.component(($$renderer) => { - $$renderer.async(async ($$renderer) => { - let yes1 = (await $.save(1))(); - let yes2 = foo((await $.save(1))()); + var yes1, yes2, no1, no2; - let no1 = (async () => { - return await 1; - })(); + var $$promises = $$renderer.run([ + async () => yes1 = await 1, + async () => yes2 = foo(await 1), - let no2 = async () => { + () => no1 = (async () => { return await 1; - }; - - $$renderer.async(async ($$renderer) => { - if (true) { - $$renderer.push(''); - - const yes1 = (await $.save(1))(); - const yes2 = foo((await $.save(1))()); - - const no1 = (async () => { - return await 1; - })(); + })(), - const no2 = (async () => { - return await 1; - })(); - } else { - $$renderer.push(''); - } - }); - - $$renderer.push(``); + () => no2 = async () => { + return await 1; + } + ]); + + $$renderer.async_block([], async ($$renderer) => { + if (true) { + $$renderer.push(''); + + const yes1 = (await $.save(1))(); + const yes2 = foo((await $.save(1))()); + + const no1 = (async () => { + return await 1; + })(); + + const no2 = (async () => { + return await 1; + })(); + } else { + $$renderer.push(''); + } }); + + $$renderer.push(``); }); } \ No newline at end of file From 83746adcf79c41e9b95b41225ee156dc3c08f5b7 Mon Sep 17 00:00:00 2001 From: Alejandro Torres Date: Tue, 28 Oct 2025 12:51:19 -0400 Subject: [PATCH 18/30] Fix spelling in code comments (#17059) --- packages/svelte/src/internal/client/proxy.js | 2 +- packages/svelte/src/internal/server/renderer.js | 2 +- packages/svelte/tests/runtime-browser/assert.js | 2 +- .../tests/runtime-runes/samples/mount-props-updates/_config.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 49cef451b3..5b028d8d09 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -267,7 +267,7 @@ export function proxy(value) { if (other_s !== undefined) { set(other_s, UNINITIALIZED); } else if (i in target) { - // If the item exists in the original, we need to create a uninitialized source, + // If the item exists in the original, we need to create an uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. other_s = with_parent(() => source(UNINITIALIZED, stack)); diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 5dc845e376..479175c2eb 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -441,7 +441,7 @@ export class Renderer { } /** - * Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call + * Collect all of the `onDestroy` callbacks registered during rendering. In an async context, this is only safe to call * after awaiting `collect_async`. * * Child renderers are "porous" and don't affect execution order, but component body renderers diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index e331c8b677..249c5ad33d 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -166,7 +166,7 @@ export function test(args) { return args; } -// TypeScript needs the type of assertions to be directly visible, not infered, which is why +// TypeScript needs the type of assertions to be directly visible, not inferred, which is why // we can't have it on the test suite type. /** * @param {any} value diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js index ff7af2d524..57e4d276ff 100644 --- a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js @@ -22,7 +22,7 @@ export default test({ target.innerHTML, // bar is not set in the parent because it's a readonly property // baz is not set in the parent because while it's a bindable property, - // it wasn't set initially so it's treated as a readonly proeprty + // it wasn't set initially so it's treated as a readonly property ` foo 3
1 2 3 4
From 70d020bd9128bf0cc596780636c7f4b0081088a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:35:50 +0100 Subject: [PATCH 19/30] Version Packages (#17055) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bitter-rings-help.md | 5 ----- .changeset/loud-chairs-tease.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/bitter-rings-help.md delete mode 100644 .changeset/loud-chairs-tease.md diff --git a/.changeset/bitter-rings-help.md b/.changeset/bitter-rings-help.md deleted file mode 100644 index f71b9f96b2..0000000000 --- a/.changeset/bitter-rings-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: out-of-order rendering diff --git a/.changeset/loud-chairs-tease.md b/.changeset/loud-chairs-tease.md deleted file mode 100644 index e08d111cdd..0000000000 --- a/.changeset/loud-chairs-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: settle batch after DOM updates diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8dabe54b33..0a8637a321 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.43.0 + +### Minor Changes + +- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038)) + +### Patch Changes + +- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054)) + ## 5.42.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1ee6c50121..a66afa61fe 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.42.3", + "version": "5.43.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 999aacc998..225f50220c 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.42.3'; +export const VERSION = '5.43.0'; export const PUBLIC_VERSION = '5'; From 9477f18b7705d731ea22bc7dc4e369baec0f31b4 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:51:23 +0100 Subject: [PATCH 20/30] fix: transform `$bindable` after `await` expressions (#17066) Fixes #17064 --- .changeset/hot-crews-notice.md | 5 +++ .../3-transform/shared/transform-async.js | 2 +- .../samples/async-bindable-prop/Child.svelte | 8 +++++ .../samples/async-bindable-prop/_config.js | 35 +++++++++++++++++++ .../samples/async-bindable-prop/main.svelte | 9 +++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/hot-crews-notice.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte diff --git a/.changeset/hot-crews-notice.md b/.changeset/hot-crews-notice.md new file mode 100644 index 0000000000..5b11a82fda --- /dev/null +++ b/.changeset/hot-crews-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform `$bindable` after `await` expressions diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js index 444b8d7d94..6ec7893452 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -55,7 +55,7 @@ export function transform_body(instance_body, runner, transform) { if (visited.declarations.length === 1) { return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), + b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0), s.has_await ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte new file mode 100644 index 0000000000..4214a85f37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte @@ -0,0 +1,8 @@ + + + +{value} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js new file mode 100644 index 0000000000..a4ff70a8dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: ` + initial

initial

+ `, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + initial +

initial

+ ` + ); + + const button = target.querySelector('button'); + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + updated +

updated

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte new file mode 100644 index 0000000000..e053fdda6a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte @@ -0,0 +1,9 @@ + + + +

{value}

+ From 8ebc3b1337bd12931f5608ab608b9668e10aa6c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:56:25 +0100 Subject: [PATCH 21/30] Version Packages (#17076) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hot-crews-notice.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/hot-crews-notice.md diff --git a/.changeset/hot-crews-notice.md b/.changeset/hot-crews-notice.md deleted file mode 100644 index 5b11a82fda..0000000000 --- a/.changeset/hot-crews-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform `$bindable` after `await` expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0a8637a321..2a2cd28698 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.43.1 + +### Patch Changes + +- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066)) + ## 5.43.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a66afa61fe..a7e0c618bf 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.43.0", + "version": "5.43.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 225f50220c..4c0a8f05c8 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.43.0'; +export const VERSION = '5.43.1'; export const PUBLIC_VERSION = '5'; From a791e9178b229e720adbbc0156eb03aa08941698 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Oct 2025 15:15:47 +0100 Subject: [PATCH 22/30] fix: treat each blocks with async dependencies as uncontrolled (#17077) --- .changeset/fair-files-cover.md | 5 +++++ .../phases/3-transform/client/visitors/shared/fragment.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fair-files-cover.md diff --git a/.changeset/fair-files-cover.md b/.changeset/fair-files-cover.md new file mode 100644 index 0000000000..1009773e5b --- /dev/null +++ b/.changeset/fair-files-cover.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat each blocks with async dependencies as uncontrolled diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 3588f2843a..c7f843af48 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) { is_element && // In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled // TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally) - !(node.body.metadata.has_await || node.metadata.expression.has_await) + !(node.body.metadata.has_await || node.metadata.expression.is_async()) ) { node.metadata.is_controlled = true; } else { From 723c421fbb1abb8e50d13505b9ef5999df884a9d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:14:11 +0100 Subject: [PATCH 23/30] Version Packages (#17078) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fair-files-cover.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fair-files-cover.md diff --git a/.changeset/fair-files-cover.md b/.changeset/fair-files-cover.md deleted file mode 100644 index 1009773e5b..0000000000 --- a/.changeset/fair-files-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat each blocks with async dependencies as uncontrolled diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2a2cd28698..bcf17fe45e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.43.2 + +### Patch Changes + +- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077)) + ## 5.43.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a7e0c618bf..f7a1cca616 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.43.1", + "version": "5.43.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4c0a8f05c8..0a28702778 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.43.1'; +export const VERSION = '5.43.2'; export const PUBLIC_VERSION = '5'; From 0e709e3fec59a66635962b8fac3bbbacdd765e9e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 31 Oct 2025 07:17:12 +0100 Subject: [PATCH 24/30] fix: change title only after any pending work has completed (#17061) * fix: change title only after any pending work has completed We have to use an effect - not a render effect - for updating the title, and always. That way we change the title only after any pending work has completed. Fixes #17060 * fix --- .changeset/legal-mangos-peel.md | 5 ++++ .../client/visitors/TitleElement.js | 23 ++++++++++++++---- .../src/internal/client/reactivity/batch.js | 21 +++++++--------- .../src/internal/client/reactivity/effects.js | 5 ++-- .../samples/async-head-title-1/Inner.svelte | 15 ++++++++++++ .../samples/async-head-title-1/_config.js | 24 +++++++++++++++++++ .../samples/async-head-title-1/main.svelte | 12 ++++++++++ .../samples/async-head-title-2/Inner.svelte | 13 ++++++++++ .../samples/async-head-title-2/_config.js | 23 ++++++++++++++++++ .../samples/async-head-title-2/main.svelte | 12 ++++++++++ 10 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 .changeset/legal-mangos-peel.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md new file mode 100644 index 0000000000..bddad21bff --- /dev/null +++ b/.changeset/legal-mangos-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: change title only after any pending work has completed diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 98d7880b25..edd8835e00 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -1,16 +1,19 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_template_chunk } from './shared/utils.js'; +import { build_template_chunk, Memoizer } from './shared/utils.js'; /** * @param {AST.TitleElement} node * @param {ComponentContext} context */ export function TitleElement(node, context) { + const memoizer = new Memoizer(); const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context + context, + context.state, + (value, metadata) => memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); @@ -26,9 +29,21 @@ export function TitleElement(node, context) { ) ); + // Always in an $effect so it only changes the title once async work is done if (has_state) { - context.state.update.push(statement); + context.state.after_update.push( + b.stmt( + b.call( + '$.template_effect', + b.arrow(memoizer.apply(), b.block([statement])), + memoizer.sync_values(), + memoizer.async_values(), + memoizer.blockers(), + b.true + ) + ) + ); } else { - context.state.init.push(statement); + context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement]))))); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9d61b6bbf9..27c90d7708 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,33 +14,25 @@ import { MAYBE_DIRTY, DERIVED, BOUNDARY_EFFECT, - EAGER_EFFECT + EAGER_EFFECT, + HEAD_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, get, - increment_write_version, is_dirty, is_updating_effect, set_is_updating_effect, set_signal_status, - tick, update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { - flush_eager_effects, - eager_effects, - old_values, - set_eager_effects, - source, - update -} from './sources.js'; +import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; /** @@ -800,7 +792,12 @@ export function schedule_effect(signal) { // if the effect is being scheduled because a parent (each/await/etc) block // updated an internal source, bail out or we'll cause a second flush - if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) { + if ( + is_flushing && + effect === active_effect && + (flags & BLOCK_EFFECT) !== 0 && + (flags & HEAD_EFFECT) === 0 + ) { return; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 4a9fce7286..8c4b84438c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -366,10 +366,11 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Array>} blockers + * @param {boolean} defer */ -export function template_effect(fn, sync = [], async = [], blockers = []) { +export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte new file mode 100644 index 0000000000..089ba43607 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte @@ -0,0 +1,15 @@ + + + + title + + +

{await push()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte new file mode 100644 index 0000000000..b2a8656276 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte @@ -0,0 +1,13 @@ + + + + {await push()} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js new file mode 100644 index 0000000000..b89dce62d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} From 8ebbb3c7bccfba4f509456b5b4f5b81ccf13d65c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 1 Nov 2025 23:35:20 +0100 Subject: [PATCH 25/30] fix: preserve symbols when creating derived rest properties (#17096) fixes #17094 --- .changeset/sixty-comics-bow.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 14 ++++++++++---- .../derived-rest-includes-symbol/_config.js | 5 +++++ .../derived-rest-includes-symbol/main.svelte | 8 ++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .changeset/sixty-comics-bow.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md new file mode 100644 index 0000000000..2463e52430 --- /dev/null +++ b/.changeset/sixty-comics-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve symbols when creating derived rest properties diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2197f34d16..6485d21ec2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -763,12 +763,12 @@ export function set_signal_status(signal, status) { } /** - * @param {Record} obj - * @param {string[]} keys - * @returns {Record} + * @param {Record} obj + * @param {Array} keys + * @returns {Record} */ export function exclude_from_object(obj, keys) { - /** @type {Record} */ + /** @type {Record} */ var result = {}; for (var key in obj) { @@ -777,6 +777,12 @@ export function exclude_from_object(obj, keys) { } } + for (var symbol of Object.getOwnPropertySymbols(obj)) { + if (!keys.includes(symbol)) { + result[symbol] = obj[symbol]; + } + } + return result; } diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js new file mode 100644 index 0000000000..eec60928a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

42

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte new file mode 100644 index 0000000000..ee161696e1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte @@ -0,0 +1,8 @@ + + +

{b[symbol]}

From b7625fd42c067b7cd03fea9589ef45349f9dedbe Mon Sep 17 00:00:00 2001 From: 7nik Date: Sun, 2 Nov 2025 11:40:32 +0200 Subject: [PATCH 26/30] fix: do not spread non-enumerable symbols (#17097) --- packages/svelte/src/internal/client/runtime.js | 2 +- .../samples/derived-rest-includes-symbol/_config.js | 2 +- .../samples/derived-rest-includes-symbol/main.svelte | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6485d21ec2..76531d3320 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -778,7 +778,7 @@ export function exclude_from_object(obj, keys) { } for (var symbol of Object.getOwnPropertySymbols(obj)) { - if (!keys.includes(symbol)) { + if (Object.propertyIsEnumerable.call(obj, symbol) && !keys.includes(symbol)) { result[symbol] = obj[symbol]; } } diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js index eec60928a2..d0633983d2 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `

42

` + html: `

true false

` }); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte index ee161696e1..2454e98ab7 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte @@ -1,8 +1,11 @@ -

{b[symbol]}

+

{symbol1 in b} {symbol2 in b}

From eea8a18cf2ff8245f0687e8012d5923c5d508d03 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:02:09 +0100 Subject: [PATCH 27/30] fix: ensure fork always accesses correct values (#17098) Not all batches will flush right after being activated, some will be activated and then `get` is called on a signal. In that case the value was wrong because we did not apply the changes of that batch. By doing `this.apply()` during `activate()` we ensure we do, which fixes (among other things, likely) a forking bug where old values where sneaking in. Fixes #17079 --- .changeset/eight-news-laugh.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 13 +++++++++---- .../samples/async-fork-if/Child.svelte | 8 ++++++++ .../samples/async-fork-if/_config.js | 12 ++++++++++++ .../samples/async-fork-if/main.svelte | 17 +++++++++++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .changeset/eight-news-laugh.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md new file mode 100644 index 0000000000..e120b19f5e --- /dev/null +++ b/.changeset/eight-news-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure fork always accesses correct values diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 27c90d7708..57aa185a31 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -15,7 +15,8 @@ import { DERIVED, BOUNDARY_EFFECT, EAGER_EFFECT, - HEAD_EFFECT + HEAD_EFFECT, + ERROR_VALUE } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -285,12 +286,16 @@ export class Batch { this.previous.set(source, value); } - this.current.set(source, source.v); - batch_values?.set(source, source.v); + // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` + if ((source.f & ERROR_VALUE) === 0) { + this.current.set(source, source.v); + batch_values?.set(source, source.v); + } } activate() { current_batch = this; + this.apply(); } deactivate() { @@ -492,7 +497,7 @@ export class Batch { } apply() { - if (!async_mode_flag || batches.size === 1) return; + if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; // if there are multiple batches, we are 'time travelling' — // we need to override values with the ones in this batch... diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte new file mode 100644 index 0000000000..6ef7d03eea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte @@ -0,0 +1,8 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js new file mode 100644 index 0000000000..1bc168d9ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + await new Promise((r) => setTimeout(r, 2)); + assert.htmlEqual(target.innerHTML, ` universe`); + assert.deepEqual(logs, ['universe', 'universe']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte new file mode 100644 index 0000000000..625040ec13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte @@ -0,0 +1,17 @@ + + + + +{#if x === 'universe'} + +{/if} From 7a2435471c96c43d6f00144cdb5889772703d786 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:53:51 -0500 Subject: [PATCH 28/30] Version Packages (#17087) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eight-news-laugh.md | 5 ----- .changeset/legal-mangos-peel.md | 5 ----- .changeset/sixty-comics-bow.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/eight-news-laugh.md delete mode 100644 .changeset/legal-mangos-peel.md delete mode 100644 .changeset/sixty-comics-bow.md diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md deleted file mode 100644 index e120b19f5e..0000000000 --- a/.changeset/eight-news-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure fork always accesses correct values diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md deleted file mode 100644 index bddad21bff..0000000000 --- a/.changeset/legal-mangos-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: change title only after any pending work has completed diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md deleted file mode 100644 index 2463e52430..0000000000 --- a/.changeset/sixty-comics-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve symbols when creating derived rest properties diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index bcf17fe45e..bc2f815908 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.43.3 + +### Patch Changes + +- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098)) + +- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061)) + +- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096)) + ## 5.43.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f7a1cca616..f178444593 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.43.2", + "version": "5.43.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 0a28702778..5ad40ddee6 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.43.2'; +export const VERSION = '5.43.3'; export const PUBLIC_VERSION = '5'; From 46e9d2d357d724479fbf6db6c0d9ebb22bab9796 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 6 Nov 2025 05:47:39 -0500 Subject: [PATCH 29/30] chore: remove `UNOWNED` flag (#17105) Fixes #17024 Fixes #17049 (comment) (and therefore everything that was still buggy in that issue I think) * chore: remove unowned check when calling `e.effect_in_unowned_derived` * WIP * all non-unit tests passing * tidy * WIP * WIP * WIP * note to self * fix * fix * hmm maybe not * try this * simplify * remove skip_reaction * docs * add changeset, in case this results in changed behaviour * Update packages/svelte/src/internal/client/reactivity/effects.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * fix #17024 * fix comment * revert * fix * dry * changeset * fix WAS_MARKED logic * failing test (that uncovered other unrelated bug) + fix * fix: delete from batch_values on updates (#17115) * fix: delete from batch_values on updates This fixes a bug where a derived would still show its old value even after it was indirectly updated again within the same batch. This can for example happen by reading a derived on an effect, then writing to a source in that same effect that makes the derived update, and then read the derived value in a sibling effect - it still shows the old value without the fix. The fix is to _delete_ the value from batch_values, as it's now the newest value across all batches. In order to not prevent breakage on other batches we have to leave the status of deriveds as-is, i.e. within is_dirty and update_derived we cannot update its status. That might be a bit more inefficient as you now have to traverse the graph for each `get` of that derived (it's a bit like they are all disconnected) but we can always optimize that later if need be. Another advantage of this fix is that we can get rid of duplicate logic we had to add about unmarking and reconnecting deriveds, because that logic was only needed for the case where deriveds are read after they are updated, which now no longer hits that if-branch * keep derived cache, but clear it in mark_reactions (#17116) --------- Co-authored-by: Rich Harris Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/four-paths-cheer.md | 5 + .changeset/whole-webs-stick.md | 5 + .../svelte/src/internal/client/constants.js | 9 +- .../internal/client/reactivity/deriveds.js | 23 ++-- .../src/internal/client/reactivity/effects.js | 14 +- .../src/internal/client/reactivity/sources.js | 21 ++- .../svelte/src/internal/client/runtime.js | 130 ++++++------------ .../Component.svelte | 27 ++++ .../_config.js | 16 +++ .../main.svelte | 19 +++ .../async-derived-unowned/Component.svelte | 6 + .../samples/async-derived-unowned/_config.js | 30 ++++ .../samples/async-derived-unowned/main.svelte | 19 +++ 13 files changed, 212 insertions(+), 112 deletions(-) create mode 100644 .changeset/four-paths-cheer.md create mode 100644 .changeset/whole-webs-stick.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte diff --git a/.changeset/four-paths-cheer.md b/.changeset/four-paths-cheer.md new file mode 100644 index 0000000000..54a697a8a4 --- /dev/null +++ b/.changeset/four-paths-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify connection/disconnection logic diff --git a/.changeset/whole-webs-stick.md b/.changeset/whole-webs-stick.md new file mode 100644 index 0000000000..fe8b614a01 --- /dev/null +++ b/.changeset/whole-webs-stick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reconnect deriveds to effect tree when time-travelling diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index c2f7861b78..b39afef516 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -6,6 +6,13 @@ export const BLOCK_EFFECT = 1 << 4; export const BRANCH_EFFECT = 1 << 5; export const ROOT_EFFECT = 1 << 6; export const BOUNDARY_EFFECT = 1 << 7; +/** + * Indicates that a reaction is connected to an effect root — either it is an effect, + * or it is a derived that is depended on by at least one effect. If a derived has + * no dependents, we can disconnect it from the graph, allowing it to either be + * GC'd or reconnected later if an effect comes to depend on it again + */ +export const CONNECTED = 1 << 9; export const CLEAN = 1 << 10; export const DIRTY = 1 << 11; export const MAYBE_DIRTY = 1 << 12; @@ -26,8 +33,6 @@ export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; // Flags exclusive to deriveds -export const UNOWNED = 1 << 8; -export const DISCONNECTED = 1 << 9; /** * Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase. * Will be lifted during execution of the derived and during checking its dirty state (both are necessary diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 1eb640ad26..7e6f3c6f60 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,15 +9,14 @@ import { EFFECT_PRESERVED, MAYBE_DIRTY, STALE_REACTION, - UNOWNED, ASYNC, - WAS_MARKED + WAS_MARKED, + CONNECTED } from '#client/constants'; import { active_reaction, active_effect, set_signal_status, - skip_reaction, update_reaction, increment_write_version, set_active_effect, @@ -27,7 +26,7 @@ import { import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; -import { async_effect, destroy_effect, teardown } from './effects.js'; +import { async_effect, destroy_effect, effect_tracking, teardown } from './effects.js'; import { eager_effects, internal_set, set_eager_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -61,9 +60,7 @@ export function derived(fn) { ? /** @type {Derived} */ (active_reaction) : null; - if (active_effect === null || (parent_derived !== null && (parent_derived.f & UNOWNED) !== 0)) { - flags |= UNOWNED; - } else { + if (active_effect !== null) { // Since deriveds are evaluated lazily, any effects created inside them are // created too late to ensure that the parent effect is added to the tree active_effect.f |= EFFECT_PRESERVED; @@ -368,12 +365,16 @@ export function update_derived(derived) { return; } + // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens if (batch_values !== null) { - batch_values.set(derived, derived.v); + // only cache the value if we're in a tracking context, otherwise we won't + // clear the cache in `mark_reactions` when dependencies are updated + if (effect_tracking()) { + batch_values.set(derived, derived.v); + } } else { - var status = - (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; - + var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN; set_signal_status(derived, status); } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 8c4b84438c..5d7c0ef871 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -25,7 +25,6 @@ import { ROOT_EFFECT, EFFECT_TRANSPARENT, DERIVED, - UNOWNED, CLEAN, EAGER_EFFECT, HEAD_EFFECT, @@ -33,7 +32,8 @@ import { EFFECT_PRESERVED, STALE_REACTION, USER_EFFECT, - ASYNC + ASYNC, + CONNECTED } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -48,11 +48,11 @@ import { without_reactive_context } from '../dom/elements/bindings/shared.js'; * @param {'$effect' | '$effect.pre' | '$inspect'} rune */ export function validate_effect(rune) { - if (active_effect === null && active_reaction === null) { - e.effect_orphan(rune); - } + if (active_effect === null) { + if (active_reaction === null) { + e.effect_orphan(rune); + } - if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0 && active_effect === null) { e.effect_in_unowned_derived(); } @@ -103,7 +103,7 @@ function create_effect(type, fn, sync, push = true) { deps: null, nodes_start: null, nodes_end: null, - f: type | DIRTY, + f: type | DIRTY | CONNECTED, first: null, fn, last: null, diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b480d4155a..8ae406b57b 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -23,18 +23,18 @@ import { DIRTY, BRANCH_EFFECT, EAGER_EFFECT, - UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, ROOT_EFFECT, ASYNC, - WAS_MARKED + WAS_MARKED, + CONNECTED } from '#client/constants'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack, tag_proxy } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; -import { Batch, eager_block_effects, schedule_effect } from './batch.js'; +import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; @@ -211,7 +211,8 @@ export function internal_set(source, value) { if ((source.f & DIRTY) !== 0) { execute_derived(/** @type {Derived} */ (source)); } - set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); + + set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY); } source.wv = increment_write_version(); @@ -333,9 +334,17 @@ function mark_reactions(signal, status) { } if ((flags & DERIVED) !== 0) { + var derived = /** @type {Derived} */ (reaction); + + batch_values?.delete(derived); + if ((flags & WAS_MARKED) === 0) { - reaction.f |= WAS_MARKED; - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + // Only connected deriveds can be reliably unmarked right away + if (flags & CONNECTED) { + reaction.f |= WAS_MARKED; + } + + mark_reactions(derived, MAYBE_DIRTY); } } else if (not_dirty) { if ((flags & BLOCK_EFFECT) !== 0) { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 76531d3320..258f6962fa 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -4,6 +4,7 @@ import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js' import { destroy_block_effect_children, destroy_effect_children, + effect_tracking, execute_effect_teardown } from './reactivity/effects.js'; import { @@ -11,13 +12,12 @@ import { MAYBE_DIRTY, CLEAN, DERIVED, - UNOWNED, DESTROYED, BRANCH_EFFECT, STATE_SYMBOL, BLOCK_EFFECT, ROOT_EFFECT, - DISCONNECTED, + CONNECTED, REACTION_IS_UPDATING, STALE_REACTION, ERROR_VALUE, @@ -137,10 +137,6 @@ export function set_update_version(value) { update_version = value; } -// If we are working with a get() chain that has no active container, -// to prevent memory leaks, we skip adding the reaction. -export let skip_reaction = false; - export function increment_write_version() { return ++write_version; } @@ -158,55 +154,18 @@ export function is_dirty(reaction) { return true; } + if (flags & DERIVED) { + reaction.f &= ~WAS_MARKED; + } + if ((flags & MAYBE_DIRTY) !== 0) { var dependencies = reaction.deps; - var is_unowned = (flags & UNOWNED) !== 0; - - if (flags & DERIVED) { - reaction.f &= ~WAS_MARKED; - } if (dependencies !== null) { - var i; - var dependency; - var is_disconnected = (flags & DISCONNECTED) !== 0; - var is_unowned_connected = is_unowned && active_effect !== null && !skip_reaction; var length = dependencies.length; - // 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, unless the effect has already been destroyed - // (which can happen if the derived is read by an async derived) - if ( - (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, 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) { - 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; - } - } - - for (i = 0; i < length; i++) { - dependency = dependencies[i]; + for (var i = 0; i < length; i++) { + var dependency = dependencies[i]; if (is_dirty(/** @type {Derived} */ (dependency))) { update_derived(/** @type {Derived} */ (dependency)); @@ -218,9 +177,12 @@ export function is_dirty(reaction) { } } - // Unowned signals should never be marked as clean unless they - // are used within an active_effect without skip_reaction - if (!is_unowned || (active_effect !== null && !skip_reaction)) { + if ( + (flags & CONNECTED) !== 0 && + // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens + batch_values === null + ) { set_signal_status(reaction, CLEAN); } } @@ -263,7 +225,6 @@ export function update_reaction(reaction) { var previous_skipped_deps = skipped_deps; var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; - var previous_skip_reaction = skip_reaction; var previous_sources = current_sources; var previous_component_context = component_context; var previous_untracking = untracking; @@ -274,8 +235,6 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - skip_reaction = - (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; current_sources = null; @@ -311,12 +270,7 @@ export function update_reaction(reaction) { reaction.deps = deps = new_deps; } - if ( - !skip_reaction || - // Deriveds that already have reactions can cleanup, so we still add them as reactions - ((flags & DERIVED) !== 0 && - /** @type {import('#client').Derived} */ (reaction).reactions !== null) - ) { + if (is_updating_effect && effect_tracking() && (reaction.f & CONNECTED) !== 0) { for (i = skipped_deps; i < deps.length; i++) { (deps[i].reactions ??= []).push(reaction); } @@ -373,7 +327,6 @@ export function update_reaction(reaction) { skipped_deps = previous_skipped_deps; untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; - skip_reaction = previous_skip_reaction; current_sources = previous_sources; set_component_context(previous_component_context); untracking = previous_untracking; @@ -415,9 +368,10 @@ function remove_reaction(signal, dependency) { ) { set_signal_status(dependency, MAYBE_DIRTY); // If we are working with a derived that is owned by an effect, then mark it as being - // disconnected. - if ((dependency.f & (UNOWNED | DISCONNECTED)) === 0) { - dependency.f ^= DISCONNECTED; + // disconnected and remove the mark flag, as it cannot be reliably removed otherwise + if ((dependency.f & CONNECTED) !== 0) { + dependency.f ^= CONNECTED; + dependency.f &= ~WAS_MARKED; } // Disconnect any reactions owned by this reaction destroy_derived_effects(/** @type {Derived} **/ (dependency)); @@ -564,10 +518,7 @@ export function get(signal) { skipped_deps++; } else if (new_deps === null) { new_deps = [signal]; - } 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 + } else if (!new_deps.includes(signal)) { new_deps.push(signal); } } @@ -585,20 +536,6 @@ export function get(signal) { } } } - } else if ( - is_derived && - /** @type {Derived} */ (signal).deps === null && - /** @type {Derived} */ (signal).effects === null - ) { - var derived = /** @type {Derived} */ (signal); - var parent = derived.parent; - - if (parent !== null && (parent.f & UNOWNED) === 0) { - // If the derived is owned by another derived then mark it as unowned - // as the derived value might have been referenced in a different context - // since and thus its parent might not be its true owner anymore - derived.f ^= UNOWNED; - } } if (DEV) { @@ -657,7 +594,7 @@ export function get(signal) { } if (is_derived) { - derived = /** @type {Derived} */ (signal); + var derived = /** @type {Derived} */ (signal); var value = derived.v; @@ -684,9 +621,11 @@ export function get(signal) { if (is_dirty(derived)) { update_derived(derived); } - } - if (batch_values?.has(signal)) { + if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + reconnect(derived); + } + } else if (batch_values?.has(signal)) { return batch_values.get(signal); } @@ -697,6 +636,25 @@ export function get(signal) { return signal.v; } +/** + * (Re)connect a disconnected derived, so that it is notified + * of changes in `mark_reactions` + * @param {Derived} derived + */ +function reconnect(derived) { + if (derived.deps === null) return; + + derived.f ^= CONNECTED; + + for (const dep of derived.deps) { + (dep.reactions ??= []).push(derived); + + if ((dep.f & DERIVED) !== 0 && (dep.f & CONNECTED) === 0) { + reconnect(/** @type {Derived} */ (dep)); + } + } +} + /** @param {Derived} derived */ function depends_on_old_values(derived) { if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte new file mode 100644 index 0000000000..200778dc5b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/Component.svelte @@ -0,0 +1,27 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js new file mode 100644 index 0000000000..15bb42074f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + assert.deepEqual(logs, [5]); + + button?.click(); + await tick(); + assert.deepEqual(logs, [5, 7]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte new file mode 100644 index 0000000000..bd82e35a3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-multiple-effects/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte new file mode 100644 index 0000000000..f7d138a3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/Component.svelte @@ -0,0 +1,6 @@ + + +

{double}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js new file mode 100644 index 0000000000..fc0135623d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/_config.js @@ -0,0 +1,30 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte new file mode 100644 index 0000000000..bd82e35a3b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unowned/main.svelte @@ -0,0 +1,19 @@ + + + + {await new Promise((r) => { + // long enough for the test to do all its other stuff while this is pending + setTimeout(r, 10); + })} + {#snippet pending()}{/snippet} + + + + +{#if count > 0} + +{/if} From 3a4dc7d83de9ccdbb1ae46827cd6ef01138e27e7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:46:51 +0100 Subject: [PATCH 30/30] Version Packages (#17119) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/four-paths-cheer.md | 5 ----- .changeset/whole-webs-stick.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/four-paths-cheer.md delete mode 100644 .changeset/whole-webs-stick.md diff --git a/.changeset/four-paths-cheer.md b/.changeset/four-paths-cheer.md deleted file mode 100644 index 54a697a8a4..0000000000 --- a/.changeset/four-paths-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify connection/disconnection logic diff --git a/.changeset/whole-webs-stick.md b/.changeset/whole-webs-stick.md deleted file mode 100644 index fe8b614a01..0000000000 --- a/.changeset/whole-webs-stick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reconnect deriveds to effect tree when time-travelling diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index bc2f815908..9543ad4d7b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.43.4 + +### Patch Changes + +- chore: simplify connection/disconnection logic ([#17105](https://github.com/sveltejs/svelte/pull/17105)) + +- fix: reconnect deriveds to effect tree when time-travelling ([#17105](https://github.com/sveltejs/svelte/pull/17105)) + ## 5.43.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f178444593..0988828c44 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.43.3", + "version": "5.43.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 5ad40ddee6..9fe92195c4 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.43.3'; +export const VERSION = '5.43.4'; export const PUBLIC_VERSION = '5';