Merge branch 'svelte-custom-renderer' into fix-interleaving

fix-interleaving
paoloricciuti 7 hours ago
commit 23856236f9

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent untracked derived reads from retaining disconnected dependencies

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: cancel deferred event listeners during cleanup

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: speed up parser interactions with Acorn or avoid them where possible

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: serialize input default values during server rendering

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: restore hydration state when custom element attribute updates throw

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: flush anything pending before invoking flushSync callback function

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve location information for `await` wrappers

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: keep a reactive assignment with no dependencies when `migrate` cannot turn it into a declaration

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't hang `migrate` on a declaration that shares a line with its script tag

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't warn about a redundant `link` role on `<area>` elements without an `href`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't overwrite an unchanged spread `value`, preserving incomplete number input

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: read batch-local array on each-block commit

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error at compile time when a declaration in a snippet redeclares one of its parameters

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error when using `let:` directives on a component with a `children` snippet

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ignore stale boundary reset callbacks

@ -1,5 +0,0 @@
---
'svelte': patch
---
perf: avoid regex matching in parser where possible

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle deferred transitions aborted before initialization

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent hydration mismatch recovery from being intercepted by error boundaries

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: preserve dynamic element connections during hydration

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: in non-async mode, only push variable to current_sources when active_reaction is updating

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes

