pull/16280/head
Rich Harris 3 months ago
commit 8262c2b06e

@ -1,5 +0,0 @@
---
'svelte': patch
---
chore: simplify props

@ -1,5 +0,0 @@
---
'svelte': minor
---
feat: add `getAbortSignal()`

@ -1,5 +1,35 @@
# svelte # svelte
## 5.35.3
### Patch Changes
- fix: account for mounting when `select_option` in `attribute_effect` ([#16309](https://github.com/sveltejs/svelte/pull/16309))
- fix: do not proxify the value assigned to a derived ([#16302](https://github.com/sveltejs/svelte/pull/16302))
## 5.35.2
### Patch Changes
- fix: bump esrap ([#16295](https://github.com/sveltejs/svelte/pull/16295))
## 5.35.1
### Patch Changes
- feat: add parent hierarchy to `__svelte_meta` objects ([#16255](https://github.com/sveltejs/svelte/pull/16255))
## 5.35.0
### Minor Changes
- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266))
### Patch Changes
- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270))
## 5.34.9 ## 5.34.9
### 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.34.9", "version": "5.35.3",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -171,7 +171,7 @@
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"esm-env": "^1.2.1", "esm-env": "^1.2.1",
"esrap": "^2.0.0", "esrap": "^2.1.0",
"is-reference": "^3.0.3", "is-reference": "^3.0.3",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.11", "magic-string": "^0.30.11",

@ -137,6 +137,7 @@ function build_assignment(operator, left, right, context) {
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' && binding.kind !== 'bindable_prop' &&
binding.kind !== 'raw_state' && binding.kind !== 'raw_state' &&
binding.kind !== 'derived' &&
binding.kind !== 'store_sub' && binding.kind !== 'store_sub' &&
context.state.analysis.runes && context.state.analysis.runes &&
should_proxy(right, context.state.scope) && should_proxy(right, context.state.scope) &&

@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { create_derived } from '../utils.js'; import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.AwaitBlock} node * @param {AST.AwaitBlock} node
@ -54,7 +54,7 @@ export function AwaitBlock(node, context) {
} }
context.state.init.push( context.state.init.push(
b.stmt( add_svelte_meta(
b.call( b.call(
'$.await', '$.await',
context.state.node, context.state.node,
@ -64,7 +64,9 @@ export function AwaitBlock(node, context) {
: b.null, : b.null,
then_block, then_block,
catch_block catch_block
) ),
node,
'await'
) )
); );
} }

@ -13,7 +13,7 @@ import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js'; import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { get_value } from './shared/declarations.js'; import { get_value } from './shared/declarations.js';
import { build_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.EachBlock} node * @param {AST.EachBlock} node
@ -337,7 +337,7 @@ export function EachBlock(node, context) {
); );
} }
context.state.init.push(b.stmt(b.call('$.each', ...args))); context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each'));
} }
/** /**

@ -2,7 +2,7 @@
/** @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_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.IfBlock} node * @param {AST.IfBlock} node
@ -74,7 +74,7 @@ export function IfBlock(node, context) {
args.push(b.id('$$elseif')); args.push(b.id('$$elseif'));
} }
statements.push(b.stmt(b.call('$.if', ...args))); statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if'));
context.state.init.push(b.block(statements)); context.state.init.push(b.block(statements));
} }

@ -2,7 +2,7 @@
/** @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_expression } from './shared/utils.js'; import { build_expression, add_svelte_meta } from './shared/utils.js';
/** /**
* @param {AST.KeyBlock} node * @param {AST.KeyBlock} node
@ -15,6 +15,10 @@ export function KeyBlock(node, context) {
const body = /** @type {Expression} */ (context.visit(node.fragment)); const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push( context.state.init.push(
b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) add_svelte_meta(
b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)),
node,
'key'
)
); );
} }

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */ /** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js'; import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js'; import { add_svelte_meta, build_expression } from './shared/utils.js';
/** /**
* @param {AST.RenderTag} node * @param {AST.RenderTag} node
@ -48,16 +48,22 @@ export function RenderTag(node, context) {
} }
context.state.init.push( context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
); );
} else { } else {
context.state.init.push( context.state.init.push(
b.stmt( add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function, snippet_function,
context.state.node, context.state.node,
...args ...args
) ),
node,
'render'
) )
); );
} }

@ -4,7 +4,12 @@
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 { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; import {
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';
@ -483,7 +488,8 @@ export function build_component(node, component_name, context) {
); );
} else { } else {
context.state.template.push_comment(); context.state.template.push_comment();
statements.push(b.stmt(fn(anchor)));
statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name }));
} }
return statements.length > 1 ? b.block(statements) : statements[0]; return statements.length > 1 ? b.block(statements) : statements[0];

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -7,7 +7,7 @@ import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
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 } from '../../../../../state.js'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js';
import { build_getter, create_derived } from '../../utils.js'; import { build_getter, create_derived } from '../../utils.js';
/** /**
@ -424,3 +424,34 @@ export function build_expression(context, expression, metadata, state = context.
return sequence; return sequence;
} }
/**
* Wraps a statement/expression with dev stack tracking in dev mode
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {{ start?: number }} node - AST node for location info
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
*/
export function add_svelte_meta(expression, node, type, additional) {
if (!dev) {
return b.stmt(expression);
}
const location = node.start !== undefined && locator(node.start);
if (!location) {
return b.stmt(expression);
}
return b.stmt(
b.call(
'$.add_svelte_meta',
b.arrow([], expression),
b.literal(type),
b.id(component_name),
b.literal(location.line),
b.literal(location.column),
additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v))))
)
);
}

