aaa
Rich Harris 8 months ago
commit 6ea6e2d724

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't throw for `undefined` non delegated event handlers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly handle `novalidate` attribute casing

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid double deriveds in component props

@ -1,5 +1,11 @@
# svelte # svelte
## 5.19.2
### Patch Changes
- fix: address regression with untrack ([#15079](https://github.com/sveltejs/svelte/pull/15079))
## 5.19.1 ## 5.19.1
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.19.1", "version": "5.19.2",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -269,41 +269,6 @@ export function should_proxy(node, scope) {
return true; return true;
} }
/**
* @param {Pattern} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} 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 * Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state

@ -1,8 +1,10 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ /** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @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 * 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 * @param {AST.AwaitBlock} node
@ -65,3 +67,38 @@ export function AwaitBlock(node, context) {
) )
); );
} }
/**
* @param {Pattern} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentClientTransformState>} 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 };
}

@ -541,8 +541,10 @@ function build_element_attribute_update_assignment(
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml'; const is_mathml = context.state.metadata.namespace === 'mathml';
let { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
get_expression_id(state, value, is_async) metadata.has_call || metadata.is_async
? get_expression_id(state, value, metadata.is_async)
: value
); );
if (name === 'autofocus') { 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) { function build_element_special_value_attribute(element, node_id, attribute, context) {
const state = context.state; const state = context.state;
const { value, has_state } = build_attribute_value(attribute.value, context, (value, is_async) => const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) =>
get_expression_id(state, value, is_async) metadata.has_call || metadata.is_async
? get_expression_id(state, value, metadata.is_async)
: value
); );
const inner_assignment = b.assignment( const inner_assignment = b.assignment(

@ -30,8 +30,10 @@ export function SlotElement(node, context) {
if (attribute.type === 'SpreadAttribute') { if (attribute.type === 'SpreadAttribute') {
spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute)))); spreads.push(b.thunk(/** @type {Expression} */ (context.visit(attribute))));
} else if (attribute.type === 'Attribute') { } else if (attribute.type === 'Attribute') {
const { value, has_state } = build_attribute_value(attribute.value, context, (value) => const { value, has_state } = build_attribute_value(
memoize_expression(context.state, value) attribute.value,
context,
(value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value)
); );
if (attribute.name === 'name') { if (attribute.name === 'name') {

@ -4,7 +4,6 @@
import { dev, is_ignored } from '../../../../../state.js'; import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.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_bind_this, memoize_expression, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js'; import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.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( custom_css_props.push(
b.init( b.init(
attribute.name, 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 // TODO put the derived in the local block
memoize_expression(context.state, value) metadata.has_call ? memoize_expression(context.state, value) : value
).value ).value
) )
); );
@ -151,31 +150,29 @@ export function build_component(node, component_name, context, anchor = context.
has_children_prop = true; has_children_prop = true;
} }
const { value, has_state } = build_attribute_value(attribute.value, context, (value) => const { value, has_state } = build_attribute_value(
memoize_expression(context.state, value) attribute.value,
); context,
(value, metadata) => {
if (has_state) { if (!metadata.has_state) return value;
let arg = value;
// When we have a non-simple computation, anything other than an Identifier or Member expression,
// 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
// 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}`)
// child component. const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => {
const should_wrap_in_derived = get_attribute_chunks(attribute.value).some((n) => { return (
return ( n.type === 'ExpressionTag' &&
n.type === 'ExpressionTag' && n.expression.type !== 'Identifier' &&
n.expression.type !== 'Identifier' && n.expression.type !== 'MemberExpression'
n.expression.type !== 'MemberExpression' );
); });
});
if (should_wrap_in_derived) { return should_wrap_in_derived ? memoize_expression(context.state, value) : value;
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);
} }
);
push_prop(b.get(attribute.name, [b.return(arg)])); if (has_state) {
push_prop(b.get(attribute.name, [b.return(value)]));
} else { } else {
push_prop(b.init(attribute.name, value)); push_prop(b.init(attribute.name, value));
} }