@ -13,7 +13,7 @@ import {
sbench_create_4to1,
sbench_create_signals
} from './sbench.js';
import { fileURLToPath } from 'node:url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { create_test } from './util.js';
// This benchmark has been adapted from the js-reactivity-benchmark (https://github.com/milomg/js-reactivity-benchmark)
@ -39,7 +39,8 @@ for (const file of fs.readdirSync(`${dirname}/tests`)) {
const name = file.replace('.bench.js', '');
const module = await import(`${dirname}/tests/${file}`);
const module_url = pathToFileURL(path.join(dirname, 'tests', file));
const module = await import(module_url.href);
const { owned, unowned } = create_test(name, module.default);
reactivity_benchmarks.push(owned, unowned);

@ -25,12 +25,12 @@ export const wrapper_bench = {
// Do 3 loops to warm up JIT
for (let i = 0; i < 3; i++) {
render(App);
render(App).body;
}
return await fastest_test(10, () => {
for (let i = 0; i < 100; i++) {
render(App);
render(App).body;
}
});
}

@ -683,6 +683,12 @@ Cannot use `$$restProps` in runes mode
`let:` directive at invalid position
```
### let_directive_snippet_conflict
```
Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
```
### mixed_event_handler_syntaxes
```

@ -1,5 +1,37 @@
# svelte
## 5.57.1
### Patch Changes
- fix: cancel deferred event listeners during cleanup ([#18749](https://github.com/sveltejs/svelte/pull/18749))
- fix: preserve global CSS in components without scopable elements ([#18793](https://github.com/sveltejs/svelte/pull/18793))
- fix: reduce SSR render result garbage collection ([#18798](https://github.com/sveltejs/svelte/pull/18798))
- fix: resolve the fallback of an each block in the enclosing scope ([#18803](https://github.com/sveltejs/svelte/pull/18803))
- perf: speed up parser interactions with Acorn or avoid them where possible ([#18740](https://github.com/sveltejs/svelte/pull/18740))
- fix: prevent effect tree of batches from interfering with each other ([#18508](https://github.com/sveltejs/svelte/pull/18508))
- fix: serialize input default values during server rendering ([#18733](https://github.com/sveltejs/svelte/pull/18733))
- fix: remove `WAS_MARKED` flag in favor of `Set` ([#18127](https://github.com/sveltejs/svelte/pull/18127))
- fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR ([#18739](https://github.com/sveltejs/svelte/pull/18739))
- fix: make Object.hasOwn reactive for state proxy ownership changes ([#18838](https://github.com/sveltejs/svelte/pull/18838))
- fix: keep `$state.eager` when used as a variable initializer ([#18809](https://github.com/sveltejs/svelte/pull/18809))
- perf: avoid regex matching in parser where possible ([#18736](https://github.com/sveltejs/svelte/pull/18736))
- fix: in non-async mode, only push variable to current_sources when active_reaction is updating ([#18550](https://github.com/sveltejs/svelte/pull/18550))
- fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes ([#18765](https://github.com/sveltejs/svelte/pull/18765))
## 5.57.0
### Minor Changes

@ -283,6 +283,10 @@ The same applies to components:
> `let:` directive at invalid position
## let_directive_snippet_conflict
> Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax

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

@ -1223,6 +1223,16 @@ export function let_directive_invalid_placement(node) {
e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
}
/**
* Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function let_directive_snippet_conflict(node, name) {
e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
}
/**
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
* @param {null | number | NodeLike} node

@ -911,8 +911,10 @@ const instance_script = {
start = /** @type {number} */ (parent.start);
end = /** @type {number} */ (parent.end);
}
while (state.str.original[start] !== '\n') start--;
while (state.str.original[end] !== '\n') end++;
// remove the declaration's whole line, or what there is of it inside the script
const script = /** @type {{ start: number; end: number }} */ (state.analysis.instance.ast);
while (start > script.start && state.str.original[start] !== '\n') start--;
while (end < script.end && state.str.original[end] !== '\n') end++;
state.str.update(start, end, '');
}
},
@ -988,8 +990,11 @@ const instance_script = {
return;
}
let declared = 0;
for (const binding of bindings) {
if (binding.reassigned && (ids.includes(binding.node) || expression_ids.length === 0)) {
// a binding this statement declares needs its `let` whether or not it is reassigned
if (ids.includes(binding.node) || (binding.reassigned && expression_ids.length === 0)) {
declared++;
check_rune_binding('state');
const init =
binding.kind === 'state'
@ -1005,7 +1010,13 @@ const instance_script = {
}
}
if (expression_ids.length === 0 && bindings.every((b) => b.kind !== 'store_sub')) {
// the statement goes only once every target has a declaration standing in for it
if (
expression_ids.length === 0 &&
bindings.length > 0 &&
declared === bindings.length &&
bindings.every((b) => b.kind !== 'store_sub')
) {
state.str.remove(/** @type {number} */ (node.start), /** @type {number} */ (node.end));
return;
}

@ -5,10 +5,9 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js';
import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import { create_fragment, disallow_children } from '../nodes.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
import * as state from '../../state.js';
import { is_whitespace } from './utils/whitespace.js';

@ -9,8 +9,12 @@ import read_style from '../read/style.js';
import { decode_character_references } from '../utils/html.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { create_fragment } from '../utils/create.js';
import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js';
import {
create_attribute,
create_fragment,
ExpressionMetadata,
is_element_node
} from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';

@ -3,11 +3,10 @@
/** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js';
import { create_fragment, ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at, parse_statement_at } from '../acorn.js';
import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js';
import { create_fragment } from '../utils/create.js';
import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
const regex_whitespace_with_closing_curly_brace = /\s*}/y;

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
/**
* @param {any} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}

@ -145,6 +145,12 @@ export function prune(stylesheet, elements) {
},
ComplexSelector(node) {
const selectors = get_relative_selectors(node);
const rule = /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule);
// Global and ICSS export rules do not depend on an element in this component
if (every_is_global(selectors, 0, selectors.length, rule)) {
node.metadata.used = true;
}
for (const element of elements) {
seen.clear();
@ -153,12 +159,7 @@ export function prune(stylesheet, elements) {
// Elements rendered through <svelte:head> are not style-scopable.
// Prevent css hash injection (class="s-...") on tags like <meta>, <link>, <script>.
!is_inside_svelte_head(element) &&
apply_selector(
selectors,
/** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule),
element,
BACKWARD
)
apply_selector(selectors, rule, element, BACKWARD)
) {
node.metadata.used = true;
}

@ -14,7 +14,7 @@ import {
} from '../../utils/ast.js';
import * as b from '#compiler/builders';
import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js';
import check_graph_for_cycles from './utils/check_graph_for_cycles.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import { create_attribute, is_custom_element_node } from '../nodes.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';

@ -9,7 +9,7 @@ import { validate_assignment } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { binding_properties } from '../../bindings.js';
import fuzzymatch from '../../1-parse/utils/fuzzymatch.js';
import fuzzymatch from '../../../utils/fuzzymatch.js';
import { is_content_editable_binding, is_svg } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { custom_renderer } from '../../../state.js';

@ -1,7 +1,7 @@
/** @import { ClassDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import * as w from '../../../warnings.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {ClassDeclaration} node

@ -1,7 +1,7 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { Context } from '../types' */
import { visit_function } from './shared/function.js';
import { validate_identifier_name } from './shared/utils.js';
import { validate_identifier_name } from '../../scope.js';
/**
* @param {FunctionDeclaration} node

@ -1,12 +1,11 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Context } from '../types' */
import is_reference from 'is-reference';
import { should_proxy } from '../../3-transform/client/utils.js';
import { should_proxy, get_rune } from '../../scope.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { is_rune } from '../../../../utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
import { get_rune } from '../../scope.js';
import { is_component_node } from '../../nodes.js';
/**

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { determine_slot } from '../../../utils/slot.js';
/**
* @param {AST.LetDirective} node
@ -21,4 +22,25 @@ export function LetDirective(node, context) {
) {
e.let_directive_invalid_placement(node);
}
// an explicit `children` snippet replaces the default slot content, so the `let:`
// directive would never be provided. if the component has a `slot` attribute, the
// directive applies to the component itself instead, which is fine
if (
(parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf') &&
!determine_slot(parent) &&
parent.fragment.nodes.some(
(child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
)
) {
const { expression } = node;
const pattern =
expression === null || (expression.type === 'Identifier' && expression.name === node.name)
? node.name
: `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
e.let_directive_snippet_conflict(node, pattern);
}
}

@ -3,7 +3,7 @@
import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import { check_global_event_reference } from './shared/utils.js';
/**

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js';

@ -1,6 +1,6 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
import { disallow_children } from '../../nodes.js';
import * as e from '../../../errors.js';
import { custom_renderer } from '../../../state.js';
import { is_event_attribute } from '../../../utils/ast.js';

@ -1,8 +1,8 @@
/** @import { Expression, Identifier, Literal, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types' */
import { get_rune } from '../../scope.js';
import { ensure_no_module_import_conflict, validate_identifier_name } from './shared/utils.js';
import { get_rune, validate_identifier_name } from '../../scope.js';
import { ensure_no_module_import_conflict } from './shared/utils.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { extract_paths } from '../../../utils/ast.js';

@ -45,7 +45,7 @@ import {
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { list } from '../../../../../utils/string.js';
import { walk } from 'zimmerframe';
import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js';
import fuzzymatch from '../../../../../utils/fuzzymatch.js';
import { is_content_editable_binding } from '../../../../../../utils.js';
import * as w from '../../../../../warnings.js';
@ -173,8 +173,8 @@ export function check_element(node, context) {
current_role === get_implicit_role(node.name, attribute_map) &&
// <ul role="list"> is ok because CSS list-style:none removes the semantics and this is a way to bring them back
!['ul', 'ol', 'li', 'menu'].includes(node.name) &&
// <a role="link" /> is ok because without href the a tag doesn't have a role of link
!(node.name === 'a' && !attribute_map.has('href'))
// <a role="link" /> and <area role="link" /> are ok because without href they don't have a role of link
!((node.name === 'a' || node.name === 'area') && !attribute_map.has('href'))
) {
w.a11y_no_redundant_roles(attribute, current_role);
}

@ -1,16 +0,0 @@
/** @import { AST } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

@ -246,44 +246,6 @@ export function is_pure(node, context) {
return false;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}
/**
* Checks that the exported name is not a derived or reassigned state variable.
* @param {Node} node

@ -127,44 +127,6 @@ export function is_prop_source(binding, state) {
);
}
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* An async thunk. If an `await` inside restores the reaction context via `$.save`,
* the body exits through `$.unsave` so the context cannot leak into foreign microtasks

@ -9,10 +9,10 @@ import {
is_expression_async
} from '../../../../utils/ast.js';
import { dev, locate_node } from '../../../../state.js';
import { build_getter, should_proxy } from '../utils.js';
import { build_getter } from '../utils.js';
import { should_proxy, get_rune } from '../../../scope.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
import { validate_mutation } from './shared/utils.js';
import { get_rune } from '../../../scope.js';
import { get_name } from '../../../nodes.js';
/**

@ -18,7 +18,8 @@ export function AwaitExpression(node, context) {
// in dev, note which values are read inside a reactive expression,
// but don't track them
else if (dev && !is_ignored(node, 'await_reactivity_loss')) {
return b.call(b.await(b.call('$.track_reactivity_loss', argument)));
// don't use b.await here, this way we preserve location information
return b.call({ ...node, argument: b.call('$.track_reactivity_loss', argument) });
}
return argument === node.argument ? node : { ...node, argument };

@ -2,8 +2,7 @@
/** @import { Context } from '../types' */
import { dev, is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { get_inspect_args } from '../../utils.js';
/**

@ -5,14 +5,8 @@ import { dev, is_ignored, locate_node } from '../../../../state.js';
import { extract_paths, save } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js';
import {
async_thunk,
get_prop_source,
is_prop_source,
is_state_source,
should_proxy
} from '../utils.js';
import { get_rune, should_proxy } from '../../../scope.js';
import { async_thunk, get_prop_source, is_prop_source, is_state_source } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
@ -35,6 +29,7 @@ export function VariableDeclaration(node, context) {
rune === '$inspect' ||
rune === '$inspect.trace' ||
rune === '$state.snapshot' ||
rune === '$state.eager' ||
rune === '$host'
) {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));

@ -9,7 +9,7 @@ import {
import * as e from '../../errors.js';
import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import check_graph_for_cycles from '../../utils/check_graph_for_cycles.js';
import is_reference from 'is-reference';
import { set_scope } from '../scope.js';
import { custom_renderer } from '../../state.js';

@ -1,6 +1,7 @@
/** @import { Expression, PrivateIdentifier, SourceLocation } from 'estree' */
/** @import { AST, Binding } from '#compiler' */
import * as b from '#compiler/builders';
import * as e from '../errors.js';
/**
* All nodes that can appear elsewhere than the top level, have attributes and can contain children
@ -45,6 +46,35 @@ export function is_custom_element_node(node) {
);
}
/**
* @param {boolean} transparent
* @returns {AST.Fragment}
*/
export function create_fragment(transparent = false) {
return {
type: 'Fragment',
nodes: [],
metadata: {
transparent,
dynamic: false
}
};
}
/**
* @param {AST.SvelteBody | AST.SvelteDocument | AST.SvelteOptionsRaw | AST.SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}
/**
* @param {string} name
* @param {SourceLocation | null} name_loc

@ -14,7 +14,6 @@ import {
} from '../utils/ast.js';
import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js';
const UNKNOWN = Symbol('unknown');
/** Includes `BigInt` */
@ -1233,6 +1232,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
EachBlock(node, { state, visit }) {
visit(node.expression);
if (node.fallback) visit(node.fallback);
// context and children are a new scope
const scope = state.scope.child();
@ -1278,7 +1278,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const child of node.body.nodes) {
visit(child, { scope });
}
if (node.fallback) visit(node.fallback, { scope });
node.metadata = {
expression: new ExpressionMetadata(),
@ -1344,6 +1343,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
context.next({ scope: child_scope });
// the parameters and what the body declares end up in the same function
const body_scope = /** @type {Scope} */ (scopes.get(node.body));
for (const [name, binding] of body_scope.declarations) {
if (child_scope.declarations.has(name)) {
e.declaration_duplicate(binding.node, name);
}
}
},
Fragment: (node, context) => {
@ -1469,3 +1476,79 @@ function get_global_keypath(node, scope) {
return n.name + joined;
}
/**
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy(node, scope) {
if (
!node ||
node.type === 'Literal' ||
node.type === 'TemplateLiteral' ||
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined')
) {
return false;
}
if (node.type === 'Identifier' && scope !== null) {
const binding = scope.get(node.name);
// Let's see if the reference is something that can be proxied
if (
binding !== null &&
!binding.reassigned &&
binding.initial !== null &&
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}
}
return true;
}
/**
* Checks if the name is valid, which it is when it's not starting with (or is) a dollar sign or if it's a function parameter.
* The second argument is the depth of the scope, which is there for backwards compatibility reasons: In Svelte 4, you
* were allowed to define `$`-prefixed variables anywhere below the top level of components. Once legacy mode is gone, this
* argument can be removed / the call sites adjusted accordingly.
* @param {Binding | null} binding
* @param {number | undefined} [function_depth]
*/
export function validate_identifier_name(binding, function_depth) {
if (!binding) return;
const declaration_kind = binding.declaration_kind;
if (
declaration_kind !== 'synthetic' &&
declaration_kind !== 'param' &&
declaration_kind !== 'rest_param' &&
(!function_depth || function_depth <= 1)
) {
const node = binding.node;
if (node.name === '$') {
e.dollar_binding_invalid(node);
} else if (
node.name.startsWith('$') &&
// import type { $Type } from "" - these are normally already filtered out,
// but for the migration they aren't, and throwing here is preventing the migration to complete
// TODO -> once migration script is gone we can remove this check
!(
binding.initial?.type === 'ImportDeclaration' &&
/** @type {any} */ (binding.initial).importKind === 'type'
)
) {
e.dollar_prefix_invalid(node);
}
}
}

@ -1,5 +1,5 @@
import { IGNORABLE_RUNTIME_WARNINGS } from '../../constants.js';
import fuzzymatch from '../phases/1-parse/utils/fuzzymatch.js';
import fuzzymatch from './fuzzymatch.js';
import * as w from '../warnings.js';
const regex_svelte_ignore = /^\s*svelte-ignore\s/;

@ -1,10 +1,10 @@
/** @import { ValidatedCompileOptions } from '#compiler' */
/** @import { Processed } from '../preprocess/public.js' */
/** @import { SourceMap } from 'magic-string' */
/** @import { Source } from '../preprocess/private.js' */
/** @import { DecodedSourceMap, SourceMapSegment, RawSourceMap } from '@jridgewell/remapping' */
import remapping from '@jridgewell/remapping';
import { push_array } from './push_array.js';
/**
* @param {string} s
@ -12,6 +12,18 @@ import { push_array } from './push_array.js';
function last_line_length(s) {
return s.length - s.lastIndexOf('\n') - 1;
}
/**
* `array.push(...items)` overflows the stack for large `items`.
* @template T
* @param {T[]} array
* @param {T[]} items
*/
function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}
// mutate map in-place
/**

@ -1,13 +0,0 @@
/**
* Pushes all `items` into `array` using `push`, therefore mutating the array.
* We do this for memory and perf reasons, and because `array.push(...items)` would
* run into a "max call stack size exceeded" error with too many items (~65k).
* @template T
* @param {T[]} array
* @param {T[]} items
*/
export function push_array(array, items) {
for (let i = 0; i < items.length; i++) {
array.push(items[i]);
}
}

@ -17,7 +17,7 @@ if (DEV) {
if (!(rune in globalThis)) {
// TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message
/** @type {any} */
let value; // let's hope noone modifies this global, but belts and braces
let value; // let's hope no one modifies this global, but belts and braces
Object.defineProperty(globalThis, rune, {
configurable: true,
// eslint-disable-next-line getter-return

@ -51,15 +51,6 @@ export const EFFECT_PRESERVED = 1 << 19;
export const USER_EFFECT = 1 << 20;
export const EFFECT_OFFSCREEN = 1 << 25;
// Flags exclusive to deriveds
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed). This is a pure performance optimization flag and
* should not be used for any other purpose!
*/
export const WAS_MARKED = 1 << 16;
// Flags used for async
export const REACTION_IS_UPDATING = 1 << 21;
export const ASYNC = 1 << 22;

@ -15,7 +15,6 @@ import {
MAYBE_DIRTY,
RENDER_EFFECT,
ROOT_EFFECT,
WAS_MARKED,
MANAGED_EFFECT
} from '#client/constants';
import { snapshot } from '../../shared/clone.js';
@ -204,7 +203,6 @@ export function log_reactions(signal) {
if ((flags & DIRTY) !== 0) names.push('DIRTY');
if ((flags & MAYBE_DIRTY) !== 0) names.push('MAYBE_DIRTY');
if ((flags & CONNECTED) !== 0) names.push('CONNECTED');
if ((flags & WAS_MARKED) !== 0) names.push('WAS_MARKED');
if ((flags & INERT) !== 0) names.push('INERT');
if ((flags & DESTROYED) !== 0) names.push('DESTROYED');

@ -131,8 +131,10 @@ export function trace(label, fn) {
}
/**
* @param {Value} source
* @template {Value} T
* @param {T} source
* @param {string} label
* @returns {T}
*/
export function tag(source, label) {
source.label = label;

@ -1,6 +1,16 @@
/** @import { Effect, Source, TemplateNode, } from '#client' */
import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants';
import { HYDRATION_START_ELSE, HYDRATION_START_FAILED } from '../../../../constants.js';
import {
BOUNDARY_EFFECT,
DESTROYED,
DESTROYING,
EFFECT_PRESERVED,
EFFECT_TRANSPARENT
} from '#client/constants';
import {
HYDRATION_ERROR,
HYDRATION_START_ELSE,
HYDRATION_START_FAILED
} from '../../../../constants.js';
import { component_context, set_component_context } from '../../context.js';
import { invoke_error_boundary } from '../../error-handling.js';
import {
@ -232,6 +242,8 @@ export class Boundary {
var calling_on_error = false;
const reset = () => {
if (this.#is_destroyed()) return;
if (did_reset) {
w.svelte_boundary_reset_noop();
return;
@ -255,6 +267,8 @@ export class Boundary {
};
const invoke_onerror = () => {
if (this.#is_destroyed()) return;
try {
calling_on_error = true;
this.#props.onerror?.(error, reset);
@ -267,6 +281,10 @@ export class Boundary {
return { reset, invoke_onerror };
}
#is_destroyed() {
return (this.#effect.f & (DESTROYED | DESTROYING)) !== 0;
}
#hydrate_pending_content() {
const pending = this.#props.pending;
if (!pending) return;
@ -275,6 +293,8 @@ export class Boundary {
this.#pending_effect = branch(() => pending(this.#anchor));
queue_micro_task(() => {
if (this.#is_destroyed()) return;
var pop_renderer = push_renderer(this.#effect.r, this.#effect.pr);
try {
@ -471,6 +491,10 @@ export class Boundary {
/** @param {unknown} error */
error(error) {
if (error === HYDRATION_ERROR) {
throw error;
}
// 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 (!this.#props.onerror && !this.#props.failed) {
@ -483,7 +507,7 @@ export class Boundary {
if (this.#failed_effect) current_batch.skip_effect(this.#failed_effect);
current_batch.oncommit(() => {
this.#handle_error(error);
if (!this.#is_destroyed()) this.#handle_error(error);
});
} else {
this.#handle_error(error);
@ -519,11 +543,13 @@ export class Boundary {
/** @param {unknown} transformed_error */
const handle_error_result = (transformed_error) => {
if (this.#is_destroyed()) return;
const { reset, invoke_onerror } = this.#create_reset(transformed_error);
invoke_onerror();
if (failed) {
if (failed && !this.#is_destroyed()) {
this.#failed_effect = this.#run(() => {
try {
return branch(() => {
@ -549,6 +575,8 @@ export class Boundary {
};
queue_micro_task(() => {
if (this.#is_destroyed()) return;
// Run the error through the API-level transformError transform (e.g. SvelteKit's handleError)
/** @type {unknown} */
var result;

@ -1,4 +1,4 @@
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
/** @import { EachItem, EachOutroGroup, EachState, Effect, EffectNodes, MaybeSource, TemplateNode, TransitionManager } from '#client' */
/** @import { Batch } from '../../reactivity/batch.js'; */
import {
EACH_INDEX_REACTIVE,
@ -260,9 +260,6 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
tag(each_array, '{#each ...}');
}
/** @type {V[]} */
var array;
/** @type {Map<Batch, Set<any>>} */
var pending = new Map();
@ -281,6 +278,11 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
try {
state.pending.delete(batch);
// The effect doesn't necessarily re-run in a batch right before that batch commits
// (its view of the collection may not have changed), so we read the collection
// as the committing batch sees it rather than using the most recent block run's result
var array = get(each_array);
state.fallback = fallback;
reconcile(state, array, anchor, flags, get_key);
@ -318,7 +320,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
var pop_renderer = push_renderer(renderer, renderer);
var effect = block(() => {
array = /** @type {V[]} */ (get(each_array));
var array = /** @type {V[]} */ (get(each_array));
var length = array.length;
/** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -722,7 +724,7 @@ function reconcile(state, array, anchor, flags, get_key) {
* @param {V} value
* @param {unknown} key
* @param {number} index
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>, collection: () => V[]) => void} render_fn
* @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>, collection: () => V[]) => void} render_fn
* @param {number} flags
* @param {() => V[]} get_collection
* @returns {EachItem}

@ -80,6 +80,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
branches.ensure(next_tag, (anchor) => {
if (next_tag) {
var is_hydrating = hydrating;
element = hydrating ? /** @type {Element} */ (element) : create_element(next_tag, ns);
if (DEV && location) {
@ -134,7 +135,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
// we do this after calling `render_fn` so that child effects don't override `nodes.end`
/** @type {Effect & { nodes: EffectNodes }} */ (active_effect).nodes.end = element;
insert_before(anchor, element);
// we only move the node if we are not hydrating since a claimed element is already in place
if (!is_hydrating) insert_before(anchor, element);
}
if (hydrating) {

@ -12,7 +12,7 @@ export function append_styles(anchor, css) {
// Use an effect to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results
effect(() => {
// Bit of a hack: branches.js/each.js use offscreen fragments with temporary text nodes that will
// never be connected to the real dom. Therfore walk up to the branch that has created the component
// never be connected to the real dom. Therefore walk up to the branch that has created the component
// whose styles we want to append, and check its node instead. It will be connected by the time we get here.
anchor = active_effect?.parent?.nodes?.start ?? anchor;
var root = anchor.getRootNode();

@ -310,6 +310,7 @@ function set_attributes(
var is_option_element = node_name(element) === OPTION_TAG;
var is_select_element = node_name(element) === SELECT_TAG;
try {
for (var key in prev) {
// don't null our internal $$onX listeners
if (!(key in next) && key[0] + key[1] !== '$$') {
@ -450,7 +451,19 @@ function set_attributes(
// @ts-ignore We're not running this for custom elements because __value is actually
// how Lit stores the current value on the element, and messing with that would break things.
element.__value = value;
// we don't set the value if it hasn't changed. This supports invalid number inputs like `1e` because
// 1. user types 1e
// 2. the state is updated reading e.target.value which is ''
// 3. the spreaded value is ''
// 4. updating input.value would thus, clear the user value
if (
prev_value == null ||
// @ts-ignore
element.value !== value ||
(value === 0 && element.nodeName === PROGRESS_TAG)
) {
set_element_value(element, value);
}
} else if (key === 'selected' && is_option_element) {
set_selected(/** @type {HTMLOptionElement} */ (element), value);
} else {
@ -512,10 +525,11 @@ function set_attributes(
}
}
}
} finally {
if (is_hydrating_custom_element) {
set_hydrating(true);
}
}
return current;
}

@ -359,9 +359,9 @@ function animate(element, options, counterpart, t2, on_begin, on_finish) {
aborted = true;
a?.abort();
},
deactivate: () => a.deactivate(),
reset: () => a.reset(),
t: () => a.t()
deactivate: () => a?.deactivate(),
reset: () => a?.reset(),
t: () => a?.t() ?? 1 - t2
};
}

@ -1,7 +1,7 @@
/** @import { Derived, Effect } from '#client' */
/** @import { Boundary } from './dom/blocks/boundary.js' */
import { DEV } from 'esm-env';
import { FILENAME } from '../../constants.js';
import { FILENAME, HYDRATION_ERROR } from '../../constants.js';
import { is_firefox } from './dom/operations.js';
import {
ERROR_VALUE,
@ -52,6 +52,10 @@ export function handle_error(error) {
* @param {Effect | null} effect
*/
export function invoke_error_boundary(error, effect) {
if (error === HYDRATION_ERROR) {
throw error;
}
if (effect !== null && (effect.f & DESTROYED) !== 0) {
return;
}

@ -204,16 +204,21 @@ export function proxy(value) {
},
getOwnPropertyDescriptor(target, prop) {
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
this.has?.(target, prop);
if (descriptor && 'value' in descriptor) {
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
var s = sources.get(prop);
if (s) descriptor.value = get(s);
} else if (descriptor === undefined) {
var source = sources.get(prop);
var value = source?.v;
if (source !== undefined && value !== UNINITIALIZED) {
if (s !== undefined) {
var value = get(s);
if (value === UNINITIALIZED) {
return undefined;
}
if (descriptor && 'value' in descriptor) {
descriptor.value = value;
} else {
return {
enumerable: true,
configurable: true,

@ -22,7 +22,6 @@ import {
import { async_mode_flag } from '../../flags/index.js';
import { deferred, define_property, includes } from '../../shared/utils.js';
import {
active_effect,
active_reaction,
get,
increment_write_version,
@ -38,7 +37,6 @@ import { eager_effect, teardown, unlink_effect } from './effects.js';
import { defer_effect } from './utils.js';
import { UNINITIALIZED } from '../../../constants.js';
import { set_signal_status } from './status.js';
import { legacy_is_updating_store } from './store.js';
import { invariant } from '../../shared/dev.js';
import { log_effect_tree } from '../dev/debug.js';
import { OBSOLETE } from './deriveds.js';
@ -160,10 +158,17 @@ export class Batch {
#deferred = null;
/**
* The root effects that need to be flushed
* Effects that were scheduled in this batch but not yet 'resolved' into the
* root effects that need to be flushed. Resolving — the upwards traversal that
* marks the path to each effect on the shared effect tree (see #resolve) — is
* deferred until the batch is processed, so that the markers are created and
* consumed within a single traversal. Scheduling into other batches (which can
* happen concurrently, e.g. while a batch is committed) can therefore never
* observe (and be confused by) this batch's markers.
* May contain duplicates — deduplication happens during resolving
* @type {Effect[]}
*/
#roots = [];
#scheduled = [];
/**
* Effects created while this batch was active.
@ -273,14 +278,57 @@ export class Batch {
this.#unskipped_branches.add(effect);
}
#process() {
this.#started = true;
/**
* Convert the effects that were scheduled in this batch into the root effects
* that need to be traversed, marking the path to each effect (by clearing the
* `CLEAN` flag on ancestor branches) so that the traversal can find them.
* This happens right before traversal rather than at scheduling time, so that
* the markers left on the (shared) effect tree are created and consumed within
* a single traversal — scheduling into other batches can never observe them
* @returns {Effect[]}
*/
#resolve() {
/** @type {Effect[]} */
var roots = [];
if (flush_count++ > 1000) {
this.#unlink();
infinite_loop_guard();
for (const effect of this.#scheduled) {
// skip effects that are destroyed, or that already ran (e.g. because
// they were reached by the traversal that preceded a drain iteration,
// or because they were scheduled twice)
if ((effect.f & DESTROYED) !== 0 || (effect.f & (DIRTY | MAYBE_DIRTY)) === 0) continue;
var e = effect;
var covered = false;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// the path to the root was already marked, meaning the
// root was already collected — nothing left to do
covered = true;
break;
}
e.f ^= CLEAN;
}
}
if (!covered) {
roots.push(e);
}
}
this.#scheduled = [];
return roots;
}
#process() {
this.#started = true;
if (DEV) {
// track all the values that were updated during this flush,
// so that they can be reset afterwards
@ -304,9 +352,6 @@ export class Batch {
this.schedule(e);
}
const roots = this.#roots;
this.#roots = [];
this.apply();
/** @type {Effect[]} */
@ -321,7 +366,16 @@ export class Batch {
*/
var updates = (legacy_updates = []);
for (const root of roots) {
// Effects can be scheduled during traversal (e.g. because a parent each/await/etc
// block updated an internal source, or because an effect invalidated itself)
// hence we loop until there are no more scheduled effects.
while (this.#scheduled.length > 0) {
if (flush_count++ > 1000) {
this.#unlink();
infinite_loop_guard(); // TODO try to reset_all() here?
}
for (const root of this.#resolve()) {
try {
this.#traverse(root, effects, render_effects);
} catch (e) {
@ -335,6 +389,7 @@ export class Batch {
throw e;
}
}
}
// any writes should take effect in a subsequent batch
current_batch = null;
@ -393,7 +448,7 @@ export class Batch {
var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch));
if (this.#pending === 0 && (this.#roots.length === 0 || next_batch !== null)) {
if (this.#pending === 0 && (this.#scheduled.length === 0 || next_batch !== null)) {
this.#unlink();
// Order matters here - we need to commit and THEN continue flushing new batches, not the other way around,
@ -408,12 +463,15 @@ export class Batch {
}
// Edge case: During traversal new branches might create effects that run immediately and set state,
// causing an effect and therefore a root to be scheduled again. We need to traverse the current batch
// causing an effect to be scheduled again. We need to traverse the current batch
// once more in that case - most of the time this will just clean up dirty branches.
if (this.#roots.length > 0) {
if (this.#scheduled.length > 0) {
if (next_batch !== null) {
const batch = next_batch;
batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r)));
for (const e of this.#scheduled) {
next_batch.#scheduled.push(e);
}
this.#scheduled = [];
} else {
next_batch = this;
}
@ -712,7 +770,7 @@ export class Batch {
// The microtask queue can contain the batch already scheduled to run right
// after this one is finished, so throwing the invariant would be wrong here.
if (DEV && !batch.#decrement_queued) {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
invariant(batch.#scheduled.length === 0, 'Batch has scheduled effects');
}
// A batch was unskipped in a later batch -> tell prior batches to unskip it, too
@ -768,14 +826,12 @@ export class Batch {
// Only apply and traverse when we know we triggered async work with marking the effects
// and know this won't run anyway right afterwards
if (batch.#roots.length > 0 && !batch.#decrement_queued) {
if (batch.#scheduled.length > 0 && !batch.#decrement_queued) {
batch.apply();
for (var root of batch.#roots) {
for (var root of batch.#resolve()) {
batch.#traverse(root, [], []);
}
batch.#roots = [];
}
batch.deactivate();
@ -939,43 +995,7 @@ export class Batch {
return;
}
var e = effect;
while (e.parent !== null) {
e = e.parent;
var flags = e.f;
// if the effect is being scheduled because a parent (each/await/etc) block
// updated an internal source, or because a branch is being unskipped,
// bail out or we'll cause a second flush
if (collected_effects !== null && e === active_effect) {
if (async_mode_flag) return;
// in sync mode, render effects run during traversal. in an extreme edge case
// — namely that we're setting a value inside a derived read during traversal —
// they can be made dirty after they have already been visited, in which
// case we shouldn't bail out. we also shouldn't bail out if we're
// updating a store inside a `$:`, since this might invalidate
// effects that were already visited
if (
(active_reaction === null || (active_reaction.f & DERIVED) === 0) &&
!legacy_is_updating_store
) {
return;
}
}
if ((flags & (ROOT_EFFECT | BRANCH_EFFECT)) !== 0) {
if ((flags & CLEAN) === 0) {
// branch is already dirty, bail
return;
}
e.f ^= CLEAN;
}
}
this.#roots.push(e);
this.#scheduled.push(effect);
}
#unlink() {
@ -1012,15 +1032,15 @@ export class Batch {
*/
export function flushSync(fn) {
var was_flushing_sync = is_flushing_sync;
var prev_previous_batch = previous_batch;
previous_batch = null;
is_flushing_sync = true;
try {
var result;
if (fn) {
if (current_batch !== null && !current_batch.is_fork) {
current_batch.flush();
}
flushSync(); // flush anything pending through the while loop below
result = fn();
}
@ -1036,6 +1056,7 @@ export function flushSync(fn) {
}
} finally {
is_flushing_sync = was_flushing_sync;
previous_batch = prev_previous_batch;
}
}
@ -1275,6 +1296,10 @@ export function eager(fn) {
let version = version_map.get(parent) ?? source(0);
version_map.set(parent, version);
if (DEV) {
version.label ??= '$state.eager version';
}
teardown(() => {
if (parent.f & DESTROYING) version_map.delete(parent);
});
@ -1382,6 +1407,8 @@ export function fork(fn) {
e.fork_timing();
}
flushSync();
var batch = Batch.ensure();
batch.is_fork = true;
batch_values = new Map();
@ -1389,7 +1416,8 @@ export function fork(fn) {
var committed = false;
var settled = batch.settled();
flushSync(fn);
fn();
flushSync();
return {
commit: async () => {

@ -9,7 +9,6 @@ import {
EFFECT_PRESERVED,
STALE_REACTION,
ASYNC,
WAS_MARKED,
DESTROYED,
CLEAN,
REACTION_RAN,
@ -365,7 +364,6 @@ export function execute_derived(derived) {
stack.push(derived);
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {
@ -375,7 +373,6 @@ export function execute_derived(derived) {
}
} else {
try {
derived.f &= ~WAS_MARKED;
destroy_derived_effects(derived);
value = update_reaction(derived);
} finally {

@ -25,10 +25,7 @@ import {
MAYBE_DIRTY,
BLOCK_EFFECT,
ROOT_EFFECT,
ASYNC,
WAS_MARKED,
CONNECTED,
REACTION_IS_UPDATING
ASYNC
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
@ -49,7 +46,7 @@ import { set_signal_status, update_derived_status } from './status.js';
/** @type {Set<Effect>} */
export let eager_effects = new Set();
/** @type {Map<Source, any>} */
/** @type {Map<Value, any>} */
export const old_values = new Map();
/**
@ -144,7 +141,7 @@ export function mutate(source, value) {
/**
* @template V
* @param {Source<V>} source
* @param {Value<V>} source
* @param {V} value
* @param {boolean} [should_proxy]
* @returns {V}
@ -171,9 +168,20 @@ export function set(source, value, should_proxy = false) {
return internal_set(source, new_value, legacy_updates);
}
/**
* A set of signals we have already seen while traversing in mark_reactions.
* Not always set to balance the common case of sources only having a couple
* of (transitive) dependencies (where always creating a Set would be bad for perf)
* with the edge case of extremely deep or wide dependency arrays with cycles.
* @type {Set<any> | null}
*/
var seen = null;
/** Number of transitive dependencies, see {@link seen} for more info */
var count_deps = 0;
/**
* @template V
* @param {Source<V>} source
* @param {Value<V>} source
* @param {V} value
* @param {Effect[] | null} [updated_during_traversal]
* @returns {V}
@ -240,7 +248,10 @@ export function internal_set(source, value, updated_during_traversal = null) {
// For debugging, in case you want to know which reactions are being scheduled:
// log_reactions(source);
seen = null;
count_deps = 0;
mark_reactions(source, DIRTY, updated_during_traversal);
seen = null;
// It's possible that the current reaction might not have up-to-date dependencies
// whilst it's actively running. So in the case of ensuring it registers the reaction
@ -347,6 +358,18 @@ function mark_reactions(signal, status, updated_during_traversal) {
var runes = is_runes();
var length = reactions.length;
count_deps += length;
// Activate the `seen` Set if we think from the unusually high number of deps that
// there might be cycles in the graph, to avoid repeated lookups for reactions
// Example: https://github.com/sveltejs/svelte/issues/16658 has a graph with one source
// reaching ~10000 distinct deriveds/effects each, resulting in 65 million walks through repeated visits.
if (count_deps > 100000 && seen === null) seen = new Set();
if (seen !== null) {
if (seen.has(signal)) return;
seen.add(signal);
}
for (var i = 0; i < length; i++) {
var reaction = reactions[i];
var flags = reaction.f;
@ -370,18 +393,7 @@ function mark_reactions(signal, status, updated_during_traversal) {
var derived = /** @type {Derived} */ (reaction);
batch_values?.delete(derived);
if ((flags & WAS_MARKED) === 0) {
// Only connected deriveds being executed outside the update cycle can be reliably unmarked right away
if (
flags & CONNECTED &&
(active_effect === null || (active_effect.f & REACTION_IS_UPDATING) === 0)
) {
reaction.f |= WAS_MARKED;
}
mark_reactions(derived, MAYBE_DIRTY, updated_during_traversal);
}
} else if (not_dirty) {
var effect = /** @type {Effect} */ (reaction);

@ -1,24 +1,7 @@
/** @import { Derived, Effect, Value } from '#client' */
import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants';
/** @import { Effect } from '#client' */
import { CLEAN, DIRTY, MAYBE_DIRTY } from '#client/constants';
import { set_signal_status } from './status.js';
/**
* @param {Value[] | null} deps
*/
function clear_marked(deps) {
if (deps === null) return;
for (const dep of deps) {
if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) {
continue;
}
dep.f ^= WAS_MARKED;
clear_marked(/** @type {Derived} */ (dep).deps);
}
}
/**
* @param {Effect} effect
* @param {Set<Effect>} dirty_effects
@ -31,10 +14,6 @@ export function defer_effect(effect, dirty_effects, maybe_dirty_effects) {
maybe_dirty_effects.add(effect);
}
// Since we're not executing these effects now, we need to clear any WAS_MARKED flags
// so that other batches can correctly reach these effects during their own traversal
clear_marked(effect.deps);
// mark as clean so they get scheduled if they depend on pending async state
set_signal_status(effect, CLEAN);
}

@ -21,7 +21,6 @@ import {
REACTION_IS_UPDATING,
STALE_REACTION,
ERROR_VALUE,
WAS_MARKED,
MANAGED_EFFECT,
REACTION_RAN
} from './constants.js';
@ -62,11 +61,6 @@ import { set_signal_status, update_derived_status } from './reactivity/status.js
import * as w from './warnings.js';
import { push_renderer } from './custom-renderer/state.js';
/**
* True if updating in an effect context that is reactive (i.e. not branch/root effects)
*/
let is_updating_effect = false;
export let is_destroying_effect = false;
/** @param {boolean} value */
@ -95,7 +89,7 @@ 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 | Set<Source>}
* @type {null | Set<Value>}
*/
export let current_sources = null;
@ -123,11 +117,11 @@ export let skipped_deps = 0;
/**
* Tracks writes that the effect it's executed in doesn't listen to yet,
* so that the dependency can be added to the effect later on if it then reads it
* @type {null | Source[]}
* @type {null | Value[]}
*/
export let untracked_writes = null;
/** @param {null | Source[]} value */
/** @param {null | Value[]} value */
export function set_untracked_writes(value) {
untracked_writes = value;
}
@ -165,10 +159,6 @@ export function is_dirty(reaction) {
return true;
}
if (flags & DERIVED) {
reaction.f &= ~WAS_MARKED;
}
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = /** @type {Value[]} */ (reaction.deps);
var length = dependencies.length;
@ -409,17 +399,14 @@ function remove_reaction(signal, dependency) {
) {
var derived = /** @type {Derived} */ (dependency);
// If we are working with a derived that is owned by an effect, then mark it as being
// disconnected and remove the mark flag, as it cannot be reliably removed otherwise
if ((derived.f & CONNECTED) !== 0) {
derived.f ^= CONNECTED;
derived.f &= ~WAS_MARKED;
}
// In a fork it's possible that a derived is executed and gets reactions, then commits, but is
// never re-executed. This is possible when the derived is only executed once in the context
// of a new branch which happens before fork.commit() runs. In this case, the derived still has
// UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
// UNINITIALIZED as its value, and then when it's losing its reactions we need to ensure it stays
// DIRTY so it is reexecuted once someone wants its value again.
if (derived.v !== UNINITIALIZED) {
update_derived_status(derived);
@ -471,10 +458,8 @@ export function update_effect(effect) {
set_signal_status(effect, CLEAN);
var previous_effect = active_effect;
var was_updating_effect = is_updating_effect;
active_effect = effect;
is_updating_effect = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0; // Branch/root effects are not reactive contexts
var pop_renderer = push_renderer(effect.r, effect.pr);
@ -509,7 +494,6 @@ export function update_effect(effect) {
}
}
} finally {
is_updating_effect = was_updating_effect;
active_effect = previous_effect;
pop_renderer?.();
@ -693,13 +677,12 @@ export function get(signal) {
return value;
}
// connect disconnected deriveds if we are reading them inside an effect,
// or inside another derived that is already connected
// connect disconnected deriveds when reading them inside a connected reaction
var should_connect =
(derived.f & CONNECTED) === 0 &&
!untracking &&
active_reaction !== null &&
(is_updating_effect || (active_reaction.f & CONNECTED) !== 0);
(active_reaction.f & CONNECTED) !== 0;
var is_new = (derived.f & REACTION_RAN) === 0;

@ -168,7 +168,7 @@ export function attributes(attrs, css_hash, classes, styles, flags = 0) {
// value/checked takes precedence over defaultValue/defaultChecked
name = name === 'defaultvalue' ? 'value' : 'checked';
if (name in attrs) continue;
// We're checking prior entries aswell because "name in attrs" is not enough as the attributes may have different casing
// We're checking prior entries as well because "name in attrs" is not enough as the attributes may have different casing
for (let j = 0; j < names.length; j++) {
if (names[j].toLowerCase() === name) continue outer;
}

@ -23,6 +23,87 @@ import { escape_html } from '../../escaping.js';
* @typedef {string | Renderer} RendererItem
*/
class RenderResult {
/** @type {() => AccumulatedContent} */
#render;
/** @type {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} */
#render_async;
/** @type {AccumulatedContent | undefined} */
#sync;
/** @type {{ script: '' }} */
#hashes = { script: '' };
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
#promise;
/**
* @param {() => AccumulatedContent} render
* @param {() => Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }>} render_async
*/
constructor(render, render_async) {
this.#render = render;
this.#render_async = render_async;
}
#get() {
return (this.#sync ??= this.#render());
}
get html() {
return this.#get().body;
}
get head() {
return this.#get().head;
}
get body() {
return this.#get().body;
}
get hashes() {
return this.#hashes;
}
/**
* This is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param {(value: SyncRenderOutput) => TResult1} onfulfilled
* @param {(reason: unknown) => TResult2} onrejected
*/
then(onfulfilled, onrejected) {
if (!async_mode_flag) {
const result = this.#get();
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
this.#promise ??= this.#render_async().then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return result;
});
return this.#promise.then(
(result) => onfulfilled(/** @type {SyncRenderOutput} */ (result)),
onrejected
);
}
}
/**
* Renderers are basically a tree of `string | Renderer`s, where each `Renderer` in the tree represents
* work that may or may not have completed. A renderer can be {@link collect}ed to aggregate the
@ -534,73 +615,17 @@ export class Renderer {
* @returns {RenderOutput}
*/
static render(component, options = {}) {
/** @type {AccumulatedContent | undefined} */
let sync;
/** @type {Promise<AccumulatedContent & { hashes: { script: Sha256Source[] } }> | undefined} */
let async;
const result = /** @type {RenderOutput} */ ({});
// making these properties non-enumerable so that console.logging
// doesn't trigger a sync render
Object.defineProperties(result, {
html: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
head: {
get: () => {
return (sync ??= Renderer.#render(component, options)).head;
}
},
body: {
get: () => {
return (sync ??= Renderer.#render(component, options)).body;
}
},
hashes: {
value: {
script: ''
}
},
then: {
value:
/**
* this is not type-safe, but honestly it's the best I can do right now, and it's a straightforward function.
*
* @template TResult1
* @template [TResult2=never]
* @param { (value: SyncRenderOutput) => TResult1 } onfulfilled
* @param { (reason: unknown) => TResult2 } onrejected
*/
(onfulfilled, onrejected) => {
if (!async_mode_flag) {
const result = (sync ??= Renderer.#render(component, options));
const user_result = onfulfilled({
head: result.head,
body: result.body,
html: result.body,
hashes: { script: [] }
});
return Promise.resolve(user_result);
}
async ??= init_render_context().then(() =>
return /** @type {RenderOutput} */ (
/** @type {unknown} */ (
new RenderResult(
() => Renderer.#render(component, options),
() =>
init_render_context().then(() =>
with_render_context(() => Renderer.#render_async(component, options))
)
)
)
);
return async.then((result) => {
Object.defineProperty(result, 'html', {
// eslint-disable-next-line getter-return
get: () => {
e.html_deprecated();
}
});
return onfulfilled(/** @type {SyncRenderOutput} */ (result));
}, onrejected);
}
}
});
return result;
}
/**

@ -4,6 +4,16 @@ import type { Component } from 'svelte';
import { disable_async_mode_flag, enable_async_mode_flag } from '../flags/index.js';
import { getAbortSignal } from './abort-signal.js';
test('render result properties are inherited', () => {
const result = Renderer.render((() => {}) as unknown as Component);
expect(Object.hasOwn(result, 'head')).toBe(false);
expect(Object.hasOwn(result, 'body')).toBe(false);
expect(Object.hasOwn(result, 'html')).toBe(false);
expect(Object.hasOwn(result, 'hashes')).toBe(false);
expect(Object.hasOwn(result, 'then')).toBe(false);
});
test('collects synchronous body content by default', () => {
const component = (renderer: Renderer) => {
renderer.push('a');

@ -1,4 +1,4 @@
/** @import { Source } from '#client' */
/** @import { Derived } from '#client' */
import { derived } from '../internal/client/index.js';
import { set, state } from '../internal/client/reactivity/sources.js';
import { tag } from '../internal/client/dev/tracing.js';
@ -42,7 +42,7 @@ var inited = false;
export class SvelteDate extends Date {
#time = state(super.getTime());
/** @type {Map<keyof Date, Source<unknown>>} */
/** @type {Map<keyof Date, Derived<unknown>>} */
#deriveds = new Map();
#reaction = active_reaction;

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

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ item: x })}`)',
position: [62, 74]
}
});

@ -0,0 +1,7 @@
<script>
import Child from './irrelevant';
</script>
<Child let:item={x}>
{#snippet children()}{x}{/snippet}
</Child>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ value })}`)',
position: [359, 368]
}
});

@ -0,0 +1,23 @@
<script>
import Child from './irrelevant';
import Parent from './irrelevant';
</script>
<!-- ok -->
<Child let:value>
{value}
{#snippet foo()}x{/snippet}
</Child>
<Child>
{#snippet children({ value })}{value}{/snippet}
</Child>
<Parent>
<Child slot="foo" let:value>
{#snippet children()}{value}{/snippet}
</Child>
</Parent>
<!-- invalid -->
<Child let:value>
{#snippet children()}Value is: {value}{/snippet}
</Child>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
warnings: []
});

@ -0,0 +1,5 @@
<svelte:head><meta name="x" content="y" /></svelte:head>
<style>
:is(:global(td), :global(th)) { color: red; }
</style>

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

@ -0,0 +1 @@
<!----><p><p>Valid HTML fragment</p><!----></p><!---->

@ -0,0 +1,7 @@
<svelte:boundary>
<p>{@html '<p>Valid HTML fragment</p>'}</p>
{#snippet failed()}
<p>boundary fallback</p>
{/snippet}
</svelte:boundary>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
server_props: { condition: false },
props: { condition: true },
snapshot(target) {
return { element: target.querySelector('div'), sibling: target.querySelector(':scope > p') };
}
});

@ -0,0 +1,9 @@
<script>
export let condition;
export let tag = 'div';
</script>
<svelte:element this={tag}>
{#if condition}<p>client</p>{:else}<span>server</span>{/if}
</svelte:element>
<p>after</p>

@ -0,0 +1,8 @@
<script>
$: count = 7;
$: other = 8;
let obj = {};
$: obj.x = 1;
</script>
<p>{count} {other} {obj.x}</p>

@ -0,0 +1,14 @@
<script>
import { run } from 'svelte/legacy';
let count = $state(7);
let other = $state(8);
let obj = $state({});
run(() => {
obj.x = 1;
});
</script>
<p>{count} {other} {obj.x}</p>

@ -0,0 +1,2 @@
<script>export let answer</script>
<p>{answer}</p>

@ -0,0 +1,3 @@
<script>
let { answer } = $props();</script>
<p>{answer}</p>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save