@ -16,6 +16,9 @@ export let warnings = [];
*/ */
export let filename; export let filename;
/**
* The name of the component that is used in the `export default function ...` statement.
*/
export let component_name = '<unknown>'; export let component_name = '<unknown>';
/** /**

@ -15,18 +15,18 @@ export const DESTROYED = 1 << 14;
export const EFFECT_RAN = 1 << 15; export const EFFECT_RAN = 1 << 15;
/** 'Transparent' effects do not create a transition boundary */ /** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 16; export const EFFECT_TRANSPARENT = 1 << 16;
export const INSPECT_EFFECT = 1 << 18; export const INSPECT_EFFECT = 1 << 17;
export const HEAD_EFFECT = 1 << 19; export const HEAD_EFFECT = 1 << 18;
export const EFFECT_HAS_DERIVED = 1 << 20; export const EFFECT_PRESERVED = 1 << 19;
export const EFFECT_IS_UPDATING = 1 << 21; export const EFFECT_IS_UPDATING = 1 << 20;
export const USER_EFFECT = 1 << 22; export const USER_EFFECT = 1 << 21;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const LEGACY_PROPS = Symbol('legacy props'); export const LEGACY_PROPS = Symbol('legacy props');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');
export const PROXY_PATH_SYMBOL = Symbol('proxy path'); export const PROXY_PATH_SYMBOL = Symbol('proxy path');
// allow users to ignore aborted signal errors if `reason.stale` /** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */
export const STALE_REACTION = new (class StaleReactionError extends Error { export const STALE_REACTION = new (class StaleReactionError extends Error {
name = 'StaleReactionError'; name = 'StaleReactionError';
message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; message = 'The reaction that called `getAbortSignal()` was re-run or destroyed';

@ -1,4 +1,4 @@
/** @import { ComponentContext } 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 { lifecycle_outside_component } from '../shared/errors.js';
@ -11,6 +11,7 @@ import {
} from './runtime.js'; } from './runtime.js';
import { create_user_effect, teardown } from './reactivity/effects.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';
/** @type {ComponentContext | null} */ /** @type {ComponentContext | null} */
export let component_context = null; export let component_context = null;
@ -20,6 +21,43 @@ export function set_component_context(context) {
component_context = context; component_context = context;
} }
/** @type {DevStackEntry | null} */
export let dev_stack = null;
/** @param {DevStackEntry | null} stack */
export function set_dev_stack(stack) {
dev_stack = stack;
}
/**
* Execute a callback with a new dev stack entry
* @param {() => any} callback - Function to execute
* @param {DevStackEntry['type']} type - Type of block/component
* @param {any} component - Component function
* @param {number} line - Line number
* @param {number} column - Column number
* @param {Record<string, any>} [additional] - Any additional properties to add to the dev stack entry
* @returns {any}
*/
export function add_svelte_meta(callback, type, component, line, column, additional) {
const parent = dev_stack;
dev_stack = {
type,
file: component[FILENAME],
line,
column,
parent,
...additional
};
try {
return callback();
} finally {
dev_stack = parent;
}
}
/** /**
* The current component function. Different from current component context: * The current component function. Different from current component context:
* ```html * ```html

@ -2,6 +2,7 @@
import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants';
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js'; import { hydrating } from '../dom/hydration.js';
import { dev_stack } from '../context.js';
/** /**
* @param {any} fn * @param {any} fn
@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) {
function assign_location(element, filename, location) { function assign_location(element, filename, location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { element.__svelte_meta = {
parent: dev_stack,
loc: { file: filename, line: location[0], column: location[1] } loc: { file: filename, line: location[0], column: location[1] }
}; };

@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js';
import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js';
import { import {
component_context, component_context,
dev_stack,
is_runes, is_runes,
set_component_context, set_component_context,
set_dev_current_component_function set_dev_current_component_function,
set_dev_stack
} from '../../context.js'; } from '../../context.js';
const PENDING = 0; const PENDING = 0;
@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */ /** @type {any} */
var component_function = DEV ? component_context?.function : null; var component_function = DEV ? component_context?.function : null;
var dev_original_stack = DEV ? dev_stack : null;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */ /** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED; var input = UNINITIALIZED;
@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
set_active_effect(effect); set_active_effect(effect);
set_active_reaction(effect); // TODO do we need both? set_active_reaction(effect); // TODO do we need both?
set_component_context(active_component_context); set_component_context(active_component_context);
if (DEV) set_dev_current_component_function(component_function); if (DEV) {
set_dev_current_component_function(component_function);
set_dev_stack(dev_original_stack);
}
} }
try { try {
@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
} }
} finally { } finally {
if (restore) { if (restore) {
if (DEV) set_dev_current_component_function(null); if (DEV) {
set_dev_current_component_function(null);
set_dev_stack(null);
}
set_component_context(null); set_component_context(null);
set_active_reaction(null); set_active_reaction(null);
set_active_effect(null); set_active_effect(null);

@ -1,6 +1,5 @@
/** @import { Effect, TemplateNode, } from '#client' */ /** @import { Effect, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants';
import { component_context, set_component_context } from '../../context.js'; import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js'; import { invoke_error_boundary } from '../../error-handling.js';
import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
@ -21,116 +20,170 @@ import {
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
/** /**
* @param {Effect} boundary * @typedef {{
* @param {() => void} fn * onerror?: (error: unknown, reset: () => void) => void;
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void;
* }} BoundaryProps
*/ */
function with_boundary(boundary, fn) {
var previous_effect = active_effect; var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT;
var previous_reaction = active_reaction;
var previous_ctx = component_context;
set_active_effect(boundary);
set_active_reaction(boundary);
set_component_context(boundary.ctx);
try {
fn();
} finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
/** /**
* @param {TemplateNode} node * @param {TemplateNode} node
* @param {{ * @param {BoundaryProps} props
* onerror?: (error: unknown, reset: () => void) => void, * @param {((anchor: Node) => void)} children
* failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void
* }} props
* @param {((anchor: Node) => void)} boundary_fn
* @returns {void} * @returns {void}
*/ */
export function boundary(node, props, boundary_fn) { export function boundary(node, props, children) {
var anchor = node; new Boundary(node, props, children);
}
export class Boundary {
/** @type {TemplateNode} */
#anchor;
/** @type {TemplateNode} */
#hydrate_open;
/** @type {BoundaryProps} */
#props;
/** @type {((anchor: Node) => void)} */
#children;
/** @type {Effect} */ /** @type {Effect} */
var boundary_effect; #effect;
block(() => {
var boundary = /** @type {Effect} */ (active_effect);
var hydrate_open = hydrate_node;
var is_creating_fallback = false;
// We re-use the effect's fn property to avoid allocation of an additional field
boundary.fn = (/** @type {unknown}} */ error) => {
var onerror = props.onerror;
let failed = props.failed;
// If we have nothing to capture the error, or if we hit an error while
// rendering the fallback, re-throw for another boundary to handle
if ((!onerror && !failed) || is_creating_fallback) {
throw error;
}
var reset = () => { /** @type {Effect | null} */
pause_effect(boundary_effect); #main_effect = null;
with_boundary(boundary, () => { /** @type {Effect | null} */
is_creating_fallback = false; #failed_effect = null;
boundary_effect = branch(() => boundary_fn(anchor));
});
};
var previous_reaction = active_reaction; #is_creating_fallback = false;
try { /**
set_active_reaction(null); * @param {TemplateNode} node
onerror?.(error, reset); * @param {BoundaryProps} props
} finally { * @param {((anchor: Node) => void)} children
set_active_reaction(previous_reaction); */
constructor(node, props, children) {
this.#anchor = node;
this.#props = props;
this.#children = children;
this.#hydrate_open = hydrate_node;
this.#effect = block(() => {
/** @type {Effect} */ (active_effect).b = this;
if (hydrating) {
hydrate_next();
} }
if (boundary_effect) { try {
destroy_effect(boundary_effect); this.#main_effect = branch(() => children(this.#anchor));
} else if (hydrating) { } catch (error) {
set_hydrate_node(hydrate_open); this.error(error);
next();
set_hydrate_node(remove_nodes());
} }
}, flags);
if (failed) { if (hydrating) {
// Render the `failed` snippet in a microtask this.#anchor = hydrate_node;
queue_micro_task(() => { }
with_boundary(boundary, () => { }
is_creating_fallback = true;
/**
try { * @param {() => Effect | null} fn
boundary_effect = branch(() => { */
failed( #run(fn) {
anchor, var previous_effect = active_effect;
() => error, var previous_reaction = active_reaction;
() => reset var previous_ctx = component_context;
);
}); set_active_effect(this.#effect);
} catch (error) { set_active_reaction(this.#effect);
invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); set_component_context(this.#effect.ctx);
}
try {
is_creating_fallback = false; return fn();
}); } finally {
set_active_effect(previous_effect);
set_active_reaction(previous_reaction);
set_component_context(previous_ctx);
}
}
/** @param {unknown} error */
error(error) {
var onerror = this.#props.onerror;
let failed = this.#props.failed;
const reset = () => {
if (this.#failed_effect !== null) {
pause_effect(this.#failed_effect, () => {
this.#failed_effect = null;
}); });
} }
this.#main_effect = this.#run(() => {
this.#is_creating_fallback = false;
return branch(() => this.#children(this.#anchor));
});
}; };
if (hydrating) { // If we have nothing to capture the error, or if we hit an error while
hydrate_next(); // rendering the fallback, re-throw for another boundary to handle
if (this.#is_creating_fallback || (!onerror && !failed)) {
throw error;
}
var previous_reaction = active_reaction;
try {
set_active_reaction(null);
onerror?.(error, reset);
} finally {
set_active_reaction(previous_reaction);
} }
boundary_effect = branch(() => boundary_fn(anchor)); if (this.#main_effect) {
}, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); destroy_effect(this.#main_effect);
this.#main_effect = null;
}
if (hydrating) { if (this.#failed_effect) {
anchor = hydrate_node; destroy_effect(this.#failed_effect);
this.#failed_effect = null;
}
if (hydrating) {
set_hydrate_node(this.#hydrate_open);
next();
set_hydrate_node(remove_nodes());
}
if (failed) {
queue_micro_task(() => {
this.#failed_effect = this.#run(() => {
this.#is_creating_fallback = true;
try {
return branch(() => {
failed(
this.#anchor,
() => error,
() => reset
);
});
} catch (error) {
invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent));
return null;
} finally {
this.#is_creating_fallback = false;
}
});
});
}
} }
} }

