diff --git a/.changeset/clever-cherries-hear.md b/.changeset/clever-cherries-hear.md new file mode 100644 index 0000000000..df37943b46 --- /dev/null +++ b/.changeset/clever-cherries-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't throw for `undefined` non delegated event handlers diff --git a/.changeset/pink-wolves-search.md b/.changeset/pink-wolves-search.md new file mode 100644 index 0000000000..cd734062c6 --- /dev/null +++ b/.changeset/pink-wolves-search.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly handle `novalidate` attribute casing diff --git a/.changeset/tasty-pigs-greet.md b/.changeset/tasty-pigs-greet.md new file mode 100644 index 0000000000..63decceed8 --- /dev/null +++ b/.changeset/tasty-pigs-greet.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid double deriveds in component props diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d16bdbc327..e15c0e0a09 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.19.2 + +### Patch Changes + +- fix: address regression with untrack ([#15079](https://github.com/sveltejs/svelte/pull/15079)) + ## 5.19.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b725e52fed..48287cf901 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.19.1", + "version": "5.19.2", "type": "module", "types": "./types/index.d.ts", "engines": { 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 c59a5544df..421118cf68 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -269,41 +269,6 @@ export function should_proxy(node, scope) { return true; } -/** - * @param {Pattern} node - * @param {import('zimmerframe').Context} context - * @returns {{ id: Pattern, declarations: null | Statement[] }} - */ -export function create_derived_block_argument(node, context) { - if (node.type === 'Identifier') { - context.state.transform[node.name] = { read: get_value }; - return { id: node, declarations: null }; - } - - const pattern = /** @type {Pattern} */ (context.visit(node)); - const identifiers = extract_identifiers(node); - - const id = b.id('$$source'); - const value = b.id('$$value'); - - const block = b.block([ - b.var(pattern, b.call('$.get', id)), - b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier)))) - ]); - - const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; - - for (const id of identifiers) { - context.state.transform[id.name] = { read: get_value }; - - declarations.push( - b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) - ); - } - - return { id, declarations }; -} - /** * Svelte legacy mode should use safe equals in most places, runes mode shouldn't * @param {ComponentClientTransformState} state diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 146f75d405..e0aef2d316 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,8 +1,10 @@ /** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../types' */ +/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ +import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '../../../../utils/builders.js'; -import { create_derived_block_argument } from '../utils.js'; +import { create_derived } from '../utils.js'; +import { get_value } from './shared/declarations.js'; /** * @param {AST.AwaitBlock} node @@ -65,3 +67,38 @@ export function AwaitBlock(node, context) { ) ); } + +/** + * @param {Pattern} node + * @param {import('zimmerframe').Context} context + * @returns {{ id: Pattern, declarations: null | Statement[] }} + */ +function create_derived_block_argument(node, context) { + if (node.type === 'Identifier') { + context.state.transform[node.name] = { read: get_value }; + return { id: node, declarations: null }; + } + + const pattern = /** @type {Pattern} */ (context.visit(node)); + const identifiers = extract_identifiers(node); + + const id = b.id('$$source'); + const value = b.id('$$value'); + + const block = b.block([ + b.var(pattern, b.call('$.get', id)), + b.return(b.object(identifiers.map((identifier) => b.prop('init', identifier, identifier)))) + ]); + + const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))]; + + for (const id of identifiers) { + context.state.transform[id.name] = { read: get_value }; + + declarations.push( + b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id)))) + ); + } + + return { id, declarations }; +} 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 32ff9d530e..3c306b241f 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 @@ -541,8 +541,10 @@ function build_element_attribute_update_assignment( const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; - let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => - get_expression_id(state, value, is_async) + let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => + metadata.has_call || metadata.is_async + ? get_expression_id(state, value, metadata.is_async) + : value ); if (name === 'autofocus') { @@ -669,8 +671,10 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co */ function build_element_special_value_attribute(element, node_id, attribute, context) { const state = context.state; - const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => - get_expression_id(state, value, is_async) + const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => + metadata.has_call || metadata.is_async + ? get_expression_id(state, value, metadata.is_async) + : value ); const inner_assignment = b.assignment( 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 f1b08acbc6..fdd705e32e 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 @@ -30,8 +30,10 @@ export function SlotElement(node, context) { if (attribute.type === 'SpreadAttribute') { spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute)))); } else if (attribute.type === 'Attribute') { - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - memoize_expression(context.state, value) + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value) ); if (attribute.name === 'name') { 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 9ac0bac120..15e4f68e9e 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 @@ -4,7 +4,6 @@ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { create_derived } from '../../utils.js'; import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; @@ -134,9 +133,9 @@ export function build_component(node, component_name, context, anchor = context. custom_css_props.push( b.init( attribute.name, - build_attribute_value(attribute.value, context, (value) => + build_attribute_value(attribute.value, context, (value, metadata) => // TODO put the derived in the local block - memoize_expression(context.state, value) + metadata.has_call ? memoize_expression(context.state, value) : value ).value ) ); @@ -151,31 +150,29 @@ export function build_component(node, component_name, context, anchor = context. has_children_prop = true; } - const { value, has_state } = build_attribute_value(attribute.value, context, (value) => - memoize_expression(context.state, value) - ); - - if (has_state) { - let arg = value; - - // When we have a non-simple computation, anything other than an Identifier or Member expression, - // then there's a good chance it needs to be memoized to avoid over-firing when read within the - // child component. - const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { - return ( - n.type === 'ExpressionTag' && - n.expression.type !== 'Identifier' && - n.expression.type !== 'MemberExpression' - ); - }); + const { value, has_state } = build_attribute_value( + attribute.value, + context, + (value, metadata) => { + if (!metadata.has_state) return value; + + // When we have a non-simple computation, anything other than an Identifier or Member expression, + // then there's a good chance it needs to be memoized to avoid over-firing when read within the + // child component (e.g. `active={i === index}`) + const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { + return ( + n.type === 'ExpressionTag' && + n.expression.type !== 'Identifier' && + n.expression.type !== 'MemberExpression' + ); + }); - if (should_wrap_in_derived) { - const id = b.id(context.state.scope.generate(attribute.name)); - context.state.init.push(b.var(id, create_derived(context.state, b.thunk(value)))); - arg = b.call('$.get', id); + return should_wrap_in_derived ? memoize_expression(context.state, value) : value; } + ); - push_prop(b.get(attribute.name, [b.return(arg)])); + if (has_state) { + push_prop(b.get(attribute.name, [b.return(value)])); } else { push_prop(b.init(attribute.name, 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 06c32333dc..097b309345 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,11 +1,11 @@ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */ -/** @import { AST } from '#compiler' */ +/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_getter, create_derived } from '../../utils.js'; +import { build_getter } from '../../utils.js'; import { build_template_chunk, get_expression_id } from './utils.js'; /** @@ -38,7 +38,10 @@ export function build_set_attributes( const { value, has_state } = build_attribute_value( attribute.value, context, - (value, is_async) => get_expression_id(context.state, value, is_async) + (value, metadata) => + metadata.has_call || metadata.is_async + ? get_expression_id(context.state, value, metadata.is_async) + : value ); if ( @@ -60,11 +63,10 @@ export function build_set_attributes( let value = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_call) { - const id = b.id(state.scope.generate('spread_with_call')); - state.init.push(b.const(id, create_derived(state, b.thunk(value)))); - value = b.call('$.get', id); + if (attribute.metadata.expression.has_call || attribute.metadata.expression.is_async) { + value = get_expression_id(context.state, value, attribute.metadata.expression.is_async); } + values.push(b.spread(value)); } } @@ -113,8 +115,10 @@ export function build_style_directives( let value = directive.value === true ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value, is_async) => - get_expression_id(context.state, value, is_async) + : build_attribute_value(directive.value, context, (value, metadata) => + metadata.has_call || metadata.is_async + ? get_expression_id(context.state, value, metadata.is_async) + : value ).value; const update = b.stmt( @@ -171,30 +175,26 @@ export function build_class_directives( /** * @param {AST.Attribute['value']} value * @param {ComponentContext} context - * @param {(value: Expression, is_async: boolean) => Expression} memoize - * @returns {{ value: Expression, has_state: boolean, is_async: boolean }} + * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize + * @returns {{ value: Expression, has_state: boolean }} */ export function build_attribute_value(value, context, memoize = (value) => value) { if (value === true) { - return { value: b.literal(true), has_state: false, is_async: false }; + return { value: b.literal(true), has_state: false }; } if (!Array.isArray(value) || value.length === 1) { const chunk = Array.isArray(value) ? value[0] : value; if (chunk.type === 'Text') { - return { value: b.literal(chunk.data), has_state: false, is_async: false }; + return { value: b.literal(chunk.data), has_state: false }; } let expression = /** @type {Expression} */ (context.visit(chunk.expression)); return { - value: - chunk.metadata.expression.has_call || chunk.metadata.expression.is_async - ? memoize(expression, chunk.metadata.expression.is_async) - : expression, - has_state: chunk.metadata.expression.has_state, - is_async: chunk.metadata.expression.is_async + value: memoize(expression, chunk.metadata.expression), + has_state: chunk.metadata.expression.has_state }; } 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 ac33e9686c..2bfbc5ff8a 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 { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ -/** @import { AST } from '#compiler' */ +/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; @@ -83,14 +83,17 @@ function compare_expressions(a, b) { * @param {Array} values * @param {(node: AST.SvelteNode, state: any) => any} visit * @param {ComponentClientTransformState} state - * @param {(value: Expression, is_async: boolean) => Expression} memoize - * @returns {{ value: Expression, has_state: boolean, is_async: boolean }} + * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize + * @returns {{ value: Expression, has_state: boolean }} */ export function build_template_chunk( values, visit, state, - memoize = (value, is_async) => get_expression_id(state, value, is_async) + memoize = (value, metadata) => + metadata.has_call || metadata.is_async + ? get_expression_id(state, value, metadata.is_async) + : value ) { /** @type {Expression[]} */ const expressions = []; @@ -111,19 +114,18 @@ export function build_template_chunk( quasi.value.cooked += node.expression.value + ''; } } else { - let value = /** @type {Expression} */ (visit(node.expression, state)); + let value = memoize( + /** @type {Expression} */ (visit(node.expression, state)), + node.metadata.expression + ); is_async ||= node.metadata.expression.is_async; has_state ||= is_async || node.metadata.expression.has_state; - if (node.metadata.expression.has_call || node.metadata.expression.is_async) { - value = memoize(value, node.metadata.expression.is_async); - } - if (values.length === 1) { // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). - return { value, has_state, is_async }; + return { value, has_state }; } else { let expression = value; // only add nullish coallescence if it hasn't been added already @@ -154,7 +156,7 @@ export function build_template_chunk( const value = b.template(quasis, expressions); - return { value, has_state, is_async }; + return { value, has_state }; } /** diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 4144a13fac..c2b7901f49 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -49,10 +49,10 @@ export function replay_events(dom) { /** * @param {string} event_name * @param {EventTarget} dom - * @param {EventListener} handler - * @param {AddEventListenerOptions} options + * @param {EventListener} [handler] + * @param {AddEventListenerOptions} [options] */ -export function create_event(event_name, dom, handler, options) { +export function create_event(event_name, dom, handler, options = {}) { /** * @this {EventTarget} */ @@ -63,7 +63,7 @@ export function create_event(event_name, dom, handler, options) { } if (!event.cancelBubble) { return without_reactive_context(() => { - return handler.call(this, event); + return handler?.call(this, event); }); } } @@ -108,8 +108,8 @@ export function on(element, type, handler, options = {}) { /** * @param {string} event_name * @param {Element} dom - * @param {EventListener} handler - * @param {boolean} capture + * @param {EventListener} [handler] + * @param {boolean} [capture] * @param {boolean} [passive] * @returns {void} */ diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index d10008dae2..c2448c9ee5 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -116,7 +116,7 @@ export function mutable_state(v, immutable = false) { */ /*#__NO_SIDE_EFFECTS__*/ function push_derived_source(source) { - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { + if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) { if (derived_sources === null) { set_derived_sources([source]); } else { diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 76486d32ac..b16c0551f1 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -196,7 +196,8 @@ const ATTRIBUTE_ALIASES = { readonly: 'readOnly', defaultvalue: 'defaultValue', defaultchecked: 'defaultChecked', - srcobject: 'srcObject' + srcobject: 'srcObject', + novalidate: 'noValidate' }; /** diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b0ea99b6f4..7a3f016aaa 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.19.1'; +export const VERSION = '5.19.2'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js new file mode 100644 index 0000000000..4fdf3632d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + html: ` +
+
+` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte new file mode 100644 index 0000000000..1e8115ff62 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-novalidate-casing/main.svelte @@ -0,0 +1,6 @@ + + +
+
diff --git a/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js new file mode 100644 index 0000000000..012fedb160 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/_config.js @@ -0,0 +1,9 @@ +import { ok, test } from '../../test'; + +export default test({ + async test({ target }) { + const button = target.querySelector('button'); + ok(button); + button.dispatchEvent(new window.MouseEvent('mouseenter')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte new file mode 100644 index 0000000000..ea4b4443e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/undefined-event-handler/main.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js new file mode 100644 index 0000000000..1fe92ed2d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `3` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte new file mode 100644 index 0000000000..0873eb741d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-derived-local/main.svelte @@ -0,0 +1,47 @@ + + + + +{test}