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 are not style-scopable. // Prevent css hash injection (class="s-...") on tags like , , + + + {#snippet children()}{x}{/snippet} + diff --git a/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js new file mode 100644 index 0000000000..27b6a88667 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js @@ -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] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte new file mode 100644 index 0000000000..bdfe3de9b5 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte @@ -0,0 +1,23 @@ + + + + + {value} + {#snippet foo()}x{/snippet} + + + {#snippet children({ value })}{value}{/snippet} + + + + {#snippet children()}{value}{/snippet} + + + + + + {#snippet children()}Value is: {value}{/snippet} + diff --git a/packages/svelte/tests/css/samples/global-head-only/_config.js b/packages/svelte/tests/css/samples/global-head-only/_config.js new file mode 100644 index 0000000000..292c6c49ac --- /dev/null +++ b/packages/svelte/tests/css/samples/global-head-only/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + warnings: [] +}); diff --git a/packages/svelte/tests/css/samples/global-head-only/expected.css b/packages/svelte/tests/css/samples/global-head-only/expected.css new file mode 100644 index 0000000000..d2047e57d2 --- /dev/null +++ b/packages/svelte/tests/css/samples/global-head-only/expected.css @@ -0,0 +1 @@ + :is(td, th) { color: red; } diff --git a/packages/svelte/tests/css/samples/global-head-only/input.svelte b/packages/svelte/tests/css/samples/global-head-only/input.svelte new file mode 100644 index 0000000000..2ade1defdf --- /dev/null +++ b/packages/svelte/tests/css/samples/global-head-only/input.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js new file mode 100644 index 0000000000..cf22ff2c85 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html new file mode 100644 index 0000000000..8286c59709 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/_expected.html @@ -0,0 +1 @@ +

Valid HTML fragment

diff --git a/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte new file mode 100644 index 0000000000..8b9cc0770e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-mismatch-recovery/main.svelte @@ -0,0 +1,7 @@ + +