@ -18,7 +18,7 @@ import {
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js'; import { current_each_item, set_current_each_item } from './each.js';
import { active_effect } from '../../runtime.js'; import { active_effect } from '../../runtime.js';
import { component_context } from '../../context.js'; import { component_context, dev_stack } from '../../context.js';
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { assign_nodes } from '../template.js'; import { assign_nodes } from '../template.js';
@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
if (DEV && location) { if (DEV && location) {
// @ts-expect-error // @ts-expect-error
element.__svelte_meta = { element.__svelte_meta = {
parent: dev_stack,
loc: { loc: {
file: filename, file: filename,
line: location[0], line: location[0],

@ -491,7 +491,7 @@ export function attribute_effect(
var current = set_attributes(element, prev, next, css_hash, skip_warning); var current = set_attributes(element, prev, next, css_hash, skip_warning);
if (inited && is_select && 'value' in next) { if (inited && is_select && 'value' in next) {
select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); select_option(/** @type {HTMLSelectElement} */ (element), next.value);
} }
for (let symbol of Object.getOwnPropertySymbols(effects)) { for (let symbol of Object.getOwnPropertySymbols(effects)) {
@ -516,7 +516,7 @@ export function attribute_effect(
var select = /** @type {HTMLSelectElement} */ (element); var select = /** @type {HTMLSelectElement} */ (element);
effect(() => { effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value); select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
init_select(select); init_select(select);
}); });
} }

@ -9,9 +9,9 @@ import * as w from '../../../warnings.js';
* @template V * @template V
* @param {HTMLSelectElement} select * @param {HTMLSelectElement} select
* @param {V} value * @param {V} value
* @param {boolean} [mounting] * @param {boolean} mounting
*/ */
export function select_option(select, value, mounting) { export function select_option(select, value, mounting = false) {
if (select.multiple) { if (select.multiple) {
// If value is null or undefined, keep the selection as is // If value is null or undefined, keep the selection as is
if (value == undefined) { if (value == undefined) {

@ -1,4 +1,5 @@
/** @import { Effect } from '#client' */ /** @import { Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js'; import { FILENAME } from '../../constants.js';
import { is_firefox } from './dom/operations.js'; import { is_firefox } from './dom/operations.js';
@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) {
while (effect !== null) { while (effect !== null) {
if ((effect.f & BOUNDARY_EFFECT) !== 0) { if ((effect.f & BOUNDARY_EFFECT) !== 0) {
try { try {
// @ts-expect-error /** @type {Boundary} */ (effect.b).error(error);
effect.fn(error);
return; return;
} catch {} } catch {}
} }

@ -1,6 +1,6 @@
export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { createAttachmentKey as attachment } from '../../attachments/index.js';
export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js';
export { push, pop } from './context.js'; export { push, pop, add_svelte_meta } from './context.js';
export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js';
export { cleanup_styles } from './dev/css.js'; export { cleanup_styles } from './dev/css.js';
export { add_locations } from './dev/elements.js'; export { add_locations } from './dev/elements.js';

@ -1,6 +1,6 @@
/** @import { Derived, Effect } from '#client' */ /** @import { Derived, Effect } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '#client/constants'; import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants';
import { import {
active_reaction, active_reaction,
active_effect, active_effect,
@ -38,7 +38,7 @@ export function derived(fn) {
} else { } else {
// Since deriveds are evaluated lazily, any effects created inside them are // Since deriveds are evaluated lazily, any effects created inside them are
// created too late to ensure that the parent effect is added to the tree // created too late to ensure that the parent effect is added to the tree
active_effect.f |= EFFECT_HAS_DERIVED; active_effect.f |= EFFECT_PRESERVED;
} }
/** @type {Derived<V>} */ /** @type {Derived<V>} */

@ -31,7 +31,7 @@ import {
INSPECT_EFFECT, INSPECT_EFFECT,
HEAD_EFFECT, HEAD_EFFECT,
MAYBE_DIRTY, MAYBE_DIRTY,
EFFECT_HAS_DERIVED, EFFECT_PRESERVED,
BOUNDARY_EFFECT, BOUNDARY_EFFECT,
STALE_REACTION, STALE_REACTION,
USER_EFFECT USER_EFFECT
@ -42,7 +42,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js'; import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js'; import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js'; import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js'; import { component_context, dev_current_component_function, dev_stack } from '../context.js';
/** /**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune * @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -105,6 +105,7 @@ function create_effect(type, fn, sync, push = true) {
last: null, last: null,
next: null, next: null,
parent, parent,
b: parent && parent.b,
prev: null, prev: null,
teardown: null, teardown: null,
transitions: null, transitions: null,
@ -136,7 +137,7 @@ function create_effect(type, fn, sync, push = true) {
effect.first === null && effect.first === null &&
effect.nodes_start === null && effect.nodes_start === null &&
effect.teardown === null && effect.teardown === null &&
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; (effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0;
if (!inert && push) { if (!inert && push) {
if (parent !== null) { if (parent !== null) {
@ -366,7 +367,11 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags * @param {number} flags
*/ */
export function block(fn, flags = 0) { export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
} }
/** /**

@ -11,7 +11,7 @@ import {
untrack, untrack,
increment_write_version, increment_write_version,
update_effect, update_effect,
reaction_sources, source_ownership,
check_dirtiness, check_dirtiness,
untracking, untracking,
is_destroying_effect, is_destroying_effect,
@ -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 &&
!(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) !(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source))
) { ) {
e.state_unsafe_mutation(); e.state_unsafe_mutation();
} }

@ -1,4 +1,11 @@
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; import type {
ComponentContext,
DevStackEntry,
Equals,
TemplateNode,
TransitionManager
} from '#client';
import type { Boundary } from '../dom/blocks/boundary';
export interface Signal { export interface Signal {
/** Flags bitmask */ /** Flags bitmask */
@ -78,8 +85,12 @@ export interface Effect extends Reaction {
last: null | Effect; last: null | Effect;
/** Parent effect */ /** Parent effect */
parent: Effect | null; parent: Effect | null;
/** The boundary this effect belongs to */
b: Boundary | null;
/** Dev only */ /** Dev only */
component_function?: any; component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
dev_stack?: DevStackEntry | null;
} }
export type Source<V = unknown> = Value<V>; export type Source<V = unknown> = Value<V>;

@ -35,12 +35,13 @@ import { tracing_expressions, get_stack } from './dev/tracing.js';
import { import {
component_context, component_context,
dev_current_component_function, dev_current_component_function,
dev_stack,
is_runes, is_runes,
set_component_context, set_component_context,
set_dev_current_component_function set_dev_current_component_function,
set_dev_stack
} from './context.js'; } from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js'; import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
let is_flushing = false; let is_flushing = false;
@ -86,17 +87,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 | [active_reaction: Reaction, sources: Source[]]} * @type {null | { reaction: Reaction, sources: Source[] }}
*/ */
export let reaction_sources = null; export let source_ownership = 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 (reaction_sources === null) { if (source_ownership === null) {
reaction_sources = [active_reaction, [value]]; source_ownership = { reaction: active_reaction, sources: [value] };
} else { } else {
reaction_sources[1].push(value); source_ownership.sources.push(value);
} }
} }
} }
@ -235,7 +236,12 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true)
for (var i = 0; i < reactions.length; i++) { for (var i = 0; i < reactions.length; i++) {
var reaction = reactions[i]; var reaction = reactions[i];
if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue; if (
source_ownership?.reaction === active_reaction &&
source_ownership.sources.includes(signal)
) {
continue;
}
if ((reaction.f & DERIVED) !== 0) { if ((reaction.f & DERIVED) !== 0) {
schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false);
@ -257,7 +263,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 = reaction_sources; var previous_reaction_sources = source_ownership;
var previous_component_context = component_context; var previous_component_context = component_context;
var previous_untracking = untracking; var previous_untracking = untracking;
@ -270,7 +276,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;
reaction_sources = null; source_ownership = null;
set_component_context(reaction.ctx); set_component_context(reaction.ctx);
untracking = false; untracking = false;
read_version++; read_version++;
@ -358,7 +364,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;
reaction_sources = previous_reaction_sources; source_ownership = previous_reaction_sources;
set_component_context(previous_component_context); set_component_context(previous_component_context);
untracking = previous_untracking; untracking = previous_untracking;
@ -445,6 +451,9 @@ export function update_effect(effect) {
if (DEV) { if (DEV) {
var previous_component_fn = dev_current_component_function; var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function); set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
} }
try { try {
@ -479,6 +488,7 @@ export function update_effect(effect) {
if (DEV) { if (DEV) {
set_dev_current_component_function(previous_component_fn); set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
} }
} }
} }
@ -747,7 +757,10 @@ 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 (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) { if (
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;

@ -187,4 +187,13 @@ export type SourceLocation =
| [line: number, column: number] | [line: number, column: number]
| [line: number, column: number, SourceLocation[]]; | [line: number, column: number, SourceLocation[]];
export interface DevStackEntry {
file: string;
type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
line: number;
column: number;
parent: DevStackEntry | null;
componentTag?: string;
}
export * from './reactivity/types'; export * from './reactivity/types';

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

@ -0,0 +1,9 @@
import { ok, test } from '../../test';
export default test({
async test({ assert, target, instance }) {
const select = target.querySelector('select');
ok(select);
assert.equal(select.selectedIndex, 1);
}
});

@ -0,0 +1,8 @@
<script>
let others = {onclick: ()=> {}}
</script>
<select {...others}>
<option>o1</option>
<option selected>o2</option>
</select>

@ -0,0 +1,186 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['client', 'hydrate'],
compileOptions: {
dev: true
},
html: `
<p>no parent</p>
<button>toggle</button>
<p>if</p>
<p>each</p>
<p>loading</p>
<p>key</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
`,
async test({ target, assert }) {
function check() {
const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] =
target.querySelectorAll('p');
// @ts-expect-error
assert.deepEqual(main.__svelte_meta.parent, null);
// @ts-expect-error
assert.deepEqual(if_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'if',
line: 12,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(each.__svelte_meta.parent, {
file: 'main.svelte',
type: 'each',
line: 16,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(await_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'await',
line: 20,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(key.__svelte_meta.parent, {
file: 'main.svelte',
type: 'key',
line: 26,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child1.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 30,
column: 0,
parent: null
});
// @ts-expect-error
assert.deepEqual(child2.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 33,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 32,
column: 0,
parent: null
}
}
});
// @ts-expect-error
assert.deepEqual(child3.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 38,
column: 2,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 37,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 36,
column: 0,
parent: null
}
}
}
}
});
// @ts-expect-error
assert.deepEqual(child4.__svelte_meta.parent, {
file: 'passthrough.svelte',
type: 'render',
line: 8,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'if',
line: 7,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 43,
column: 1,
parent: {
file: 'main.svelte',
type: 'if',
line: 42,
column: 0,
parent: null
}
}
}
});
// @ts-expect-error
assert.deepEqual(dynamic.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'x.y',
line: 50,
column: 0,
parent: null
});
}
await tick();
check();
// Test that stack is kept when re-rendering
const button = target.querySelector('button');
button?.click();
await tick();
button?.click();
await tick();
check();
}
});