@ -1,11 +1,11 @@
/** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { normalize_attribute } from '../../../../../../utils.js'; import { normalize_attribute } from '../../../../../../utils.js';
import { is_ignored } from '../../../../../state.js'; import { is_ignored } from '../../../../../state.js';
import { is_event_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.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'; 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( const { value, has_state } = build_attribute_value(
attribute.value, attribute.value,
context, 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 ( if (
@ -60,11 +63,10 @@ export function build_set_attributes(
let value = /** @type {Expression} */ (context.visit(attribute)); let value = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_call) { if (attribute.metadata.expression.has_call || attribute.metadata.expression.is_async) {
const id = b.id(state.scope.generate('spread_with_call')); value = get_expression_id(context.state, value, attribute.metadata.expression.is_async);
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
value = b.call('$.get', id);
} }
values.push(b.spread(value)); values.push(b.spread(value));
} }
} }
@ -113,8 +115,10 @@ export function build_style_directives(
let value = let value =
directive.value === true directive.value === true
? build_getter({ name: directive.name, type: 'Identifier' }, context.state) ? build_getter({ name: directive.name, type: 'Identifier' }, context.state)
: build_attribute_value(directive.value, context, (value, is_async) => : build_attribute_value(directive.value, context, (value, metadata) =>
get_expression_id(context.state, value, is_async) metadata.has_call || metadata.is_async
? get_expression_id(context.state, value, metadata.is_async)
: value
).value; ).value;
const update = b.stmt( const update = b.stmt(
@ -171,30 +175,26 @@ export function build_class_directives(
/** /**
* @param {AST.Attribute['value']} value * @param {AST.Attribute['value']} value
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(value: Expression, is_async: boolean) => Expression} memoize * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean, is_async: boolean }} * @returns {{ value: Expression, has_state: boolean }}
*/ */
export function build_attribute_value(value, context, memoize = (value) => value) { export function build_attribute_value(value, context, memoize = (value) => value) {
if (value === true) { 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) { if (!Array.isArray(value) || value.length === 1) {
const chunk = Array.isArray(value) ? value[0] : value; const chunk = Array.isArray(value) ? value[0] : value;
if (chunk.type === 'Text') { 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)); let expression = /** @type {Expression} */ (context.visit(chunk.expression));
return { return {
value: value: memoize(expression, chunk.metadata.expression),
chunk.metadata.expression.has_call || chunk.metadata.expression.is_async has_state: chunk.metadata.expression.has_state
? memoize(expression, chunk.metadata.expression.is_async)
: expression,
has_state: chunk.metadata.expression.has_state,
is_async: chunk.metadata.expression.is_async
}; };
} }

@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */ /** @import { Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Statement, Super } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState } from '../../types' */ /** @import { ComponentClientTransformState } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js'; import { object } from '../../../../../utils/ast.js';
@ -83,14 +83,17 @@ function compare_expressions(a, b) {
* @param {Array<AST.Text | AST.ExpressionTag>} values * @param {Array<AST.Text | AST.ExpressionTag>} values
* @param {(node: AST.SvelteNode, state: any) => any} visit * @param {(node: AST.SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state * @param {ComponentClientTransformState} state
* @param {(value: Expression, is_async: boolean) => Expression} memoize * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize
* @returns {{ value: Expression, has_state: boolean, is_async: boolean }} * @returns {{ value: Expression, has_state: boolean }}
*/ */
export function build_template_chunk( export function build_template_chunk(
values, values,
visit, visit,
state, 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[]} */ /** @type {Expression[]} */
const expressions = []; const expressions = [];
@ -111,19 +114,18 @@ export function build_template_chunk(
quasi.value.cooked += node.expression.value + ''; quasi.value.cooked += node.expression.value + '';
} }
} else { } 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; is_async ||= node.metadata.expression.is_async;
has_state ||= is_async || node.metadata.expression.has_state; 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 (values.length === 1) {
// If we have a single expression, then pass that in directly to possibly avoid doing // 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). // 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 { } else {
let expression = value; let expression = value;
// only add nullish coallescence if it hasn't been added already // 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); const value = b.template(quasis, expressions);
return { value, has_state, is_async }; return { value, has_state };
} }
/** /**

@ -49,10 +49,10 @@ export function replay_events(dom) {
/** /**
* @param {string} event_name * @param {string} event_name
* @param {EventTarget} dom * @param {EventTarget} dom
* @param {EventListener} handler * @param {EventListener} [handler]
* @param {AddEventListenerOptions} options * @param {AddEventListenerOptions} [options]
*/ */
export function create_event(event_name, dom, handler, options) { export function create_event(event_name, dom, handler, options = {}) {
/** /**
* @this {EventTarget} * @this {EventTarget}
*/ */
@ -63,7 +63,7 @@ export function create_event(event_name, dom, handler, options) {
} }
if (!event.cancelBubble) { if (!event.cancelBubble) {
return without_reactive_context(() => { 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 {string} event_name
* @param {Element} dom * @param {Element} dom
* @param {EventListener} handler * @param {EventListener} [handler]
* @param {boolean} capture * @param {boolean} [capture]
* @param {boolean} [passive] * @param {boolean} [passive]
* @returns {void} * @returns {void}
*/ */

@ -116,7 +116,7 @@ export function mutable_state(v, immutable = false) {
*/ */
/*#__NO_SIDE_EFFECTS__*/ /*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) { 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) { if (derived_sources === null) {
set_derived_sources([source]); set_derived_sources([source]);
} else { } else {

@ -196,7 +196,8 @@ const ATTRIBUTE_ALIASES = {
readonly: 'readOnly', readonly: 'readOnly',
defaultvalue: 'defaultValue', defaultvalue: 'defaultValue',
defaultchecked: 'defaultChecked', defaultchecked: 'defaultChecked',
srcobject: 'srcObject' srcobject: 'srcObject',
novalidate: 'noValidate'
}; };
/** /**

@ -4,5 +4,5 @@
* The current version, as set in package.json. * The current version, as set in package.json.
* @type {string} * @type {string}
*/ */
export const VERSION = '5.19.1'; export const VERSION = '5.19.2';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,8 @@
import { test } from '../../test';
export default test({
html: `
<form novalidate></form>
<form novalidate></form>
`
});

@ -0,0 +1,6 @@
<script>
let noValidate = $state(true);
</script>
<form novalidate={true}></form>
<form {noValidate}></form>

@ -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'));
}
});

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `3`
});

@ -0,0 +1,47 @@
<script module>
import { untrack } from 'svelte'
import { SvelteMap } from 'svelte/reactivity'
class Foo {
id
updateTime = $state(Date.now())
constructor(id) {
this.id = id
}
}
class Store {
cache = new SvelteMap()
ids = $state([1, 2, 3])
getOrDefault(id) {
let ret = this.cache.get(id)
if (ret) {
return ret
}
ret = untrack(() => {
ret = new Foo(id)
this.cache.set(id, ret)
return ret
})
this.cache.get(id)
return ret
}
get values() {
return this.ids.map(id => this.getOrDefault(id)).sort((a, b) => b.updateTime - a.updateTime)
}
}
const store = new Store()
</script>
<script>
const test = $derived.by(() => store.values.length)
</script>
{test}
Loading…
Cancel
Save