pull/16339/head
Rich Harris 2 months ago
commit f3dd26b0f7

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: simplify internal component `pop()`

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: clean up a11y analysis code

@ -50,7 +50,7 @@ todos.push({
}); });
``` ```
> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. > [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you desire to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==).
Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring:

@ -1,5 +1,23 @@
# svelte # svelte
## 5.35.7
### Patch Changes
- fix: silence autofocus a11y warning inside `<dialog>` ([#16341](https://github.com/sveltejs/svelte/pull/16341))
- fix: don't show adjusted error messages in boundaries ([#16360](https://github.com/sveltejs/svelte/pull/16360))
- chore: replace inline regex with variable ([#16340](https://github.com/sveltejs/svelte/pull/16340))
## 5.35.6
### Patch Changes
- chore: simplify reaction/source ownership tracking ([#16333](https://github.com/sveltejs/svelte/pull/16333))
- chore: simplify internal component `pop()` ([#16331](https://github.com/sveltejs/svelte/pull/16331))
## 5.35.5 ## 5.35.5
### Patch Changes ### Patch Changes

@ -1,10 +1,6 @@
{ {
"$schema": "https://unpkg.com/knip@5/schema.json", "$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [ "entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js", "tests/**/*.js",
"tests/**/*.ts", "tests/**/*.ts",
"!tests/**/*.svelte", "!tests/**/*.svelte",

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

@ -1,5 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
export * from '../shared/errors.js';
/** /**
* MESSAGE * MESSAGE
* @param {string} PARAMETER * @param {string} PARAMETER

@ -1,3 +1,5 @@
export * from '../shared/errors.js';
/** /**
* MESSAGE * MESSAGE
* @param {string} PARAMETER * @param {string} PARAMETER

@ -9,7 +9,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.js'; import { create_attribute, is_custom_element_node } from '../../nodes.js';
import { regex_starts_with_newline } from '../../patterns.js'; import { regex_starts_with_newline } from '../../patterns.js';
import { check_element } from './shared/a11y.js'; import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js'; import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */ /** @import { Context } from '../types' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js'; import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.js';
import { is_text_attribute } from '../../../utils/ast.js'; import { is_text_attribute } from '../../../utils/ast.js';
import { check_element } from './shared/a11y.js'; import { check_element } from './shared/a11y/index.js';
import { validate_element } from './shared/element.js'; import { validate_element } from './shared/element.js';
import { mark_subtree_dynamic } from './shared/fragment.js'; import { mark_subtree_dynamic } from './shared/fragment.js';

@ -0,0 +1,319 @@
/** @import { ARIARoleRelationConcept } from 'aria-query' */
import { roles as roles_map, elementRoles } from 'aria-query';
// @ts-expect-error package doesn't provide typings
import { AXObjects, elementAXObjects } from 'axobject-query';
export const aria_attributes =
'activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext'.split(
' '
);
/** @type {Record<string, string[]>} */
export const a11y_required_attributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
// html-has-lang
html: ['lang'],
// iframe-has-title
iframe: ['title'],
img: ['alt'],
object: ['title', 'aria-label', 'aria-labelledby']
};
export const a11y_distracting_elements = ['blink', 'marquee'];
// this excludes `<a>` and `<button>` because they are handled separately
export const a11y_required_content = [
// heading-has-content
'h1',
'h2',
'h3',
'h4',
'h5',
'h6'
];
export const a11y_labelable = [
'button',
'input',
'keygen',
'meter',
'output',
'progress',
'select',
'textarea'
];
export const a11y_interactive_handlers = [
// Keyboard events
'keypress',
'keydown',
'keyup',
// Click events
'click',
'contextmenu',
'dblclick',
'drag',
'dragend',
'dragenter',
'dragexit',
'dragleave',
'dragover',
'dragstart',
'drop',
'mousedown',
'mouseenter',
'mouseleave',
'mousemove',
'mouseout',
'mouseover',
'mouseup'
];
export const a11y_recommended_interactive_handlers = [
'click',
'mousedown',
'mouseup',
'keypress',
'keydown',
'keyup'
];
export const a11y_nested_implicit_semantics = new Map([
['header', 'banner'],
['footer', 'contentinfo']
]);
export const a11y_implicit_semantics = new Map([
['a', 'link'],
['area', 'link'],
['article', 'article'],
['aside', 'complementary'],
['body', 'document'],
['button', 'button'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
['dialog', 'dialog'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['figure', 'figure'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
['h3', 'heading'],
['h4', 'heading'],
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
['img', 'img'],
['li', 'listitem'],
['link', 'link'],
['main', 'main'],
['menu', 'list'],
['meter', 'progressbar'],
['nav', 'navigation'],
['ol', 'list'],
['option', 'option'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['table', 'table'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
['thead', 'rowgroup'],
['tr', 'row'],
['ul', 'list']
]);
export const menuitem_type_to_implicit_role = new Map([
['command', 'menuitem'],
['checkbox', 'menuitemcheckbox'],
['radio', 'menuitemradio']
]);
export const input_type_to_implicit_role = new Map([
['button', 'button'],
['image', 'button'],
['reset', 'button'],
['submit', 'button'],
['checkbox', 'checkbox'],
['radio', 'radio'],
['range', 'slider'],
['number', 'spinbutton'],
['email', 'textbox'],
['search', 'searchbox'],
['tel', 'textbox'],
['text', 'textbox'],
['url', 'textbox']
]);
/**
* Exceptions to the rule which follows common A11y conventions
* TODO make this configurable by the user
* @type {Record<string, string[]>}
*/
export const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'],
li: ['menuitem', 'option', 'row', 'tab', 'treeitem'],
table: ['grid'],
td: ['gridcell'],
fieldset: ['radiogroup', 'presentation']
};
export const combobox_if_list = ['email', 'search', 'tel', 'text', 'url'];
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls:-the-autocomplete-attribute
export const address_type_tokens = ['shipping', 'billing'];
export const autofill_field_name_tokens = [
'',
'on',
'off',
'name',
'honorific-prefix',
'given-name',
'additional-name',
'family-name',
'honorific-suffix',
'nickname',
'username',
'new-password',
'current-password',
'one-time-code',
'organization-title',
'organization',
'street-address',
'address-line1',
'address-line2',
'address-line3',
'address-level4',
'address-level3',
'address-level2',
'address-level1',
'country',
'country-name',
'postal-code',
'cc-name',
'cc-given-name',
'cc-additional-name',
'cc-family-name',
'cc-number',
'cc-exp',
'cc-exp-month',
'cc-exp-year',
'cc-csc',
'cc-type',
'transaction-currency',
'transaction-amount',
'language',
'bday',
'bday-day',
'bday-month',
'bday-year',
'sex',
'url',
'photo'
];
export const contact_type_tokens = ['home', 'work', 'mobile', 'fax', 'pager'];
export const autofill_contact_field_name_tokens = [
'tel',
'tel-country-code',
'tel-national',
'tel-area-code',
'tel-local',
'tel-local-prefix',
'tel-local-suffix',
'tel-extension',
'email',
'impp'
];
export const ElementInteractivity = /** @type {const} */ ({
Interactive: 'interactive',
NonInteractive: 'non-interactive',
Static: 'static'
});
export const invisible_elements = ['meta', 'html', 'script', 'style'];
export const aria_roles = roles_map.keys();
export const abstract_roles = aria_roles.filter((role) => roles_map.get(role)?.abstract);
const non_abstract_roles = aria_roles.filter((name) => !abstract_roles.includes(name));
export const non_interactive_roles = non_abstract_roles
.filter((name) => {
const role = roles_map.get(name);
return (
// 'toolbar' does not descend from widget, but it does support
// aria-activedescendant, thus in practice we treat it as a widget.
// focusable tabpanel elements are recommended if any panels in a set contain content where the first element in the panel is not focusable.
// 'generic' is meant to have no semantic meaning.
// 'cell' is treated as CellRole by the AXObject which is interactive, so we treat 'cell' it as interactive as well.
!['toolbar', 'tabpanel', 'generic', 'cell'].includes(name) &&
!role?.superClass.some((classes) => classes.includes('widget') || classes.includes('window'))
);
})
.concat(
// The `progressbar` is descended from `widget`, but in practice, its
// value is always `readonly`, so we treat it as a non-interactive role.
'progressbar'
);
export const interactive_roles = non_abstract_roles.filter(
(name) =>
!non_interactive_roles.includes(name) &&
// 'generic' is meant to have no semantic meaning.
name !== 'generic'
);
export const presentation_roles = ['presentation', 'none'];
/** @type {ARIARoleRelationConcept[]} */
export const non_interactive_element_role_schemas = [];
/** @type {ARIARoleRelationConcept[]} */
export const interactive_element_role_schemas = [];
for (const [schema, roles] of elementRoles.entries()) {
if ([...roles].every((role) => role !== 'generic' && non_interactive_roles.includes(role))) {
non_interactive_element_role_schemas.push(schema);
}
if ([...roles].every((role) => interactive_roles.includes(role))) {
interactive_element_role_schemas.push(schema);
}
}
const interactive_ax_objects = [...AXObjects.keys()].filter(
(name) => AXObjects.get(name).type === 'widget'
);
/** @type {ARIARoleRelationConcept[]} */
export const interactive_element_ax_object_schemas = [];
/** @type {ARIARoleRelationConcept[]} */
export const non_interactive_element_ax_object_schemas = [];
const non_interactive_ax_objects = [...AXObjects.keys()].filter((name) =>
['windows', 'structure'].includes(AXObjects.get(name).type)
);
for (const [schema, ax_object] of elementAXObjects.entries()) {
if ([...ax_object].every((role) => interactive_ax_objects.includes(role))) {
interactive_element_ax_object_schemas.push(schema);
}
if ([...ax_object].every((role) => non_interactive_ax_objects.includes(role))) {
non_interactive_element_ax_object_schemas.push(schema);
}
}

@ -1,9 +1,9 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property } from 'estree' */ /** @import { BlockStatement, Expression, ExpressionStatement, Literal, Property, Statement } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_attribute_value } from './shared/element.js'; import { build_attribute_value } from './shared/element.js';
import { memoize_expression } from './shared/utils.js'; import { Memoizer } from './shared/utils.js';
/** /**
* @param {AST.SlotElement} node * @param {AST.SlotElement} node
@ -22,7 +22,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */ /** @type {ExpressionStatement[]} */
const lets = []; const lets = [];
let is_default = true; const memoizer = new Memoizer();
let name = b.literal('default'); let name = b.literal('default');
@ -33,12 +33,11 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value( const { value, has_state } = build_attribute_value(
attribute.value, attribute.value,
context, context,
(value, metadata) => (metadata.has_call ? memoize_expression(context.state, value) : value) (value, metadata) => (metadata.has_call ? b.call('$.get', memoizer.add(value)) : value)
); );
if (attribute.name === 'name') { if (attribute.name === 'name') {
name = /** @type {Literal} */ (value); name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') { } else if (attribute.name !== 'slot') {
if (has_state) { if (has_state) {
props.push(b.get(attribute.name, [b.return(value)])); props.push(b.get(attribute.name, [b.return(value)]));
@ -51,9 +50,14 @@ export function SlotElement(node, context) {
} }
} }
memoizer.apply();
// Let bindings first, they can be used on attributes // Let bindings first, they can be used on attributes
context.state.init.push(...lets); context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
const props_expression = const props_expression =
spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads); spreads.length === 0 ? b.object(props) : b.call('$.spread_props', b.object(props), ...spreads);
@ -62,14 +66,9 @@ export function SlotElement(node, context) {
? b.null ? b.null
: b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call( statements.push(
'$.slot', b.stmt(b.call('$.slot', context.state.node, b.id('$$props'), name, props_expression, fallback))
context.state.node,
b.id('$$props'),
name,
props_expression,
fallback
); );
context.state.init.push(b.stmt(slot)); context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
} }

@ -4,12 +4,7 @@
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 '#compiler/builders'; import * as b from '#compiler/builders';
import { import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
build_bind_this,
memoize_expression,
validate_binding,
add_svelte_meta
} 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';
import { determine_slot } from '../../../../../utils/slot.js'; import { determine_slot } from '../../../../../utils/slot.js';
@ -48,6 +43,8 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */ /** @type {Record<string, Expression[]>} */
const events = {}; const events = {};
const memoizer = new Memoizer();
/** @type {Property[]} */ /** @type {Property[]} */
const custom_css_props = []; const custom_css_props = [];
@ -133,15 +130,13 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') { } else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute)); const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state) {
let value = expression; props_and_spreads.push(
b.thunk(
if (attribute.metadata.expression.has_call) { attribute.metadata.expression.has_call
const id = b.id(context.state.scope.generate('spread_element')); ? b.call('$.get', memoizer.add(expression))
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); : expression
value = b.call('$.get', id); )
} );
props_and_spreads.push(b.thunk(value));
} else { } else {
props_and_spreads.push(expression); props_and_spreads.push(expression);
} }
@ -150,10 +145,10 @@ export function build_component(node, component_name, context) {
custom_css_props.push( custom_css_props.push(
b.init( b.init(
attribute.name, attribute.name,
build_attribute_value(attribute.value, context, (value, metadata) => build_attribute_value(attribute.value, context, (value, metadata) => {
// TODO put the derived in the local block // TODO put the derived in the local block
metadata.has_call ? memoize_expression(context.state, value) : value return metadata.has_call ? b.call('$.get', memoizer.add(value)) : value;
).value }).value
) )
); );
continue; continue;
@ -184,7 +179,7 @@ export function build_component(node, component_name, context) {
); );
}); });
return should_wrap_in_derived ? memoize_expression(context.state, value) : value; return should_wrap_in_derived ? b.call('$.get', memoizer.add(value)) : value;
} }
); );
@ -444,7 +439,7 @@ export function build_component(node, component_name, context) {
}; };
} }
const statements = [...snippet_declarations]; const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)];
if (is_component_dynamic) { if (is_component_dynamic) {
const prev = fn; const prev = fn;
@ -492,5 +487,7 @@ export function build_component(node, component_name, context) {
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name })); statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
} }
memoizer.apply();
return statements.length > 1 ? b.block(statements) : statements[0]; return statements.length > 1 ? b.block(statements) : statements[0];
} }

