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:

@ -1,5 +1,23 @@
# 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
### Patch Changes

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

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

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

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

@ -9,7 +9,7 @@ import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_attribute, is_custom_element_node } from '../../nodes.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 { mark_subtree_dynamic } from './shared/fragment.js';

@ -2,7 +2,7 @@
/** @import { Context } from '../types' */
import { NAMESPACE_MATHML, NAMESPACE_SVG } from '../../../../constants.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 { 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 { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
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
@ -22,7 +22,7 @@ export function SlotElement(node, context) {
/** @type {ExpressionStatement[]} */
const lets = [];
let is_default = true;
const memoizer = new Memoizer();
let name = b.literal('default');
@ -33,12 +33,11 @@ export function SlotElement(node, context) {
const { value, has_state } = build_attribute_value(
attribute.value,
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') {
name = /** @type {Literal} */ (value);
is_default = false;
} else if (attribute.name !== 'slot') {
if (has_state) {
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
context.state.init.push(...lets);
/** @type {Statement[]} */
const statements = memoizer.deriveds(context.state.analysis.runes);
const props_expression =
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.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call(
'$.slot',
context.state.node,
b.id('$$props'),
name,
props_expression,
fallback
statements.push(
b.stmt(b.call('$.slot', 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 { get_attribute_chunks, object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
import {
build_bind_this,
memoize_expression,
validate_binding,
add_svelte_meta
} from '../shared/utils.js';
import { add_svelte_meta, build_bind_this, Memoizer, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
@ -48,6 +43,8 @@ export function build_component(node, component_name, context) {
/** @type {Record<string, Expression[]>} */
const events = {};
const memoizer = new Memoizer();
/** @type {Property[]} */
const custom_css_props = [];
@ -133,15 +130,13 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
let value = expression;
if (attribute.metadata.expression.has_call) {
const id = b.id(context.state.scope.generate('spread_element'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value))));
value = b.call('$.get', id);
}
props_and_spreads.push(b.thunk(value));
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_call
? b.call('$.get', memoizer.add(expression))
: expression
)
);
} else {
props_and_spreads.push(expression);
}
@ -150,10 +145,10 @@ export function build_component(node, component_name, context) {
custom_css_props.push(
b.init(
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
metadata.has_call ? memoize_expression(context.state, value) : value
).value
return metadata.has_call ? b.call('$.get', memoizer.add(value)) : value;
}).value
)
);
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) {
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 }));
}
memoizer.apply();
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 is_reference from 'is-reference';
import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } 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);
}
import { build_getter } from '../../utils.js';
/**
* 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_bidirectional_control_characters =
/[\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');
/** Includes `BigInt` */
export const NUMBER = Symbol('number');
export const STRING = Symbol('string');
export const FUNCTION = Symbol('string');
const NUMBER = Symbol('number');
const STRING = Symbol('string');
const FUNCTION = Symbol('string');
/** @type {Record<string, [type: NUMBER | STRING | UNKNOWN, fn?: Function]>} */
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 {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
* @returns {Validator}

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

@ -1,15 +1,8 @@
/** @import { ComponentContext, DevStackEntry } from '#client' */
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 {
active_effect,
active_reaction,
set_active_effect,
set_active_reaction
} from './runtime.js';
import { create_user_effect, teardown } from './reactivity/effects.js';
import { create_user_effect } from './reactivity/effects.js';
import { legacy_mode_flag } from '../flags/index.js';
import { FILENAME } from '../../constants.js';
@ -205,7 +198,7 @@ export function is_runes() {
*/
function get_or_init_context_map(name) {
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));

@ -1,15 +1,16 @@
import { invalid_snippet_arguments } from '../../shared/errors.js';
import * as e from '../errors.js';
/**
* @param {Node} anchor
* @param {...(()=>any)[]} args
*/
export function validate_snippet_args(anchor, ...args) {
if (typeof anchor !== 'object' || !(anchor instanceof Node)) {
invalid_snippet_arguments();
e.invalid_snippet_arguments();
}
for (let arg of args) {
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 { active_effect } from './runtime.js';
const adjustments = new WeakMap();
/**
* @param {unknown} error
*/
export function handle_error(error) {
var effect = /** @type {Effect} */ (active_effect);
if (DEV && error instanceof Error) {
adjust_error(error, effect);
if (DEV && error instanceof Error && !adjustments.has(error)) {
adjustments.set(error, get_adjustments(error, effect));
}
if ((effect.f & EFFECT_RAN) === 0) {
@ -48,21 +50,19 @@ export function invoke_error_boundary(error, effect) {
effect = effect.parent;
}
if (error instanceof Error) {
apply_adjustments(error);
}
throw error;
}
/** @type {WeakSet<Error>} */
const adjusted_errors = new WeakSet();
/**
* Add useful information to the error message/stack in development
* @param {Error} error
* @param {Effect} effect
*/
function adjust_error(error, effect) {
if (adjusted_errors.has(error)) return;
adjusted_errors.add(error);
function get_adjustments(error, effect) {
const message_descriptor = get_descriptor(error, 'message');
// 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;
}
define_property(error, 'message', {
value: error.message + `\n${component_stack}\n`
});
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', {
value: adjusted.message
});
if (error.stack) {
// Filter out internal modules
define_property(error, 'stack', {
value: error.stack
.split('\n')
.filter((line) => !line.includes('svelte/src/internal'))
.join('\n')
value: adjusted.stack
});
}
}

@ -2,6 +2,8 @@
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
* @returns {never}

@ -1,6 +1,13 @@
/** @import { Source } from '#client' */
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 {
array_prototype,
get_descriptor,
@ -41,7 +48,7 @@ export function proxy(value) {
var version = source(0);
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
@ -49,13 +56,23 @@ export function proxy(value) {
* @param {() => T} fn
*/
var with_parent = (fn) => {
var previous_reaction = active_reaction;
set_active_reaction(reaction);
if (update_version === parent_version) {
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();
set_active_reaction(previous_reaction);
set_active_reaction(reaction);
set_update_version(version);
return result;
};

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

@ -11,8 +11,8 @@ import {
untrack,
increment_write_version,
update_effect,
source_ownership,
check_dirtiness,
current_sources,
is_dirty,
untracking,
is_destroying_effect,
push_reaction_value
@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) {
(!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) &&
is_runes() &&
(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();
}
@ -218,7 +218,7 @@ export function internal_set(source, value) {
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
if (is_dirty(effect)) {
update_effect(effect);
}
}

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

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

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

@ -5,7 +5,7 @@ import {
is_tag_valid_with_parent
} from '../../html-tree-validation.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';
/**
@ -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
!(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! */
export * from '../shared/errors.js';
/**
* `%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 * as w from './warnings.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 { hydrate, mount, unmount } from '../internal/client/render.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 * as e from '../internal/client/errors.js';
import * as w from '../internal/client/warnings.js';
import { DEV } from 'esm-env';
import { FILENAME } from '../constants.js';
@ -245,7 +245,7 @@ export function handlers(...handlers) {
export function createBubbler() {
const active_component_context = component_context;
if (active_component_context === null) {
lifecycle_outside_component('createBubbler');
e.lifecycle_outside_component('createBubbler');
}
return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => {

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

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

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

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

Loading…
Cancel
Save