diff --git a/.changeset/calm-derived-connections.md b/.changeset/calm-derived-connections.md
new file mode 100644
index 0000000000..4ce38365ff
--- /dev/null
+++ b/.changeset/calm-derived-connections.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: prevent untracked derived reads from retaining disconnected dependencies
diff --git a/.changeset/calm-events-cleanup.md b/.changeset/calm-events-cleanup.md
deleted file mode 100644
index 4f5c2357f1..0000000000
--- a/.changeset/calm-events-cleanup.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: cancel deferred event listeners during cleanup
diff --git a/.changeset/easy-points-tan.md b/.changeset/easy-points-tan.md
deleted file mode 100644
index 60edbf94eb..0000000000
--- a/.changeset/easy-points-tan.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-perf: speed up parser interactions with Acorn or avoid them where possible
diff --git a/.changeset/green-inputs-ssr.md b/.changeset/green-inputs-ssr.md
deleted file mode 100644
index e034d0e79a..0000000000
--- a/.changeset/green-inputs-ssr.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: serialize input default values during server rendering
diff --git a/.changeset/hydration-custom-element-error-boundary.md b/.changeset/hydration-custom-element-error-boundary.md
new file mode 100644
index 0000000000..f94aa6265a
--- /dev/null
+++ b/.changeset/hydration-custom-element-error-boundary.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: restore hydration state when custom element attribute updates throw
diff --git a/.changeset/major-papayas-run.md b/.changeset/major-papayas-run.md
new file mode 100644
index 0000000000..59909fb448
--- /dev/null
+++ b/.changeset/major-papayas-run.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: flush anything pending before invoking flushSync callback function
diff --git a/.changeset/major-roses-help.md b/.changeset/major-roses-help.md
new file mode 100644
index 0000000000..c3c06792a4
--- /dev/null
+++ b/.changeset/major-roses-help.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: preserve location information for `await` wrappers
diff --git a/.changeset/migrate-keep-assignment.md b/.changeset/migrate-keep-assignment.md
new file mode 100644
index 0000000000..6b5be9d6be
--- /dev/null
+++ b/.changeset/migrate-keep-assignment.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: keep a reactive assignment with no dependencies when `migrate` cannot turn it into a declaration
diff --git a/.changeset/migrate-tag-line.md b/.changeset/migrate-tag-line.md
new file mode 100644
index 0000000000..414e4a249c
--- /dev/null
+++ b/.changeset/migrate-tag-line.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't hang `migrate` on a declaration that shares a line with its script tag
diff --git a/.changeset/olive-mice-argue.md b/.changeset/olive-mice-argue.md
deleted file mode 100644
index 6d749bcc3b..0000000000
--- a/.changeset/olive-mice-argue.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: throw `set_context_after_init` when `setContext` is called after an `await` during SSR
diff --git a/.changeset/quiet-areas-link.md b/.changeset/quiet-areas-link.md
new file mode 100644
index 0000000000..af4e31f3cb
--- /dev/null
+++ b/.changeset/quiet-areas-link.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't warn about a redundant `link` role on `` elements without an `href`
diff --git a/.changeset/quiet-inputs-keep.md b/.changeset/quiet-inputs-keep.md
new file mode 100644
index 0000000000..0efcfcc058
--- /dev/null
+++ b/.changeset/quiet-inputs-keep.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: don't overwrite an unchanged spread `value`, preserving incomplete number input
diff --git a/.changeset/short-radios-sneeze.md b/.changeset/short-radios-sneeze.md
new file mode 100644
index 0000000000..847c3eef17
--- /dev/null
+++ b/.changeset/short-radios-sneeze.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: read batch-local array on each-block commit
diff --git a/.changeset/snippet-parameter-redeclared.md b/.changeset/snippet-parameter-redeclared.md
new file mode 100644
index 0000000000..9bb14cf3af
--- /dev/null
+++ b/.changeset/snippet-parameter-redeclared.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: error at compile time when a declaration in a snippet redeclares one of its parameters
diff --git a/.changeset/sour-lets-hide.md b/.changeset/sour-lets-hide.md
new file mode 100644
index 0000000000..6175027cff
--- /dev/null
+++ b/.changeset/sour-lets-hide.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: error when using `let:` directives on a component with a `children` snippet
diff --git a/.changeset/stale-boundary-reset.md b/.changeset/stale-boundary-reset.md
new file mode 100644
index 0000000000..1f158c5713
--- /dev/null
+++ b/.changeset/stale-boundary-reset.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: ignore stale boundary reset callbacks
diff --git a/.changeset/tall-kids-juggle.md b/.changeset/tall-kids-juggle.md
deleted file mode 100644
index f116590a28..0000000000
--- a/.changeset/tall-kids-juggle.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-perf: avoid regex matching in parser where possible
diff --git a/.changeset/tidy-ducks-reset.md b/.changeset/tidy-ducks-reset.md
new file mode 100644
index 0000000000..41a00cf649
--- /dev/null
+++ b/.changeset/tidy-ducks-reset.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: handle deferred transitions aborted before initialization
diff --git a/.changeset/tidy-pandas-hydrate.md b/.changeset/tidy-pandas-hydrate.md
new file mode 100644
index 0000000000..944797a760
--- /dev/null
+++ b/.changeset/tidy-pandas-hydrate.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: prevent hydration mismatch recovery from being intercepted by error boundaries
diff --git a/.changeset/tidy-trees-stay.md b/.changeset/tidy-trees-stay.md
new file mode 100644
index 0000000000..65bf7de619
--- /dev/null
+++ b/.changeset/tidy-trees-stay.md
@@ -0,0 +1,5 @@
+---
+'svelte': patch
+---
+
+fix: preserve dynamic element connections during hydration
diff --git a/.changeset/tricky-cooks-help.md b/.changeset/tricky-cooks-help.md
deleted file mode 100644
index f61c7944a1..0000000000
--- a/.changeset/tricky-cooks-help.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: in non-async mode, only push variable to current_sources when active_reaction is updating
diff --git a/.changeset/wild-cases-smile.md b/.changeset/wild-cases-smile.md
deleted file mode 100644
index ac5edacfd1..0000000000
--- a/.changeset/wild-cases-smile.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'svelte': patch
----
-
-fix: recognise `aria-braillelabel` and `aria-brailleroledescription` as known ARIA attributes
diff --git a/benchmarking/benchmarks/reactivity/index.js b/benchmarking/benchmarks/reactivity/index.js
index 3fe9639376..d971b25e9b 100644
--- a/benchmarking/benchmarks/reactivity/index.js
+++ b/benchmarking/benchmarks/reactivity/index.js
@@ -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);
diff --git a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js
index 9a8dda617d..50634ff4e6 100644
--- a/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js
+++ b/benchmarking/benchmarks/ssr/wrapper/wrapper_bench.js
@@ -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;
}
});
}
diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md
index 2ec8985c66..8bf17f9330 100644
--- a/documentation/docs/98-reference/.generated/compile-errors.md
+++ b/documentation/docs/98-reference/.generated/compile-errors.md
@@ -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
```
diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md
index e88e078486..75592eba84 100644
--- a/packages/svelte/CHANGELOG.md
+++ b/packages/svelte/CHANGELOG.md
@@ -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
diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md
index 7fdb56ce55..797b2c53c2 100644
--- a/packages/svelte/messages/compile-errors/template.md
+++ b/packages/svelte/messages/compile-errors/template.md
@@ -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
diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index c27984e13e..b9af94c904 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -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": {
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index f43e8cae63..98f1e79279 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -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
diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js
index 76c061a275..ac667bf26b 100644
--- a/packages/svelte/src/compiler/migrate/index.js
+++ b/packages/svelte/src/compiler/migrate/index.js
@@ -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;
}
diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js
index 8edd403487..64a03630ab 100644
--- a/packages/svelte/src/compiler/phases/1-parse/index.js
+++ b/packages/svelte/src/compiler/phases/1-parse/index.js
@@ -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';
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js
index 220fe24b9d..d6871bc01e 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/element.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/element.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';
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js
index 9cc0a41375..97095e993a 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.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;
diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js
deleted file mode 100644
index 6030f1bd7b..0000000000
--- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js
+++ /dev/null
@@ -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
- }
- };
-}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
index dff6bb9b5a..0c8fdb79f2 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js
@@ -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 Valid HTML fragment
{@html '
Valid HTML fragment
'} + + {#snippet failed()} +boundary fallback
+ {/snippet} +client
after
diff --git a/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/main.svelte new file mode 100644 index 0000000000..839a06981f --- /dev/null +++ b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/main.svelte @@ -0,0 +1,9 @@ + + +client
{:else}server{/if} +after
diff --git a/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte new file mode 100644 index 0000000000..552dc3ff55 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte @@ -0,0 +1,8 @@ + + +{count} {other} {obj.x}
diff --git a/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte new file mode 100644 index 0000000000..b2ec179aec --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte @@ -0,0 +1,14 @@ + + +{count} {other} {obj.x}
\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte new file mode 100644 index 0000000000..dd623897bd --- /dev/null +++ b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte @@ -0,0 +1,2 @@ + +{answer}
diff --git a/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte new file mode 100644 index 0000000000..add32531da --- /dev/null +++ b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte @@ -0,0 +1,3 @@ + +{answer}
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index 249c5ad33d..9fdb917048 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -128,6 +128,7 @@ function normalize_children(node) { * id_prefix?: string; * props?: Props; * compileOptions?: Partiala
a
aa
1
`); + + // start two independent batches, both blocked on their awaited expression + a.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}a
a
aa
1
`); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}a
a
aa
1
`); + + // resolve the newer (b) batch first. Committing it must not commit the + // still-pending `a` batch, whose async work has not completed — `a` must + // still read 'a', and the unrelated `c` update must not be blocked + pop.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}a
b
ab
2
`); + + // stale promise from the `a` batch's first run — resolving it does nothing + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}a
b
ab
2
`); + + // the `a` batch's re-run await ('bb') resolves — everything is committed + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `${buttons}b
b
bb
2
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/main.svelte new file mode 100644 index 0000000000..ae2746a3b8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/main.svelte @@ -0,0 +1,38 @@ + + + + + + + +{a}
+{b}
+{await push(a + b)}
+{c}
diff --git a/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js new file mode 100644 index 0000000000..367da37815 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, 'Deferred outro
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte new file mode 100644 index 0000000000..396a8b2528 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte @@ -0,0 +1,24 @@ + + + + +{#if visible} +Deferred outro
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/_config.js b/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/_config.js new file mode 100644 index 0000000000..8761693873 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '', + + test({ assert, target }) { + const button = target.querySelector('button'); + flushSync(() => { + button?.click(); + }); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/main.svelte new file mode 100644 index 0000000000..7ebb37e2f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-fallback-outer-scope/main.svelte @@ -0,0 +1,10 @@ + + +{#each items as item} +{item}
+{:else} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js new file mode 100644 index 0000000000..01d57158bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js @@ -0,0 +1,50 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +/** @type {Array<() => void>} */ +const resolvers = []; + +export default test({ + transformError: (error) => new Promise((resolve) => resolvers.push(() => resolve(error))), + + async test({ assert, target, logs }) { + const [error, toggle, reset, destroy] = target.querySelectorAll('button'); + const paragraph = /** @type {HTMLParagraphElement} */ (target.querySelector('p')); + + error.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // A retained reset is inert after its boundary has been destroyed + toggle.click(); + await tick(); + reset.click(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // Resolving an error transform cannot resume a destroyed boundary + toggle.click(); + await tick(); + error.click(); + await tick(); + toggle.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // A failed snippet's reset is also inert while the boundary is being destroyed + toggle.click(); + await tick(); + error.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + destroy.click(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom,boom'); + assert.deepEqual(logs, ['render', 'render', 'render']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte new file mode 100644 index 0000000000..e8a1db83d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte @@ -0,0 +1,47 @@ + + + + + + +{errors.join(',')}
+ +{#if show} +failed: setter error
after
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte new file mode 100644 index 0000000000..a47217d7a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte @@ -0,0 +1,22 @@ + + +failed: {error.message}
+ {/snippet} +after
diff --git a/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/_config.js new file mode 100644 index 0000000000..17344b343d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../test'; +import { tick } from 'svelte'; + +export default test({ + html: `false
`, + + async test({ assert, target }) { + const [add, remove] = target.querySelectorAll('button'); + + add.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `true
` + ); + + remove.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `false
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/main.svelte new file mode 100644 index 0000000000..bd0773ae83 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/object-has-own-reactive/main.svelte @@ -0,0 +1,7 @@ + + + + +{Object.hasOwn(state, 'y')}
diff --git a/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/_config.js b/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/_config.js new file mode 100644 index 0000000000..61811ad0d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `20
` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/main.svelte new file mode 100644 index 0000000000..ed5e175c55 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-eager-declaration/main.svelte @@ -0,0 +1,7 @@ + + +{x}
diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 285dc1b3ce..afc866951f 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -15,7 +15,7 @@ import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; -import { DESTROYED } from '../../src/internal/client/constants'; +import { CONNECTED, DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; @@ -1516,12 +1516,68 @@ describe('signals', () => { destroy(); - // a was spuriously added to s.reactions via is_updating_effect + // a was spuriously added to s.reactions // even though the entire derived chain was read in an untracked context assert.equal(s.reactions, null); }; }); + test('untracked derived reads inside effects do not reconnect disconnected dependencies', () => { + return () => { + const source = state({ n: 1, items: [1] }); + const data = derived(() => $.get(source)); + const items = derived(() => $.get(data).items); + const count = derived(() => Math.max(1, $.get(items).length)); + const snapshot = derived(() => ({ n: $.get(data).n, count: $.get(count) })); + const show = state(true); + const trigger = state(0); + let rendered = -1; + let seen: { n: number; count: number } | undefined; + + const destroy = effect_root(() => { + render_effect(() => { + if ($.get(show)) { + render_effect(() => { + rendered = $.get(snapshot).count; + }); + } + }); + + render_effect(() => { + $.get(trigger); + seen = $.untrack(() => $.get(snapshot)); + }); + }); + + flushSync(); + assert.equal(rendered, 1); + + flushSync(() => set(show, false)); + assert.equal(source.reactions, null); + + flushSync(() => set(source, { n: 2, items: [1, 2] })); + flushSync(() => set(trigger, 1)); + + assert.deepEqual(seen, { n: 2, count: 2 }); + assert.equal(source.reactions, null); + assert.equal(items.reactions, null); + assert.equal(count.reactions, null); + assert.equal(items.f & CONNECTED, 0); + assert.equal(count.f & CONNECTED, 0); + + flushSync(() => set(show, true)); + assert.equal(rendered, 2); + assert.equal(source.reactions?.length, 1); + + flushSync(() => set(source, { n: 3, items: [1] })); + assert.equal(rendered, 1); + + destroy(); + flushSync(); + assert.equal(source.reactions, null); + }; + }); + // https://github.com/sveltejs/svelte/issues/18414 test('a reaction that throws after first-reading a fresh derived does not leak it', () => { const src = state(0); diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte index 9e06ebe420..68965fb5f4 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte @@ -43,6 +43,11 @@{item}
+{/if} +{/snippet} + +{@render row1(5)} + + +{#snippet row2(item)} + {const item = 1} +{item}
+{/snippet} + +{@render row2(5)}