@ -0,0 +1,50 @@
<script>
import Child from "./child.svelte";
import Passthrough from "./passthrough.svelte";
let x = { y: Child }
let key = 'test';
let show = $state(true);
</script>
<p>no parent</p>
<button onclick={() => show = !show}>toggle</button>
{#if true}
<p>if</p>
{/if}
{#each [1]}
<p>each</p>
{/each}
{#await Promise.resolve()}
<p>loading</p>
{:then}
<p>await</p>
{/await}
{#key key}
<p>key</p>
{/key}
<Child />
<Passthrough>
<Child />
</Passthrough>
<Passthrough>
<Passthrough>
<Child />
</Passthrough>
</Passthrough>
{#if show}
<Passthrough>
{#snippet named()}
<p>hi</p>
{/snippet}
</Passthrough>
{/if}
<x.y />

@ -0,0 +1,9 @@
<script>
let { children, named } = $props();
</script>
{@render children?.()}
{#if true}
{@render named?.()}
{/if}

@ -0,0 +1,23 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const [btn1, btn2, btn3] = target.getElementsByTagName('button');
const [div] = target.getElementsByTagName('div');
flushSync(() => btn1.click());
assert.equal(div.textContent, '1');
flushSync(() => btn2.click());
assert.equal(div.textContent, '1');
flushSync(() => btn3.click());
assert.equal(div.textContent, '2');
flushSync(() => btn1.click());
assert.equal(div.textContent, '2');
flushSync(() => btn2.click());
assert.equal(div.textContent, '2');
flushSync(() => btn3.click());
assert.equal(div.textContent, '3');
}
});

@ -0,0 +1,9 @@
<script>
let count = $derived({ value: 1 });
</script>
<button onclick={() => count.value++}>mutate</button>
<button onclick={() => count = count}>assign self</button>
<button onclick={() => count = {...count} }>assign copy</button>
<div>{count.value}</div>

@ -87,8 +87,8 @@ importers:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1 version: 1.2.1
esrap: esrap:
specifier: ^2.0.0 specifier: ^2.1.0
version: 2.0.0 version: 2.1.0
is-reference: is-reference:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@ -1261,8 +1261,8 @@ packages:
resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
esrap@2.0.0: esrap@2.1.0:
resolution: {integrity: sha512-zhw1TDqno99Ld5wOpe0t47rzVyxfGc1fvxNzPsqk4idUf5dcAePkAyfTceLJaSTytjiWDu26S5tI+grjvymXJA==} resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==}
esrecurse@4.3.0: esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@ -3622,7 +3622,7 @@ snapshots:
dependencies: dependencies:
estraverse: 5.3.0 estraverse: 5.3.0
esrap@2.0.0: esrap@2.1.0:
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/sourcemap-codec': 1.5.0

Loading…
Cancel
Save