{@html '

Valid HTML fragment

'}

+ + {#snippet failed()} +

boundary fallback

+ {/snippet} +
diff --git a/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_config.js b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_config.js new file mode 100644 index 0000000000..cdd8f4484b --- /dev/null +++ b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_config.js @@ -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') }; + } +}); diff --git a/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_expected.html b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_expected.html new file mode 100644 index 0000000000..5f68a45e48 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/dynamic-element-child-mismatch/_expected.html @@ -0,0 +1,2 @@ +

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 @@ + + + + {#if condition}

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?: Partial; + * before_test?: () => void; * test?: (args: { * assert: typeof assert & { * htmlEqual(a: string, b: string, description?: string): void; diff --git a/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/_config.js b/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/_config.js new file mode 100644 index 0000000000..beabb5b1fa --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/_config.js @@ -0,0 +1,71 @@ +import { flushSync } from 'svelte'; +import { assert_ok, test } from '../../assert'; + +/** @type {Record} */ +const connections = {}; +/** @type {string[]} */ +const disconnections = []; +/** @type {Element[]} */ +let claimed; +/** @type {MutationObserver} */ +let observer; + +export default test({ + before_test() { + const target = document.querySelector('main'); + assert_ok(target); + claimed = Array.from(target.children); + customElements.define( + 'connection-probe', + class extends HTMLElement { + connectedCallback() { + connections[this.id] = (connections[this.id] || 0) + 1; + } + disconnectedCallback() { + disconnections.push(this.id); + } + } + ); + observer = new MutationObserver(() => {}); + observer.observe(target, { childList: true }); + }, + + test({ assert, component, target }) { + const removed = observer.takeRecords().flatMap((record) => Array.from(record.removedNodes)); + observer.disconnect(); + assert.deepEqual( + removed + .filter((node) => node instanceof Element) + .filter((node) => claimed.includes(node)) + .map((node) => node.id), + [] + ); + assert.deepEqual(connections, { child: 1, custom: 1 }); + assert.deepEqual(disconnections, []); + + flushSync(() => { + component.tag = 'section'; + component.empty_tag = 'span'; + component.void_tag = 'hr'; + component.custom_tag = 'aside'; + }); + assert.equal(target.querySelector('#parent')?.tagName, 'SECTION'); + assert.equal(target.querySelector('#empty')?.tagName, 'SPAN'); + assert.equal(target.querySelector('#void')?.tagName, 'HR'); + assert.equal(target.querySelector('#custom')?.tagName, 'ASIDE'); + assert.deepEqual(connections, { child: 2, custom: 1 }); + assert.deepEqual(disconnections, ['child', 'custom']); + + flushSync(() => { + component.tag = null; + }); + assert.equal(target.querySelector('#parent'), null); + assert.deepEqual(disconnections, ['child', 'custom', 'child']); + + flushSync(() => { + component.tag = 'div'; + }); + assert.equal(target.querySelector('#parent')?.tagName, 'DIV'); + assert.deepEqual(connections, { child: 3, custom: 1 }); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/main.svelte b/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/main.svelte new file mode 100644 index 0000000000..44e902188c --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/dynamic-element-hydration-preserves-connection/main.svelte @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js new file mode 100644 index 0000000000..cec1e179ac --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../assert'; + +export default test({ + async test({ assert, target }) { + const input = target.querySelector('input'); + ok(input); + + input.focus(); + + // we need to use `document.execCommand('insertText', false, ...)` to simulate user input + // because directly setting an invalid value to `input.value` would simply clear the input + // and dispatching an event would not update the input correctly + document.execCommand('insertText', false, '1'); + flushSync(); + // `1e` is incomplete on every platform, unlike `1.` which Chromium on Linux accepts as `1` + document.execCommand('insertText', false, 'e'); + flushSync(); + + assert.equal(input.value, ''); + assert.equal(input.validity.badInput, true); + + document.execCommand('insertText', false, '5'); + flushSync(); + + assert.equal(input.value, '1e5'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte new file mode 100644 index 0000000000..3e7a5cc335 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/spread-input-number-incomplete-value/main.svelte @@ -0,0 +1,5 @@ + + + (value = e.currentTarget.value)} /> diff --git a/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/_config.js b/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/_config.js new file mode 100644 index 0000000000..5557c7ebbb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-commit-effect-overlap/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +const buttons = ' '; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [a, b, , pop] = target.querySelectorAll('button'); + const shift = target.querySelectorAll('button')[2]; + + assert.htmlEqual(target.innerHTML, `${buttons}

a

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} + { errors.push(error.message); reset = fn; }}> + {track_render()} + + {must_throw ? throw_error() : ''} + + {#snippet failed(_, failed_reset)} + + {/snippet} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js new file mode 100644 index 0000000000..dd20992c26 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

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 @@ + + + + + + {#snippet failed(error)} +

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 @@
+ +foo + + + diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json index e041037d71..8e94fef00a 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json @@ -454,5 +454,17 @@ "column": 8, "line": 44 } + }, + { + "code": "a11y_no_redundant_roles", + "end": { + "column": 26, + "line": 47 + }, + "message": "Redundant role 'link'", + "start": { + "column": 15, + "line": 47 + } } ] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json new file mode 100644 index 0000000000..45e51d5f01 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_duplicate", + "message": "`item` has already been declared", + "start": { + "line": 13, + "column": 8 + }, + "end": { + "line": 13, + "column": 12 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte new file mode 100644 index 0000000000..b59496dda1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte @@ -0,0 +1,17 @@ + +{#snippet row1(item)} +{#if item} +{const item = 1} +

{item}

+{/if} +{/snippet} + +{@render row1(5)} + + +{#snippet row2(item)} + {const item = 1} +

{item}

+{/snippet} + +{@render row2(5)}