From 0e7e873a1b9a4b5c999ccc2525172d658fc73e25 Mon Sep 17 00:00:00 2001 From: "Dominik G." Date: Thu, 10 Jul 2025 15:41:47 +0200 Subject: [PATCH 01/76] chore: update svelte-ecosystem-ci trigger (#16315) --- .github/workflows/ecosystem-ci-trigger.yml | 43 +++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 71df3242e8..7c6b740370 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -8,9 +8,17 @@ jobs: trigger: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + permissions: + issues: write # to add / delete reactions + pull-requests: read # to read PR data + actions: read # to check workflow status + contents: read # to clone the repo steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - - uses: actions/github-script@v6 + - name: monitor action permissions + uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: check user authorization # user needs triage permission + uses: actions/github-script@v7 + id: check-permissions with: script: | const user = context.payload.sender.login @@ -29,7 +37,7 @@ jobs: } if (hasTriagePermission) { - console.log('Allowed') + console.log('User is allowed. Adding +1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -37,16 +45,18 @@ jobs: content: '+1', }) } else { - console.log('Not allowed') + console.log('User is not allowed. Adding -1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: '-1', }) - throw new Error('not allowed') + throw new Error('User does not have the necessary permissions.') } - - uses: actions/github-script@v6 + + - name: Get PR Data + uses: actions/github-script@v7 id: get-pr-data with: script: | @@ -59,21 +69,27 @@ jobs: return { num: context.issue.number, branchName: pr.head.ref, + commit: pr.head.sha, repo: pr.head.repo.full_name } - - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 + + - name: Generate Token + id: generate-token + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} - private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} - repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' - - uses: actions/github-script@v6 + app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repositories: | + svelte + svelte-ecosystem-ci + + - name: Trigger Downstream Workflow + uses: actions/github-script@v7 id: trigger env: COMMENT: ${{ github.event.comment.body }} with: github-token: ${{ steps.generate-token.outputs.token }} - result-encoding: string script: | const comment = process.env.COMMENT.trim() const prData = ${{ steps.get-pr-data.outputs.result }} @@ -89,6 +105,7 @@ jobs: prNumber: '' + prData.num, branchName: prData.branchName, repo: prData.repo, + commit: prData.commit, suite: suite === '' ? '-' : suite } }) From 443e76e4baa6b57739b263d4dbb74868508ec2e1 Mon Sep 17 00:00:00 2001 From: "Dominik G." Date: Thu, 10 Jul 2025 16:56:41 +0200 Subject: [PATCH 02/76] fix: set write permission for PR in ecosystem-ci-trigger workflow (#16337) * fix: set write permission for PR in ecosystem-ci-trigger workflow * chore: match wording with vites trigger --- .github/workflows/ecosystem-ci-trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 7c6b740370..9be1f00104 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -10,7 +10,7 @@ jobs: if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') permissions: issues: write # to add / delete reactions - pull-requests: read # to read PR data + pull-requests: write # to read PR data, and to add labels actions: read # to check workflow status contents: read # to clone the repo steps: From 6f0aec527177d4c3ec56b94d1e8b21d28266bbb7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:36:10 -0400 Subject: [PATCH 03/76] chore: simplify source ownership (#16333) * simplify source ownership * rename * changeset * make it unnecessary to hand onto `current_sources` past the initial update --- .changeset/eight-walls-mate.md | 5 ++++ packages/svelte/src/internal/client/proxy.js | 29 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 4 +-- .../svelte/src/internal/client/runtime.js | 28 +++++++++--------- 4 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 .changeset/eight-walls-mate.md diff --git a/.changeset/eight-walls-mate.md b/.changeset/eight-walls-mate.md new file mode 100644 index 0000000000..a7de4e6278 --- /dev/null +++ b/.changeset/eight-walls-mate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify reaction/source ownership tracking diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 97c8da9d33..5da1b7e188 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,13 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; +import { + get, + active_effect, + update_version, + active_reaction, + set_update_version, + set_active_reaction +} from './runtime.js'; import { array_prototype, get_descriptor, @@ -41,7 +48,7 @@ export function proxy(value) { var version = source(0); var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; - var reaction = active_reaction; + var parent_version = update_version; /** * Executes the proxy in the context of the reaction it was originally created in, if any @@ -49,13 +56,23 @@ export function proxy(value) { * @param {() => T} fn */ var with_parent = (fn) => { - var previous_reaction = active_reaction; - set_active_reaction(reaction); + if (update_version === parent_version) { + return fn(); + } + + // child source is being created after the initial proxy — + // prevent it from being associated with the current reaction + var reaction = active_reaction; + var version = update_version; + + set_active_reaction(null); + set_update_version(parent_version); - /** @type {T} */ var result = fn(); - set_active_reaction(previous_reaction); + set_active_reaction(reaction); + set_update_version(version); + return result; }; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9f08354cc0..f84312e31c 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,7 +11,7 @@ import { untrack, increment_write_version, update_effect, - source_ownership, + current_sources, check_dirtiness, untracking, is_destroying_effect, @@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) { (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 && - !(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source)) + !current_sources?.includes(source) ) { e.state_unsafe_mutation(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d6e7325ba3..6477e2942a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -88,17 +88,17 @@ export function set_active_effect(effect) { /** * When sources are created within a reaction, reading and writing * them within that reaction should not cause a re-run - * @type {null | { reaction: Reaction, sources: Source[] }} + * @type {null | Source[]} */ -export let source_ownership = null; +export let current_sources = null; /** @param {Value} value */ export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { - if (source_ownership === null) { - source_ownership = { reaction: active_reaction, sources: [value] }; + if (current_sources === null) { + current_sources = [value]; } else { - source_ownership.sources.push(value); + current_sources.push(value); } } } @@ -136,6 +136,11 @@ let read_version = 0; export let update_version = read_version; +/** @param {number} value */ +export function set_update_version(value) { + update_version = value; +} + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -236,7 +241,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) var reactions = signal.reactions; if (reactions === null) return; - if (source_ownership?.reaction === active_reaction && source_ownership.sources.includes(signal)) { + if (current_sources?.includes(signal)) { return; } @@ -263,7 +268,7 @@ export function update_reaction(reaction) { var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; var previous_skip_reaction = skip_reaction; - var previous_reaction_sources = source_ownership; + var previous_sources = current_sources; var previous_component_context = component_context; var previous_untracking = untracking; var previous_update_version = update_version; @@ -277,7 +282,7 @@ export function update_reaction(reaction) { (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - source_ownership = null; + current_sources = null; set_component_context(reaction.ctx); untracking = false; update_version = ++read_version; @@ -365,7 +370,7 @@ export function update_reaction(reaction) { untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; skip_reaction = previous_skip_reaction; - source_ownership = previous_reaction_sources; + current_sources = previous_sources; set_component_context(previous_component_context); untracking = previous_untracking; update_version = previous_update_version; @@ -759,10 +764,7 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - if ( - source_ownership?.reaction !== active_reaction || - !source_ownership?.sources.includes(signal) - ) { + if (!current_sources?.includes(signal)) { var deps = active_reaction.deps; if (signal.rv < read_version) { signal.rv = read_version; From ca1eb55e970243dbed1c032e038860218325d63a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:41:02 -0400 Subject: [PATCH 04/76] Version Packages (#16334) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eight-walls-mate.md | 5 ----- .changeset/new-candles-marry.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/eight-walls-mate.md delete mode 100644 .changeset/new-candles-marry.md diff --git a/.changeset/eight-walls-mate.md b/.changeset/eight-walls-mate.md deleted file mode 100644 index a7de4e6278..0000000000 --- a/.changeset/eight-walls-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify reaction/source ownership tracking diff --git a/.changeset/new-candles-marry.md b/.changeset/new-candles-marry.md deleted file mode 100644 index 4d55980c72..0000000000 --- a/.changeset/new-candles-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify internal component `pop()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 19aa1466c0..8f158c528f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.6 + +### Patch Changes + +- chore: simplify reaction/source ownership tracking ([#16333](https://github.com/sveltejs/svelte/pull/16333)) + +- chore: simplify internal component `pop()` ([#16331](https://github.com/sveltejs/svelte/pull/16331)) + ## 5.35.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4ac497ed41..4eed145f3f 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.35.5", + "version": "5.35.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index eb68753d71..266b0b9491 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.5'; +export const VERSION = '5.35.6'; export const PUBLIC_VERSION = '5'; From e802d3b2cc0b6b5f96f853833fc6852705554a6c Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Fri, 11 Jul 2025 20:31:22 +0300 Subject: [PATCH 05/76] chore: replace inline regex with variable (#16340) * chore: replace inline regex with variable * Update packages/svelte/src/compiler/phases/patterns.js Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> * Update a11y.js * Update a11y.js --------- Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> --- .changeset/tiny-news-whisper.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/a11y.js | 6 ++++-- packages/svelte/src/compiler/phases/patterns.js | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/tiny-news-whisper.md diff --git a/.changeset/tiny-news-whisper.md b/.changeset/tiny-news-whisper.md new file mode 100644 index 0000000000..8bf877085d --- /dev/null +++ b/.changeset/tiny-news-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: replace inline regex with variable diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index 1f58a28cad..e103e3eb80 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -6,7 +6,9 @@ import { roles as roles_map, aria, elementRoles } from 'aria-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import { regex_heading_tags, + regex_js_prefix, regex_not_whitespace, + regex_redundant_img_alt, regex_starts_with_vowel, regex_whitespaces } from '../../../patterns.js'; @@ -1011,7 +1013,7 @@ export function check_element(node, context) { if (href) { const href_value = get_static_text_value(href); if (href_value !== null) { - if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) { + if (href_value === '' || href_value === '#' || regex_js_prefix.test(href_value)) { w.a11y_invalid_attribute(href, href_value, href.name); } } @@ -1061,7 +1063,7 @@ export function check_element(node, context) { const alt_attribute = get_static_text_value(attribute_map.get('alt')); const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); if (alt_attribute && !aria_hidden && !has_spread) { - if (/\b(image|picture|photo)\b/i.test(alt_attribute)) { + if (regex_redundant_img_alt.test(alt_attribute)) { w.a11y_img_redundant_alt(node); } } diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index 2bee717131..448be7f949 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -23,3 +23,5 @@ export const regex_heading_tags = /^h[1-6]$/; export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/; export const regex_bidirectional_control_characters = /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g; +export const regex_js_prefix = /^\W*javascript:/i; +export const regex_redundant_img_alt = /\b(image|picture|photo)\b/i; From 96c1a10042475c4ef8b55dfdca8538a25e792626 Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Fri, 11 Jul 2025 20:41:59 +0300 Subject: [PATCH 06/76] fix: silence autofocus a11y warning inside `` (#16341) --- .changeset/cuddly-humans-end.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/a11y.js | 2 +- .../tests/validator/samples/a11y-no-autofocus/input.svelte | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/cuddly-humans-end.md diff --git a/.changeset/cuddly-humans-end.md b/.changeset/cuddly-humans-end.md new file mode 100644 index 0000000000..d02e08de7a --- /dev/null +++ b/.changeset/cuddly-humans-end.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: silence autofocus a11y warning inside `` diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index e103e3eb80..152e679bf5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -877,7 +877,7 @@ export function check_element(node, context) { } // no-autofocus - if (name === 'autofocus') { + if (name === 'autofocus' && node.name !== 'dialog' && !is_parent(context.path, ['dialog'])) { w.a11y_autofocus(attribute); } diff --git a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte index 769dbe8c5b..7b1dccd1e8 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte @@ -1 +1,6 @@ -
\ No newline at end of file +
+ + + + + From 58baf80a70433a27a58bf5a642b4ccfd0c23349b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 13 Jul 2025 02:40:26 -0700 Subject: [PATCH 07/76] docs: add note about proxying state proxies (#16354) --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 8e6c91fad7..7c4571e575 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -50,7 +50,7 @@ todos.push({ }); ``` -> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. +> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you desire to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: From 61f75651d6a4f1106a55c2cd357751d473729e55 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 12:48:08 -0400 Subject: [PATCH 08/76] chore: re-export shared errors (#16356) --- .../process-messages/templates/client-errors.js | 2 ++ .../process-messages/templates/server-errors.js | 2 ++ packages/svelte/src/index-client.js | 11 +++++------ packages/svelte/src/internal/client/context.js | 4 ++-- packages/svelte/src/internal/client/dev/validation.js | 7 ++++--- packages/svelte/src/internal/client/errors.js | 2 ++ packages/svelte/src/internal/server/context.js | 2 +- packages/svelte/src/internal/server/dev.js | 4 ++-- packages/svelte/src/internal/server/errors.js | 2 +- packages/svelte/src/legacy/legacy-client.js | 4 ++-- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/svelte/scripts/process-messages/templates/client-errors.js b/packages/svelte/scripts/process-messages/templates/client-errors.js index c72e9f9d5e..ef749b4ba3 100644 --- a/packages/svelte/scripts/process-messages/templates/client-errors.js +++ b/packages/svelte/scripts/process-messages/templates/client-errors.js @@ -1,5 +1,7 @@ import { DEV } from 'esm-env'; +export * from '../shared/errors.js'; + /** * MESSAGE * @param {string} PARAMETER diff --git a/packages/svelte/scripts/process-messages/templates/server-errors.js b/packages/svelte/scripts/process-messages/templates/server-errors.js index 6fb7924564..0bbe801abc 100644 --- a/packages/svelte/scripts/process-messages/templates/server-errors.js +++ b/packages/svelte/scripts/process-messages/templates/server-errors.js @@ -1,3 +1,5 @@ +export * from '../shared/errors.js'; + /** * MESSAGE * @param {string} PARAMETER diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 0d962aacd1..ae1caf16d9 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -5,7 +5,6 @@ import { active_reaction, untrack } from './internal/client/runtime.js'; import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; -import { lifecycle_outside_component } from './internal/shared/errors.js'; import { legacy_mode_flag } from './internal/flags/index.js'; import { component_context } from './internal/client/context.js'; import { DEV } from 'esm-env'; @@ -91,7 +90,7 @@ export function getAbortSignal() { */ export function onMount(fn) { if (component_context === null) { - lifecycle_outside_component('onMount'); + e.lifecycle_outside_component('onMount'); } if (legacy_mode_flag && component_context.l !== null) { @@ -115,7 +114,7 @@ export function onMount(fn) { */ export function onDestroy(fn) { if (component_context === null) { - lifecycle_outside_component('onDestroy'); + e.lifecycle_outside_component('onDestroy'); } onMount(() => () => untrack(fn)); @@ -158,7 +157,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false export function createEventDispatcher() { const active_component_context = component_context; if (active_component_context === null) { - lifecycle_outside_component('createEventDispatcher'); + e.lifecycle_outside_component('createEventDispatcher'); } return (type, detail, options) => { @@ -196,7 +195,7 @@ export function createEventDispatcher() { */ export function beforeUpdate(fn) { if (component_context === null) { - lifecycle_outside_component('beforeUpdate'); + e.lifecycle_outside_component('beforeUpdate'); } if (component_context.l === null) { @@ -219,7 +218,7 @@ export function beforeUpdate(fn) { */ export function afterUpdate(fn) { if (component_context === null) { - lifecycle_outside_component('afterUpdate'); + e.lifecycle_outside_component('afterUpdate'); } if (component_context.l === null) { diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 6876a89f57..eae326f9bb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, DevStackEntry } from '#client' */ import { DEV } from 'esm-env'; -import { lifecycle_outside_component } from '../shared/errors.js'; +import * as e from './errors.js'; import { source } from './reactivity/sources.js'; import { active_effect, @@ -205,7 +205,7 @@ export function is_runes() { */ function get_or_init_context_map(name) { if (component_context === null) { - lifecycle_outside_component(name); + e.lifecycle_outside_component(name); } return (component_context.c ??= new Map(get_parent_context(component_context) || undefined)); diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js index e41e4c4628..60d140c718 100644 --- a/packages/svelte/src/internal/client/dev/validation.js +++ b/packages/svelte/src/internal/client/dev/validation.js @@ -1,15 +1,16 @@ -import { invalid_snippet_arguments } from '../../shared/errors.js'; +import * as e from '../errors.js'; /** * @param {Node} anchor * @param {...(()=>any)[]} args */ export function validate_snippet_args(anchor, ...args) { if (typeof anchor !== 'object' || !(anchor instanceof Node)) { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } + for (let arg of args) { if (typeof arg !== 'function') { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } } } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 5c3f5340e1..64dc34e8d0 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -2,6 +2,8 @@ import { DEV } from 'esm-env'; +export * from '../shared/errors.js'; + /** * Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead * @returns {never} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 4e547f48cb..bae93beb53 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,7 +1,7 @@ /** @import { Component } from '#server' */ import { DEV } from 'esm-env'; import { on_destroy } from './index.js'; -import * as e from '../shared/errors.js'; +import * as e from './errors.js'; /** @type {Component | null} */ export var current_component = null; diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index efc761d7c5..3c320f9698 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -5,7 +5,7 @@ import { is_tag_valid_with_parent } from '../../html-tree-validation.js'; import { current_component } from './context.js'; -import { invalid_snippet_arguments } from '../shared/errors.js'; +import * as e from './errors.js'; import { HeadPayload, Payload } from './payload.js'; /** @@ -102,6 +102,6 @@ export function validate_snippet_args(payload) { // for some reason typescript consider the type of payload as never after the first instanceof !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) ) { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index e47530c9aa..458937218f 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -1,6 +1,6 @@ /* This file is generated by scripts/process-messages/index.js. Do not edit! */ - +export * from '../shared/errors.js'; /** * `%name%(...)` is not available on the server diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 45c478ecab..61acbeaa28 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -4,8 +4,8 @@ import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; -import { lifecycle_outside_component } from '../internal/shared/errors.js'; import { define_property, is_array } from '../internal/shared/utils.js'; +import * as e from '../internal/client/errors.js'; import * as w from '../internal/client/warnings.js'; import { DEV } from 'esm-env'; import { FILENAME } from '../constants.js'; @@ -245,7 +245,7 @@ export function handlers(...handlers) { export function createBubbler() { const active_component_context = component_context; if (active_component_context === null) { - lifecycle_outside_component('createBubbler'); + e.lifecycle_outside_component('createBubbler'); } return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => { From 4ef53a75a9f7428efe169ab2e037e87508c3ac87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:15:02 -0400 Subject: [PATCH 09/76] chore: tidy up some stuff (#16357) * chore: tidy up some stuff * shut up dumbass --- packages/svelte/knip.json | 4 ---- packages/svelte/src/compiler/phases/scope.js | 6 +++--- packages/svelte/src/internal/client/context.js | 9 +-------- packages/svelte/src/internal/server/abort-signal.js | 2 +- packages/svelte/src/internal/shared/validate.js | 2 -- packages/svelte/src/utils.js | 2 +- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/svelte/knip.json b/packages/svelte/knip.json index 7a27a64a91..0d1bf17e9f 100644 --- a/packages/svelte/knip.json +++ b/packages/svelte/knip.json @@ -1,10 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "entry": [ - "src/*/index.js", - "src/index-client.ts", - "src/index-server.ts", - "src/index.d.ts", "tests/**/*.js", "tests/**/*.ts", "!tests/**/*.svelte", diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8a2cc39ba7..662924b52c 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -18,9 +18,9 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ -export const NUMBER = Symbol('number'); -export const STRING = Symbol('string'); -export const FUNCTION = Symbol('string'); +const NUMBER = Symbol('number'); +const STRING = Symbol('string'); +const FUNCTION = Symbol('string'); /** @type {Record} */ const globals = { diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index eae326f9bb..a9ceafcd11 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,15 +1,8 @@ /** @import { ComponentContext, DevStackEntry } from '#client' */ - import { DEV } from 'esm-env'; import * as e from './errors.js'; import { source } from './reactivity/sources.js'; -import { - active_effect, - active_reaction, - set_active_effect, - set_active_reaction -} from './runtime.js'; -import { create_user_effect, teardown } from './reactivity/effects.js'; +import { create_user_effect } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js index da579b2592..a769a46e3d 100644 --- a/packages/svelte/src/internal/server/abort-signal.js +++ b/packages/svelte/src/internal/server/abort-signal.js @@ -1,7 +1,7 @@ import { STALE_REACTION } from '#client/constants'; /** @type {AbortController | null} */ -export let controller = null; +let controller = null; export function abort() { controller?.abort(STALE_REACTION); diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index bbb237594b..8f3e2807e7 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,5 +1,3 @@ -/** @import { TemplateNode } from '#client' */ -/** @import { Getters } from '#shared' */ import { is_void } from '../../utils.js'; import * as w from './warnings.js'; import * as e from './errors.js'; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 921eaec57c..2fc21220f0 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -428,7 +428,7 @@ export function is_mathml(name) { return MATHML_ELEMENTS.includes(name); } -export const STATE_CREATION_RUNES = /** @type {const} */ ([ +const STATE_CREATION_RUNES = /** @type {const} */ ([ '$state', '$state.raw', '$derived', From a23599a196380ce7fc3fb374dac4daee173776b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 18:20:29 -0400 Subject: [PATCH 10/76] fix: don't show adjusted error messages in boundaries (#16360) --- .changeset/large-balloons-agree.md | 5 ++ .../src/internal/client/error-handling.js | 47 ++++++++++++------- .../samples/error-boundary-3/_config.js | 2 +- .../samples/error-boundary-3/main.svelte | 6 +-- 4 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 .changeset/large-balloons-agree.md diff --git a/.changeset/large-balloons-agree.md b/.changeset/large-balloons-agree.md new file mode 100644 index 0000000000..9355336862 --- /dev/null +++ b/.changeset/large-balloons-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't show adjusted error messages in boundaries diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 378f7408ef..b12f21adfc 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -7,14 +7,16 @@ import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect } from './runtime.js'; +const adjustments = new WeakMap(); + /** * @param {unknown} error */ export function handle_error(error) { var effect = /** @type {Effect} */ (active_effect); - if (DEV && error instanceof Error) { - adjust_error(error, effect); + if (DEV && error instanceof Error && !adjustments.has(error)) { + adjustments.set(error, get_adjustments(error, effect)); } if ((effect.f & EFFECT_RAN) === 0) { @@ -48,21 +50,19 @@ export function invoke_error_boundary(error, effect) { effect = effect.parent; } + if (error instanceof Error) { + apply_adjustments(error); + } + throw error; } -/** @type {WeakSet} */ -const adjusted_errors = new WeakSet(); - /** * Add useful information to the error message/stack in development * @param {Error} error * @param {Effect} effect */ -function adjust_error(error, effect) { - if (adjusted_errors.has(error)) return; - adjusted_errors.add(error); - +function get_adjustments(error, effect) { const message_descriptor = get_descriptor(error, 'message'); // if the message was already changed and it's not configurable we can't change it @@ -78,17 +78,28 @@ function adjust_error(error, effect) { context = context.p; } - define_property(error, 'message', { - value: error.message + `\n${component_stack}\n` - }); + return { + message: error.message + `\n${component_stack}\n`, + stack: error.stack + ?.split('\n') + .filter((line) => !line.includes('svelte/src/internal')) + .join('\n') + }; +} + +/** + * @param {Error} error + */ +function apply_adjustments(error) { + const adjusted = adjustments.get(error); + + if (adjusted) { + define_property(error, 'message', { + value: adjusted.message + }); - if (error.stack) { - // Filter out internal modules define_property(error, 'stack', { - value: error.stack - .split('\n') - .filter((line) => !line.includes('svelte/src/internal')) - .join('\n') + value: adjusted.stack }); } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js index 040e13676e..06da8f667c 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js @@ -9,6 +9,6 @@ export default test({ flushSync(); assert.deepEqual(logs, ['error caught']); - assert.htmlEqual(target.innerHTML, `
Fallback!
`); + assert.htmlEqual(target.innerHTML, `
oh no!
`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte index bad84666c0..bc7fe072c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte @@ -1,6 +1,6 @@ + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js new file mode 100644 index 0000000000..3de81a507b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/_config.js @@ -0,0 +1,18 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

pending

+ `, + + async test({ assert, target }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte new file mode 100644 index 0000000000..00a11cac43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute-without-state/main.svelte @@ -0,0 +1,7 @@ + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js new file mode 100644 index 0000000000..0a64738409 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/_config.js @@ -0,0 +1,29 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [cool, neat, reset] = target.querySelectorAll('button'); + + cool.click(); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.outerHTML, '

hello

'); + + reset.click(); + assert.htmlEqual(p.outerHTML, '

hello

'); + + neat.click(); + await tick(); + assert.htmlEqual(p.outerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte new file mode 100644 index 0000000000..6332a9802d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attribute/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

hello

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js new file mode 100644 index 0000000000..a29c99860d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte new file mode 100644 index 0000000000..a93eb7dc25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-destroy-during-init/main.svelte @@ -0,0 +1,28 @@ + + + + + + + {#if count % 2 === 0} +

true

+

{await push()}

+ {:else} +

false

+

{await push()}

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js new file mode 100644 index 0000000000..e2718a35d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'loading'); + await tick(); + assert.htmlEqual(target.innerHTML, 'nope'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte new file mode 100644 index 0000000000..412da7268e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-during-init/main.svelte @@ -0,0 +1,8 @@ + + {#if await Promise.reject(new Error('nope'))} + hi + {/if} + + {#snippet pending()}loading{/snippet} + {#snippet failed(e)}{e.message}{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js new file mode 100644 index 0000000000..c5dae7fee2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, resolve, reject] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + + reject.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

false

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte new file mode 100644 index 0000000000..1ad6cb84de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/main.svelte @@ -0,0 +1,29 @@ + + + + + + + + {#if count % 2 === 0} +

true

+ {#each await push() as count}

{count}

{/each} + {:else} +

false

+ {#each await push() as count}

{count}

{/each} + {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js new file mode 100644 index 0000000000..18175de4dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/_config.js @@ -0,0 +1,53 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [override, release, resolve] = target.querySelectorAll('button'); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

before

+

before

+ ` + ); + + override.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

during

+

during

+ ` + ); + + release.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

after

+

after

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte new file mode 100644 index 0000000000..256ad68f4a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-rerun/main.svelte @@ -0,0 +1,45 @@ + + + + + + + + + + {#each await indirect() as entry} +

{entry}

+ {/each} + + {#each current as entry} +

{entry}

+ {/each} + + {#snippet pending()} +

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js new file mode 100644 index 0000000000..325cb1dcd6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/_config.js @@ -0,0 +1,55 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

loading

+ `, + + async test({ assert, target }) { + target.querySelector('button')?.click(); + await tick(); + + const [button1, button2] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

A

+

a

+ ` + ); + + flushSync(() => button2.click()); + flushSync(() => button2.click()); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AA

+

aa

+ ` + ); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

AAA

+

aaa

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte new file mode 100644 index 0000000000..edb0eaea44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-child-effect/main.svelte @@ -0,0 +1,26 @@ + + + + + + +

{await push(input.toUpperCase())}

+ + {#if true} +

{input}

+ {/if} + + {#snippet pending()} +

loading

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js new file mode 100644 index 0000000000..3186ed2069 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/_config.js @@ -0,0 +1,20 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +
one
+
two
+
red
+
blue
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte new file mode 100644 index 0000000000..f0f27e4830 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-class-directive/main.svelte @@ -0,0 +1,11 @@ + +
one
+
two
+ +
red
+
blue
+ + {#snippet pending()} + loading + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte new file mode 100644 index 0000000000..fb47377513 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/Child.svelte @@ -0,0 +1,5 @@ + + +

{n}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js new file mode 100644 index 0000000000..914b311c97 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const button = target.querySelector('button'); + + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte new file mode 100644 index 0000000000..a53381c2d5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-in-if/main.svelte @@ -0,0 +1,17 @@ + + + + + + {#if show} + + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte new file mode 100644 index 0000000000..ffcd8b46b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/Child.svelte @@ -0,0 +1,9 @@ + + +

{(await d).value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js new file mode 100644 index 0000000000..fee8e2e6bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/_config.js @@ -0,0 +1,33 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, errors }) { + const [toggle, resolve1, resolve2] = target.querySelectorAll('button'); + + toggle.click(); + resolve1.click(); + resolve2.click(); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

two

+ ` + ); + + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte new file mode 100644 index 0000000000..9babdb2fe2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-invalidation-during-init/main.svelte @@ -0,0 +1,20 @@ + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte new file mode 100644 index 0000000000..f803a30c37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/Child.svelte @@ -0,0 +1,20 @@ + + +

{derived.value}{console.log(`template ${derived.value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js new file mode 100644 index 0000000000..f7d1d28fde --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/_config.js @@ -0,0 +1,93 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [reset, a, b, increment] = target.querySelectorAll('button'); + + a.click(); + + // TODO why is this necessary? why isn't `await tick()` enough? + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + flushSync(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +

42

+ ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +

84

+ ` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +

84

+ ` + ); + + b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +

86

+ ` + ); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 42 1', + 'template 42 1', + '$effect 42 1', + '$effect.pre 84 2', + 'template 84 2', + 'outside boundary 2', + '$effect 84 2', + '$effect.pre 86 2', + 'template 86 2', + '$effect 86 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte new file mode 100644 index 0000000000..2c83e1d23d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js new file mode 100644 index 0000000000..a53fbb8c6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-module/state.svelte.js @@ -0,0 +1,9 @@ +export async function create_derived(get_promise, get_num) { + let value = $derived((await get_promise()) * get_num()); + + return { + get value() { + return value; + } + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte new file mode 100644 index 0000000000..a90a9dedf7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/Component.svelte @@ -0,0 +1,29 @@ + + + + + +

{n}: {Math.min(current, 3)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js new file mode 100644 index 0000000000..016c311f98 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/_config.js @@ -0,0 +1,33 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0: 0

+ ` + ); + + const [shift, increment] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + for (let i = 1; i < 5; i += 1) { + flushSync(() => increment.click()); + } + + for (let i = 1; i < 5; i += 1) { + shift.click(); + await tick(); + + assert.equal(p.innerHTML, `${i}: ${Math.min(i, 3)}`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte new file mode 100644 index 0000000000..2d5ddca4db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unchanging/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte new file mode 100644 index 0000000000..b59fd7c08f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/Child.svelte @@ -0,0 +1,15 @@ + + +

{value}{console.log(`template ${value} ${num}`)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js new file mode 100644 index 0000000000..7239643464 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/_config.js @@ -0,0 +1,48 @@ +import { flushSync, settled, tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` + + + + +

pending

+ `, + + async test({ assert, target, logs }) { + const [resolve_a, resolve_b, reset, increment] = target.querySelectorAll('button'); + + flushSync(() => resolve_a.click()); + await tick(); + + const p = target.querySelector('p'); + ok(p); + assert.htmlEqual(p.innerHTML, '1a'); + + flushSync(() => increment.click()); + await tick(); + assert.htmlEqual(p.innerHTML, '2a'); + + reset.click(); + assert.htmlEqual(p.innerHTML, '2a'); + + resolve_b.click(); + await tick(); + assert.htmlEqual(p.innerHTML, '2b'); + + assert.deepEqual(logs, [ + 'outside boundary 1', + '$effect.pre 1a 1', + 'template 1a 1', + '$effect 1a 1', + '$effect.pre 2a 2', + 'template 2a 2', + 'outside boundary 2', + '$effect 2a 2', + '$effect.pre 2b 2', + 'template 2b 2', + '$effect 2b 2' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte new file mode 100644 index 0000000000..1404ae0299 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived/main.svelte @@ -0,0 +1,21 @@ + + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
+ +{console.log(`outside boundary ${num}`)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js new file mode 100644 index 0000000000..54aa68eeb2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

a

b

c

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

b

c

d

e

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte new file mode 100644 index 0000000000..eddcf2b749 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-await-item/main.svelte @@ -0,0 +1,39 @@ + + + + + + + + + + {#each items as deferred} +

{await deferred.promise}

+ {/each} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js new file mode 100644 index 0000000000..43d3a0f876 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: ` + + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two, three] = target.querySelectorAll('button'); + + one.click(); + await tick(); + + const [div] = target.querySelectorAll('div'); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + reset.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

a

b

c

'); + + two.click(); + await tick(); + assert.htmlEqual(div.innerHTML, '

d

e

f

g

'); + + reset.click(); + await tick(); + three.click(); + await tick(); + + assert.include(target.innerHTML, '

each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte new file mode 100644 index 0000000000..e2f8263780 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-keyed/main.svelte @@ -0,0 +1,24 @@ + + + + + + + + +

+ {#each await deferred.promise as item (item)} +

{item}

+ {/each} +
+ + {#snippet failed(e)} +

{e.message}

+ {/snippet} + + {#snippet pending()} +

pending

+ {/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js new file mode 100644 index 0000000000..50aa055130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/_config.js @@ -0,0 +1,48 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, abc, defg] = target.querySelectorAll('button'); + + abc.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

b

c

` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

b

c

` + ); + + defg.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

d

e

f

g

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte new file mode 100644 index 0000000000..8e4412811a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#each await deferred.promise as item} +

{item}

+ {/each} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js new file mode 100644 index 0000000000..2679785cff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `loading`, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'oops'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte new file mode 100644 index 0000000000..a49a5c9540 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-in-block-expression/main.svelte @@ -0,0 +1,8 @@ + + {#each (await Promise.reject(new Error('oops'))) as x} + hi + {/each} + + {#snippet pending()}loading{/snippet} + {#snippet failed()}oops{/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js new file mode 100644 index 0000000000..1613bf9c61 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/_config.js @@ -0,0 +1,87 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + +

pending...

+ `, + + compileOptions: { + // this tests some behaviour that was broken in dev + dev: true + }, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + + let [button] = target.querySelectorAll('button'); + let [p] = target.querySelectorAll('p'); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

1

+ ` + ); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + + button.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + button2.click(); + await tick(); + + [p] = target.querySelectorAll('p'); + + assert.htmlEqual( + target.innerHTML, + ` + +

4

+ ` + ); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

5

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte new file mode 100644 index 0000000000..d5246d330e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error-recovery/main.svelte @@ -0,0 +1,24 @@ + + + + + +

{await process(count)}

+ + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/_config.js b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js new file mode 100644 index 0000000000..dfbd238eeb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + let [button1, button2, button3] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

oops!

' + ); + + button2.click(); + + const reset = /** @type {HTMLButtonElement} */ (target.querySelector('[data-id="reset"]')); + reset.click(); + + assert.htmlEqual( + target.innerHTML, + '

pending

' + ); + + button3.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

wheee

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte new file mode 100644 index 0000000000..9af5bbaa16 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-error/main.svelte @@ -0,0 +1,20 @@ + + + + + + + +

{await deferred.promise}

+ + {#snippet pending()} +

pending

+ {/snippet} + + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js new file mode 100644 index 0000000000..3a66ea709f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/_config.js @@ -0,0 +1,56 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target, raf }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); + + hello.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+

updating...

+ ` + ); + + goodbye.click(); + await Promise.resolve(); + raf.tick(0); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte new file mode 100644 index 0000000000..42536ab02a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-expression/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{await deferred.promise}

+ + {#if $effect.pending()} +

updating...

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js new file mode 100644 index 0000000000..f5b1f3d2c4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, goodbye] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + goodbye.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

goodbye

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte new file mode 100644 index 0000000000..980bb16d5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-html-tag/main.svelte @@ -0,0 +1,15 @@ + + + + + + + +

{@html await deferred.promise}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js new file mode 100644 index 0000000000..3cd67952c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reset, t, f] = target.querySelectorAll('button'); + + t.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

yes

' + ); + + f.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + '

no

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte new file mode 100644 index 0000000000..21a4cbef97 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + {#if await deferred.promise} +

yes

+ {:else} +

no

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/_config.js b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js new file mode 100644 index 0000000000..e7e5db3dd8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, one, two] = target.querySelectorAll('button'); + + const html = ` + + + +

hello

+ `; + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + const h1 = target.querySelector('h1'); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + one.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + assert.equal(target.querySelector('h1'), h1); + + reset.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + + two.click(); + await tick(); + assert.htmlEqual(target.innerHTML, html); + assert.notEqual(target.querySelector('h1'), h1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte new file mode 100644 index 0000000000..5fbdbd47d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-key/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + {#key await deferred.promise} +

hello

+ {/key} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js new file mode 100644 index 0000000000..cb8e0cfca9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + const [both, a, b] = target.querySelectorAll('button'); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 * 2 = 2

+

2 * 2 = 4

+ ` + ); + + both.click(); + b.click(); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2 * 2 = 4

+

4 * 2 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte new file mode 100644 index 0000000000..432eed976c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-different-deriveds/main.svelte @@ -0,0 +1,17 @@ + + + + + + + +

{a} * 2 = {await (a * 2)}

+

{b} * 2 = {b * 2}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js new file mode 100644 index 0000000000..5e522ebdb5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/_config.js @@ -0,0 +1,30 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, reset1, reset2, resolve1, resolve2] = target.querySelectorAll('button'); + + resolve1.click(); + await tick(); + + const p = /** @type {HTMLElement} */ (target.querySelector('#test')); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + flushSync(() => reset1.click()); + flushSync(() => a.click()); + flushSync(() => reset2.click()); + flushSync(() => b.click()); + + resolve2.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '1 + 2 = 3'); + + resolve1.click(); + await tick(); + + assert.htmlEqual(p.innerHTML, '2 + 3 = 5'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte new file mode 100644 index 0000000000..cc82db0d75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-linear-order-same-derived/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + + + + + +

{a} + {b} = {await add(a, b)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte new file mode 100644 index 0000000000..546494f4c3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/Child.svelte @@ -0,0 +1,11 @@ + + +

{indirect}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js new file mode 100644 index 0000000000..172b44e6e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/_config.js @@ -0,0 +1,14 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

0

'); + + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, '

1

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte new file mode 100644 index 0000000000..f6b0afe98c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-nested-derived/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte new file mode 100644 index 0000000000..85d212b1a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -0,0 +1,5 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js new file mode 100644 index 0000000000..66690c120c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, again] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + again.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello again

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte new file mode 100644 index 0000000000..38388607ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/main.svelte @@ -0,0 +1,17 @@ + + + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js new file mode 100644 index 0000000000..f8a7cfd479 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + html: `

pending

`, + + async test({ assert, target, warnings }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

3

3

'); + + assert.equal( + warnings[0], + 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' + ); + + assert.equal(warnings[1].name, 'TracedAtError'); + + assert.equal(warnings.length, 2); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte new file mode 100644 index 0000000000..bdb1b095c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

{await a_plus_b()}

+

{await a + await b}

+ + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js new file mode 100644 index 0000000000..17bb79af08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, c, ok] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

pending...

+ ` + ); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + ok.click(); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte new file mode 100644 index 0000000000..b1bb291e2e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect-initial/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

+ {/if} + + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js new file mode 100644 index 0000000000..ebbe642860 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + ` +

a

+ + + + +

a

+ ` + ); + + const [a, b, c, ok] = target.querySelectorAll('button'); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

c

+ + + + +

c

+ ` + ); + + ok.click(); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

b

+ + + + +

b

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte new file mode 100644 index 0000000000..bf5fdf9ed3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-redirect/main.svelte @@ -0,0 +1,43 @@ + + +

{route}

+ + + + + + + {#if route === 'a'} +

a

+ {/if} + + {#if route === 'b'} + {#if ok} +

b

+ {:else} + {await goto('c')} + {/if} + {/if} + + {#if route === 'c'} +

c

+ {/if} + + {#snippet pending()} +

pending...

+ {/snippet} + + {#snippet failed(error, reset)} + + {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js new file mode 100644 index 0000000000..6f3473f592 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, hello, wheee] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + wheee.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

wheee

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte new file mode 100644 index 0000000000..b59bc319d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-render-tag/main.svelte @@ -0,0 +1,19 @@ + + + + + + +{#snippet hello(message)} +

{message}

+{/snippet} + + + {@render hello(await deferred.promise)} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte new file mode 100644 index 0000000000..c32f869f63 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/Child.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js new file mode 100644 index 0000000000..3d54d24259 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/_config.js @@ -0,0 +1,13 @@ +import { tick } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + html: ` +

loading...

+ `, + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte new file mode 100644 index 0000000000..badd60746d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-slot/main.svelte @@ -0,0 +1,13 @@ + + + + +

{message}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js new file mode 100644 index 0000000000..8276c5be41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived-2/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO this one is tricky + + async test({ assert, target }) { + const [increment, a, b] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a: 0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + a.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a: 0

+ ` + ); + + b.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

b: 0

+ let count = $state(0); + + let a = []; + let b = []; + + function push(deferreds, value) { + const deferred = Promise.withResolvers(); + deferreds.push({ deferred, value }); + return deferred.promise; + } + + + + + + + + {#if count % 2 === 0} +

a: {await push(a, count)}

+ {:else} +

b: {await push(b, count)}

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js new file mode 100644 index 0000000000..884b27d865 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/_config.js @@ -0,0 +1,52 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment, shift] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

2

+ ` + ); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

delayed: 3

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte new file mode 100644 index 0000000000..eeefffbee6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-stale-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + {#if count % 2} +

delayed: {await push()}

+ {:else} +

{await count}

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js new file mode 100644 index 0000000000..dc25be10c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,51 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

pending

+ `, + + async test({ assert, target }) { + const [reset, h1, h2] = target.querySelectorAll('button'); + + h1.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + + h2.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte new file mode 100644 index 0000000000..f8165784dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte new file mode 100644 index 0000000000..7ad618f130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js new file mode 100644 index 0000000000..73c9b50a69 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/_config.js @@ -0,0 +1,40 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, hello] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + toggle.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + + hello.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

condition is true

+

hello

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte new file mode 100644 index 0000000000..d111ce6fe3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-in-if/main.svelte @@ -0,0 +1,20 @@ + + + + + + + {#if condition} +

condition is {condition}

+ + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte new file mode 100644 index 0000000000..7ad618f130 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/Child.svelte @@ -0,0 +1,7 @@ + + +

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js new file mode 100644 index 0000000000..b2200201c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [hello] = target.querySelectorAll('button'); + + hello.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte new file mode 100644 index 0000000000..78ad3ba04a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level/main.svelte @@ -0,0 +1,15 @@ + + + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js new file mode 100644 index 0000000000..e9ccbba2b6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + + increment.click(); + await tick(); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte new file mode 100644 index 0000000000..e0619a1fe4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/main.svelte @@ -0,0 +1,19 @@ + + + + + + {#if count % 2} +

{await new Promise(() => {})}

+ {:else} +

{await count}

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js new file mode 100644 index 0000000000..e2c8b851c1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +
+

pending

+ `, + + async test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + button1.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+

pending

+ ` + ); + + button2.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +
+ +

true

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte new file mode 100644 index 0000000000..86af9bb07e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-waterfall-on-init/main.svelte @@ -0,0 +1,22 @@ + + + + + +
+ + + {#if await d1.promise} + +

{await d2.promise}

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js new file mode 100644 index 0000000000..837dd976e2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/_config.js @@ -0,0 +1,53 @@ +import { flushSync, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + const [log, x, other] = target.querySelectorAll('button'); + + flushSync(() => x.click()); + flushSync(() => other.click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1

+

1

+

1

+ ` + ); + + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

2

+

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte new file mode 100644 index 0000000000..764007e082 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-with-sync-derived/main.svelte @@ -0,0 +1,19 @@ + + + + + + + +

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index e55733c148..416f61d23a 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -1,3 +1,4 @@ +import { async_mode } from '../../../helpers'; import { test } from '../../test'; import { flushSync } from 'svelte'; @@ -10,6 +11,12 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0']); + + // With async mode (which is on by default for runtime-runes) this works as expected, without it + // it works differently: https://github.com/sveltejs/svelte/pull/15564 + assert.deepEqual( + logs, + async_mode ? ['init 0', 'cleanup 0', null, 'init 2', 'cleanup 2', null, 'init 4'] : ['init 0'] + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte index 2cdcfdfb58..da38374f82 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte @@ -14,4 +14,4 @@ }) - + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js index e092d0e7c7..f34668ec45 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-18/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + skip: true, // TODO unskip once tagged values are in and we can fix this properly + test({ assert, target }) { let btn = target.querySelector('button'); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte new file mode 100644 index 0000000000..122a316726 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/Child.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js new file mode 100644 index 0000000000..1bf7e71176 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, logs }) { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + assert.ok(logs[0].startsWith('set_context_after_init')); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte new file mode 100644 index 0000000000..65d0e623cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} + ... + {/snippet} + diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js new file mode 100644 index 0000000000..4569f42a73 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; +import { async_mode } from '../../../helpers'; + +export default test({ + async test({ target, assert, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.ok( + async_mode + ? logs[0].startsWith('set_context_after_init') + : logs[0] === 'works without experimental async but really shouldnt' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte new file mode 100644 index 0000000000..0c3b6c3a0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js index 339cec55c5..25414d4b47 100644 --- a/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/tick-timing/_config.js @@ -3,6 +3,8 @@ import { test, ok } from '../../test'; // Tests that tick only resolves after all pending effects have been cleared export default test({ + skip: true, // weirdly, this works if you run it by itself + async test({ assert, target }) { const btn = target.querySelector('button'); ok(btn); diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js index 18062b86fb..b728c3c0be 100644 --- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + // In async mode we _do_ want to run effects that react to their own state changing + skip_async: true, test({ assert, target, logs }) { const button = target.querySelector('button'); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 719c936df0..937324727b 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -6,16 +6,18 @@ import { effect, effect_root, render_effect, - user_effect + user_effect, + user_pre_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; -import type { Derived, Effect, Value } from '../../src/internal/client/types'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; 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 { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; /** * @param runes runes mode @@ -557,7 +559,7 @@ describe('signals', () => { }; }); - test('schedules rerun when writing to signal before reading it', (runes) => { + test.skip('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; const error = console.error; @@ -1049,31 +1051,85 @@ describe('signals', () => { }; }); - test('effects do not depend on state they own', () => { + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + user_effect(() => { - const value = state(0); - set(value, $.get(value) + 1); + raw = state(0); + proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); }); return () => { flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); }; }); test('nested effects depend on state of upper effects', () => { const logs: number[] = []; - user_effect(() => { + user_pre_effect(() => { const raw = state(0); const proxied = proxy({ current: 0 }); // We need those separate, else one working and rerunning the effect // could mask the other one not rerunning - user_effect(() => { + user_pre_effect(() => { logs.push($.get(raw)); }); - user_effect(() => { + user_pre_effect(() => { logs.push(proxied.current); }); @@ -1081,7 +1137,7 @@ describe('signals', () => { // together with the reading effects flushSync(); - user_effect(() => { + user_pre_effect(() => { $.untrack(() => { set(raw, $.get(raw) + 1); proxied.current += 1; diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index 77cecca7e5..ecb22c1be6 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -11,6 +11,7 @@ import { } from 'svelte/store'; import { source, set } from '../../src/internal/client/reactivity/sources'; import * as $ from '../../src/internal/client/runtime'; +import { flushSync } from '../../src/internal/client/reactivity/batch'; import { effect_root, render_effect } from 'svelte/internal/client'; describe('writable', () => { @@ -602,7 +603,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); unsubscribe(); @@ -625,7 +626,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); store.set(2); @@ -654,11 +655,11 @@ describe('fromStore', () => { assert.deepEqual(log, [0]); store.set(1); - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1]); count.current = 2; - $.flushSync(); + flushSync(); assert.deepEqual(log, [0, 1, 2]); assert.equal(get(store), 2); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 0ae06e727f..6954b8b683 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -35,7 +35,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], - should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', + should_skip_variant: (variant: Variants, config: Test, test_name: string) => boolean | 'no-test', common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { @@ -46,11 +46,11 @@ export function suite_with_variants void): void; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -443,29 +448,6 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -539,6 +521,29 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, + * have resolved and the DOM has been updated + * */ + export function settled(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; type Getters = { [K in keyof T]: () => T[K]; }; @@ -1111,6 +1116,11 @@ declare module 'svelte/compiler' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` @@ -3021,6 +3031,11 @@ declare module 'svelte/types/compiler/interfaces' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning_1) => boolean; + /** Experimental options */ + experimental?: { + /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + async?: boolean; + }; } /** * - `html` — the default, for e.g. `
` or `` diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index 845538abf0..d70409ffb6 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -14,6 +14,12 @@ import { mount, hydrate, unmount } from 'svelte'; import App from '/src/App.svelte'; + globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); + }; + const root = document.getElementById('root'); const render = root.firstChild?.nextSibling ? hydrate : mount; diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 2029937f52..639b755020 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -76,7 +76,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { dev: false, filename: input, generate, - runes: argv.values.runes + runes: argv.values.runes, + experimental: { + async: true + } }); for (const warning of compiled.warnings) { @@ -94,7 +97,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { filename: input, generate, runes: argv.values.runes, - fragments: 'tree' + fragments: 'tree', + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}.tree.js`; @@ -116,7 +122,10 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const compiled = compileModule(source, { dev: false, filename: input, - generate + generate, + experimental: { + async: true + } }); const output_js = `${cwd}/output/${generate}/${file}`; diff --git a/playgrounds/sandbox/ssr-common.js b/playgrounds/sandbox/ssr-common.js new file mode 100644 index 0000000000..db3e085508 --- /dev/null +++ b/playgrounds/sandbox/ssr-common.js @@ -0,0 +1,17 @@ +Promise.withResolvers ??= () => { + let resolve; + let reject; + + const promise = new Promise((f, r) => { + resolve = f; + reject = r; + }); + + return { promise, resolve, reject }; +}; + +globalThis.delayed = (v, ms = 1000) => { + return new Promise((f) => { + setTimeout(() => f(v), ms); + }); +}; diff --git a/playgrounds/sandbox/ssr-dev.js b/playgrounds/sandbox/ssr-dev.js index 01ce14e266..e019b234a6 100644 --- a/playgrounds/sandbox/ssr-dev.js +++ b/playgrounds/sandbox/ssr-dev.js @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url'; import polka from 'polka'; import { createServer as createViteServer } from 'vite'; import { render } from 'svelte/server'; +import './ssr-common.js'; const PORT = process.env.PORT || '5173'; diff --git a/playgrounds/sandbox/ssr-prod.js b/playgrounds/sandbox/ssr-prod.js index 1ed9435249..e8f74ee93a 100644 --- a/playgrounds/sandbox/ssr-prod.js +++ b/playgrounds/sandbox/ssr-prod.js @@ -3,6 +3,7 @@ import path from 'node:path'; import polka from 'polka'; import { render } from 'svelte/server'; import App from './src/App.svelte'; +import './ssr-common.js'; const { head, body } = render(App); diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index 65f739bdb6..68ac605385 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,5 +1,9 @@ export default { compilerOptions: { - hmr: true + hmr: false, + + experimental: { + async: true + } } }; From 63b5ebf36eea3fb1fab138068f3731b122228edb Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Mon, 14 Jul 2025 23:02:25 +0300 Subject: [PATCH 17/76] fix: silence a11y warning for inert elements (#16339) * fix: silence a11y warning for inert elements * changeset * handle `inert=""` * oops --------- Co-authored-by: Rich Harris --- .changeset/tall-avocados-repair.md | 5 +++++ .../compiler/phases/2-analyze/visitors/shared/a11y/index.js | 5 ++++- .../samples/a11y-consider-explicit-label/input.svelte | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/tall-avocados-repair.md diff --git a/.changeset/tall-avocados-repair.md b/.changeset/tall-avocados-repair.md new file mode 100644 index 0000000000..cf11fc9610 --- /dev/null +++ b/.changeset/tall-avocados-repair.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: silence a11y warning for inert elements diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js index 5db31314d4..f45a6c9a80 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js @@ -387,7 +387,10 @@ export function check_element(node, context) { switch (node.name) { case 'a': case 'button': { - const is_hidden = get_static_value(attribute_map.get('aria-hidden')) === 'true'; + const is_hidden = + get_static_value(attribute_map.get('aria-hidden')) === 'true' || + get_static_value(attribute_map.get('inert')) !== null; + if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) { w.a11y_consider_explicit_label(node); } diff --git a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte index 11dc007352..e97951065d 100644 --- a/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-consider-explicit-label/input.svelte @@ -5,6 +5,7 @@ + From 616d29f5cd439b0854280927e3ce5b91753c3fa1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:05:11 -0400 Subject: [PATCH 18/76] Version Packages (#16364) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eleven-weeks-dance.md | 5 ----- .changeset/tall-avocados-repair.md | 5 ----- .changeset/warm-olives-applaud.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/eleven-weeks-dance.md delete mode 100644 .changeset/tall-avocados-repair.md delete mode 100644 .changeset/warm-olives-applaud.md diff --git a/.changeset/eleven-weeks-dance.md b/.changeset/eleven-weeks-dance.md deleted file mode 100644 index 91245df0eb..0000000000 --- a/.changeset/eleven-weeks-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: support `await` in components when using the `experimental.async` compiler option diff --git a/.changeset/tall-avocados-repair.md b/.changeset/tall-avocados-repair.md deleted file mode 100644 index cf11fc9610..0000000000 --- a/.changeset/tall-avocados-repair.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: silence a11y warning for inert elements diff --git a/.changeset/warm-olives-applaud.md b/.changeset/warm-olives-applaud.md deleted file mode 100644 index 63a7803b99..0000000000 --- a/.changeset/warm-olives-applaud.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: clean up a11y analysis code diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 7b822ee9b1..dfa3376e25 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.36.0 + +### Minor Changes + +- feat: support `await` in components when using the `experimental.async` compiler option ([#15844](https://github.com/sveltejs/svelte/pull/15844)) + +### Patch Changes + +- fix: silence a11y warning for inert elements ([#16339](https://github.com/sveltejs/svelte/pull/16339)) + +- chore: clean up a11y analysis code ([#16345](https://github.com/sveltejs/svelte/pull/16345)) + ## 5.35.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0dc8b20f98..a9fead571d 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.35.7", + "version": "5.36.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 0674d46652..248b1a3f11 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.7'; +export const VERSION = '5.36.0'; export const PUBLIC_VERSION = '5'; From be0818552de9d282f5033bf8d1be3de34ce01794 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 16:20:14 -0400 Subject: [PATCH 19/76] chore: squelch hydration warning in test suite (#16336) --- .../runtime-runes/samples/error-boundary-9/_config.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js index 9664c233b7..9bb12e768d 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js @@ -1,7 +1,13 @@ import { test } from '../../test'; export default test({ - test({ assert, target, logs }) { + test({ assert, target, logs, warnings, variant }) { + if (variant === 'hydrate') { + assert.deepEqual(warnings, [ + 'Hydration failed because the initial UI does not match what was rendered on the server' + ]); + } + assert.deepEqual(logs, ['error caught']); assert.htmlEqual(target.innerHTML, `
Error!
`); } From 96ff125fcfa8d60af978e01e300224386d86ef01 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 21:38:53 -0400 Subject: [PATCH 20/76] chore: fix error boundary test (#16368) --- .../runtime-runes/samples/error-boundary-9/_config.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js index 9bb12e768d..9664c233b7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-9/_config.js @@ -1,13 +1,7 @@ import { test } from '../../test'; export default test({ - test({ assert, target, logs, warnings, variant }) { - if (variant === 'hydrate') { - assert.deepEqual(warnings, [ - 'Hydration failed because the initial UI does not match what was rendered on the server' - ]); - } - + test({ assert, target, logs }) { assert.deepEqual(logs, ['error caught']); assert.htmlEqual(target.innerHTML, `
Error!
`); } From 2e49783afa5185dc8ccdfede9feee3fe15248294 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 21:45:30 -0400 Subject: [PATCH 21/76] docs: fix code snippets (#16367) --- .../.generated/client-warnings.md | 19 +++++++++++++++++++ .../messages/client-warnings/warnings.md | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 1c75faef53..7548428e97 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -43,6 +43,9 @@ Detected reactivity loss when reading `%name%`. This happens when state is read Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- let total = $derived(await a + b); ``` @@ -51,6 +54,9 @@ let total = $derived(await a + b); This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this... ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- async function sum() { return await a + b; } @@ -61,6 +67,13 @@ let total = $derived(await sum()); ...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function: ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- +/** + * @param {Promise} a + * @param {number} b + */ async function sum(a, b) { return await a + b; } @@ -77,6 +90,9 @@ An async derived, `%name%` (%location%) was not read immediately after it resolv In a case like this... ```js +async function one() { return 1 } +async function two() { return 2 } +// ---cut--- let a = $derived(await one()); let b = $derived(await two()); ``` @@ -88,6 +104,9 @@ let b = $derived(await two()); You can solve this by creating the promises first and _then_ awaiting them: ```js +async function one() { return 1 } +async function two() { return 2 } +// ---cut--- let aPromise = $derived(one()); let bPromise = $derived(two()); diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 498c19a547..13d9bfcd3b 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -37,6 +37,9 @@ function add() { Svelte's signal-based reactivity works by tracking which bits of state are read when a template or `$derived(...)` expression executes. If an expression contains an `await`, Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- let total = $derived(await a + b); ``` @@ -45,6 +48,9 @@ let total = $derived(await a + b); This does _not_ apply to an `await` that is not 'visible' inside the expression. In a case like this... ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- async function sum() { return await a + b; } @@ -55,6 +61,13 @@ let total = $derived(await sum()); ...`total` will depend on `a` (which is read immediately) but not `b` (which is not). The solution is to pass the values into the function: ```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- +/** + * @param {Promise} a + * @param {number} b + */ async function sum(a, b) { return await a + b; } @@ -69,6 +82,9 @@ let total = $derived(await sum(a, b)); In a case like this... ```js +async function one() { return 1 } +async function two() { return 2 } +// ---cut--- let a = $derived(await one()); let b = $derived(await two()); ``` @@ -80,6 +96,9 @@ let b = $derived(await two()); You can solve this by creating the promises first and _then_ awaiting them: ```js +async function one() { return 1 } +async function two() { return 2 } +// ---cut--- let aPromise = $derived(one()); let bPromise = $derived(two()); From 79904b711329db9ad1927df2d5bde7eeb2909815 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 22:08:08 -0400 Subject: [PATCH 22/76] docs: tweak (#16369) --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 7c4571e575..8d3510e50c 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -50,7 +50,7 @@ todos.push({ }); ``` -> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you desire to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). +> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you need to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: From 1e4547b005d875aa1da0fa4f2f367a949700d93e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 14 Jul 2025 22:24:39 -0400 Subject: [PATCH 23/76] chore: add `@since` tags for `settled` and `experimental.async` (#16371) * chore: add `@since` tag for `settled` * same for compiler options * regenerate --- packages/svelte/src/compiler/types/index.d.ts | 10 ++++++-- .../svelte/src/internal/client/runtime.js | 1 + packages/svelte/types/index.d.ts | 23 +++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index c4f41b724a..6211e69bd3 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -224,9 +224,15 @@ export interface ModuleCompileOptions { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; - /** Experimental options */ + /** + * Experimental options + * @since 5.36 + */ experimental?: { - /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + /** + * Allow `await` keyword in deriveds, template expressions, and the top level of components + * @since 5.36 + */ async?: boolean; }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b5f6822207..9fdb87239b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -526,6 +526,7 @@ export async function tick() { * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, * have resolved and the DOM has been updated * @returns {Promise} + * @since 5.36 */ export function settled() { return Batch.ensure().settled(); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 72eb871db6..60795e6681 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -528,7 +528,8 @@ declare module 'svelte' { /** * Returns a promise that resolves once any state changes, and asynchronous work resulting from them, * have resolved and the DOM has been updated - * */ + * @since 5.36 + */ export function settled(): Promise; /** * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), @@ -1116,9 +1117,15 @@ declare module 'svelte/compiler' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning) => boolean; - /** Experimental options */ + /** + * Experimental options + * @since 5.36 + */ experimental?: { - /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + /** + * Allow `await` keyword in deriveds, template expressions, and the top level of components + * @since 5.36 + */ async?: boolean; }; } @@ -3031,9 +3038,15 @@ declare module 'svelte/types/compiler/interfaces' { * Use this to filter out warnings. Return `true` to keep the warning, `false` to discard it. */ warningFilter?: (warning: Warning_1) => boolean; - /** Experimental options */ + /** + * Experimental options + * @since 5.36 + */ experimental?: { - /** Allow `await` keyword in deriveds, template expressions, and the top level of components */ + /** + * Allow `await` keyword in deriveds, template expressions, and the top level of components + * @since 5.36 + */ async?: boolean; }; } From 9134caca242f29170125e1c9fcb71bd8e2473ae1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Jul 2025 11:55:30 -0400 Subject: [PATCH 24/76] fix: only skip updating bound `` if the input was the source of the change (#16373) * fix: only skip updating bound `` if the input was the source of the change * import Batch as type, not value --- .changeset/healthy-garlics-do.md | 5 +++ .../client/dom/elements/bindings/input.js | 14 ++++++- .../binding-update-while-focused/_config.js | 40 +++++++++++++++++++ .../binding-update-while-focused/main.svelte | 16 ++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .changeset/healthy-garlics-do.md create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte diff --git a/.changeset/healthy-garlics-do.md b/.changeset/healthy-garlics-do.md new file mode 100644 index 0000000000..c27ace34de --- /dev/null +++ b/.changeset/healthy-garlics-do.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: only skip updating bound `` if the input was the source of the change diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 569d1179e6..7c1fccea0f 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -1,3 +1,4 @@ +/** @import { Batch } from '../../../reactivity/batch.js' */ import { DEV } from 'esm-env'; import { render_effect, teardown } from '../../../reactivity/effects.js'; import { listen_to_event_and_reset_event } from './shared.js'; @@ -7,6 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; +import { current_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -17,6 +19,8 @@ import { is_runes } from '../../../context.js'; export function bind_value(input, get, set = get) { var runes = is_runes(); + var batches = new WeakSet(); + listen_to_event_and_reset_event(input, 'input', (is_reset) => { if (DEV && input.type === 'checkbox') { // TODO should this happen in prod too? @@ -28,6 +32,10 @@ export function bind_value(input, get, set = get) { value = is_numberlike_input(input) ? to_number(value) : value; set(value); + if (current_batch !== null) { + batches.add(current_batch); + } + // In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // because we use mutable state which ensures the render effect always runs) if (runes && value !== (value = get())) { @@ -54,6 +62,10 @@ export function bind_value(input, get, set = get) { (untrack(get) == null && input.value) ) { set(is_numberlike_input(input) ? to_number(input.value) : input.value); + + if (current_batch !== null) { + batches.add(current_batch); + } } render_effect(() => { @@ -64,7 +76,7 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement) { + if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js new file mode 100644 index 0000000000..1d2e6dd470 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/_config.js @@ -0,0 +1,40 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + async test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); + }); + assert.equal(input.value, '2'); + assert.htmlEqual( + target.innerHTML, + ` + +

value = 2

+ ` + ); + + flushSync(() => { + input.focus(); + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); + }); + assert.equal(input.value, '1'); + assert.htmlEqual( + target.innerHTML, + ` + +

value = 1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte new file mode 100644 index 0000000000..4cc174e404 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused/main.svelte @@ -0,0 +1,16 @@ + + + + +

value = {value}

From b23f1e0a43a06263e52b07296de57a12e53da750 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:57:52 -0400 Subject: [PATCH 25/76] Version Packages (#16374) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/healthy-garlics-do.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/healthy-garlics-do.md diff --git a/.changeset/healthy-garlics-do.md b/.changeset/healthy-garlics-do.md deleted file mode 100644 index c27ace34de..0000000000 --- a/.changeset/healthy-garlics-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: only skip updating bound `` if the input was the source of the change diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index dfa3376e25..d9f1a2629c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.1 + +### Patch Changes + +- fix: only skip updating bound `` if the input was the source of the change ([#16373](https://github.com/sveltejs/svelte/pull/16373)) + ## 5.36.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a9fead571d..aa76abaae4 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.36.0", + "version": "5.36.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 248b1a3f11..f50b7d23cc 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.0'; +export const VERSION = '5.36.1'; export const PUBLIC_VERSION = '5'; From 9f591223b1ee79fb7ec83567e22077bde912bfe7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 15 Jul 2025 15:21:06 -0400 Subject: [PATCH 26/76] docs: async stuff (#16376) * WIP async docs * WIP * docs * update $effect namespace * changeset * oops * typo * fixes * tweak * missing link * fix * tweak * Update documentation/docs/03-template-syntax/19-await-expressions.md Co-authored-by: Conduitry --------- Co-authored-by: Conduitry --- .changeset/long-drinks-reply.md | 5 + documentation/docs/02-runes/02-$state.md | 2 +- documentation/docs/02-runes/04-$effect.md | 15 ++ .../19-await-expressions.md | 144 ++++++++++++++++++ .../05-special-elements/01-svelte-boundary.md | 32 +++- packages/svelte/src/ambient.d.ts | 7 + packages/svelte/types/index.d.ts | 7 + 7 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 .changeset/long-drinks-reply.md create mode 100644 documentation/docs/03-template-syntax/19-await-expressions.md diff --git a/.changeset/long-drinks-reply.md b/.changeset/long-drinks-reply.md new file mode 100644 index 0000000000..d46b22f493 --- /dev/null +++ b/.changeset/long-drinks-reply.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `$effect.pending()` to types diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 8d3510e50c..aea427a8ec 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -119,7 +119,7 @@ class Todo { } ``` -> Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). +> [NOTE!] Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). ## `$state.raw` diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 0e129973d5..5820e178a0 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -221,6 +221,21 @@ The `$effect.tracking` rune is an advanced feature that tells you whether or not It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler). +## `$effect.pending` + +When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)): + +```svelte + + + +

{a} + {b} = {await add(a, b)}

+ +{#if $effect.pending()} +

pending promises: {$effect.pending()}

+{/if} +``` + ## `$effect.root` The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for nested effects that you want to manually control. This rune also allows for the creation of effects outside of the component initialisation phase. diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md new file mode 100644 index 0000000000..4e5ec28b26 --- /dev/null +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -0,0 +1,144 @@ +--- +title: await +--- + +As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable: + +- at the top level of your component's ` + + + + +

{a} + {b} = {await add(a, b)}

+``` + +...if you increment `a`, the contents of the `

` will _not_ immediately update to read this — + +```html +

2 + 2 = 3

+``` + +— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves. + +Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing. + +## Concurrency + +Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... + +```svelte +

{await one()}

+

{await two()}

+``` + +...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. + +This does not apply to sequential `await` expressions inside your ` + + + + + {#snippet pending()}{/snippet} + diff --git a/playgrounds/sandbox/index.html b/playgrounds/sandbox/index.html index d70409ffb6..639409b877 100644 --- a/playgrounds/sandbox/index.html +++ b/playgrounds/sandbox/index.html @@ -12,7 +12,7 @@ + + + + + +

{await push(value)}

+

{await push(value)}

+

{await push(value)}

+ +

pending: {$effect.pending()}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
+ + From 8e73fd4b0343cae992429b7e75812acf0ec6cb6c Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Wed, 16 Jul 2025 05:14:19 -0700 Subject: [PATCH 31/76] fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` (#16385) * fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` * add test * fix --- .changeset/big-readers-lie.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 2 +- .../samples/async-reactivity-loss/_config.js | 7 +++++-- .../samples/async-reactivity-loss/main.svelte | 11 +++++++---- 4 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 .changeset/big-readers-lie.md diff --git a/.changeset/big-readers-lie.md b/.changeset/big-readers-lie.md new file mode 100644 index 0000000000..9f5dd166c1 --- /dev/null +++ b/.changeset/big-readers-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 9fdb87239b..6c4d92bbad 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -609,7 +609,7 @@ export function get(signal) { var tracking = (current_async_effect.f & REACTION_IS_UPDATING) !== 0; var was_read = current_async_effect.deps?.includes(signal); - if (!tracking && !was_read) { + if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); var trace = get_stack('TracedAt'); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index f8a7cfd479..16318a3b44 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -6,11 +6,14 @@ export default test({ dev: true }, - html: `

pending

`, + html: `

pending

`, async test({ assert, target, warnings }) { await tick(); - assert.htmlEqual(target.innerHTML, '

3

3

'); + assert.htmlEqual( + target.innerHTML, + '

6

6

' + ); assert.equal( warnings[0], diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte index bdb1b095c9..03596ce051 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/main.svelte @@ -1,18 +1,21 @@ + -

{await a_plus_b()}

-

{await a + await b}

+

{await a_plus_b_plus_c()}

+

{await a + await b + await c}

{#snippet pending()}

pending

From 6cf3a193428f08a1c15b403dad681627425c0f1d Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 16 Jul 2025 16:55:15 +0300 Subject: [PATCH 32/76] fix: better handle $inspect on array mutations (#16389) * fix: better handle $inspect on array mutations * increase stack trace limit, revert test change * second changeset --------- Co-authored-by: Rich Harris --- .changeset/curvy-houses-jog.md | 5 ++ .changeset/metal-coats-thank.md | 5 ++ packages/svelte/src/internal/client/proxy.js | 60 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 40 ++++++++----- .../samples/array-delete-item/_config.js | 13 ++++ .../samples/array-delete-item/main.svelte | 8 +++ .../samples/inspect-deep-array/_config.js | 13 +--- packages/svelte/tests/suite.ts | 3 + 8 files changed, 110 insertions(+), 37 deletions(-) create mode 100644 .changeset/curvy-houses-jog.md create mode 100644 .changeset/metal-coats-thank.md create mode 100644 packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte diff --git a/.changeset/curvy-houses-jog.md b/.changeset/curvy-houses-jog.md new file mode 100644 index 0000000000..0a2323d8e4 --- /dev/null +++ b/.changeset/curvy-houses-jog.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better handle $inspect on array mutations diff --git a/.changeset/metal-coats-thank.md b/.changeset/metal-coats-thank.md new file mode 100644 index 0000000000..b307fd3d45 --- /dev/null +++ b/.changeset/metal-coats-thank.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: leave proxied array `length` untouched when deleting properties diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 5da1b7e188..3ae4b87ed5 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -15,7 +15,13 @@ import { is_array, object_prototype } from '../shared/utils.js'; -import { state as source, set, increment } from './reactivity/sources.js'; +import { + state as source, + set, + increment, + flush_inspect_effects, + set_inspect_effects_deferred +} from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -80,6 +86,9 @@ export function proxy(value) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy sources.set('length', source(/** @type {any[]} */ (value).length, stack)); + if (DEV) { + value = /** @type {any} */ (inspectable_array(/** @type {any[]} */ (value))); + } } /** Used in dev for $inspect.trace() */ @@ -142,16 +151,6 @@ export function proxy(value) { } } } else { - // When working with arrays, we need to also ensure we update the length when removing - // an indexed property - if (is_proxied_array && typeof prop === 'string') { - var ls = /** @type {Source} */ (sources.get('length')); - var n = Number(prop); - - if (Number.isInteger(n) && n < ls.v) { - set(ls, n); - } - } set(s, UNINITIALIZED); increment(version); } @@ -388,3 +387,42 @@ export function get_proxied_value(value) { export function is(a, b) { return Object.is(get_proxied_value(a), get_proxied_value(b)); } + +const ARRAY_MUTATING_METHODS = new Set([ + 'copyWithin', + 'fill', + 'pop', + 'push', + 'reverse', + 'shift', + 'sort', + 'splice', + 'unshift' +]); + +/** + * Wrap array mutating methods so $inspect is triggered only once and + * to prevent logging an array in intermediate state (e.g. with an empty slot) + * @param {any[]} array + */ +function inspectable_array(array) { + return new Proxy(array, { + get(target, prop, receiver) { + var value = Reflect.get(target, prop, receiver); + if (!ARRAY_MUTATING_METHODS.has(/** @type {string} */ (prop))) { + return value; + } + + /** + * @this {any[]} + * @param {any[]} args + */ + return function (...args) { + set_inspect_effects_deferred(); + var result = value.apply(this, args); + flush_inspect_effects(); + return result; + }; + } + }); +} diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 29c657bdd0..bd55b9d935 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -49,6 +49,12 @@ export function set_inspect_effects(v) { inspect_effects = v; } +let inspect_effects_deferred = false; + +export function set_inspect_effects_deferred() { + inspect_effects_deferred = true; +} + /** * @template V * @param {V} v @@ -213,26 +219,32 @@ export function internal_set(source, value) { } } - if (DEV && inspect_effects.size > 0) { - const inspects = Array.from(inspect_effects); + if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) { + flush_inspect_effects(); + } + } + + return value; +} - for (const effect of inspects) { - // Mark clean inspect-effects as maybe dirty and then check their dirtiness - // instead of just updating the effects - this way we avoid overfiring. - if ((effect.f & CLEAN) !== 0) { - set_signal_status(effect, MAYBE_DIRTY); - } +export function flush_inspect_effects() { + inspect_effects_deferred = false; - if (is_dirty(effect)) { - update_effect(effect); - } - } + const inspects = Array.from(inspect_effects); - inspect_effects.clear(); + for (const effect of inspects) { + // Mark clean inspect-effects as maybe dirty and then check their dirtiness + // instead of just updating the effects - this way we avoid overfiring. + if ((effect.f & CLEAN) !== 0) { + set_signal_status(effect, MAYBE_DIRTY); + } + + if (is_dirty(effect)) { + update_effect(effect); } } - return value; + inspect_effects.clear(); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js new file mode 100644 index 0000000000..7134117067 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/_config.js @@ -0,0 +1,13 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => btn?.click()); + + assert.deepEqual(logs[0], [0, , 2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte new file mode 100644 index 0000000000..ca00a85491 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-delete-item/main.svelte @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js index 0e6b12508b..49f1b5de41 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js @@ -13,17 +13,6 @@ export default test({ button?.click(); }); - assert.deepEqual(logs, [ - 'init', - [1, 2, 3, 7], - 'update', - [2, 2, 3, 7], - 'update', - [2, 3, 3, 7], - 'update', - [2, 3, 7, 7], - 'update', - [2, 3, 7] - ]); + assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]); } }); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 6954b8b683..bbd252b8e1 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -20,6 +20,9 @@ const filter = process.env.FILTER ) : /./; +// this defaults to 10, which is too low for some of our tests +Error.stackTraceLimit = 100; + export function suite(fn: (config: Test, test_dir: string) => void) { return { test: (config: Test) => config, From be44f8b23ecf14bb5945c06dd775000bcc483eaf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:45:47 -0400 Subject: [PATCH 33/76] Version Packages (#16384) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/big-readers-lie.md | 5 ----- .changeset/curvy-houses-jog.md | 5 ----- .changeset/metal-coats-thank.md | 5 ----- .changeset/popular-tips-lie.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/big-readers-lie.md delete mode 100644 .changeset/curvy-houses-jog.md delete mode 100644 .changeset/metal-coats-thank.md delete mode 100644 .changeset/popular-tips-lie.md diff --git a/.changeset/big-readers-lie.md b/.changeset/big-readers-lie.md deleted file mode 100644 index 9f5dd166c1..0000000000 --- a/.changeset/big-readers-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` diff --git a/.changeset/curvy-houses-jog.md b/.changeset/curvy-houses-jog.md deleted file mode 100644 index 0a2323d8e4..0000000000 --- a/.changeset/curvy-houses-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better handle $inspect on array mutations diff --git a/.changeset/metal-coats-thank.md b/.changeset/metal-coats-thank.md deleted file mode 100644 index b307fd3d45..0000000000 --- a/.changeset/metal-coats-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: leave proxied array `length` untouched when deleting properties diff --git a/.changeset/popular-tips-lie.md b/.changeset/popular-tips-lie.md deleted file mode 100644 index 45bd3e23f3..0000000000 --- a/.changeset/popular-tips-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: update `$effect.pending()` immediately after a batch is removed diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8c0b21364d..2ea2f58de8 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.36.3 + +### Patch Changes + +- fix: don't log `await_reactivity_loss` warning when signal is read in `untrack` ([#16385](https://github.com/sveltejs/svelte/pull/16385)) + +- fix: better handle $inspect on array mutations ([#16389](https://github.com/sveltejs/svelte/pull/16389)) + +- fix: leave proxied array `length` untouched when deleting properties ([#16389](https://github.com/sveltejs/svelte/pull/16389)) + +- fix: update `$effect.pending()` immediately after a batch is removed ([#16382](https://github.com/sveltejs/svelte/pull/16382)) + ## 5.36.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6e000e2bbf..119c4142cf 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.36.2", + "version": "5.36.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 9ccd2ff0ce..e8b0e6f1ee 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.2'; +export const VERSION = '5.36.3'; export const PUBLIC_VERSION = '5'; From 4947283fa5547691adcb129e145eed77c38f39fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 12:08:45 -0400 Subject: [PATCH 34/76] fix: avoid microtask in flushSync (#16394) * fix: avoid microtask in flushSync * fix/simplify * on second thoughts * changeset --- .changeset/soft-moles-work.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 20 ++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 .changeset/soft-moles-work.md diff --git a/.changeset/soft-moles-work.md b/.changeset/soft-moles-work.md new file mode 100644 index 0000000000..c3573e4bc5 --- /dev/null +++ b/.changeset/soft-moles-work.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid microtask in flushSync diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index f881330e90..1d08b5c3d8 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -416,19 +416,21 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } - static ensure() { + static ensure(autoflush = true) { if (current_batch === null) { const batch = (current_batch = new Batch()); batches.add(current_batch); - queueMicrotask(() => { - if (current_batch !== batch) { - // a flushSync happened in the meantime - return; - } + if (autoflush) { + queueMicrotask(() => { + if (current_batch !== batch) { + // a flushSync happened in the meantime + return; + } - batch.flush(); - }); + batch.flush(); + }); + } } return current_batch; @@ -449,7 +451,7 @@ export function flushSync(fn) { var result; - const batch = Batch.ensure(); + const batch = Batch.ensure(false); if (fn) { batch.flush_effects(); From ee1ef6083ad9fad44c677192d3dafefb4d731f1e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:20:44 +0200 Subject: [PATCH 35/76] fix: ensure compiler state is reset before compilation (#16396) #16268 introduced a slight regression where the state is not reset completely upon compilation. It did reset warnings but not other state, which meant if file A succeeds but file B fails in the parsing state (before the state was reset for real) it would get wrong filename info. This fixes it by setting the filename at the very beginning. --- .changeset/two-terms-draw.md | 5 +++ packages/svelte/src/compiler/index.js | 6 +-- packages/svelte/src/compiler/migrate/index.js | 6 +-- .../src/compiler/phases/2-analyze/index.js | 8 ++-- .../phases/2-analyze/visitors/SvelteSelf.js | 6 +-- packages/svelte/src/compiler/state.js | 37 ++++++++++++------- .../src/compiler/utils/compile_diagnostic.js | 2 +- packages/svelte/tests/compiler-errors/test.ts | 14 ++++++- 8 files changed, 55 insertions(+), 29 deletions(-) create mode 100644 .changeset/two-terms-draw.md diff --git a/.changeset/two-terms-draw.md b/.changeset/two-terms-draw.md new file mode 100644 index 0000000000..88ef4be86e --- /dev/null +++ b/.changeset/two-terms-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure compiler state is reset before compilation diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 9ba23c1485..a378af34ee 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -20,7 +20,7 @@ export { default as preprocess } from './preprocess/index.js'; */ export function compile(source, options) { source = remove_bom(source); - state.reset_warnings(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_component_options(options, ''); let parsed = _parse(source); @@ -63,7 +63,7 @@ export function compile(source, options) { */ export function compileModule(source, options) { source = remove_bom(source); - state.reset_warnings(options.warningFilter); + state.reset({ warning: options.warningFilter, filename: options.filename }); const validated = validate_module_options(options, ''); const analysis = analyze_module(source, validated); @@ -111,7 +111,7 @@ export function compileModule(source, options) { */ export function parse(source, { modern, loose } = {}) { source = remove_bom(source); - state.reset_warnings(() => false); + state.reset({ warning: () => false, filename: undefined }); const ast = _parse(source, loose); return to_public_ast(source, ast, modern); diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 0e2fe019ed..6b2e6cda70 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js'; import { regex_valid_component_name } from '../phases/1-parse/state/element.js'; import { analyze_component } from '../phases/2-analyze/index.js'; import { get_rune } from '../phases/scope.js'; -import { reset, reset_warnings } from '../state.js'; +import { reset, UNKNOWN_FILENAME } from '../state.js'; import { extract_identifiers, extract_all_identifiers_from_expression, @@ -134,7 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) { return start + style_placeholder + end; }); - reset_warnings(() => false); + reset({ warning: () => false, filename }); let parsed = parse(source); @@ -145,7 +145,7 @@ export function migrate(source, { filename, use_ts } = {}) { ...validate_component_options({}, ''), ...parsed_options, customElementOptions, - filename: filename ?? '(unknown)', + filename: filename ?? UNKNOWN_FILENAME, experimental: { async: true } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 258b59018a..d407b44556 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -279,9 +279,8 @@ export function analyze_module(source, options) { classes: new Map() }; - state.reset({ + state.adjust({ dev: options.dev, - filename: options.filename, rootDir: options.rootDir, runes: true }); @@ -531,12 +530,11 @@ export function analyze_component(root, source, options) { async_deriveds: new Set() }; - state.reset({ + state.adjust({ component_name: analysis.name, dev: options.dev, - filename: options.filename, rootDir: options.rootDir, - runes: true + runes }); if (!runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js index b87f082de0..652a447165 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteSelf.js @@ -3,7 +3,7 @@ import { visit_component } from './shared/component.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; -import { filename } from '../../../state.js'; +import { filename, UNKNOWN_FILENAME } from '../../../state.js'; /** * @param {AST.SvelteSelf} node @@ -23,9 +23,9 @@ export function SvelteSelf(node, context) { } if (context.state.analysis.runes) { - const name = filename === '(unknown)' ? 'Self' : context.state.analysis.name; + const name = filename === UNKNOWN_FILENAME ? 'Self' : context.state.analysis.name; const basename = - filename === '(unknown)' + filename === UNKNOWN_FILENAME ? 'Self.svelte' : /** @type {string} */ (filename.split(/[/\\]/).pop()); diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 5eb25dd6bb..725d03b802 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,11 @@ export let warnings = []; */ export let filename; +/** + * This is the fallback used when no filename is specified. + */ +export const UNKNOWN_FILENAME = '(unknown)'; + /** * The name of the component that is used in the `export default function ...` statement. */ @@ -80,15 +85,6 @@ export function pop_ignore() { ignore_stack.pop(); } -/** - * - * @param {(warning: Warning) => boolean} fn - */ -export function reset_warnings(fn = () => true) { - warning_filter = fn; - warnings = []; -} - /** * @param {AST.SvelteNode | NodeLike} node * @param {import('../constants.js').IGNORABLE_RUNTIME_WARNINGS[number]} code @@ -99,21 +95,36 @@ export function is_ignored(node, code) { } /** + * Call this to reset the compiler state. Should be called before each compilation. + * @param {{ warning?: (warning: Warning) => boolean; filename: string | undefined }} state + */ +export function reset(state) { + dev = false; + runes = false; + component_name = UNKNOWN_FILENAME; + source = ''; + locator = () => undefined; + filename = (state.filename ?? UNKNOWN_FILENAME).replace(/\\/g, '/'); + warning_filter = state.warning ?? (() => true); + warnings = []; +} + +/** + * Adjust the compiler state based on the provided state object. + * Call this after parsing and basic analysis happened. * @param {{ * dev: boolean; - * filename: string; * component_name?: string; * rootDir?: string; * runes: boolean; * }} state */ -export function reset(state) { +export function adjust(state) { const root_dir = state.rootDir?.replace(/\\/g, '/'); - filename = state.filename.replace(/\\/g, '/'); dev = state.dev; runes = state.runes; - component_name = state.component_name ?? '(unknown)'; + component_name = state.component_name ?? UNKNOWN_FILENAME; if (typeof root_dir === 'string' && filename.startsWith(root_dir)) { // make filename relative to rootDir diff --git a/packages/svelte/src/compiler/utils/compile_diagnostic.js b/packages/svelte/src/compiler/utils/compile_diagnostic.js index db938cf2bd..c5df49e01c 100644 --- a/packages/svelte/src/compiler/utils/compile_diagnostic.js +++ b/packages/svelte/src/compiler/utils/compile_diagnostic.js @@ -61,7 +61,7 @@ export class CompileDiagnostic { this.code = code; this.message = message; - if (state.filename) { + if (state.filename !== state.UNKNOWN_FILENAME) { this.filename = state.filename; } diff --git a/packages/svelte/tests/compiler-errors/test.ts b/packages/svelte/tests/compiler-errors/test.ts index 5e57a3a032..13b9280dde 100644 --- a/packages/svelte/tests/compiler-errors/test.ts +++ b/packages/svelte/tests/compiler-errors/test.ts @@ -1,5 +1,5 @@ import * as fs from 'node:fs'; -import { assert, expect } from 'vitest'; +import { assert, expect, it } from 'vitest'; import { compile, compileModule, type CompileError } from 'svelte/compiler'; import { suite, type BaseTest } from '../suite'; import { read_file } from '../helpers.js'; @@ -78,3 +78,15 @@ const { test, run } = suite((config, cwd) => { export { test }; await run(__dirname); + +it('resets the compiler state including filename', () => { + // start with something that succeeds + compile('
hello
', { filename: 'foo.svelte' }); + // then try something that fails in the parsing stage + try { + compile('

hello

invalid

', { filename: 'bar.svelte' }); + expect.fail('Expected an error'); + } catch (e: any) { + expect(e.toString()).toContain('bar.svelte'); + } +}); From 58788db27b33f2c86a649d603bc328fa330e8f6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:42:25 -0400 Subject: [PATCH 36/76] Version Packages (#16397) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/soft-moles-work.md | 5 ----- .changeset/two-terms-draw.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/soft-moles-work.md delete mode 100644 .changeset/two-terms-draw.md diff --git a/.changeset/soft-moles-work.md b/.changeset/soft-moles-work.md deleted file mode 100644 index c3573e4bc5..0000000000 --- a/.changeset/soft-moles-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid microtask in flushSync diff --git a/.changeset/two-terms-draw.md b/.changeset/two-terms-draw.md deleted file mode 100644 index 88ef4be86e..0000000000 --- a/.changeset/two-terms-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure compiler state is reset before compilation diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2ea2f58de8..6d4f31480d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.4 + +### Patch Changes + +- fix: avoid microtask in flushSync ([#16394](https://github.com/sveltejs/svelte/pull/16394)) + +- fix: ensure compiler state is reset before compilation ([#16396](https://github.com/sveltejs/svelte/pull/16396)) + ## 5.36.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 119c4142cf..3e9022a091 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.36.3", + "version": "5.36.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e8b0e6f1ee..f8d23b44b1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.3'; +export const VERSION = '5.36.4'; export const PUBLIC_VERSION = '5'; From c11c5ec0e381fc45f9f6ead1bce42f219eb8de61 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 14:41:16 -0400 Subject: [PATCH 37/76] docs: tweak createSubscriber explanation (#16398) * docs: tweak createSubscriber explanation * regenerate --- packages/svelte/src/reactivity/create-subscriber.js | 10 +++++++--- packages/svelte/types/index.d.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index afcea9c5b4..4dcac4e6f6 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -6,10 +6,13 @@ import { DEV } from 'esm-env'; import { queue_micro_task } from '../internal/client/dom/task.js'; /** - * Returns a `subscribe` function that, if called in an effect (including expressions in the template), - * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. + * It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. * - * If `start` returns a function, it will be called when the effect is destroyed. + * If `subscribe` is called inside an effect (including indirectly, for example inside a getter), + * the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + * + * If `start` returns a cleanup function, it will be called when the effect is destroyed. * * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects * are active, and the returned teardown function will only be called when all effects are destroyed. @@ -37,6 +40,7 @@ import { queue_micro_task } from '../internal/client/dom/task.js'; * } * * get current() { + * // This makes the getter reactive, if read in an effect * this.#subscribe(); * * // Return the current state of the query, whether or not we're in an effect diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d356762a3f..a8b769d6d4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2335,10 +2335,13 @@ declare module 'svelte/reactivity' { constructor(query: string, fallback?: boolean | undefined); } /** - * Returns a `subscribe` function that, if called in an effect (including expressions in the template), - * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity. + * It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`. * - * If `start` returns a function, it will be called when the effect is destroyed. + * If `subscribe` is called inside an effect (including indirectly, for example inside a getter), + * the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs. + * + * If `start` returns a cleanup function, it will be called when the effect is destroyed. * * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects * are active, and the returned teardown function will only be called when all effects are destroyed. @@ -2366,6 +2369,7 @@ declare module 'svelte/reactivity' { * } * * get current() { + * // This makes the getter reactive, if read in an effect * this.#subscribe(); * * // Return the current state of the query, whether or not we're in an effect From 45cd000890202aff68e4c8aeeead1203ee369c7b Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:44:03 -0700 Subject: [PATCH 38/76] chore: fix peer dependency warning (#16401) --- package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 3e609db87f..458bf34084 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@sveltejs/eslint-config": "^8.1.0", "@svitejs/changesets-changelog-github-compact": "^1.1.0", "@types/node": "^20.11.5", - "@vitest/coverage-v8": "^2.0.5", + "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.9.1", "eslint-plugin-lube": "^0.4.3", "jsdom": "25.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0982dc791..5f902186ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^20.11.5 version: 20.12.7 '@vitest/coverage-v8': - specifier: ^2.0.5 - version: 2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + specifier: ^2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) eslint: specifier: ^9.9.1 version: 9.9.1 @@ -869,10 +869,14 @@ packages: resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@2.0.5': - resolution: {integrity: sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==} + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} peerDependencies: - vitest: 2.0.5 + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -1683,8 +1687,8 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magicast@0.3.4: - resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -3173,7 +3177,7 @@ snapshots: '@typescript-eslint/types': 8.32.1 eslint-visitor-keys: 4.2.0 - '@vitest/coverage-v8@2.0.5(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -3183,7 +3187,7 @@ snapshots: istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.1.7 magic-string: 0.30.17 - magicast: 0.3.4 + magicast: 0.3.5 std-env: 3.8.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 @@ -4045,7 +4049,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - magicast@0.3.4: + magicast@0.3.5: dependencies: '@babel/parser': 7.25.4 '@babel/types': 7.25.4 @@ -4053,7 +4057,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.2 merge2@1.4.1: {} From ead409120229a3c8677d357823b86242fa196dda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 18:54:45 -0400 Subject: [PATCH 39/76] docs: diligently describe destructured derived declarations (#16400) --- documentation/docs/02-runes/03-$derived.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 2464aa9295..5f253cf6d1 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -94,6 +94,23 @@ let selected = $derived(items[index]); ...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect. +## Destructuring + +If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this... + +```js +let { a, b, c } = $derived(stuff()); +``` + +...is roughly equivalent to this: + +```js +let _stuff = $derived(stuff()); +let a = $derived(_stuff.a); +let b = $derived(_stuff.b); +let c = $derived(_stuff.c); +``` + ## Update propagation Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull'). From 3edebd51035404c30c6c9f694c79037e2f93d433 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 18:54:56 -0400 Subject: [PATCH 40/76] chore: update to new pnpm syntax (#16399) --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e2628f84f..c2d3e45049 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,10 +105,10 @@ Test samples are kept in `/test/xxx/samples` folder. pnpm test validator ``` -1. To filter tests _within_ a test suite, use `pnpm test -- -t `, for example: +1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: ```bash - pnpm test validator -- -t a11y-alt-text + pnpm test validator -t a11y-alt-text ``` (You can also do `FILTER= pnpm test ` which removes other tests rather than simply skipping them — this will result in faster and more compact test results, but it's non-idiomatic. Choose your fighter.) From 09c9a3c16533d2223e17700684718b4829cda9c6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 18:55:24 -0400 Subject: [PATCH 41/76] fix: silence `$inspect` errors when the effect is about to be destroyed (#16391) * fix: silence `$inspect` errors when the effect is about to be destroyed * changeset --- .changeset/six-swans-rush.md | 5 ++ .../svelte/src/internal/client/dev/inspect.js | 51 ++++++++++++------- .../samples/inspect-exception/_config.js | 2 +- 3 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 .changeset/six-swans-rush.md diff --git a/.changeset/six-swans-rush.md b/.changeset/six-swans-rush.md new file mode 100644 index 0000000000..cfb5b97400 --- /dev/null +++ b/.changeset/six-swans-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: silence `$inspect` errors when the effect is about to be destroyed diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index e15c66901c..c593f2622c 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -1,6 +1,6 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; -import { inspect_effect, validate_effect } from '../reactivity/effects.js'; +import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js'; import { untrack } from '../runtime.js'; /** @@ -12,29 +12,44 @@ export function inspect(get_value, inspector = console.log) { validate_effect('$inspect'); let initial = true; + let error = /** @type {any} */ (UNINITIALIZED); + // Inspect effects runs synchronously so that we can capture useful + // stack traces. As a consequence, reading the value might result + // in an error (an `$inspect(object.property)` will run before the + // `{#if object}...{/if}` that contains it) inspect_effect(() => { - /** @type {any} */ - var value = UNINITIALIZED; - - // Capturing the value might result in an exception due to the inspect effect being - // sync and thus operating on stale data. In the case we encounter an exception we - // can bail-out of reporting the value. Instead we simply console.error the error - // so at least it's known that an error occured, but we don't stop execution try { - value = get_value(); - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); + var value = get_value(); + } catch (e) { + error = e; + return; } - if (value !== UNINITIALIZED) { - var snap = snapshot(value, true); - untrack(() => { - inspector(initial ? 'init' : 'update', ...snap); - }); - } + var snap = snapshot(value, true); + untrack(() => { + inspector(initial ? 'init' : 'update', ...snap); + }); initial = false; }); + + // If an error occurs, we store it (along with its stack trace). + // If the render effect subsequently runs, we log the error, + // but if it doesn't run it's because the `$inspect` was + // destroyed, meaning we don't need to bother + render_effect(() => { + try { + // call `get_value` so that this runs alongside the inspect effect + get_value(); + } catch { + // ignore + } + + if (error !== UNINITIALIZED) { + // eslint-disable-next-line no-console + console.error(error); + error = UNINITIALIZED; + } + }); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js index e155ff236a..83e0eb9443 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-exception/_config.js @@ -11,7 +11,7 @@ export default test({ b1?.click(); flushSync(); - assert.ok(errors.length > 0); + assert.equal(errors.length, 0); assert.deepEqual(logs, ['init', 'a', 'init', 'b']); } }); From a67b5862f15ce763c0d3a038f88abbb4b08d2d80 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 19:24:57 -0400 Subject: [PATCH 42/76] fix: more informative error when effects run in an infinite loop (#16405) * update effect_update_depth_exceeded docs * log update locations * remove dev_effect_stack stuff, it's not very helpful * tidy up * test * fix test * changeset * fix --- .changeset/slimy-doors-fetch.md | 5 ++ .../98-reference/.generated/client-errors.md | 40 ++++++++++- .../svelte/messages/client-errors/errors.md | 40 ++++++++++- .../svelte/src/internal/client/dev/tracing.js | 64 +++++++++-------- packages/svelte/src/internal/client/errors.js | 4 +- .../src/internal/client/reactivity/batch.js | 71 ++++++++----------- .../src/internal/client/reactivity/sources.js | 18 ++++- .../src/internal/client/reactivity/types.d.ts | 4 +- .../svelte/src/internal/client/runtime.js | 12 +--- .../samples/effect-loop-infinite/_config.js | 21 ++++++ .../samples/effect-loop-infinite/main.svelte | 12 ++++ .../samples/error-boundary-20/_config.js | 4 +- 12 files changed, 202 insertions(+), 93 deletions(-) create mode 100644 .changeset/slimy-doors-fetch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte diff --git a/.changeset/slimy-doors-fetch.md b/.changeset/slimy-doors-fetch.md new file mode 100644 index 0000000000..8dec24a98d --- /dev/null +++ b/.changeset/slimy-doors-fetch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: more informative error when effects run in an infinite loop diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index c7d6ec8ac9..3b17ef9f9b 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -89,9 +89,47 @@ Effect cannot be created inside a `$derived` value that was not itself created i ### effect_update_depth_exceeded ``` -Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops +Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state ``` +If an effect updates some state that it also depends on, it will re-run, potentially in a loop: + +```js +let count = $state(0); + +$effect(() => { + // this both reads and writes `count`, + // so will run in an infinite loop + count += 1; +}); +``` + +(Svelte intervenes before this can crash your browser tab.) + +The same applies to array mutations, since these both read and write to the array: + +```js +let array = $state([]); + +$effect(() => { + array.push('hello'); +}); +``` + +Note that it's fine for an effect to re-run itself as long as it 'settles': + +```js +let array = ['a', 'b', 'c']; +// ---cut--- +$effect(() => { + // this is okay, because sorting an already-sorted array + // won't result in a mutation + array.sort(); +}); +``` + +Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency. + ### flush_sync_in_effect ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 2ce25dbd53..d6af859881 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -60,7 +60,45 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## effect_update_depth_exceeded -> Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops +> Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state + +If an effect updates some state that it also depends on, it will re-run, potentially in a loop: + +```js +let count = $state(0); + +$effect(() => { + // this both reads and writes `count`, + // so will run in an infinite loop + count += 1; +}); +``` + +(Svelte intervenes before this can crash your browser tab.) + +The same applies to array mutations, since these both read and write to the array: + +```js +let array = $state([]); + +$effect(() => { + array.push('hello'); +}); +``` + +Note that it's fine for an effect to re-run itself as long as it 'settles': + +```js +let array = ['a', 'b', 'c']; +// ---cut--- +$effect(() => { + // this is okay, because sorting an already-sorted array + // won't result in a mutation + array.sort(); +}); +``` + +Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency. ## flush_sync_in_effect diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 128942ceb2..b7a6a38548 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -56,8 +56,10 @@ function log_entry(signal, entry) { } if (dirty && signal.updated) { - // eslint-disable-next-line no-console - console.log(signal.updated); + for (const updated of signal.updated.values()) { + // eslint-disable-next-line no-console + console.log(updated.error); + } } if (entry) { @@ -120,44 +122,46 @@ export function trace(label, fn) { /** * @param {string} label + * @returns {Error & { stack: string } | null} */ export function get_stack(label) { let error = Error(); const stack = error.stack; - if (stack) { - const lines = stack.split('\n'); - const new_lines = ['\n']; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - - if (line === 'Error') { - continue; - } - if (line.includes('validate_each_keys')) { - return null; - } - if (line.includes('svelte/src/internal')) { - continue; - } - new_lines.push(line); - } + if (!stack) return null; - if (new_lines.length === 1) { + const lines = stack.split('\n'); + const new_lines = ['\n']; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line === 'Error') { + continue; + } + if (line.includes('validate_each_keys')) { return null; } + if (line.includes('svelte/src/internal')) { + continue; + } + new_lines.push(line); + } - define_property(error, 'stack', { - value: new_lines.join('\n') - }); - - define_property(error, 'name', { - // 'Error' suffix is required for stack traces to be rendered properly - value: `${label}Error` - }); + if (new_lines.length === 1) { + return null; } - return error; + + define_property(error, 'stack', { + value: new_lines.join('\n') + }); + + define_property(error, 'name', { + // 'Error' suffix is required for stack traces to be rendered properly + value: `${label}Error` + }); + + return /** @type {Error & { stack: string }} */ (error); } /** diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index a491dc683d..edd66a7400 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -214,12 +214,12 @@ export function effect_pending_outside_reaction() { } /** - * Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops + * Maximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state * @returns {never} */ export function effect_update_depth_exceeded() { if (DEV) { - const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops\nhttps://svelte.dev/e/effect_update_depth_exceeded`); + const error = new Error(`effect_update_depth_exceeded\nMaximum update depth exceeded. This typically indicates that an effect reads and writes the same piece of state\nhttps://svelte.dev/e/effect_update_depth_exceeded`); error.name = 'Svelte error'; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 1d08b5c3d8..cdce971b18 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -46,9 +46,6 @@ export let current_batch = null; */ export let batch_deriveds = null; -/** @type {Effect[]} Stack of effects, dev only */ -export let dev_effect_stack = []; - /** @type {Set<() => void>} */ export let effect_pending_updates = new Set(); @@ -345,6 +342,28 @@ export class Batch { while (queued_root_effects.length > 0) { if (flush_count++ > 1000) { + if (DEV) { + var updates = new Map(); + + for (const source of this.#current.keys()) { + for (const [stack, update] of source.updated ?? []) { + var entry = updates.get(stack); + + if (!entry) { + entry = { error: update.error, count: 0 }; + updates.set(stack, entry); + } + + entry.count += update.count; + } + } + + for (const update of updates.values()) { + // eslint-disable-next-line no-console + console.error(update.error); + } + } + infinite_loop_guard(); } @@ -356,9 +375,6 @@ export class Batch { set_is_updating_effect(was_updating_effect); last_scheduled_effect = null; - if (DEV) { - dev_effect_stack = []; - } } } @@ -471,10 +487,6 @@ export function flushSync(fn) { // we need to reset it here as well in case the first time there's 0 queued root effects last_scheduled_effect = null; - if (DEV) { - dev_effect_stack = []; - } - return /** @type {T} */ (result); } @@ -482,45 +494,18 @@ export function flushSync(fn) { } } -function log_effect_stack() { - // eslint-disable-next-line no-console - console.error( - 'Last ten effects were: ', - dev_effect_stack.slice(-10).map((d) => d.fn) - ); - dev_effect_stack = []; -} - function infinite_loop_guard() { try { e.effect_update_depth_exceeded(); } catch (error) { if (DEV) { - // stack is garbage, ignore. Instead add a console.error message. - define_property(error, 'stack', { - value: '' - }); - } - // Try and handle the error so it can be caught at a boundary, that's - // if there's an effect available from when it was last scheduled - if (last_scheduled_effect !== null) { - if (DEV) { - try { - invoke_error_boundary(error, last_scheduled_effect); - } catch (e) { - // Only log the effect stack if the error is re-thrown - log_effect_stack(); - throw e; - } - } else { - invoke_error_boundary(error, last_scheduled_effect); - } - } else { - if (DEV) { - log_effect_stack(); - } - throw error; + // stack contains no useful information, replace it + define_property(error, 'stack', { value: '' }); } + + // Best effort: invoke the boundary nearest the most recent + // effect and hope that it's relevant to the infinite loop + invoke_error_boundary(error, last_scheduled_effect); } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index bd55b9d935..9b534d2d71 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -182,8 +182,22 @@ export function internal_set(source, value) { const batch = Batch.ensure(); batch.capture(source, old_value); - if (DEV && tracing_mode_flag) { - source.updated = get_stack('UpdatedAt'); + if (DEV) { + if (tracing_mode_flag || active_effect !== null) { + const error = get_stack('UpdatedAt'); + + if (error !== null) { + source.updated ??= new Map(); + let entry = source.updated.get(error.stack); + + if (!entry) { + entry = { error, count: 0 }; + source.updated.set(error.stack, entry); + } + + entry.count++; + } + } if (active_effect !== null) { source.set_during_effect = true; diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 90f922df68..72187e84a7 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -29,8 +29,8 @@ export interface Value extends Signal { label?: string; /** An error with a stack trace showing when the source was created */ created?: Error | null; - /** An error with a stack trace showing when the source was last updated */ - updated?: Error | null; + /** An map of errors with stack traces showing when the source was updated, keyed by the stack trace */ + updated?: Map | null; /** * Whether or not the source was set while running an effect — if so, we need to * increment the write version so that it shows up as dirty when the effect re-runs diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6c4d92bbad..306b9b9dd9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -42,13 +42,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { - Batch, - batch_deriveds, - dev_effect_stack, - flushSync, - schedule_effect -} from './reactivity/batch.js'; +import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; @@ -491,10 +485,6 @@ export function update_effect(effect) { } } } - - if (DEV) { - dev_effect_stack.push(effect); - } } finally { is_updating_effect = was_updating_effect; active_effect = previous_effect; diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js new file mode 100644 index 0000000000..400495050c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + + compileOptions: { + dev: true + }, + + test({ assert, errors }) { + const [button] = document.querySelectorAll('button'); + + try { + flushSync(() => button.click()); + } catch (e) { + assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError + assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte new file mode 100644 index 0000000000..ddb91a90ad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/main.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js index ccff614ade..e3a3b0c7d7 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-20/_config.js @@ -2,12 +2,14 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - test({ assert, target }) { + test({ assert, target, errors }) { let btn = target.querySelector('button'); btn?.click(); flushSync(); + assert.equal(errors.length, 1); + assert.htmlEqual(target.innerHTML, `
An error occurred!
`); } }); From fcfbc9cca0720d2d57cf6a594f099f568dedfe88 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:01:58 -0400 Subject: [PATCH 43/76] Version Packages (#16406) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/six-swans-rush.md | 5 ----- .changeset/slimy-doors-fetch.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/six-swans-rush.md delete mode 100644 .changeset/slimy-doors-fetch.md diff --git a/.changeset/six-swans-rush.md b/.changeset/six-swans-rush.md deleted file mode 100644 index cfb5b97400..0000000000 --- a/.changeset/six-swans-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: silence `$inspect` errors when the effect is about to be destroyed diff --git a/.changeset/slimy-doors-fetch.md b/.changeset/slimy-doors-fetch.md deleted file mode 100644 index 8dec24a98d..0000000000 --- a/.changeset/slimy-doors-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: more informative error when effects run in an infinite loop diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6d4f31480d..56f91c395f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.5 + +### Patch Changes + +- fix: silence `$inspect` errors when the effect is about to be destroyed ([#16391](https://github.com/sveltejs/svelte/pull/16391)) + +- fix: more informative error when effects run in an infinite loop ([#16405](https://github.com/sveltejs/svelte/pull/16405)) + ## 5.36.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 3e9022a091..2a1b3cf9e5 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.36.4", + "version": "5.36.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f8d23b44b1..4d6811c409 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.4'; +export const VERSION = '5.36.5'; export const PUBLIC_VERSION = '5'; From c56914d739d70c1ba444c2a4ac9df37db7a033cf Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:08:54 -0700 Subject: [PATCH 44/76] chore: upgrade to eslint-plugin-svelte 3 (#16407) --- eslint.config.js | 3 +- package.json | 3 +- pnpm-lock.yaml | 107 ++++++++++++++++++++++++----------------------- 3 files changed, 58 insertions(+), 55 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 41d98fa428..b0f25bab03 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -80,7 +80,8 @@ export default [ files: ['packages/svelte/src/**/*'], ignores: ['packages/svelte/src/compiler/**/*'], rules: { - 'custom/no_compiler_imports': 'error' + 'custom/no_compiler_imports': 'error', + 'svelte/no-svelte-internal': 'off' } }, { diff --git a/package.json b/package.json index 458bf34084..971bd020d1 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,13 @@ }, "devDependencies": { "@changesets/cli": "^2.27.8", - "@sveltejs/eslint-config": "^8.1.0", + "@sveltejs/eslint-config": "^8.3.3", "@svitejs/changesets-changelog-github-compact": "^1.1.0", "@types/node": "^20.11.5", "@vitest/coverage-v8": "^2.1.9", "eslint": "^9.9.1", "eslint-plugin-lube": "^0.4.3", + "eslint-plugin-svelte": "^3.11.0", "jsdom": "25.0.1", "playwright": "^1.46.1", "prettier": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f902186ef..315d699e25 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^2.27.8 version: 2.27.8 '@sveltejs/eslint-config': - specifier: ^8.1.0 - version: 8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) + specifier: ^8.3.3 + version: 8.3.3(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -29,6 +29,9 @@ importers: eslint-plugin-lube: specifier: ^0.4.3 version: 0.4.3 + eslint-plugin-svelte: + specifier: ^3.11.0 + version: 3.11.0(eslint@9.9.1)(svelte@packages+svelte) jsdom: specifier: 25.0.1 version: 25.0.1 @@ -737,16 +740,16 @@ packages: peerDependencies: acorn: ^8.9.0 - '@sveltejs/eslint-config@8.1.0': - resolution: {integrity: sha512-cfgp4lPREYBjNd4ZzaP/jA85ufm7vfXiaV7h9vILXNogne80IbZRNhRCQ8XoOqTAOY/pChIzWTBuR8aDNMbAEA==} + '@sveltejs/eslint-config@8.3.3': + resolution: {integrity: sha512-vkrQgEmhokFEOpuTo7NlVXJJMJJGNzxjmkQCTkHSwIOdzQSUukDIJ4038IjdcnIERSIlo4OpLAydWLx52BVyQA==} peerDependencies: '@stylistic/eslint-plugin-js': '>= 1' eslint: '>= 9' eslint-config-prettier: '>= 9' eslint-plugin-n: '>= 17' - eslint-plugin-svelte: '>= 2.36' + eslint-plugin-svelte: '>= 3' typescript: '>= 5' - typescript-eslint: '>= 7.5' + typescript-eslint: '>= 8' '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2': resolution: {integrity: sha512-Yl9BWvEj+1j+8mICIAA04/Sx0wEHNL0n9pSIZFM8n4NWgLFmR3v41qX2k54J/r4LWE2YHTeNNH2WJqEUb3geEA==} @@ -1209,24 +1212,24 @@ packages: peerDependencies: eslint: '>=8.23.0' - eslint-plugin-svelte@2.38.0: - resolution: {integrity: sha512-IwwxhHzitx3dr0/xo0z4jjDlb2AAHBPKt+juMyKKGTLlKi1rZfA4qixMwnveU20/JTHyipM6keX4Vr7LZFYc9g==} - engines: {node: ^14.17.0 || >=16.0.0} + eslint-plugin-svelte@3.11.0: + resolution: {integrity: sha512-KliWlkieHyEa65aQIkRwUFfHzT5Cn4u3BQQsu3KlkJOs7c1u7ryn84EWaOjEzilbKgttT4OfBURA8Uc4JBSQIw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0-next.112 + eslint: ^8.57.1 || ^9.0.0 + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: svelte: optional: true - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-scope@8.0.2: resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1400,14 +1403,14 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.14.0: - resolution: {integrity: sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==} - engines: {node: '>=18'} - globals@15.15.0: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globals@16.3.0: + resolution: {integrity: sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==} + engines: {node: '>=18'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1589,8 +1592,8 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - known-css-properties@0.30.0: - resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} + known-css-properties@0.37.0: + resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} @@ -1886,11 +1889,11 @@ packages: ts-node: optional: true - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} peerDependencies: - postcss: ^8.3.3 + postcss: ^8.4.31 postcss-scss@4.0.9: resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} @@ -1898,8 +1901,8 @@ packages: peerDependencies: postcss: ^8.4.29 - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + postcss-selector-parser@7.1.0: + resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} engines: {node: '>=4'} postcss@8.5.3: @@ -2121,9 +2124,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-eslint-parser@0.43.0: - resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + svelte-eslint-parser@1.3.0: + resolution: {integrity: sha512-VCgMHKV7UtOGcGLGNFSbmdm6kEKjtzo5nnpGU/mnx4OsFY6bZ7QwRF5DUx+Hokw5Lvdyo8dpk8B1m8mliomrNg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: @@ -2998,14 +3001,14 @@ snapshots: dependencies: acorn: 8.14.0 - '@sveltejs/eslint-config@8.1.0(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': + '@sveltejs/eslint-config@8.3.3(@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1))(eslint-config-prettier@9.1.0(eslint@9.9.1))(eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4))(eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte))(eslint@9.9.1)(typescript-eslint@8.26.0(eslint@9.9.1)(typescript@5.5.4))(typescript@5.5.4)': dependencies: '@stylistic/eslint-plugin-js': 1.8.0(eslint@9.9.1) eslint: 9.9.1 eslint-config-prettier: 9.1.0(eslint@9.9.1) eslint-plugin-n: 17.16.1(eslint@9.9.1)(typescript@5.5.4) - eslint-plugin-svelte: 2.38.0(eslint@9.9.1)(svelte@packages+svelte) - globals: 15.14.0 + eslint-plugin-svelte: 3.11.0(eslint@9.9.1)(svelte@packages+svelte) + globals: 15.15.0 typescript: 5.5.4 typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) @@ -3125,7 +3128,7 @@ snapshots: fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.1 + semver: 7.7.2 ts-api-utils: 2.0.1(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: @@ -3532,33 +3535,30 @@ snapshots: - supports-color - typescript - eslint-plugin-svelte@2.38.0(eslint@9.9.1)(svelte@packages+svelte): + eslint-plugin-svelte@3.11.0(eslint@9.9.1)(svelte@packages+svelte): dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) '@jridgewell/sourcemap-codec': 1.5.0 - debug: 4.4.0 eslint: 9.9.1 - eslint-compat-utils: 0.5.1(eslint@9.9.1) esutils: 2.0.3 - known-css-properties: 0.30.0 + globals: 16.3.0 + known-css-properties: 0.37.0 postcss: 8.5.3 postcss-load-config: 3.1.4(postcss@8.5.3) - postcss-safe-parser: 6.0.0(postcss@8.5.3) - postcss-selector-parser: 6.1.2 + postcss-safe-parser: 7.0.1(postcss@8.5.3) semver: 7.7.2 - svelte-eslint-parser: 0.43.0(svelte@packages+svelte) + svelte-eslint-parser: 1.3.0(svelte@packages+svelte) optionalDependencies: svelte: link:packages/svelte transitivePeerDependencies: - - supports-color - ts-node - eslint-scope@7.2.2: + eslint-scope@8.0.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@8.0.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -3769,10 +3769,10 @@ snapshots: globals@14.0.0: {} - globals@15.14.0: {} - globals@15.15.0: {} + globals@16.3.0: {} + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -3971,7 +3971,7 @@ snapshots: kleur@4.1.5: {} - known-css-properties@0.30.0: {} + known-css-properties@0.37.0: {} levn@0.4.1: dependencies: @@ -4209,7 +4209,7 @@ snapshots: optionalDependencies: postcss: 8.5.3 - postcss-safe-parser@6.0.0(postcss@8.5.3): + postcss-safe-parser@7.0.1(postcss@8.5.3): dependencies: postcss: 8.5.3 @@ -4217,7 +4217,7 @@ snapshots: dependencies: postcss: 8.5.3 - postcss-selector-parser@6.1.2: + postcss-selector-parser@7.1.0: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -4442,13 +4442,14 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-eslint-parser@0.43.0(svelte@packages+svelte): + svelte-eslint-parser@1.3.0(svelte@packages+svelte): dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.0 + espree: 10.1.0 postcss: 8.5.3 postcss-scss: 4.0.9(postcss@8.5.3) + postcss-selector-parser: 7.1.0 optionalDependencies: svelte: link:packages/svelte From b17557cd91a223a060041300a4761574d7f94e6e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 21:14:55 -0400 Subject: [PATCH 45/76] docs: fix typo (#16408) --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index aea427a8ec..e67206b535 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -119,7 +119,7 @@ class Todo { } ``` -> [NOTE!] Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). +> [!NOTE] Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). ## `$state.raw` From e5e6c0cd129c30bd2f02dabf5c1ab3edaae9bb8f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 21:53:40 -0400 Subject: [PATCH 46/76] fix docs (#16409) * fix docs * oops * another * gah --- documentation/docs/02-runes/03-$derived.md | 4 ++++ documentation/docs/98-reference/.generated/client-errors.md | 4 ++-- packages/svelte/messages/client-errors/errors.md | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 5f253cf6d1..0123868c4e 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -99,12 +99,16 @@ let selected = $derived(items[index]); If you use destructuring with a `$derived` declaration, the resulting variables will all be reactive — this... ```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- let { a, b, c } = $derived(stuff()); ``` ...is roughly equivalent to this: ```js +function stuff() { return { a: 1, b: 2, c: 3 } } +// ---cut--- let _stuff = $derived(stuff()); let a = $derived(_stuff.a); let b = $derived(_stuff.b); diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 3b17ef9f9b..4d1de435fe 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -109,10 +109,10 @@ $effect(() => { The same applies to array mutations, since these both read and write to the array: ```js -let array = $state([]); +let array = $state(['hello']); $effect(() => { - array.push('hello'); + array.push('goodbye'); }); ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index d6af859881..a30bdd7593 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -79,10 +79,10 @@ $effect(() => { The same applies to array mutations, since these both read and write to the array: ```js -let array = $state([]); +let array = $state(['hello']); $effect(() => { - array.push('hello'); + array.push('goodbye'); }); ``` From b8b662a1ad74d54891fd854962e92d5aa3d72fe3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 16 Jul 2025 22:02:02 -0400 Subject: [PATCH 47/76] docs: make link to svelte/reactivity more prominent (#16410) --- documentation/docs/02-runes/02-$state.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index e67206b535..741e24fde0 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -119,7 +119,9 @@ class Todo { } ``` -> [!NOTE] Svelte provides reactive implementations of built-in classes like `Set` and `Map` that can be imported from [`svelte/reactivity`](svelte-reactivity). +### Built-in classes + +Svelte provides reactive implementations of built-in classes like `Set`, `Map`, `Date` and `URL` that can be imported from [`svelte/reactivity`](svelte-reactivity). ## `$state.raw` From 9e1e7139fb406e6ac94cf5fda99d04e6642aa522 Mon Sep 17 00:00:00 2001 From: Ray Thurn Void <53383860+raythurnvoid@users.noreply.github.com> Date: Thu, 17 Jul 2025 09:41:21 +0100 Subject: [PATCH 48/76] fix: robustify reset calls in error boundaries (#16171) Fixes #16134 * Add a warning when the misuse of `reset` in an `error:boundary` causes an error to be thrown when flushing the boundary content * Prevent uncaught errors to make test fails when they are expected and are fired during template effects flush * reset should just be a noop after the first call * correctly handle errors inside boundary during reset * handle errors in the correct boundary --------- Co-authored-by: Rich Harris --- .changeset/new-dogs-obey.md | 5 ++ .changeset/polite-toys-report.md | 5 ++ .../98-reference/.generated/client-errors.md | 20 +++++ .../.generated/client-warnings.md | 26 +++++++ .../svelte/messages/client-errors/errors.md | 18 +++++ .../messages/client-warnings/warnings.md | 24 ++++++ .../internal/client/dom/blocks/boundary.js | 73 +++++++++++++------ .../src/internal/client/error-handling.js | 4 +- packages/svelte/src/internal/client/errors.js | 16 ++++ .../svelte/src/internal/client/warnings.js | 11 +++ .../svelte/tests/runtime-legacy/shared.ts | 7 +- .../error-boundary-reset-onerror/_config.js | 15 ++++ .../error-boundary-reset-onerror/main.svelte | 17 +++++ .../error-boundary-reset-premature/_config.js | 28 +++++++ .../main.svelte | 26 +++++++ .../_config.js | 27 +++++++ .../main.svelte | 20 +++++ 17 files changed, 315 insertions(+), 27 deletions(-) create mode 100644 .changeset/new-dogs-obey.md create mode 100644 .changeset/polite-toys-report.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte diff --git a/.changeset/new-dogs-obey.md b/.changeset/new-dogs-obey.md new file mode 100644 index 0000000000..aa9a3d73b9 --- /dev/null +++ b/.changeset/new-dogs-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle error in correct boundary after reset diff --git a/.changeset/polite-toys-report.md b/.changeset/polite-toys-report.md new file mode 100644 index 0000000000..bf5386e069 --- /dev/null +++ b/.changeset/polite-toys-report.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make `` reset function a noop after the first call diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 4d1de435fe..8fdb7770aa 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -238,3 +238,23 @@ let odd = $derived(!even); ``` If side-effects are unavoidable, use [`$effect`]($effect) instead. + +### svelte_boundary_reset_onerror + +``` +A `` `reset` function cannot be called while an error is still being handled +``` + +If a [``](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved. + +If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick): + +```svelte + { + fixTheError(); + +++await tick();+++ + reset(); +}}> + + +``` diff --git a/documentation/docs/98-reference/.generated/client-warnings.md b/documentation/docs/98-reference/.generated/client-warnings.md index 7548428e97..6f1d677fe9 100644 --- a/documentation/docs/98-reference/.generated/client-warnings.md +++ b/documentation/docs/98-reference/.generated/client-warnings.md @@ -312,6 +312,32 @@ Reactive `$state(...)` proxies and the values they proxy have different identiti To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. +### svelte_boundary_reset_noop + +``` +A `` `reset` function only resets the boundary the first time it is called +``` + +When an error occurs while rendering the contents of a [``](https://svelte.dev/docs/svelte/svelte-boundary), the `onerror` handler is called with the error plus a `reset` function that attempts to re-render the contents. + +This `reset` function should only be called once. After that, it has no effect — in a case like this, where a reference to `reset` is stored outside the boundary, clicking the button while `` is rendered will _not_ cause the contents to be rendered again. + +```svelte + + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ### transition_slide_display ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index a30bdd7593..57ecca0489 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -184,3 +184,21 @@ let odd = $derived(!even); ``` If side-effects are unavoidable, use [`$effect`]($effect) instead. + +## svelte_boundary_reset_onerror + +> A `` `reset` function cannot be called while an error is still being handled + +If a [``](https://svelte.dev/docs/svelte/svelte-boundary) has an `onerror` function, it must not call the provided `reset` function synchronously since the boundary is still in a broken state. Typically, `reset()` is called later, once the error has been resolved. + +If it's possible to resolve the error inside the `onerror` callback, you must at least wait for the boundary to settle before calling `reset()`, for example using [`tick`](https://svelte.dev/docs/svelte/lifecycle-hooks#tick): + +```svelte + { + fixTheError(); + +++await tick();+++ + reset(); +}}> + + +``` diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 13d9bfcd3b..123c6833e6 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -272,6 +272,30 @@ To silence the warning, ensure that `value`: To resolve this, ensure you're comparing values where both values were created with `$state(...)`, or neither were. Note that `$state.raw(...)` will _not_ create a state proxy. +## svelte_boundary_reset_noop + +> A `` `reset` function only resets the boundary the first time it is called + +When an error occurs while rendering the contents of a [``](https://svelte.dev/docs/svelte/svelte-boundary), the `onerror` handler is called with the error plus a `reset` function that attempts to re-render the contents. + +This `reset` function should only be called once. After that, it has no effect — in a case like this, where a reference to `reset` is stored outside the boundary, clicking the button while `` is rendered will _not_ cause the contents to be rendered again. + +```svelte + + + + + (reset = r)}> + + + {#snippet failed(e)} +

oops! {e.message}

+ {/snippet} +
+``` + ## transition_slide_display > The `slide` transition does not work correctly for elements with `display: %value%` diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 5e678ab113..4ea137bfa8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,7 +1,12 @@ /** @import { Effect, Source, TemplateNode, } from '#client' */ -import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; +import { + BOUNDARY_EFFECT, + EFFECT_PRESERVED, + EFFECT_RAN, + EFFECT_TRANSPARENT +} from '#client/constants'; import { component_context, set_component_context } from '../../context.js'; -import { invoke_error_boundary } from '../../error-handling.js'; +import { handle_error, invoke_error_boundary } from '../../error-handling.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { active_effect, @@ -21,6 +26,7 @@ import { import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; +import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; @@ -196,6 +202,9 @@ export class Boundary { try { return fn(); + } catch (e) { + handle_error(e); + return null; } finally { set_active_effect(previous_effect); set_active_reaction(previous_reaction); @@ -257,7 +266,42 @@ export class Boundary { var onerror = this.#props.onerror; let failed = this.#props.failed; + if (this.#main_effect) { + destroy_effect(this.#main_effect); + this.#main_effect = null; + } + + if (this.#pending_effect) { + destroy_effect(this.#pending_effect); + this.#pending_effect = null; + } + + if (this.#failed_effect) { + destroy_effect(this.#failed_effect); + this.#failed_effect = null; + } + + if (hydrating) { + set_hydrate_node(this.#hydrate_open); + next(); + set_hydrate_node(remove_nodes()); + } + + var did_reset = false; + var calling_on_error = false; + const reset = () => { + if (did_reset) { + w.svelte_boundary_reset_noop(); + return; + } + + did_reset = true; + + if (calling_on_error) { + e.svelte_boundary_reset_onerror(); + } + this.#pending_count = 0; if (this.#failed_effect !== null) { @@ -290,32 +334,15 @@ export class Boundary { try { set_active_reaction(null); + calling_on_error = true; onerror?.(error, reset); + calling_on_error = false; + } catch (error) { + invoke_error_boundary(error, this.#effect && this.#effect.parent); } finally { set_active_reaction(previous_reaction); } - if (this.#main_effect) { - destroy_effect(this.#main_effect); - this.#main_effect = null; - } - - if (this.#pending_effect) { - destroy_effect(this.#pending_effect); - this.#pending_effect = null; - } - - if (this.#failed_effect) { - destroy_effect(this.#failed_effect); - this.#failed_effect = null; - } - - if (hydrating) { - set_hydrate_node(this.#hydrate_open); - next(); - set_hydrate_node(remove_nodes()); - } - if (failed) { queue_micro_task(() => { this.#failed_effect = this.#run(() => { diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index a512839181..6c83a453d5 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -53,7 +53,9 @@ export function invoke_error_boundary(error, effect) { try { /** @type {Boundary} */ (effect.b).error(error); return; - } catch {} + } catch (e) { + error = e; + } } effect = effect.parent; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index edd66a7400..937971da5e 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -423,4 +423,20 @@ export function state_unsafe_mutation() { } else { throw new Error(`https://svelte.dev/e/state_unsafe_mutation`); } +} + +/** + * A `` `reset` function cannot be called while an error is still being handled + * @returns {never} + */ +export function svelte_boundary_reset_onerror() { + if (DEV) { + const error = new Error(`svelte_boundary_reset_onerror\nA \`\` \`reset\` function cannot be called while an error is still being handled\nhttps://svelte.dev/e/svelte_boundary_reset_onerror`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/svelte_boundary_reset_onerror`); + } } \ No newline at end of file diff --git a/packages/svelte/src/internal/client/warnings.js b/packages/svelte/src/internal/client/warnings.js index dfd50a8722..dfa2a3752e 100644 --- a/packages/svelte/src/internal/client/warnings.js +++ b/packages/svelte/src/internal/client/warnings.js @@ -224,6 +224,17 @@ export function state_proxy_equality_mismatch(operator) { } } +/** + * A `` `reset` function only resets the boundary the first time it is called + */ +export function svelte_boundary_reset_noop() { + if (DEV) { + console.warn(`%c[svelte] svelte_boundary_reset_noop\n%cA \`\` \`reset\` function only resets the boundary the first time it is called\nhttps://svelte.dev/e/svelte_boundary_reset_noop`, bold, normal); + } else { + console.warn(`https://svelte.dev/e/svelte_boundary_reset_noop`); + } +} + /** * The `slide` transition does not work correctly for elements with `display: %value%` * @param {string} value diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 8d7b3544bc..05c1a982ec 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -489,10 +489,11 @@ async function run_test_variant( 'Expected component to unmount and leave nothing behind after it was destroyed' ); - // TODO: This seems useless, unhandledRejection is only triggered on the next task - // by which time the test has already finished and the next test resets it to null above + // uncaught errors like during template effects flush if (unhandled_rejection) { - throw unhandled_rejection; // eslint-disable-line no-unsafe-finally + if (!config.expect_unhandled_rejections) { + throw unhandled_rejection; // eslint-disable-line no-unsafe-finally + } } } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js new file mode 100644 index 0000000000..092d7ad37d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + + assert.throws(flushSync, 'svelte_boundary_reset_onerror'); + + // boundary content empty; only button remains + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte new file mode 100644 index 0000000000..f91048a9e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-onerror/main.svelte @@ -0,0 +1,17 @@ + + + reset()}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js new file mode 100644 index 0000000000..687961e721 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/_config.js @@ -0,0 +1,28 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + normal content + + `, + + async test({ assert, target, warnings }) { + const [btn] = target.querySelectorAll('button'); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `
err
`); + assert.deepEqual(warnings, []); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `normal content `); + assert.deepEqual(warnings, []); + + flushSync(() => btn.click()); + assert.htmlEqual(target.innerHTML, `
err
`); + + assert.deepEqual(warnings, [ + 'A `` `reset` function only resets the boundary the first time it is called' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte new file mode 100644 index 0000000000..c1462eaf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-premature/main.svelte @@ -0,0 +1,26 @@ + + + + (reset = fn)}> + {must_throw ? throw_error() : 'normal content'} + + {#snippet failed()} +
err
+ {/snippet} +
+
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js new file mode 100644 index 0000000000..844e6981bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, warnings }) { + const [toggle] = target.querySelectorAll('button'); + + flushSync(() => toggle.click()); + assert.htmlEqual( + target.innerHTML, + `

yikes!

` + ); + + const [, reset] = target.querySelectorAll('button'); + flushSync(() => reset.click()); + assert.htmlEqual( + target.innerHTML, + `

yikes!

` + ); + + flushSync(() => toggle.click()); + + const [, reset2] = target.querySelectorAll('button'); + flushSync(() => reset2.click()); + assert.htmlEqual(target.innerHTML, `

hello!

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte new file mode 100644 index 0000000000..91479a631a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-with-error/main.svelte @@ -0,0 +1,20 @@ + + + + + +

{must_throw ? throw_error() : 'hello!'}

+ + {#snippet failed(error, reset)} +

{error.message}

+ + {/snippet} +
+ + From f3431709279df1311f591c1a6b47e4c0caa9b45e Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 17 Jul 2025 01:56:24 -0700 Subject: [PATCH 49/76] docs: fix `$effect.root` jsdoc formatting (#16411) --- packages/svelte/src/ambient.d.ts | 12 ++++++------ packages/svelte/types/index.d.ts | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 7c3b941ed1..ad32eaa56f 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -295,13 +295,13 @@ declare namespace $effect { * let count = $state(0); * * const cleanup = $effect.root(() => { - * $effect(() => { - * console.log(count); - * }) + * $effect(() => { + * console.log(count); + * }) * - * return () => { - * console.log('effect root cleanup'); - * } + * return () => { + * console.log('effect root cleanup'); + * } * }); * * diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a8b769d6d4..9ea45af7e6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3369,13 +3369,13 @@ declare namespace $effect { * let count = $state(0); * * const cleanup = $effect.root(() => { - * $effect(() => { - * console.log(count); - * }) + * $effect(() => { + * console.log(count); + * }) * - * return () => { - * console.log('effect root cleanup'); - * } + * return () => { + * console.log('effect root cleanup'); + * } * }); * * From bdf6adb411cd5fd4f227778d87c116dd16c533d2 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 17 Jul 2025 11:03:53 +0200 Subject: [PATCH 50/76] fix: delegate functions with shadowed variables if declared locally (#16417) --- .changeset/fast-parrots-draw.md | 5 ++++ .../phases/2-analyze/visitors/Attribute.js | 9 ++++-- .../_expected/client/index.svelte.js | 28 +++++++++++++++++++ .../_expected/server/index.svelte.js | 13 +++++++++ .../index.svelte | 12 ++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-parrots-draw.md create mode 100644 packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte diff --git a/.changeset/fast-parrots-draw.md b/.changeset/fast-parrots-draw.md new file mode 100644 index 0000000000..e56a35ff9b --- /dev/null +++ b/.changeset/fast-parrots-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: delegate functions with shadowed variables if declared locally diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 773aa59744..b13f3f89b6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -192,8 +192,13 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - // If we are referencing a binding that is shadowed in another scope then bail out. - if (local_binding !== null && binding !== null && local_binding.node !== binding.node) { + // If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function). + if ( + local_binding !== null && + binding !== null && + local_binding.node !== binding.node && + scope.declarations.get(reference) !== binding + ) { return unhoisted; } diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js new file mode 100644 index 0000000000..0d95d8d335 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -0,0 +1,28 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/legacy'; +import * as $ from 'svelte/internal/client'; + +var on_click = (e) => { + const index = Number(e.currentTarget.dataset.index); + + console.log(index); +}; + +var root_1 = $.from_html(``); + +export default function Delegated_locally_declared_shadowed($$anchor) { + var fragment = $.comment(); + var node = $.first_child(fragment); + + $.each(node, 0, () => ({ length: 1 }), $.index, ($$anchor, $$item, index) => { + var button = root_1(); + + $.set_attribute(button, 'data-index', index); + button.__click = [on_click]; + $.append($$anchor, button); + }); + + $.append($$anchor, fragment); +} + +$.delegate(['click']); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js new file mode 100644 index 0000000000..e465af6f8b --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js @@ -0,0 +1,13 @@ +import * as $ from 'svelte/internal/server'; + +export default function Delegated_locally_declared_shadowed($$payload) { + const each_array = $.ensure_array_like({ length: 1 }); + + $$payload.out += ``; + + for (let index = 0, $$length = each_array.length; index < $$length; index++) { + $$payload.out += ``; + } + + $$payload.out += ``; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte new file mode 100644 index 0000000000..d870a6b270 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/index.svelte @@ -0,0 +1,12 @@ + + +{#each { length: 1 }, index} + +{/each} From 65e524929661fefcdeaadfa8aa79cc06b26d9a76 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:24:21 +0200 Subject: [PATCH 51/76] docs: remove mention of deprecated way to enhance typings (#16416) Since #9070 it's prefered to use the "enhance svelte elements" way of doing things. That's so long ago that we can now remove the other option from the docs, and eventually remove the backwards compatibility that still allows the old way of doing things. --- documentation/docs/07-misc/03-typescript.md | 33 +++++++-------------- packages/svelte/svelte-html.d.ts | 1 + 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/documentation/docs/07-misc/03-typescript.md b/documentation/docs/07-misc/03-typescript.md index ff33885fb8..49ecd8adb5 100644 --- a/documentation/docs/07-misc/03-typescript.md +++ b/documentation/docs/07-misc/03-typescript.md @@ -254,39 +254,24 @@ To declare that a variable expects the constructor or instance type of a compone Svelte provides a best effort of all the HTML DOM types that exist. Sometimes you may want to use experimental attributes or custom events coming from an action. In these cases, TypeScript will throw a type error, saying that it does not know these types. If it's a non-experimental standard attribute/event, this may very well be a missing typing from our [HTML typings](https://github.com/sveltejs/svelte/blob/main/packages/svelte/elements.d.ts). In that case, you are welcome to open an issue and/or a PR fixing it. -In case this is a custom or experimental attribute/event, you can enhance the typings like this: - -```ts -/// file: additional-svelte-typings.d.ts -declare namespace svelteHTML { - // enhance elements - interface IntrinsicElements { - 'my-custom-element': { someattribute: string; 'on:event': (e: CustomEvent) => void }; - } - // enhance attributes - interface HTMLAttributes { - // If you want to use the beforeinstallprompt event - onbeforeinstallprompt?: (event: any) => any; - // If you want to use myCustomAttribute={..} (note: all lowercase) - mycustomattribute?: any; // You can replace any with something more specific if you like - } -} -``` - -Then make sure that `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect. - -You can also declare the typings by augmenting the `svelte/elements` module like this: +In case this is a custom or experimental attribute/event, you can enhance the typings by augmenting the `svelte/elements` module like this: ```ts /// file: additional-svelte-typings.d.ts import { HTMLButtonAttributes } from 'svelte/elements'; declare module 'svelte/elements' { + // add a new element export interface SvelteHTMLElements { 'custom-button': HTMLButtonAttributes; } - // allows for more granular control over what element to add the typings to + // add a new global attribute that is available on all html elements + export interface HTMLAttributes { + globalattribute?: string; + } + + // add a new attribute for button elements export interface HTMLButtonAttributes { veryexperimentalattribute?: string; } @@ -294,3 +279,5 @@ declare module 'svelte/elements' { export {}; // ensure this is not an ambient module, else types will be overridden instead of augmented ``` + +Then make sure that the `d.ts` file is referenced in your `tsconfig.json`. If it reads something like `"include": ["src/**/*"]` and your `d.ts` file is inside `src`, it should work. You may need to reload for the changes to take effect. diff --git a/packages/svelte/svelte-html.d.ts b/packages/svelte/svelte-html.d.ts index 5042eaa4b8..476b24e275 100644 --- a/packages/svelte/svelte-html.d.ts +++ b/packages/svelte/svelte-html.d.ts @@ -50,6 +50,7 @@ declare global { ? SVGElementTagNameMap[Key] : any; + // TODO remove HTMLAttributes/SVGAttributes/IntrinsicElements in Svelte 6 // For backwards-compatibility and ease-of-use, in case someone enhanced the typings from import('svelte/elements').HTMLAttributes/SVGAttributes // eslint-disable-next-line @typescript-eslint/no-unused-vars interface HTMLAttributes {} From 0ae1d5afffae954d6691b19b71531e0e287c5fa3 Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Thu, 17 Jul 2025 14:25:01 +0300 Subject: [PATCH 52/76] chore: cleanup obselete stuff (#16412) * Update vitest.config.js * Update vitest.config.js * cleanup * woops * Update eslint.config.js --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .prettierignore | 9 --------- .prettierrc | 6 ------ .vscode/settings.json | 3 --- eslint.config.js | 6 +----- vitest.config.js | 2 +- 5 files changed, 2 insertions(+), 24 deletions(-) diff --git a/.prettierignore b/.prettierignore index 72cd10aca8..5e1d9b1aa7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -28,15 +28,6 @@ packages/svelte/types packages/svelte/compiler/index.js playgrounds/sandbox/src/* -# sites/svelte.dev -sites/svelte.dev/static/svelte-app.json -sites/svelte.dev/scripts/svelte-app/ -sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg -sites/svelte.dev/src/routes/_components/Supporters/contributors.js -sites/svelte.dev/src/routes/_components/Supporters/donors.jpg -sites/svelte.dev/src/routes/_components/Supporters/donors.js -sites/svelte.dev/src/lib/generated - **/node_modules **/.svelte-kit **/.vercel diff --git a/.prettierrc b/.prettierrc index c4fd5d9f2f..c2d09a4289 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,12 +17,6 @@ "useTabs": false, "tabWidth": 2 } - }, - { - "files": ["sites/svelte-5-preview/src/routes/docs/content/**/*.md"], - "options": { - "printWidth": 60 - } } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 21a2a11c84..4d360cbc8a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,3 @@ { - "search.exclude": { - "sites/svelte-5-preview/static/*": true - }, "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/eslint.config.js b/eslint.config.js index b0f25bab03..5241cb43a6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -103,11 +103,7 @@ export default [ '*.config.js', // documentation can contain invalid examples 'documentation', - // contains a fork of the REPL which doesn't adhere to eslint rules - 'sites/svelte-5-preview/**', - 'tmp/**', - // wasn't checked previously, reenable at some point - 'sites/svelte.dev/**' + 'tmp/**' ] } ]; diff --git a/vitest.config.js b/vitest.config.js index caeda27e30..ba1edb355b 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -39,7 +39,7 @@ export default defineConfig({ provider: 'v8', reporter: ['lcov', 'html'], include: ['packages/svelte/src/**'], - reportsDirectory: 'sites/svelte-5-preview/static/coverage', + reportsDirectory: 'coverage', reportOnFailure: true } } From 6c9717a91f2f6ae10641d1cf502ba13d227fbe45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:26:50 -0400 Subject: [PATCH 53/76] Version Packages (#16418) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-parrots-draw.md | 5 ----- .changeset/new-dogs-obey.md | 5 ----- .changeset/polite-toys-report.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/fast-parrots-draw.md delete mode 100644 .changeset/new-dogs-obey.md delete mode 100644 .changeset/polite-toys-report.md diff --git a/.changeset/fast-parrots-draw.md b/.changeset/fast-parrots-draw.md deleted file mode 100644 index e56a35ff9b..0000000000 --- a/.changeset/fast-parrots-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: delegate functions with shadowed variables if declared locally diff --git a/.changeset/new-dogs-obey.md b/.changeset/new-dogs-obey.md deleted file mode 100644 index aa9a3d73b9..0000000000 --- a/.changeset/new-dogs-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle error in correct boundary after reset diff --git a/.changeset/polite-toys-report.md b/.changeset/polite-toys-report.md deleted file mode 100644 index bf5386e069..0000000000 --- a/.changeset/polite-toys-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: make `` reset function a noop after the first call diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 56f91c395f..f78255e7fe 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.36.6 + +### Patch Changes + +- fix: delegate functions with shadowed variables if declared locally ([#16417](https://github.com/sveltejs/svelte/pull/16417)) + +- fix: handle error in correct boundary after reset ([#16171](https://github.com/sveltejs/svelte/pull/16171)) + +- fix: make `` reset function a noop after the first call ([#16171](https://github.com/sveltejs/svelte/pull/16171)) + ## 5.36.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2a1b3cf9e5..aec8a34478 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.36.5", + "version": "5.36.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4d6811c409..b0e4ebbab8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.5'; +export const VERSION = '5.36.6'; export const PUBLIC_VERSION = '5'; From 98fb1b1f8b95352c9882b653b45b230609fc95cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 17 Jul 2025 12:14:03 -0400 Subject: [PATCH 54/76] fix: allow instrinsic `` elements to inherit from `SvelteHTMLElements` (#16424) --- .changeset/funny-masks-build.md | 5 +++++ packages/svelte/svelte-html.d.ts | 12 ------------ 2 files changed, 5 insertions(+), 12 deletions(-) create mode 100644 .changeset/funny-masks-build.md diff --git a/.changeset/funny-masks-build.md b/.changeset/funny-masks-build.md new file mode 100644 index 0000000000..07658e2540 --- /dev/null +++ b/.changeset/funny-masks-build.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow instrinsic `` elements to inherit from `SvelteHTMLElements` diff --git a/packages/svelte/svelte-html.d.ts b/packages/svelte/svelte-html.d.ts index 476b24e275..ebd58c8cc0 100644 --- a/packages/svelte/svelte-html.d.ts +++ b/packages/svelte/svelte-html.d.ts @@ -239,18 +239,6 @@ declare global { use: HTMLProps<'use', SVGAttributes>; view: HTMLProps<'view', SVGAttributes>; - // Svelte specific - 'svelte:window': HTMLProps<'svelte:window', HTMLAttributes>; - 'svelte:body': HTMLProps<'svelte:body', HTMLAttributes>; - 'svelte:document': HTMLProps<'svelte:document', HTMLAttributes>; - 'svelte:fragment': { slot?: string }; - 'svelte:head': { [name: string]: any }; - 'svelte:boundary': { - onerror?: (error: unknown, reset: () => void) => void; - failed?: import('svelte').Snippet<[error: unknown, reset: () => void]>; - }; - // don't type svelte:options, it would override the types in svelte/elements and it isn't extendable anyway - [name: string]: { [name: string]: any }; } } From 93a8a495d2365c28588f2858da6bc197f40c3d4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:21:43 -0400 Subject: [PATCH 55/76] Version Packages (#16426) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/funny-masks-build.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/funny-masks-build.md diff --git a/.changeset/funny-masks-build.md b/.changeset/funny-masks-build.md deleted file mode 100644 index 07658e2540..0000000000 --- a/.changeset/funny-masks-build.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow instrinsic `` elements to inherit from `SvelteHTMLElements` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f78255e7fe..a82d9ddefe 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.7 + +### Patch Changes + +- fix: allow instrinsic `` elements to inherit from `SvelteHTMLElements` ([#16424](https://github.com/sveltejs/svelte/pull/16424)) + ## 5.36.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index aec8a34478..83567a6123 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.36.6", + "version": "5.36.7", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b0e4ebbab8..88cdc069a0 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.6'; +export const VERSION = '5.36.7'; export const PUBLIC_VERSION = '5'; From c2da1ebb85d65ef044766d3f920cb935df65e359 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 18 Jul 2025 18:07:06 +0200 Subject: [PATCH 56/76] fix: keep effect in the graph if it has an abort controller (#16430) --- .changeset/four-kings-drum.md | 5 +++++ .../svelte/src/internal/client/reactivity/batch.js | 4 +++- .../dependencyless-abort-signal/Component.svelte | 8 ++++++++ .../samples/dependencyless-abort-signal/_config.js | 12 ++++++++++++ .../samples/dependencyless-abort-signal/main.svelte | 10 ++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .changeset/four-kings-drum.md create mode 100644 packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte diff --git a/.changeset/four-kings-drum.md b/.changeset/four-kings-drum.md new file mode 100644 index 0000000000..8c7343145e --- /dev/null +++ b/.changeset/four-kings-drum.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep effect in the graph if it has an abort controller diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index cdce971b18..ed82af94ed 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -532,7 +532,9 @@ function flush_queued_effects(effects) { // here (rather than in `update_effect`) allows us to skip the work for // immediate effects. if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - if (effect.teardown === null) { + // if there's no teardown or abort controller we completely unlink + // the effect from the graph + if (effect.teardown === null && effect.ac === null) { // remove this effect from the graph unlink_effect(effect); } else { diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte new file mode 100644 index 0000000000..b184e0e9b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/Component.svelte @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js new file mode 100644 index 0000000000..6eb4b06712 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + flushSync(() => { + btn?.click(); + }); + assert.deepEqual(logs, ['abort']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte new file mode 100644 index 0000000000..99ea3fabb4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dependencyless-abort-signal/main.svelte @@ -0,0 +1,10 @@ + + + +{#if show} + +{/if} From f5db130e2d233564437d64430a9e54b8b8b6c52a Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 18 Jul 2025 10:09:10 -0600 Subject: [PATCH 57/76] chore: Switch `payload.out` to an array (#16428) * chore: Switch `payload.out` to an array * Apply suggestions from code review * changeset --- .changeset/old-dots-sin.md | 5 ++ .../3-transform/server/visitors/EachBlock.js | 4 +- .../3-transform/server/visitors/IfBlock.js | 6 ++- .../server/visitors/shared/utils.js | 38 +++++++++----- .../src/internal/server/blocks/snippet.js | 8 +-- packages/svelte/src/internal/server/dev.js | 2 +- packages/svelte/src/internal/server/index.js | 51 ++++++++++--------- .../svelte/src/internal/server/payload.js | 14 ++--- .../_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 6 +-- .../_expected/server/main.svelte.js | 2 +- .../_expected/server/index.svelte.js | 6 +-- .../_expected/server/index.svelte.js | 6 +-- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../hmr/_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../purity/_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- 23 files changed, 103 insertions(+), 73 deletions(-) create mode 100644 .changeset/old-dots-sin.md diff --git a/.changeset/old-dots-sin.md b/.changeset/old-dots-sin.md new file mode 100644 index 0000000000..f7e2933897 --- /dev/null +++ b/.changeset/old-dots-sin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: Switch `payload.out` to an array diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index ac6c9891a7..ee0086de2e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -44,12 +44,12 @@ export function EachBlock(node, context) { ); if (node.fallback) { - const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)); + const open = b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open)); const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback)); fallback.body.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) + b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE))) ); state.template.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index 508dcc0fdd..8c082f38d5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -17,10 +17,12 @@ export function IfBlock(node, context) { ? /** @type {BlockStatement} */ (context.visit(node.alternate)) : b.block([]); - consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open))); + consequent.body.unshift( + b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), block_open)) + ); alternate.body.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE))) + b.stmt(b.call(b.member(b.id('$$payload.out'), b.id('push')), b.literal(BLOCK_OPEN_ELSE))) ); context.state.template.push(b.if(test, consequent, alternate), block_close); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 8fcf8efa68..8a8633dd1a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -96,10 +96,10 @@ function is_statement(node) { /** * @param {Array} template * @param {Identifier} out - * @param {AssignmentOperator} operator + * @param {AssignmentOperator | 'push'} operator * @returns {Statement[]} */ -export function build_template(template, out = b.id('$$payload.out'), operator = '+=') { +export function build_template(template, out = b.id('$$payload.out'), operator = 'push') { /** @type {string[]} */ let strings = []; @@ -110,18 +110,32 @@ export function build_template(template, out = b.id('$$payload.out'), operator = const statements = []; const flush = () => { - statements.push( - b.stmt( - b.assignment( - operator, - out, - b.template( - strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)), - expressions + if (operator === 'push') { + statements.push( + b.stmt( + b.call( + b.member(out, b.id('push')), + b.template( + strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)), + expressions + ) ) ) - ) - ); + ); + } else { + statements.push( + b.stmt( + b.assignment( + operator, + out, + b.template( + strings.map((cooked, i) => b.quasi(cooked, i === strings.length - 1)), + expressions + ) + ) + ) + ); + } strings = []; expressions = []; }; diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index 9e96ae3430..bb82ca97d0 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -15,8 +15,10 @@ export function createRawSnippet(fn) { // @ts-expect-error the types are a lie return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { var getters = /** @type {Getters} */ (args.map((value) => () => value)); - payload.out += fn(...getters) - .render() - .trim(); + payload.out.push( + fn(...getters) + .render() + .trim() + ); }; } diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index 3c320f9698..2a0cb057a3 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -40,7 +40,7 @@ function print_error(payload, message) { // eslint-disable-next-line no-console console.error(message); - payload.head.out += ``; + payload.head.out.push(``); } export function reset_elements() { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 9942882d26..62ee22d6fc 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -33,23 +33,23 @@ const INVALID_ATTR_NAME_CHAR_REGEX = * @returns {void} */ export function element(payload, tag, attributes_fn = noop, children_fn = noop) { - payload.out += ''; + payload.out.push(''); if (tag) { - payload.out += `<${tag}`; + payload.out.push(`<${tag}`); attributes_fn(); - payload.out += `>`; + payload.out.push(`>`); if (!is_void(tag)) { children_fn(); if (!is_raw_text_element(tag)) { - payload.out += EMPTY_COMMENT; + payload.out.push(EMPTY_COMMENT); } - payload.out += ``; + payload.out.push(``); } } - payload.out += ''; + payload.out.push(''); } /** @@ -72,7 +72,7 @@ export function render(component, options = {}) { const prev_on_destroy = on_destroy; on_destroy = []; - payload.out += BLOCK_OPEN; + payload.out.push(BLOCK_OPEN); let reset_reset_element; @@ -97,20 +97,22 @@ export function render(component, options = {}) { reset_reset_element(); } - payload.out += BLOCK_CLOSE; + payload.out.push(BLOCK_CLOSE); for (const cleanup of on_destroy) cleanup(); on_destroy = prev_on_destroy; - let head = payload.head.out + payload.head.title; + let head = payload.head.out.join('') + payload.head.title; for (const { hash, code } of payload.css) { head += ``; } + const body = payload.out.join(''); + return { head, - html: payload.out, - body: payload.out + html: body, + body: body }; } finally { abort(); @@ -124,9 +126,9 @@ export function render(component, options = {}) { */ export function head(payload, fn) { const head_payload = payload.head; - head_payload.out += BLOCK_OPEN; + head_payload.out.push(BLOCK_OPEN); fn(head_payload); - head_payload.out += BLOCK_CLOSE; + head_payload.out.push(BLOCK_CLOSE); } /** @@ -141,21 +143,21 @@ export function css_props(payload, is_html, props, component, dynamic = false) { const styles = style_object_to_string(props); if (is_html) { - payload.out += ``; + payload.out.push(``); } else { - payload.out += ``; + payload.out.push(``); } if (dynamic) { - payload.out += ''; + payload.out.push(''); } component(); if (is_html) { - payload.out += ``; + payload.out.push(``); } else { - payload.out += ``; + payload.out.push(``); } } @@ -440,13 +442,13 @@ export function bind_props(props_parent, props_now) { */ function await_block(payload, promise, pending_fn, then_fn) { if (is_promise(promise)) { - payload.out += BLOCK_OPEN; + payload.out.push(BLOCK_OPEN); promise.then(null, noop); if (pending_fn !== null) { pending_fn(); } } else if (then_fn !== null) { - payload.out += BLOCK_OPEN_ELSE; + payload.out.push(BLOCK_OPEN_ELSE); then_fn(promise); } } @@ -493,7 +495,7 @@ export function once(get_value) { */ export function props_id(payload) { const uid = payload.uid(); - payload.out += ''; + payload.out.push(''); return uid; } @@ -562,10 +564,13 @@ export function valueless_option(payload, children) { children(); - var body = payload.out.slice(i); + var body = payload.out.slice(i).join(''); if (body.replace(//g, '') === payload.select_value) { // replace '>' with ' selected>' (closing tag will be added later) - payload.out = payload.out.slice(0, i - 1) + ' selected>' + body; + var last_item = payload.out[i - 1]; + payload.out[i - 1] = last_item.slice(0, -1) + ' selected>'; + // Remove the old items after position i and add the body as a single item + payload.out.splice(i, payload.out.length - i, body); } } diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index a31120ae16..195488e061 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -1,11 +1,12 @@ export class HeadPayload { /** @type {Set<{ hash: string; code: string }>} */ css = new Set(); - out = ''; + /** @type {string[]} */ + out = []; uid = () => ''; title = ''; - constructor(css = new Set(), out = '', title = '', uid = () => '') { + constructor(css = new Set(), /** @type {string[]} */ out = [], title = '', uid = () => '') { this.css = css; this.out = out; this.title = title; @@ -16,7 +17,8 @@ export class HeadPayload { export class Payload { /** @type {Set<{ hash: string; code: string }>} */ css = new Set(); - out = ''; + /** @type {string[]} */ + out = []; uid = () => ''; select_value = undefined; @@ -36,12 +38,12 @@ export class Payload { export function copy_payload({ out, css, head, uid }) { const payload = new Payload(); - payload.out = out; + payload.out = [...out]; payload.css = new Set(css); payload.uid = uid; payload.head = new HeadPayload(); - payload.head.out = head.out; + payload.head.out = [...head.out]; payload.head.css = new Set(head.css); payload.head.title = head.title; payload.head.uid = head.uid; @@ -56,7 +58,7 @@ export function copy_payload({ out, css, head, uid }) { * @returns {void} */ export function assign_payload(p1, p2) { - p1.out = p2.out; + p1.out = [...p2.out]; p1.css = p2.css; p1.head = p2.head; p1.uid = p2.uid; diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js index 4b6e32d58e..cc2628c852 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Await_block_scope($$payload) { counter.count += 1; } - $$payload.out += ` `; + $$payload.out.push(` `); $.await($$payload, promise, () => {}, (counter) => {}); - $$payload.out += ` ${$.escape(counter.count)}`; + $$payload.out.push(` ${$.escape(counter.count)}`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js index e2c0ee29a5..c0db7d2fd5 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import * as $ from 'svelte/internal/server'; import TextInput from './Child.svelte'; function snippet($$payload) { - $$payload.out += `Something`; + $$payload.out.push(`Something`); } export default function Bind_component_snippet($$payload) { @@ -23,7 +23,7 @@ export default function Bind_component_snippet($$payload) { } }); - $$payload.out += ` value: ${$.escape(value)}`; + $$payload.out.push(` value: ${$.escape(value)}`); } do { diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js index e465af6f8b..ac3dfcd2cb 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/server/index.svelte.js @@ -3,11 +3,11 @@ import * as $ from 'svelte/internal/server'; export default function Delegated_locally_declared_shadowed($$payload) { const each_array = $.ensure_array_like({ length: 1 }); - $$payload.out += ``; + $$payload.out.push(``); for (let index = 0, $$length = each_array.length; index < $$length; index++) { - $$payload.out += ``; + $$payload.out.push(``); } - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js index cf731d8187..9c837d4e1d 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -6,5 +6,5 @@ export default function Main($$payload) { let y = () => 'test'; - $$payload.out += `
`; + $$payload.out.push(`
`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js index 3431e36833..8fa0c5f28c 100644 --- a/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-index-non-null/_expected/server/index.svelte.js @@ -3,11 +3,11 @@ import * as $ from 'svelte/internal/server'; export default function Each_index_non_null($$payload) { const each_array = $.ensure_array_like(Array(10)); - $$payload.out += ``; + $$payload.out.push(``); for (let i = 0, $$length = each_array.length; i < $$length; i++) { - $$payload.out += `

index: ${$.escape(i)}

`; + $$payload.out.push(`

index: ${$.escape(i)}

`); } - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js index 4386c22ebe..6dbe8130da 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js @@ -3,13 +3,13 @@ import * as $ from 'svelte/internal/server'; export default function Each_string_template($$payload) { const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); - $$payload.out += ``; + $$payload.out.push(``); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let thing = each_array[$$index]; - $$payload.out += `${$.escape(thing)}, `; + $$payload.out.push(`${$.escape(thing)}, `); } - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 7d37abd97b..ce4f09ed1d 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -15,7 +15,7 @@ export default function Function_prop_no_getter($$payload) { onmouseenter: () => count = plusOne(count), children: ($$payload) => { - $$payload.out += `clicks: ${$.escape(count)}`; + $$payload.out.push(`clicks: ${$.escape(count)}`); }, $$slots: { default: true } diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js index dc49c0c213..b1a0a5f9e6 100644 --- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Functional_templating($$payload) { - $$payload.out += `

hello

child element

another child element

`; + $$payload.out.push(`

hello

child element

another child element

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js index 8766fb1300..30f6d6b74a 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Hello_world($$payload) { - $$payload.out += `

hello world

`; + $$payload.out.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js index 959e0a403e..ea1d12c83b 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/server/index.svelte.js @@ -1,5 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Hmr($$payload) { - $$payload.out += `

hello world

`; + $$payload.out.push(`

hello world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js index 3b23befcd4..18e01b4f72 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/server/index.svelte.js @@ -4,5 +4,5 @@ export default function Nullish_coallescence_omittance($$payload) { let name = 'world'; let count = 0; - $$payload.out += `

Hello, world!

123

Hello, world

`; + $$payload.out.push(`

Hello, world!

123

Hello, world

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js index 9457378c0d..29b0d0d594 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js @@ -1,7 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Purity($$payload) { - $$payload.out += `

0

${$.escape(location.href)}

`; + $$payload.out.push(`

0

${$.escape(location.href)}

`); Child($$payload, { prop: encodeURIComponent('hello') }); - $$payload.out += ``; + $$payload.out.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index 0532ec5aa9..bad475ec86 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -3,5 +3,5 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$payload, $$props) { let { title, content } = $$props; - $$payload.out += `

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`; + $$payload.out.push(`

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

`); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js index f814dd4f84..c2736b0f43 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/server/index.svelte.js @@ -11,5 +11,5 @@ export default function State_proxy_literal($$payload) { tpl = ``; } - $$payload.out += ` `; + $$payload.out.push(` `); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js index 6f019647f5..f7dc586026 100644 --- a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/server/index.svelte.js @@ -12,5 +12,5 @@ export default function Text_nodes_deriveds($$payload) { return count2; } - $$payload.out += `

${$.escape(text1())}${$.escape(text2())}

`; + $$payload.out.push(`

${$.escape(text1())}${$.escape(text2())}

`); } \ No newline at end of file From 05f6436445d54a689ca12cced4bd359d15fc2e4c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:35:20 -0400 Subject: [PATCH 58/76] Version Packages (#16435) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/four-kings-drum.md | 5 ----- .changeset/old-dots-sin.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/four-kings-drum.md delete mode 100644 .changeset/old-dots-sin.md diff --git a/.changeset/four-kings-drum.md b/.changeset/four-kings-drum.md deleted file mode 100644 index 8c7343145e..0000000000 --- a/.changeset/four-kings-drum.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: keep effect in the graph if it has an abort controller diff --git a/.changeset/old-dots-sin.md b/.changeset/old-dots-sin.md deleted file mode 100644 index f7e2933897..0000000000 --- a/.changeset/old-dots-sin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: Switch `payload.out` to an array diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a82d9ddefe..6f6b824174 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.8 + +### Patch Changes + +- fix: keep effect in the graph if it has an abort controller ([#16430](https://github.com/sveltejs/svelte/pull/16430)) + +- chore: Switch `payload.out` to an array ([#16428](https://github.com/sveltejs/svelte/pull/16428)) + ## 5.36.7 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 83567a6123..df53b69e93 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.36.7", + "version": "5.36.8", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 88cdc069a0..005a06dc19 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.7'; +export const VERSION = '5.36.8'; export const PUBLIC_VERSION = '5'; From 27c90dfa8322c9f225e22a730a776c4f4cbd2c5e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 19 Jul 2025 08:23:27 +0200 Subject: [PATCH 59/76] fix: don't reexecute derived with no dependencies on teardown (#16438) The prior logic was wrong because it reexecuted when something was clean, but we want to when it's not. The remaining fix was to also check the reactions: If an effect is destroyed and it was the last reaction of a derived then the derived is set to `MAYBE_DIRTY`. We therefore also need to check if the derived still has anyone listening to it, and only then reexecute it. Fixes #16363 --- .changeset/clever-toys-decide.md | 5 ++++ .../svelte/src/internal/client/runtime.js | 8 +++-- .../props-derived-teardown/Teardown.svelte | 11 +++++++ .../samples/props-derived-teardown/_config.js | 30 +++++++++++++++++++ .../props-derived-teardown/main.svelte | 25 ++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .changeset/clever-toys-decide.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte diff --git a/.changeset/clever-toys-decide.md b/.changeset/clever-toys-decide.md new file mode 100644 index 0000000000..57eb2b0058 --- /dev/null +++ b/.changeset/clever-toys-decide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't reexecute derived with no dependencies on teardown diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 306b9b9dd9..3d3c89975c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -653,8 +653,12 @@ export function get(signal) { var value = derived.v; - // if the derived is dirty, or depends on the values that just changed, re-execute - if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) { + // if the derived is dirty and has reactions, or depends on the values that just changed, re-execute + // (a derived can be maybe_dirty due to the effect destroy removing its last reaction) + if ( + ((derived.f & CLEAN) === 0 && derived.reactions !== null) || + depends_on_old_values(derived) + ) { value = execute_derived(derived); } diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte new file mode 100644 index 0000000000..f71890d31e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/Teardown.svelte @@ -0,0 +1,11 @@ + + +
teardown
\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js new file mode 100644 index 0000000000..c4e7d1307b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/_config.js @@ -0,0 +1,30 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual( + target.innerHTML, + ` + +
teardown
+
1
+
2
+
3
+ ` + ); + const [increment] = target.querySelectorAll('button'); + + increment.click(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + +
1
+
3
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte new file mode 100644 index 0000000000..ff9e19b2c6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-derived-teardown/main.svelte @@ -0,0 +1,25 @@ + + + + +{#if show} + +{/if} +{#each test.ids as id} +
{id}
+{/each} From 63cbe2108ac35204ffc99d16e5a520502ec7ade7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:35:43 -0700 Subject: [PATCH 60/76] fix: disallow `export { foo as default }` in ` \ No newline at end of file From ad0b58ee9606dd89038b055536e09c2b01112a0a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:21:25 -0700 Subject: [PATCH 61/76] fix: move ownership validation into async component body (#16449) * fix: move ownership validation into async component body * add test --------- Co-authored-by: Rich Harris --- .changeset/healthy-carpets-deny.md | 5 +++++ .../3-transform/client/transform-client.js | 12 +++++----- .../async-ownership-validation/Child.svelte | 7 ++++++ .../async-ownership-validation/_config.js | 22 +++++++++++++++++++ .../async-ownership-validation/main.svelte | 13 +++++++++++ 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 .changeset/healthy-carpets-deny.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md new file mode 100644 index 0000000000..94ee865fe7 --- /dev/null +++ b/.changeset/healthy-carpets-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: move ownership validation into async component body diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index c42d1b95d8..7f25e6c0d2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -364,6 +364,12 @@ export function client_component(analysis, options) { : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) ]); + if (analysis.needs_mutation_validation) { + component_block.body.unshift( + b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) + ); + } + const should_inject_context = dev || analysis.needs_context || @@ -434,12 +440,6 @@ export function client_component(analysis, options) { ); } - if (analysis.needs_mutation_validation) { - component_block.body.unshift( - b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) - ); - } - // we want the cleanup function for the stores to run as the very last thing // so that it can effectively clean up the store subscription even after the user effects runs if (should_inject_context) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte new file mode 100644 index 0000000000..d4d5cf7554 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js new file mode 100644 index 0000000000..167eee8488 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, warnings }) { + await tick(); + + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + assert.deepEqual(warnings, [ + 'Mutating unbound props (`object`, at Child.svelte:7:23) is strongly discouraged. Consider using `bind:object={...}` in main.svelte (or using a callback) instead' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte new file mode 100644 index 0000000000..ae6b43cbb1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

loading...

+ {/snippet} +
From 307ec228ffc70502b6c13c56c8e62f1abebeb73b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:24:47 -0700 Subject: [PATCH 62/76] fix: move store setup/cleanup outside of async component body (#16443) --- .changeset/new-fireants-bake.md | 5 +++++ .../3-transform/client/transform-client.js | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .changeset/new-fireants-bake.md diff --git a/.changeset/new-fireants-bake.md b/.changeset/new-fireants-bake.md new file mode 100644 index 0000000000..81bd596e43 --- /dev/null +++ b/.changeset/new-fireants-bake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: move store setup/cleanup outside of async component body diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 7f25e6c0d2..124438a9da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -209,7 +209,8 @@ export function client_component(analysis, options) { /** @type {ESTree.Statement[]} */ const store_setup = []; - + /** @type {ESTree.Statement} */ + let store_init = b.empty; /** @type {ESTree.VariableDeclaration[]} */ const legacy_reactive_declarations = []; @@ -227,8 +228,9 @@ export function client_component(analysis, options) { if (binding.kind === 'store_sub') { if (store_setup.length === 0) { needs_store_cleanup = true; - store_setup.push( - b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores')) + store_init = b.const( + b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), + b.call('$.setup_stores') ); } @@ -385,9 +387,16 @@ export function client_component(analysis, options) { analysis.slot_names.size > 0; if (analysis.instance.has_await) { + const params = [b.id('$$anchor')]; + if (should_inject_props) { + params.push(b.id('$$props')); + } + if (store_setup.length > 0) { + params.push(b.id('$$stores')); + } const body = b.function_declaration( b.id('$$body'), - should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')], + params, b.block([ b.var('$$unsuspend', b.call('$.suspend')), ...component_block.body, @@ -403,10 +412,12 @@ export function client_component(analysis, options) { component_block = b.block([ b.var('fragment', b.call('$.comment')), b.var('node', b.call('$.first_child', b.id('fragment'))), - b.stmt(b.call(body.id, b.id('node'), should_inject_props && b.id('$$props'))), + store_init, + b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) ]); } else { + component_block.body.unshift(store_init); component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); } From e1e7a75d719ab6826238ddaef4571ac6f6d22732 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:26:31 -0700 Subject: [PATCH 63/76] fix: allow async destructured deriveds (#16444) * fix: allow async destructured deriveds * add test * tweak * tweak --------- Co-authored-by: Rich Harris --- .changeset/mighty-balloons-rush.md | 5 ++ .../client/visitors/VariableDeclaration.js | 55 ++++++++++++++----- .../async-derived-destructured/Child.svelte | 13 +++++ .../async-derived-destructured/_config.js | 31 +++++++++++ .../async-derived-destructured/main.svelte | 11 ++++ svelte.config.js | 8 +++ 6 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 .changeset/mighty-balloons-rush.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte create mode 100644 svelte.config.js diff --git a/.changeset/mighty-balloons-rush.md b/.changeset/mighty-balloons-rush.md new file mode 100644 index 0000000000..ce8c09ca55 --- /dev/null +++ b/.changeset/mighty-balloons-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow async destructured deriveds diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 19a7de5715..0998dc4778 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -171,11 +171,14 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - const call = b.call('$.derived', expression); - return b.declarator( - id, - dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call - ); + let call = b.call('$.derived', expression); + + if (dev) { + const label = `[$state ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + return b.declarator(id, call); }), ...paths.map((path) => { const value = /** @type {Expression} */ (context.visit(path.expression)); @@ -228,19 +231,37 @@ export function VariableDeclaration(node, context) { } } else { const init = /** @type {CallExpression} */ (declarator.init); + let expression = /** @type {Expression} */ ( + context.visit(value, { + ...context.state, + in_derived: rune === '$derived' + }) + ); let rhs = value; if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') { const id = b.id(context.state.scope.generate('$$d')); + let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression); + rhs = b.call('$.get', id); - let expression = /** @type {Expression} */ (context.visit(value)); - if (rune === '$derived') expression = b.thunk(expression); - const call = b.call('$.derived', expression); - declarations.push( - b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) - ); + if (is_async) { + const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init); + call = b.call( + '$.async_derived', + b.thunk(expression, true), + location ? b.literal(location) : undefined + ); + call = b.call(b.await(b.call('$.save', call))); + } + + if (dev) { + const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + declarations.push(b.declarator(id, call)); } const { inserts, paths } = extract_paths(declarator.id, rhs); @@ -250,10 +271,14 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - const call = b.call('$.derived', expression); - declarations.push( - b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) - ); + let call = b.call('$.derived', expression); + + if (dev) { + const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + declarations.push(b.declarator(id, call)); } for (const path of paths) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte new file mode 100644 index 0000000000..39112b12a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte @@ -0,0 +1,13 @@ + + + + +

{count} ** 2 = {squared}

+

{count} ** 3 = {cubed}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js new file mode 100644 index 0000000000..d444e8e1d9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 ** 2 = 1

+

1 ** 3 = 1

+ ` + ); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2 ** 2 = 4

+

2 ** 3 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte new file mode 100644 index 0000000000..c5d8a12a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000..442cd7892c --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,8 @@ +// we need this so the VS Code extension doesn't yell at us +export default { + compilerOptions: { + experimental: { + async: true + } + } +}; From cc05cbcf5ccd1b19a42417dd374471c4cecb7776 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:30:38 -0700 Subject: [PATCH 64/76] Version Packages (#16445) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-toys-decide.md | 5 ----- .changeset/curly-buttons-burn.md | 5 ----- .changeset/healthy-carpets-deny.md | 5 ----- .changeset/mighty-balloons-rush.md | 5 ----- .changeset/new-fireants-bake.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/clever-toys-decide.md delete mode 100644 .changeset/curly-buttons-burn.md delete mode 100644 .changeset/healthy-carpets-deny.md delete mode 100644 .changeset/mighty-balloons-rush.md delete mode 100644 .changeset/new-fireants-bake.md diff --git a/.changeset/clever-toys-decide.md b/.changeset/clever-toys-decide.md deleted file mode 100644 index 57eb2b0058..0000000000 --- a/.changeset/clever-toys-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't reexecute derived with no dependencies on teardown diff --git a/.changeset/curly-buttons-burn.md b/.changeset/curly-buttons-burn.md deleted file mode 100644 index f5d45256fa..0000000000 --- a/.changeset/curly-buttons-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: disallow `export { foo as default }` in ` + + From 0a2a2e213ab183dc5b038e4fa4eb4913dd7761a4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Jul 2025 22:17:14 -0400 Subject: [PATCH 68/76] fix: add labels to `@const` tags and props (#16454) * fix: add labels to `@const` tags and props * changeset --- .changeset/four-spiders-type.md | 5 +++++ .../3-transform/client/visitors/ConstTag.js | 16 ++++++++++++++-- .../src/internal/client/reactivity/props.js | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/four-spiders-type.md diff --git a/.changeset/four-spiders-type.md b/.changeset/four-spiders-type.md new file mode 100644 index 0000000000..9a4056c50a --- /dev/null +++ b/.changeset/four-spiders-type.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add labels to `@const` tags and props diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index c1be1e3220..34acdd6bb9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -17,7 +17,13 @@ export function ConstTag(node, context) { // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { const init = build_expression(context, declaration.init, node.metadata.expression); - context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); + let expression = create_derived(context.state, b.thunk(init)); + + if (dev) { + expression = b.call('$.tag', expression, b.literal(declaration.id.name)); + } + + context.state.init.push(b.const(declaration.id, expression)); context.state.transform[declaration.id.name] = { read: get_value }; @@ -55,7 +61,13 @@ export function ConstTag(node, context) { ]) ); - context.state.init.push(b.const(tmp, create_derived(context.state, fn))); + let expression = create_derived(context.state, fn); + + if (dev) { + expression = b.call('$.tag', expression, b.literal('[@const]')); + } + + context.state.init.push(b.const(tmp, expression)); // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f39d45bb04..28bfa88d96 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -393,6 +393,10 @@ export function prop(props, key, flags, fallback) { return getter(); }); + if (DEV) { + d.label = key; + } + // Capture the initial value if it's bindable if (bindable) get(d); From 53fdd0f93abee0dc89418c7338363e9b298d0900 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Jul 2025 22:18:50 -0400 Subject: [PATCH 69/76] chore: remove some unused code (#16455) --- packages/svelte/src/internal/client/reactivity/props.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 28bfa88d96..05b747a1c4 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,13 +1,11 @@ -/** @import { ComponentContext } from '#client' */ -/** @import { Derived, Effect, Source } from './types.js' */ +/** @import { Effect, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - UNINITIALIZED + PROPS_IS_UPDATED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; From d3a01bd5a7d09384e29a599834d9b554a7a973ee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Jul 2025 17:19:56 -0400 Subject: [PATCH 70/76] fix: always mark reactions of deriveds (#16457) * tweak * fix * test * changeset --- .changeset/angry-hornets-hug.md | 5 ++ .../internal/client/reactivity/deriveds.js | 4 +- .../src/internal/client/reactivity/sources.js | 19 +++---- .../async-time-travelling-derived/_config.js | 56 +++++++++++++++++++ .../async-time-travelling-derived/main.svelte | 26 +++++++++ 5 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 .changeset/angry-hornets-hug.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte diff --git a/.changeset/angry-hornets-hug.md b/.changeset/angry-hornets-hug.md new file mode 100644 index 0000000000..ffe59db100 --- /dev/null +++ b/.changeset/angry-hornets-hug.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always mark reactions of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fa6a9e02a1..7f730e365e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -337,7 +337,9 @@ export function update_derived(derived) { // don't mark derived clean if we're reading it inside a // cleanup function, or it will cache a stale value - if (is_destroying_effect) return; + if (is_destroying_effect) { + return; + } if (batch_deriveds !== null) { batch_deriveds.set(derived, derived.v); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9b534d2d71..f6b14f3360 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -314,9 +314,6 @@ function mark_reactions(signal, status) { var reaction = reactions[i]; var flags = reaction.f; - // Skip any effects that are already dirty - if ((flags & DIRTY) !== 0) continue; - // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; @@ -326,15 +323,15 @@ function mark_reactions(signal, status) { continue; } - set_signal_status(reaction, status); + // don't set a DIRTY reaction to MAYBE_DIRTY + if ((flags & DIRTY) === 0) { + set_signal_status(reaction, status); + } - // If the signal a) was previously clean or b) is an unowned derived, then mark it - if ((flags & (CLEAN | UNOWNED)) !== 0) { - if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { - schedule_effect(/** @type {Effect} */ (reaction)); - } + if ((flags & DERIVED) !== 0) { + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + } else if ((flags & DIRTY) === 0) { + schedule_effect(/** @type {Effect} */ (reaction)); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js new file mode 100644 index 0000000000..06437d2e31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js @@ -0,0 +1,56 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [a, b, update] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

+ ` + ); + + b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

b

+ ` + ); + + update.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

b

+ ` + ); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte new file mode 100644 index 0000000000..5fca286a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + + + {#if condition} +

a

+ {:else} +

b

+ {/if} + + {#snippet pending()}{/snippet} +
+ From 1f99914a16e03209cb7c0aac8ce9d3d72bb32f15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Jul 2025 17:22:01 -0400 Subject: [PATCH 71/76] chore: move `capture_signals` to legacy module (#16456) --- .changeset/swift-cherries-know.md | 5 ++ .../svelte/src/internal/client/dev/tracing.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/client/legacy.js | 46 ++++++++++++++++ .../svelte/src/internal/client/runtime.js | 54 ++----------------- 5 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 .changeset/swift-cherries-know.md create mode 100644 packages/svelte/src/internal/client/legacy.js diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md new file mode 100644 index 0000000000..d8bbb1256a --- /dev/null +++ b/.changeset/swift-cherries-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: move `capture_signals` to legacy module diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 1b26c702fb..673a710fac 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -4,7 +4,7 @@ import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; -import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; +import { active_reaction, untrack } from '../runtime.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cddb432a98..90f0f9baac 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -138,11 +138,11 @@ export { mark_store_binding } from './reactivity/store.js'; export { boundary, pending } from './dom/blocks/boundary.js'; +export { invalidate_inner_signals } from './legacy.js'; export { set_text } from './render.js'; export { get, safe_get, - invalidate_inner_signals, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/legacy.js b/packages/svelte/src/internal/client/legacy.js new file mode 100644 index 0000000000..97ad7244c0 --- /dev/null +++ b/packages/svelte/src/internal/client/legacy.js @@ -0,0 +1,46 @@ +/** @import { Value } from '#client' */ +import { internal_set } from './reactivity/sources.js'; +import { untrack } from './runtime.js'; + +/** + * @type {Set | null} + * @deprecated + */ +export let captured_signals = null; + +/** + * Capture an array of all the signals that are read when `fn` is called + * @template T + * @param {() => T} fn + */ +function capture_signals(fn) { + var previous_captured_signals = captured_signals; + + try { + captured_signals = new Set(); + + untrack(fn); + + if (previous_captured_signals !== null) { + for (var signal of captured_signals) { + previous_captured_signals.add(signal); + } + } + + return captured_signals; + } finally { + captured_signals = previous_captured_signals; + } +} + +/** + * Invokes a function and captures all signals that are read during the invocation, + * then invalidates them. + * @param {() => any} fn + * @deprecated + */ +export function invalidate_inner_signals(fn) { + for (var signal of capture_signals(fn)) { + internal_set(signal, signal.v); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3d3c89975c..e86866af2a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,7 @@ import { STALE_REACTION, ERROR_VALUE } from './constants.js'; -import { internal_set, old_values } from './reactivity/sources.js'; +import { old_values } from './reactivity/sources.js'; import { destroy_derived_effects, execute_derived, @@ -45,6 +45,7 @@ import * as w from './warnings.js'; import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; +import { captured_signals } from './legacy.js'; export let is_updating_effect = false; @@ -137,14 +138,6 @@ export function set_update_version(value) { // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; -// Handle collecting all signals which are read during a specific time frame -/** @type {Set | null} */ -export let captured_signals = null; - -/** @param {Set | null} value */ -export function set_captured_signals(value) { - captured_signals = value; -} export function increment_write_version() { return ++write_version; @@ -531,9 +524,7 @@ export function get(signal) { var flags = signal.f; var is_derived = (flags & DERIVED) !== 0; - if (captured_signals !== null) { - captured_signals.add(signal); - } + captured_signals?.add(signal); // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { @@ -713,45 +704,6 @@ export function safe_get(signal) { return signal && get(signal); } -/** - * Capture an array of all the signals that are read when `fn` is called - * @template T - * @param {() => T} fn - */ -function capture_signals(fn) { - var previous_captured_signals = captured_signals; - captured_signals = new Set(); - - var captured = captured_signals; - var signal; - - try { - untrack(fn); - if (previous_captured_signals !== null) { - for (signal of captured_signals) { - previous_captured_signals.add(signal); - } - } - } finally { - captured_signals = previous_captured_signals; - } - - return captured; -} - -/** - * Invokes a function and captures all signals that are read during the invocation, - * then invalidates them. - * @param {() => any} fn - */ -export function invalidate_inner_signals(fn) { - var captured = capture_signals(() => untrack(fn)); - - for (var signal of captured) { - internal_set(signal, signal.v); - } -} - /** * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), * any state read inside `fn` will not be treated as a dependency. From 8afe5ec0c5a2e38b9b3a55b0dec94b99aa10447c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:23:10 -0400 Subject: [PATCH 72/76] Version Packages (#16453) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/angry-hornets-hug.md | 5 ----- .changeset/four-spiders-type.md | 5 ----- .changeset/silent-rockets-tease.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/angry-hornets-hug.md delete mode 100644 .changeset/four-spiders-type.md delete mode 100644 .changeset/silent-rockets-tease.md diff --git a/.changeset/angry-hornets-hug.md b/.changeset/angry-hornets-hug.md deleted file mode 100644 index ffe59db100..0000000000 --- a/.changeset/angry-hornets-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always mark reactions of deriveds diff --git a/.changeset/four-spiders-type.md b/.changeset/four-spiders-type.md deleted file mode 100644 index 9a4056c50a..0000000000 --- a/.changeset/four-spiders-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add labels to `@const` tags and props diff --git a/.changeset/silent-rockets-tease.md b/.changeset/silent-rockets-tease.md deleted file mode 100644 index 1a708c1d69..0000000000 --- a/.changeset/silent-rockets-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: tag stores for `$inspect.trace()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c20618af11..472a2b4a98 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.36.11 + +### Patch Changes + +- fix: always mark reactions of deriveds ([#16457](https://github.com/sveltejs/svelte/pull/16457)) + +- fix: add labels to `@const` tags and props ([#16454](https://github.com/sveltejs/svelte/pull/16454)) + +- fix: tag stores for `$inspect.trace()` ([#16452](https://github.com/sveltejs/svelte/pull/16452)) + ## 5.36.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 28ccb2884a..4b22911e6e 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.36.10", + "version": "5.36.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8c59098e0c..07cf3cdf4d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.10'; +export const VERSION = '5.36.11'; export const PUBLIC_VERSION = '5'; From ce4a99ed6d0b4b53c7abb7a8763e8e4bc4de5431 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:11:26 -0400 Subject: [PATCH 73/76] Version Packages (#16459) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/swift-cherries-know.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/swift-cherries-know.md diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md deleted file mode 100644 index d8bbb1256a..0000000000 --- a/.changeset/swift-cherries-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: move `capture_signals` to legacy module diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 472a2b4a98..1234efa0d5 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.12 + +### Patch Changes + +- chore: move `capture_signals` to legacy module ([#16456](https://github.com/sveltejs/svelte/pull/16456)) + ## 5.36.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4b22911e6e..629ec99af8 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.36.11", + "version": "5.36.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 07cf3cdf4d..465bd73f0f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.11'; +export const VERSION = '5.36.12'; export const PUBLIC_VERSION = '5'; From aabd333d89a4c9241bc3ed57752cb2616dd66b7c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:22:13 +0200 Subject: [PATCH 74/76] fix: ensure subscriptions are picked up correctly by deriveds (#16466) Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. If we didn't do this then the comparison of write versions would determine that the derived has a later version than the subscriber, and it would not be re-run. Fixes #16311 Fixes #15888 --- .changeset/fresh-penguins-impress.md | 5 +++ .../src/reactivity/create-subscriber.js | 4 ++ .../samples/store-inside-derived/_config.js | 45 +++++++++++++++++++ .../samples/store-inside-derived/main.svelte | 36 +++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .changeset/fresh-penguins-impress.md create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md new file mode 100644 index 0000000000..35ff4f0aaa --- /dev/null +++ b/.changeset/fresh-penguins-impress.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 4dcac4e6f6..6f9313eb0a 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -82,6 +82,10 @@ export function createSubscriber(start) { if (subscribers === 0) { stop?.(); stop = undefined; + // Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. + // If we didn't do this then the comparison of write versions would determine that the derived has a later version than + // the subscriber, and it would not be re-run. + increment(version); } }); }; diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js new file mode 100644 index 0000000000..de078b1e75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test: ({ assert, target }) => { + const [loading, increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +
$value: 0
+
valueFromStore.current: 0
+
valueDerivedCurrent: 0
+ + + ` + ); + + loading.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: Loading...
+
valueFromStore.current: Loading...
+
valueDerivedCurrent: Loading...
+ + + ` + ); + + increment.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: 1
+
valueFromStore.current: 1
+
valueDerivedCurrent: 1
+ + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte new file mode 100644 index 0000000000..06d0a0d4b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte @@ -0,0 +1,36 @@ + + +
+ $value: {isLoading ? 'Loading...' : $value} +
+ +
+ valueFromStore.current: {isLoading ? 'Loading...' : valueFromStore.current} +
+ +
+ valueDerivedCurrent: {isLoading ? 'Loading...' : valueDerivedCurrent} +
+ + + + From 28403beaeb360fd7b4096fe7c8ef98e43d0676c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:32:47 -0400 Subject: [PATCH 75/76] Version Packages (#16467) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fresh-penguins-impress.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fresh-penguins-impress.md diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md deleted file mode 100644 index 35ff4f0aaa..0000000000 --- a/.changeset/fresh-penguins-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1234efa0d5..5a5e532a08 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.13 + +### Patch Changes + +- fix: ensure subscriptions are picked up correctly by deriveds ([#16466](https://github.com/sveltejs/svelte/pull/16466)) + ## 5.36.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 629ec99af8..4bf9a5df22 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.36.12", + "version": "5.36.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 465bd73f0f..7d47fbc5f1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.12'; +export const VERSION = '5.36.13'; export const PUBLIC_VERSION = '5'; From 9412c5861c365610d7c6c0e4ecd3572ac3fe5860 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Jul 2025 22:17:25 -0400 Subject: [PATCH 76/76] chore: log effect functions in log_effect_tree (#16468) --- packages/svelte/src/internal/client/dev/debug.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index c47080ed2f..2714a3af1f 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -63,6 +63,13 @@ export function log_effect_tree(effect, depth = 0) { // eslint-disable-next-line no-console console.log(callsite); + } else { + // eslint-disable-next-line no-console + console.groupCollapsed(`%cfn`, `font-weight: normal`); + // eslint-disable-next-line no-console + console.log(effect.fn); + // eslint-disable-next-line no-console + console.groupEnd(); } if (effect.deps !== null) {