@ -8,17 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js'; import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } from '../../utils.js'; import { build_getter } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
* @param {Expression} value
*/
export function memoize_expression(state, value) {
const id = b.id(state.scope.generate('expression'));
state.init.push(b.const(id, create_derived(state, b.thunk(value))));
return b.call('$.get', id);
}
/** /**
* A utility for extracting complex expressions (such as call expressions) * A utility for extracting complex expressions (such as call expressions)

@ -23,3 +23,5 @@ export const regex_heading_tags = /^h[1-6]$/;
export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/; export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/;
export const regex_bidirectional_control_characters = export const regex_bidirectional_control_characters =
/[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g; /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g;
export const regex_js_prefix = /^\W*javascript:/i;
export const regex_redundant_img_alt = /\b(image|picture|photo)\b/i;

@ -18,9 +18,9 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown'); const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */ /** Includes `BigInt` */
export const NUMBER = Symbol('number'); const NUMBER = Symbol('number');
export const STRING = Symbol('string'); const STRING = Symbol('string');
export const FUNCTION = Symbol('string'); const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */ /** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
const globals = { const globals = {

@ -241,19 +241,6 @@ function validator(fallback, fn) {
}; };
} }
/**
* @param {number} fallback
* @returns {Validator}
*/
function number(fallback) {
return validator(fallback, (input, keypath) => {
if (typeof input !== 'number') {
throw_error(`${keypath} should be a number, if specified`);
}
return input;
});
}
/** /**
* @param {string | undefined} fallback * @param {string | undefined} fallback
* @param {boolean} allow_empty * @param {boolean} allow_empty
@ -273,20 +260,6 @@ function string(fallback, allow_empty = true) {
}); });
} }
/**
* @param {string[]} fallback
* @returns {Validator}
*/
function string_array(fallback) {
return validator(fallback, (input, keypath) => {
if (input && !Array.isArray(input)) {
throw_error(`${keypath} should be a string array, if specified`);
}
return input;
});
}
/** /**
* @param {boolean | undefined} fallback * @param {boolean | undefined} fallback
* @returns {Validator} * @returns {Validator}

@ -5,7 +5,6 @@ import { active_reaction, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js'; import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js'; import { user_effect } from './internal/client/index.js';
import * as e from './internal/client/errors.js'; import * as e from './internal/client/errors.js';
import { lifecycle_outside_component } from './internal/shared/errors.js';
import { legacy_mode_flag } from './internal/flags/index.js'; import { legacy_mode_flag } from './internal/flags/index.js';
import { component_context } from './internal/client/context.js'; import { component_context } from './internal/client/context.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
@ -91,7 +90,7 @@ export function getAbortSignal() {
*/ */
export function onMount(fn) { export function onMount(fn) {
if (component_context === null) { if (component_context === null) {
lifecycle_outside_component('onMount'); e.lifecycle_outside_component('onMount');
} }
if (legacy_mode_flag && component_context.l !== null) { if (legacy_mode_flag && component_context.l !== null) {
@ -115,7 +114,7 @@ export function onMount(fn) {
*/ */
export function onDestroy(fn) { export function onDestroy(fn) {
if (component_context === null) { if (component_context === null) {
lifecycle_outside_component('onDestroy'); e.lifecycle_outside_component('onDestroy');
} }
onMount(() => () => untrack(fn)); onMount(() => () => untrack(fn));
@ -158,7 +157,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
export function createEventDispatcher() { export function createEventDispatcher() {
const active_component_context = component_context; const active_component_context = component_context;
if (active_component_context === null) { if (active_component_context === null) {
lifecycle_outside_component('createEventDispatcher'); e.lifecycle_outside_component('createEventDispatcher');
} }
return (type, detail, options) => { return (type, detail, options) => {
@ -196,7 +195,7 @@ export function createEventDispatcher() {
*/ */
export function beforeUpdate(fn) { export function beforeUpdate(fn) {
if (component_context === null) { if (component_context === null) {
lifecycle_outside_component('beforeUpdate'); e.lifecycle_outside_component('beforeUpdate');
} }
if (component_context.l === null) { if (component_context.l === null) {
@ -219,7 +218,7 @@ export function beforeUpdate(fn) {
*/ */
export function afterUpdate(fn) { export function afterUpdate(fn) {
if (component_context === null) { if (component_context === null) {
lifecycle_outside_component('afterUpdate'); e.lifecycle_outside_component('afterUpdate');
} }
if (component_context.l === null) { if (component_context.l === null) {

@ -1,15 +1,8 @@
/** @import { ComponentContext, DevStackEntry } from '#client' */ /** @import { ComponentContext, DevStackEntry } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { lifecycle_outside_component } from '../shared/errors.js'; import * as e from './errors.js';
import { source } from './reactivity/sources.js'; import { source } from './reactivity/sources.js';
import { import { create_user_effect } from './reactivity/effects.js';
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from './runtime.js';
import { create_user_effect, teardown } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js'; import { legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
@ -205,7 +198,7 @@ export function is_runes() {
*/ */
function get_or_init_context_map(name) { function get_or_init_context_map(name) {
if (component_context === null) { if (component_context === null) {
lifecycle_outside_component(name); e.lifecycle_outside_component(name);
} }
return (component_context.c ??= new Map(get_parent_context(component_context) || undefined)); return (component_context.c ??= new Map(get_parent_context(component_context) || undefined));

@ -1,15 +1,16 @@
import { invalid_snippet_arguments } from '../../shared/errors.js'; import * as e from '../errors.js';
/** /**
* @param {Node} anchor * @param {Node} anchor
* @param {...(()=>any)[]} args * @param {...(()=>any)[]} args
*/ */
export function validate_snippet_args(anchor, ...args) { export function validate_snippet_args(anchor, ...args) {
if (typeof anchor !== 'object' || !(anchor instanceof Node)) { if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
invalid_snippet_arguments(); e.invalid_snippet_arguments();
} }
for (let arg of args) { for (let arg of args) {
if (typeof arg !== 'function') { if (typeof arg !== 'function') {
invalid_snippet_arguments(); e.invalid_snippet_arguments();
} }
} }
} }

@ -7,14 +7,16 @@ import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js';
import { define_property, get_descriptor } from '../shared/utils.js'; import { define_property, get_descriptor } from '../shared/utils.js';
import { active_effect } from './runtime.js'; import { active_effect } from './runtime.js';
const adjustments = new WeakMap();
/** /**
* @param {unknown} error * @param {unknown} error
*/ */
export function handle_error(error) { export function handle_error(error) {
var effect = /** @type {Effect} */ (active_effect); var effect = /** @type {Effect} */ (active_effect);
if (DEV && error instanceof Error) { if (DEV && error instanceof Error && !adjustments.has(error)) {
adjust_error(error, effect); adjustments.set(error, get_adjustments(error, effect));
} }
if ((effect.f & EFFECT_RAN) === 0) { if ((effect.f & EFFECT_RAN) === 0) {
@ -48,21 +50,19 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent; effect = effect.parent;
} }
if (error instanceof Error) {
apply_adjustments(error);
}
throw error; throw error;
} }
/** @type {WeakSet<Error>} */
const adjusted_errors = new WeakSet();
/** /**
* Add useful information to the error message/stack in development * Add useful information to the error message/stack in development
* @param {Error} error * @param {Error} error
* @param {Effect} effect * @param {Effect} effect
*/ */
function adjust_error(error, effect) { function get_adjustments(error, effect) {
if (adjusted_errors.has(error)) return;
adjusted_errors.add(error);
const message_descriptor = get_descriptor(error, 'message'); const message_descriptor = get_descriptor(error, 'message');
// if the message was already changed and it's not configurable we can't change it // if the message was already changed and it's not configurable we can't change it
@ -78,17 +78,28 @@ function adjust_error(error, effect) {
context = context.p; context = context.p;
} }
return {
message: error.message + `\n${component_stack}\n`,
stack: error.stack
?.split('\n')
.filter((line) => !line.includes('svelte/src/internal'))
.join('\n')
};
}
/**
* @param {Error} error
*/
function apply_adjustments(error) {
const adjusted = adjustments.get(error);
if (adjusted) {
define_property(error, 'message', { define_property(error, 'message', {
value: error.message + `\n${component_stack}\n` value: adjusted.message
}); });
if (error.stack) {
// Filter out internal modules
define_property(error, 'stack', { define_property(error, 'stack', {
value: error.stack value: adjusted.stack
.split('\n')
.filter((line) => !line.includes('svelte/src/internal'))
.join('\n')
}); });
} }
} }

@ -2,6 +2,8 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
export * from '../shared/errors.js';
/** /**
* Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead * Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead
* @returns {never} * @returns {never}

@ -1,6 +1,13 @@
/** @import { Source } from '#client' */ /** @import { Source } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; import {
get,
active_effect,
update_version,
active_reaction,
set_update_version,
set_active_reaction
} from './runtime.js';
import { import {
array_prototype, array_prototype,
get_descriptor, get_descriptor,
@ -41,7 +48,7 @@ export function proxy(value) {
var version = source(0); var version = source(0);
var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null;
var reaction = active_reaction; var parent_version = update_version;
/** /**
* Executes the proxy in the context of the reaction it was originally created in, if any * Executes the proxy in the context of the reaction it was originally created in, if any
@ -49,13 +56,23 @@ export function proxy(value) {
* @param {() => T} fn * @param {() => T} fn
*/ */
var with_parent = (fn) => { var with_parent = (fn) => {
var previous_reaction = active_reaction; if (update_version === parent_version) {
set_active_reaction(reaction); return fn();
}
// child source is being created after the initial proxy —
// prevent it from being associated with the current reaction
var reaction = active_reaction;
var version = update_version;
set_active_reaction(null);
set_update_version(parent_version);
/** @type {T} */
var result = fn(); var result = fn();
set_active_reaction(previous_reaction); set_active_reaction(reaction);
set_update_version(version);
return result; return result;
}; };

@ -1,6 +1,6 @@
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */ /** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
import { import {
check_dirtiness, is_dirty,
active_effect, active_effect,
active_reaction, active_reaction,
update_effect, update_effect,
@ -307,7 +307,7 @@ export function legacy_pre_effect_reset() {
set_signal_status(effect, MAYBE_DIRTY); set_signal_status(effect, MAYBE_DIRTY);
} }
if (check_dirtiness(effect)) { if (is_dirty(effect)) {
update_effect(effect); update_effect(effect);
} }

@ -11,8 +11,8 @@ import {
untrack, untrack,
increment_write_version, increment_write_version,
update_effect, update_effect,
source_ownership, current_sources,
check_dirtiness, is_dirty,
untracking, untracking,
is_destroying_effect, is_destroying_effect,
push_reaction_value push_reaction_value
@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) {
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() && is_runes() &&
(active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 && (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 &&
!(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source)) !current_sources?.includes(source)
) { ) {
e.state_unsafe_mutation(); e.state_unsafe_mutation();
} }
@ -218,7 +218,7 @@ export function internal_set(source, value) {
if ((effect.f & CLEAN) !== 0) { if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY); set_signal_status(effect, MAYBE_DIRTY);
} }
if (check_dirtiness(effect)) { if (is_dirty(effect)) {
update_effect(effect); update_effect(effect);
} }
} }

@ -88,17 +88,17 @@ export function set_active_effect(effect) {
/** /**
* When sources are created within a reaction, reading and writing * When sources are created within a reaction, reading and writing
* them within that reaction should not cause a re-run * them within that reaction should not cause a re-run
* @type {null | { reaction: Reaction, sources: Source[] }} * @type {null | Source[]}
*/ */
export let source_ownership = null; export let current_sources = null;
/** @param {Value} value */ /** @param {Value} value */
export function push_reaction_value(value) { export function push_reaction_value(value) {
if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) {
if (source_ownership === null) { if (current_sources === null) {
source_ownership = { reaction: active_reaction, sources: [value] }; current_sources = [value];
} else { } else {
source_ownership.sources.push(value); current_sources.push(value);
} }
} }
} }
@ -136,6 +136,11 @@ let read_version = 0;
export let update_version = read_version; export let update_version = read_version;
/** @param {number} value */
export function set_update_version(value) {
update_version = value;
}
// If we are working with a get() chain that has no active container, // If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the reaction. // to prevent memory leaks, we skip adding the reaction.
export let skip_reaction = false; export let skip_reaction = false;
@ -158,7 +163,7 @@ export function increment_write_version() {
* @param {Reaction} reaction * @param {Reaction} reaction
* @returns {boolean} * @returns {boolean}
*/ */
export function check_dirtiness(reaction) { export function is_dirty(reaction) {
var flags = reaction.f; var flags = reaction.f;
if ((flags & DIRTY) !== 0) { if ((flags & DIRTY) !== 0) {
@ -207,7 +212,7 @@ export function check_dirtiness(reaction) {
for (i = 0; i < length; i++) { for (i = 0; i < length; i++) {
dependency = dependencies[i]; dependency = dependencies[i];
if (check_dirtiness(/** @type {Derived} */ (dependency))) { if (is_dirty(/** @type {Derived} */ (dependency))) {
update_derived(/** @type {Derived} */ (dependency)); update_derived(/** @type {Derived} */ (dependency));
} }
@ -236,7 +241,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
var reactions = signal.reactions; var reactions = signal.reactions;
if (reactions === null) return; if (reactions === null) return;
if (source_ownership?.reaction === active_reaction && source_ownership.sources.includes(signal)) { if (current_sources?.includes(signal)) {
return; return;
} }
@ -263,7 +268,7 @@ export function update_reaction(reaction) {
var previous_untracked_writes = untracked_writes; var previous_untracked_writes = untracked_writes;
var previous_reaction = active_reaction; var previous_reaction = active_reaction;
var previous_skip_reaction = skip_reaction; var previous_skip_reaction = skip_reaction;
var previous_reaction_sources = source_ownership; var previous_sources = current_sources;
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_untracking = untracking; var previous_untracking = untracking;
var previous_update_version = update_version; var previous_update_version = update_version;
@ -277,7 +282,7 @@ export function update_reaction(reaction) {
(flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null);
active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
source_ownership = null; current_sources = null;
set_component_context(reaction.ctx); set_component_context(reaction.ctx);
untracking = false; untracking = false;
update_version = ++read_version; update_version = ++read_version;
@ -365,7 +370,7 @@ export function update_reaction(reaction) {
untracked_writes = previous_untracked_writes; untracked_writes = previous_untracked_writes;
active_reaction = previous_reaction; active_reaction = previous_reaction;
skip_reaction = previous_skip_reaction; skip_reaction = previous_skip_reaction;
source_ownership = previous_reaction_sources; current_sources = previous_sources;
set_component_context(previous_component_context); set_component_context(previous_component_context);
untracking = previous_untracking; untracking = previous_untracking;
update_version = previous_update_version; update_version = previous_update_version;
@ -583,7 +588,7 @@ function flush_queued_effects(effects) {
var effect = effects[i]; var effect = effects[i];
if ((effect.f & (DESTROYED | INERT)) === 0) { if ((effect.f & (DESTROYED | INERT)) === 0) {
if (check_dirtiness(effect)) { if (is_dirty(effect)) {
var wv = write_version; var wv = write_version;
update_effect(effect); update_effect(effect);
@ -670,7 +675,7 @@ function process_effects(root) {
} else if (is_branch) { } else if (is_branch) {
effect.f ^= CLEAN; effect.f ^= CLEAN;
} else { } else {
if (check_dirtiness(effect)) { if (is_dirty(effect)) {
update_effect(effect); update_effect(effect);
} }
} }
@ -759,10 +764,7 @@ export function get(signal) {
// Register the dependency on the current reaction signal. // Register the dependency on the current reaction signal.
if (active_reaction !== null && !untracking) { if (active_reaction !== null && !untracking) {
if ( if (!current_sources?.includes(signal)) {
source_ownership?.reaction !== active_reaction ||
!source_ownership?.sources.includes(signal)
) {
var deps = active_reaction.deps; var deps = active_reaction.deps;
if (signal.rv < read_version) { if (signal.rv < read_version) {
signal.rv = read_version; signal.rv = read_version;
@ -800,7 +802,7 @@ export function get(signal) {
if (is_derived && !is_destroying_effect) { if (is_derived && !is_destroying_effect) {
derived = /** @type {Derived} */ (signal); derived = /** @type {Derived} */ (signal);
if (check_dirtiness(derived)) { if (is_dirty(derived)) {
update_derived(derived); update_derived(derived);
} }
} }

@ -1,7 +1,7 @@
import { STALE_REACTION } from '#client/constants'; import { STALE_REACTION } from '#client/constants';
/** @type {AbortController | null} */ /** @type {AbortController | null} */
export let controller = null; let controller = null;
export function abort() { export function abort() {
controller?.abort(STALE_REACTION); controller?.abort(STALE_REACTION);

@ -1,7 +1,7 @@
/** @import { Component } from '#server' */ /** @import { Component } from '#server' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { on_destroy } from './index.js'; import { on_destroy } from './index.js';
import * as e from '../shared/errors.js'; import * as e from './errors.js';
/** @type {Component | null} */ /** @type {Component | null} */
export var current_component = null; export var current_component = null;

@ -5,7 +5,7 @@ import {
is_tag_valid_with_parent is_tag_valid_with_parent
} from '../../html-tree-validation.js'; } from '../../html-tree-validation.js';
import { current_component } from './context.js'; import { current_component } from './context.js';
import { invalid_snippet_arguments } from '../shared/errors.js'; import * as e from './errors.js';
import { HeadPayload, Payload } from './payload.js'; import { HeadPayload, Payload } from './payload.js';
/** /**
@ -102,6 +102,6 @@ export function validate_snippet_args(payload) {
// for some reason typescript consider the type of payload as never after the first instanceof // for some reason typescript consider the type of payload as never after the first instanceof
!(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload)
) { ) {
invalid_snippet_arguments(); e.invalid_snippet_arguments();
} }
} }

@ -1,6 +1,6 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
export * from '../shared/errors.js';
/** /**
* `%name%(...)` is not available on the server * `%name%(...)` is not available on the server

@ -1,5 +1,3 @@
/** @import { TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { is_void } from '../../utils.js'; import { is_void } from '../../utils.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';

@ -4,8 +4,8 @@ import { user_pre_effect } from '../internal/client/reactivity/effects.js';
import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js';
import { hydrate, mount, unmount } from '../internal/client/render.js'; import { hydrate, mount, unmount } from '../internal/client/render.js';
import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js';
import { lifecycle_outside_component } from '../internal/shared/errors.js';
import { define_property, is_array } from '../internal/shared/utils.js'; import { define_property, is_array } from '../internal/shared/utils.js';
import * as e from '../internal/client/errors.js';
import * as w from '../internal/client/warnings.js'; import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js'; import { FILENAME } from '../constants.js';
@ -245,7 +245,7 @@ export function handlers(...handlers) {
export function createBubbler() { export function createBubbler() {
const active_component_context = component_context; const active_component_context = component_context;
if (active_component_context === null) { if (active_component_context === null) {
lifecycle_outside_component('createBubbler'); e.lifecycle_outside_component('createBubbler');
} }
return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => { return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => {

@ -428,7 +428,7 @@ export function is_mathml(name) {
return MATHML_ELEMENTS.includes(name); return MATHML_ELEMENTS.includes(name);
} }
export const STATE_CREATION_RUNES = /** @type {const} */ ([ const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state', '$state',
'$state.raw', '$state.raw',
'$derived', '$derived',

@ -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.35.5'; export const VERSION = '5.35.7';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -9,6 +9,6 @@ export default test({
flushSync(); flushSync();
assert.deepEqual(logs, ['error caught']); assert.deepEqual(logs, ['error caught']);
assert.htmlEqual(target.innerHTML, `<div>Fallback!</div><button>+</button>`); assert.htmlEqual(target.innerHTML, `<div>oh no!</div><button>+</button>`);
} }
}); });

@ -1,6 +1,6 @@
<script> <script>
function throw_error() { function throw_error() {
throw new Error('test') throw new Error('oh no!')
} }
let count = $state(0); let count = $state(0);
@ -9,8 +9,8 @@
<svelte:boundary onerror={(e) => console.log('error caught')}> <svelte:boundary onerror={(e) => console.log('error caught')}>
{count > 0 ? throw_error() : null} {count > 0 ? throw_error() : null}
{#snippet failed()} {#snippet failed(e)}
<div>Fallback!</div> <div>{e.message}</div>
{/snippet} {/snippet}
</svelte:boundary> </svelte:boundary>

@ -1 +1,6 @@
<div autofocus></div> <div autofocus></div>
<dialog autofocus>
</dialog>
<dialog>
<input autofocus>
</dialog>

Loading…
Cancel
Save