From 4eb432e941d4d8ebf1206ea86433b0c778b71737 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 17:27:04 -0400 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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';