From 4e6104a9393e1b24831909bb3f6811568c3db413 Mon Sep 17 00:00:00 2001 From: Yash <69746573+xambassador@users.noreply.github.com> Date: Fri, 12 Dec 2025 00:31:59 +0530 Subject: [PATCH 001/142] fix typo (#17347) --- packages/svelte/tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/tests/README.md b/packages/svelte/tests/README.md index 6dde66f6cb..9e450a7176 100644 --- a/packages/svelte/tests/README.md +++ b/packages/svelte/tests/README.md @@ -1,6 +1,6 @@ # Test repo -This repo tries to migrate as many tests from the currente Svelte project over to test against the new compiler/runtime. +This repo tries to migrate as many tests from the current Svelte project over to test against the new compiler/runtime. ## Differences to the old test suite From b268ccbf44a68353abd5e5dd9e48376afcd8a1ba Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Dec 2025 15:19:40 -0500 Subject: [PATCH 002/142] fix: correctly reschedule deferred effects when reviving a batch after async work (#17332) * WIP * test * fix: correctly reschedule deferred effects when reviving a batch after async work --- .changeset/lucky-wasps-grab.md | 5 +++ .../src/internal/client/reactivity/batch.js | 33 ++++++++----------- .../Child.svelte | 3 ++ .../async-reschedule-during-flush/_config.js | 26 +++++++++++++++ .../async-reschedule-during-flush/main.svelte | 23 +++++++++++++ 5 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 .changeset/lucky-wasps-grab.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte diff --git a/.changeset/lucky-wasps-grab.md b/.changeset/lucky-wasps-grab.md new file mode 100644 index 0000000000..d2d2283ccd --- /dev/null +++ b/.changeset/lucky-wasps-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly reschedule deferred effects when reviving a batch after async work diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 24dee51419..6f941c7ff2 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -44,7 +44,6 @@ import { eager_effect, unlink_effect } from './effects.js'; * effect: Effect | null; * effects: Effect[]; * render_effects: Effect[]; - * block_effects: Effect[]; * }} EffectTarget */ @@ -128,15 +127,15 @@ export class Batch { /** * Deferred effects (which run after async work has completed) that are DIRTY - * @type {Effect[]} + * @type {Set} */ - #dirty_effects = []; + #dirty_effects = new Set(); /** * Deferred effects that are MAYBE_DIRTY - * @type {Effect[]} + * @type {Set} */ - #maybe_dirty_effects = []; + #maybe_dirty_effects = new Set(); /** * A set of branches that still exist, but will be destroyed when this batch @@ -167,8 +166,7 @@ export class Batch { parent: null, effect: null, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; for (const root of root_effects) { @@ -187,7 +185,6 @@ export class Batch { if (this.is_deferred()) { this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); - this.#defer_effects(target.block_effects); } else { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. @@ -228,8 +225,7 @@ export class Batch { parent: target, effect, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; } @@ -241,7 +237,7 @@ export class Batch { } else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) { target.render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); + if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect); update_effect(effect); } @@ -263,7 +259,6 @@ export class Batch { // once the boundary is ready? this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); - this.#defer_effects(target.block_effects); target = /** @type {EffectTarget} */ (target.parent); } @@ -279,8 +274,11 @@ export class Batch { */ #defer_effects(effects) { for (const e of effects) { - const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; - target.push(e); + if ((e.f & DIRTY) !== 0) { + this.#dirty_effects.add(e); + } else if ((e.f & MAYBE_DIRTY) !== 0) { + this.#maybe_dirty_effects.add(e); + } // Since we're not executing these effects now, we need to clear any WAS_MARKED flags // so that other batches can correctly reach these effects during their own traversal @@ -390,8 +388,7 @@ export class Batch { parent: null, effect: null, effects: [], - render_effects: [], - block_effects: [] + render_effects: [] }; for (const batch of batches) { @@ -484,6 +481,7 @@ export class Batch { revive() { for (const e of this.#dirty_effects) { + this.#maybe_dirty_effects.delete(e); set_signal_status(e, DIRTY); schedule_effect(e); } @@ -493,9 +491,6 @@ export class Batch { schedule_effect(e); } - this.#dirty_effects = []; - this.#maybe_dirty_effects = []; - this.flush(); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte new file mode 100644 index 0000000000..2684005fcf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/Child.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js new file mode 100644 index 0000000000..a837d02f9f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [a, b, resolve] = target.querySelectorAll('button'); + + a.click(); + await tick(); + + b.click(); + await tick(); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + 42 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte new file mode 100644 index 0000000000..48940017a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-reschedule-during-flush/main.svelte @@ -0,0 +1,23 @@ + + + + + + +{#if a} + {await push(42)} + +{/if} From a544a9d5f3b1be263ff029c5d1fda9abb961660f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:31:08 -0500 Subject: [PATCH 003/142] Version Packages (#17343) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/lucky-wasps-grab.md | 5 ----- .changeset/smooth-comics-shine.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/lucky-wasps-grab.md delete mode 100644 .changeset/smooth-comics-shine.md diff --git a/.changeset/lucky-wasps-grab.md b/.changeset/lucky-wasps-grab.md deleted file mode 100644 index d2d2283ccd..0000000000 --- a/.changeset/lucky-wasps-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly reschedule deferred effects when reviving a batch after async work diff --git a/.changeset/smooth-comics-shine.md b/.changeset/smooth-comics-shine.md deleted file mode 100644 index 4bf26ed9f1..0000000000 --- a/.changeset/smooth-comics-shine.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly print `!doctype` during `print` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b6ea395a9d..8acbef1e84 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.45.9 + +### Patch Changes + +- fix: correctly reschedule deferred effects when reviving a batch after async work ([#17332](https://github.com/sveltejs/svelte/pull/17332)) + +- fix: correctly print `!doctype` during `print` ([#17341](https://github.com/sveltejs/svelte/pull/17341)) + ## 5.45.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 86838513d2..abd151542d 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.45.8", + "version": "5.45.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 69dae8a24e..d17a8a9a9c 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.45.8'; +export const VERSION = '5.45.9'; export const PUBLIC_VERSION = '5'; From 00459a1d78a657e7372b2c92a8604cef8af1c511 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 11 Dec 2025 20:23:05 -0700 Subject: [PATCH 004/142] fix: race condition when importing `AsyncLocalStorage` (#17350) * fix: race condition when importing `AsyncLocalStorage` * types: * tweak * remove a line * comment * dumb esbuild * final final pt2 final * ugh --- .changeset/early-showers-marry.md | 5 ++++ .../src/internal/server/render-context.js | 28 +++++++++++++------ packages/svelte/tests/runtime-browser/test.ts | 1 + 3 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 .changeset/early-showers-marry.md diff --git a/.changeset/early-showers-marry.md b/.changeset/early-showers-marry.md new file mode 100644 index 0000000000..a599cefe64 --- /dev/null +++ b/.changeset/early-showers-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: race condition when importing `AsyncLocalStorage` diff --git a/packages/svelte/src/internal/server/render-context.js b/packages/svelte/src/internal/server/render-context.js index a33fff69d3..7a014a1b15 100644 --- a/packages/svelte/src/internal/server/render-context.js +++ b/packages/svelte/src/internal/server/render-context.js @@ -2,7 +2,7 @@ /** @import { AsyncLocalStorage } from 'node:async_hooks' */ /** @import { RenderContext } from '#server' */ -import { deferred } from '../shared/utils.js'; +import { deferred, noop } from '../shared/utils.js'; import * as e from './errors.js'; /** @type {Promise | null} */ @@ -56,14 +56,26 @@ export async function with_render_context(fn) { /** @type {AsyncLocalStorage | null} */ let als = null; +/** @type {Promise | null} */ +let als_import = null; -export async function init_render_context() { - if (als !== null) return; - try { - // @ts-ignore -- we don't include node types in the production build - const { AsyncLocalStorage } = await import('node:async_hooks'); - als = new AsyncLocalStorage(); - } catch {} +/** + * + * @returns {Promise} + */ +export function init_render_context() { + // It's important the right side of this assignment can run a maximum of one time + // otherwise it's possible for a very, very well-timed race condition to assign to `als` + // at the beginning of a render, and then another render to assign to it again, which causes + // the first render's second half to use a new instance of `als` which doesn't have its + // context anymore. + // @ts-ignore -- we don't include node types in the production build + als_import ??= import('node:async_hooks') + .then((hooks) => { + als = new hooks.AsyncLocalStorage(); + }) + .then(noop, noop); + return als_import; } // this has to be a function because rollup won't treeshake it if it's a constant diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 63e601b115..597b2909dc 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -164,6 +164,7 @@ async function run_test( } ], bundle: true, + platform: 'node', format: 'iife', globalName: 'test_ssr' }); From 5f249abeae003e8cbe535ef0ddc92c815c33dc3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:26:58 -0500 Subject: [PATCH 005/142] Version Packages (#17353) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/early-showers-marry.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/early-showers-marry.md diff --git a/.changeset/early-showers-marry.md b/.changeset/early-showers-marry.md deleted file mode 100644 index a599cefe64..0000000000 --- a/.changeset/early-showers-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: race condition when importing `AsyncLocalStorage` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8acbef1e84..d67510fd07 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.45.10 + +### Patch Changes + +- fix: race condition when importing `AsyncLocalStorage` ([#17350](https://github.com/sveltejs/svelte/pull/17350)) + ## 5.45.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index abd151542d..ebbc3a0e62 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.45.9", + "version": "5.45.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d17a8a9a9c..e1b98ad1a8 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.45.9'; +export const VERSION = '5.45.10'; export const PUBLIC_VERSION = '5'; From c86c4fdca10f9d46a024c1f672e60e1737c1f110 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Fri, 12 Dec 2025 13:59:26 -0700 Subject: [PATCH 006/142] feat: Add CSP support for `hydratable` (#17338) --- .changeset/soft-donkeys-serve.md | 5 ++ .../docs/06-runtime/05-hydratable.md | 58 +++++++++++++++++ .../98-reference/.generated/server-errors.md | 6 ++ .../svelte/messages/server-errors/errors.md | 4 ++ packages/svelte/src/internal/server/crypto.js | 41 ++++++++++++ .../svelte/src/internal/server/crypto.test.ts | 15 +++++ packages/svelte/src/internal/server/errors.js | 12 ++++ packages/svelte/src/internal/server/index.js | 9 ++- .../svelte/src/internal/server/renderer.js | 62 +++++++++++++------ .../svelte/src/internal/server/types.d.ts | 7 +++ packages/svelte/src/legacy/legacy-server.js | 13 ++-- packages/svelte/src/server/index.d.ts | 4 +- .../samples/csp-config-error/_config.js | 7 +++ .../csp-config-error/_expected_head.html | 12 ++++ .../samples/csp-config-error/main.svelte | 4 ++ .../samples/csp-hash/_config.js | 7 +++ .../samples/csp-hash/_expected_head.html | 12 ++++ .../samples/csp-hash/main.svelte | 4 ++ .../samples/csp-nonce/_config.js | 6 ++ .../samples/csp-nonce/_expected_head.html | 12 ++++ .../samples/csp-nonce/main.svelte | 4 ++ .../tests/server-side-rendering/test.ts | 11 +++- packages/svelte/types/index.d.ts | 9 +++ 23 files changed, 294 insertions(+), 30 deletions(-) create mode 100644 .changeset/soft-donkeys-serve.md create mode 100644 packages/svelte/src/internal/server/crypto.js create mode 100644 packages/svelte/src/internal/server/crypto.test.ts create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-config-error/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-config-error/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-config-error/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-hash/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-hash/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-hash/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-nonce/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-nonce/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/csp-nonce/main.svelte diff --git a/.changeset/soft-donkeys-serve.md b/.changeset/soft-donkeys-serve.md new file mode 100644 index 0000000000..f0c7236694 --- /dev/null +++ b/.changeset/soft-donkeys-serve.md @@ -0,0 +1,5 @@ +--- +"svelte": minor +--- + +feat: Add `csp` option to `render(...)`, and emit hashes when using `hydratable` diff --git a/documentation/docs/06-runtime/05-hydratable.md b/documentation/docs/06-runtime/05-hydratable.md index a5302f264d..f8d5130581 100644 --- a/documentation/docs/06-runtime/05-hydratable.md +++ b/documentation/docs/06-runtime/05-hydratable.md @@ -63,3 +63,61 @@ All data returned from a `hydratable` function must be serializable. But this do {await promises.one} {await promises.two} ``` + +## CSP + +`hydratable` adds an inline ``; + `; + + let csp_attr = ''; + if (this.global.csp.nonce) { + csp_attr = ` nonce="${this.global.csp.nonce}"`; + } else if (this.global.csp.hash) { + // note to future selves: this doesn't need to be optimized with a Map + // because the it's impossible for identical data to occur multiple times in a single render + // (this would require the same hydratable key:value pair to be serialized multiple times) + const hash = await sha256(body); + this.global.csp.script_hashes.push(`sha256-${hash}`); + } + + return `\n\t\t${body}`; } } export class SSRState { + /** @readonly @type {Csp & { script_hashes: Sha256Source[] }} */ + csp; + /** @readonly @type {'sync' | 'async'} */ mode; @@ -700,10 +724,12 @@ export class SSRState { /** * @param {'sync' | 'async'} mode - * @param {string} [id_prefix] + * @param {string} id_prefix + * @param {Csp} csp */ - constructor(mode, id_prefix = '') { + constructor(mode, id_prefix = '', csp = { hash: false }) { this.mode = mode; + this.csp = { ...csp, script_hashes: [] }; let uid = 1; this.uid = () => `${id_prefix}s${uid++}`; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index 05ee34fb17..ea6282c176 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -15,6 +15,8 @@ export interface SSRContext { element?: Element; } +export type Csp = { nonce?: string; hash?: boolean }; + export interface HydratableLookupEntry { value: unknown; serialized: string; @@ -33,6 +35,8 @@ export interface RenderContext { hydratable: HydratableContext; } +export type Sha256Source = `sha256-${string}`; + export interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; @@ -40,6 +44,9 @@ export interface SyncRenderOutput { html: string; /** HTML that goes somewhere into the `` */ body: string; + hashes: { + script: Sha256Source[]; + }; } export type RenderOutput = SyncRenderOutput & PromiseLike; diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index a50d961751..05b329bea1 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -1,14 +1,14 @@ /** @import { SvelteComponent } from '../index.js' */ +/** @import { Csp } from '#server' */ import { asClassComponent as as_class_component, createClassComponent } from './legacy-client.js'; import { render } from '../internal/server/index.js'; import { async_mode_flag } from '../internal/flags/index.js'; -import * as w from '../internal/server/warnings.js'; // By having this as a separate entry point for server environments, we save the client bundle from having to include the server runtime export { createClassComponent }; -/** @typedef {{ head: string, html: string, css: { code: string, map: null }}} LegacyRenderResult */ +/** @typedef {{ head: string, html: string, css: { code: string, map: null }; hashes?: { script: `sha256-${string}`[] } }} LegacyRenderResult */ /** * Takes a Svelte 5 component and returns a Svelte 4 compatible component constructor. @@ -25,10 +25,10 @@ export { createClassComponent }; */ export function asClassComponent(component) { const component_constructor = as_class_component(component); - /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; }) => LegacyRenderResult & PromiseLike } */ - const _render = (props, { context } = {}) => { + /** @type {(props?: {}, opts?: { $$slots?: {}; context?: Map; csp?: Csp }) => LegacyRenderResult & PromiseLike } */ + const _render = (props, { context, csp } = {}) => { // @ts-expect-error the typings are off, but this will work if the component is compiled in SSR mode - const result = render(component, { props, context }); + const result = render(component, { props, context, csp }); const munged = Object.defineProperties( /** @type {LegacyRenderResult & PromiseLike} */ ({}), @@ -65,7 +65,8 @@ export function asClassComponent(component) { return onfulfilled({ css: munged.css, head: result.head, - html: result.body + html: result.body, + hashes: result.hashes }); }, onrejected); } diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index d5a3b813e6..f54bd5a5ca 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -1,4 +1,4 @@ -import type { RenderOutput } from '#server'; +import type { Csp, RenderOutput } from '#server'; import type { ComponentProps, Component, SvelteComponent, ComponentType } from 'svelte'; /** @@ -16,6 +16,7 @@ export function render< props?: Omit; context?: Map; idPrefix?: string; + csp?: Csp; } ] : [ @@ -24,6 +25,7 @@ export function render< props: Omit; context?: Map; idPrefix?: string; + csp?: Csp; } ] ): RenderOutput; diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_config.js b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_config.js new file mode 100644 index 0000000000..03626fc37b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'], + csp: { hash: true, nonce: 'test-nonce' }, + error: 'invalid_csp' +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_expected_head.html new file mode 100644 index 0000000000..fb3c95f51f --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/_expected_head.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-config-error/main.svelte b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/main.svelte new file mode 100644 index 0000000000..2c4726edf4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-config-error/main.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-hash/_config.js b/packages/svelte/tests/server-side-rendering/samples/csp-hash/_config.js new file mode 100644 index 0000000000..fe28087d86 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-hash/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'], + csp: { hash: true }, + script_hashes: ['sha256-J0xwNm40i0NVEdHYeMRThG7y90X+P/I1ElZGnpQ0AbU='] +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-hash/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/csp-hash/_expected_head.html new file mode 100644 index 0000000000..56319255d6 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-hash/_expected_head.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-hash/main.svelte b/packages/svelte/tests/server-side-rendering/samples/csp-hash/main.svelte new file mode 100644 index 0000000000..2c4726edf4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-hash/main.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_config.js b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_config.js new file mode 100644 index 0000000000..320c0f67f8 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + mode: ['async'], + csp: { nonce: 'test-nonce' } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_expected_head.html new file mode 100644 index 0000000000..fb3c95f51f --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/_expected_head.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/csp-nonce/main.svelte b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/main.svelte new file mode 100644 index 0000000000..2c4726edf4 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/csp-nonce/main.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/test.ts b/packages/svelte/tests/server-side-rendering/test.ts index 4b33685608..2bfc84c7a1 100644 --- a/packages/svelte/tests/server-side-rendering/test.ts +++ b/packages/svelte/tests/server-side-rendering/test.ts @@ -21,6 +21,8 @@ interface SSRTest extends BaseTest { id_prefix?: string; withoutNormalizeHtml?: boolean; error?: string; + csp?: { nonce: string } | { hash: true }; + script_hashes?: string[]; } // TODO remove this shim when we can @@ -77,7 +79,8 @@ const { test, run } = suite_with_variants; context?: Map; idPrefix?: string; + csp?: Csp; } ] : [ @@ -2561,9 +2562,14 @@ declare module 'svelte/server' { props: Omit; context?: Map; idPrefix?: string; + csp?: Csp; } ] ): RenderOutput; + type Csp = { nonce?: string; hash?: boolean }; + + type Sha256Source = `sha256-${string}`; + interface SyncRenderOutput { /** HTML that goes into the `` */ head: string; @@ -2571,6 +2577,9 @@ declare module 'svelte/server' { html: string; /** HTML that goes somewhere into the `` */ body: string; + hashes: { + script: Sha256Source[]; + }; } type RenderOutput = SyncRenderOutput & PromiseLike; From 435da13fddaf1872a3fb46a7e8a20bb73322148e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 18:52:20 -0500 Subject: [PATCH 007/142] Version Packages (#17358) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/soft-donkeys-serve.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/soft-donkeys-serve.md diff --git a/.changeset/soft-donkeys-serve.md b/.changeset/soft-donkeys-serve.md deleted file mode 100644 index f0c7236694..0000000000 --- a/.changeset/soft-donkeys-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": minor ---- - -feat: Add `csp` option to `render(...)`, and emit hashes when using `hydratable` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d67510fd07..86718d924c 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.46.0 + +### Minor Changes + +- feat: Add `csp` option to `render(...)`, and emit hashes when using `hydratable` ([#17338](https://github.com/sveltejs/svelte/pull/17338)) + ## 5.45.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ebbc3a0e62..74c80536d3 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.45.10", + "version": "5.46.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e1b98ad1a8..4d102bbfbd 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.45.10'; +export const VERSION = '5.46.0'; export const PUBLIC_VERSION = '5'; From 95a895a9d91d93ea6a9f8814e2ae57ccc25ee164 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 22 Dec 2025 13:48:20 -0500 Subject: [PATCH 008/142] fix: prevent infinite loop when HMRing a component with an `await` (#17380) * fix: prevent infinite loop when HMRing a component with an `await` * update test * fix * reintroduce old logic to fix ever-growing stack of block effects * update snapshot * tweak names for clarity, simplify `update` slightly --------- Co-authored-by: Simon Holthausen --- .changeset/giant-gifts-mate.md | 5 +++ .../3-transform/client/transform-client.js | 10 +---- .../svelte/src/internal/client/dev/hmr.js | 39 ++++++++++++------- .../hmr/_expected/client/index.svelte.js | 5 +-- playgrounds/sandbox/run.js | 1 + playgrounds/sandbox/svelte.config.js | 2 +- 6 files changed, 36 insertions(+), 26 deletions(-) create mode 100644 .changeset/giant-gifts-mate.md diff --git a/.changeset/giant-gifts-mate.md b/.changeset/giant-gifts-mate.md new file mode 100644 index 0000000000..dc2486b698 --- /dev/null +++ b/.changeset/giant-gifts-mate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite loop when HMRing a component with an `await` 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 f51042eb7c..5c00aa591f 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 @@ -519,14 +519,9 @@ export function client_component(analysis, options) { if (options.hmr) { const id = b.id(analysis.name); - const HMR = b.id('$.HMR'); - - const existing = b.member(id, HMR, true); - const incoming = b.member(b.id('module.default'), HMR, true); const accept_fn_body = [ - b.stmt(b.assignment('=', b.member(incoming, 'source'), b.member(existing, 'source'))), - b.stmt(b.call('$.set', b.member(existing, 'source'), b.member(incoming, 'original'))) + b.stmt(b.call(b.member(b.member(id, b.id('$.HMR'), true), 'update'), b.id('module.default'))) ]; if (analysis.css.hash) { @@ -535,8 +530,7 @@ export function client_component(analysis, options) { } const hmr = b.block([ - b.stmt(b.assignment('=', id, b.call('$.hmr', id, b.thunk(b.member(existing, 'source'))))), - + b.stmt(b.assignment('=', id, b.call('$.hmr', id))), b.stmt(b.call('import.meta.hot.accept', b.arrow([b.id('module')], b.block(accept_fn_body)))) ]); diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 709a1b2722..9fa4e6ccbd 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -1,23 +1,25 @@ -/** @import { Source, Effect, TemplateNode } from '#client' */ +/** @import { Effect, TemplateNode } from '#client' */ import { FILENAME, HMR } from '../../../constants.js'; import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_node, hydrating } from '../dom/hydration.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js'; -import { source } from '../reactivity/sources.js'; +import { set, source } from '../reactivity/sources.js'; import { set_should_intro } from '../render.js'; import { get } from '../runtime.js'; /** * @template {(anchor: Comment, props: any) => any} Component - * @param {Component} original - * @param {() => Source} get_source + * @param {Component} fn */ -export function hmr(original, get_source) { +export function hmr(fn) { + const current = source(fn); + /** * @param {TemplateNode} anchor * @param {any} props */ function wrapper(anchor, props) { + let component = {}; let instance = {}; /** @type {Effect} */ @@ -26,8 +28,9 @@ export function hmr(original, get_source) { let ran = false; block(() => { - const source = get_source(); - const component = get(source); + if (component === (component = get(current))) { + return; + } if (effect) { // @ts-ignore @@ -62,16 +65,24 @@ export function hmr(original, get_source) { } // @ts-expect-error - wrapper[FILENAME] = original[FILENAME]; + wrapper[FILENAME] = fn[FILENAME]; // @ts-ignore wrapper[HMR] = { - // When we accept an update, we set the original source to the new component - original, - // The `get_source` parameter reads `wrapper[HMR].source`, but in the `accept` - // function we always replace it with `previous[HMR].source`, which in practice - // means we only ever update the original - source: source(original) + fn, + current, + update: (/** @type {any} */ incoming) => { + // This logic ensures that the first version of the component is the one + // whose update function and therefore block effect is preserved across updates. + // If we don't do this dance and instead just use `incoming` as the new component + // and then update, we'll create an ever-growing stack of block effects. + + // Trigger the original block effect + set(wrapper[HMR].current, incoming[HMR].fn); + + // Replace the incoming source with the original one + incoming[HMR].current = wrapper[HMR].current; + } }; return wrapper; diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 1fac1338c5..66d1a5af5a 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -11,11 +11,10 @@ function Hmr($$anchor) { } if (import.meta.hot) { - Hmr = $.hmr(Hmr, () => Hmr[$.HMR].source); + Hmr = $.hmr(Hmr); import.meta.hot.accept((module) => { - module.default[$.HMR].source = Hmr[$.HMR].source; - $.set(Hmr[$.HMR].source, module.default[$.HMR].original); + Hmr[$.HMR].update(module.default); }); } diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 35bffb67a2..f79243d4e7 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -95,6 +95,7 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { if (generate === 'server' || FROM_HTML) { from_html = compile(source, { dev: DEV, + hmr: DEV, filename: input, generate, runes: argv.values.runes, diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index 68ac605385..e1a6f1385b 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,6 +1,6 @@ export default { compilerOptions: { - hmr: false, + hmr: true, experimental: { async: true From acd752524b0973036295aac2420a515ec55a5dd8 Mon Sep 17 00:00:00 2001 From: That Richan <11839880+that-richan@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:06:54 +0100 Subject: [PATCH 009/142] feat: type `currentTarget` in `on` function (#17370) * feat: type `currentTarget` in `on` function * `EventTarget &` is redundant, `Element` already extends `EventTarget` * no need for optional chaining * add currentTarget for other overloads * update changeset --------- Co-authored-by: Rich Harris --- .changeset/bumpy-radios-live.md | 5 +++++ packages/svelte/src/events/public.d.ts | 8 ++++---- packages/svelte/tests/types/events.ts | 2 ++ packages/svelte/types/index.d.ts | 8 ++++---- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 .changeset/bumpy-radios-live.md diff --git a/.changeset/bumpy-radios-live.md b/.changeset/bumpy-radios-live.md new file mode 100644 index 0000000000..f5aa4129c7 --- /dev/null +++ b/.changeset/bumpy-radios-live.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: type `currentTarget` in `on` function diff --git a/packages/svelte/src/events/public.d.ts b/packages/svelte/src/events/public.d.ts index ff42e523b3..bf7b20c108 100644 --- a/packages/svelte/src/events/public.d.ts +++ b/packages/svelte/src/events/public.d.ts @@ -8,7 +8,7 @@ export function on( window: Window, type: Type, - handler: (this: Window, event: WindowEventMap[Type]) => any, + handler: (this: Window, event: WindowEventMap[Type] & { currentTarget: Window }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -19,7 +19,7 @@ export function on( export function on( document: Document, type: Type, - handler: (this: Document, event: DocumentEventMap[Type]) => any, + handler: (this: Document, event: DocumentEventMap[Type] & { currentTarget: Document }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -30,7 +30,7 @@ export function on( export function on( element: Element, type: Type, - handler: (this: Element, event: HTMLElementEventMap[Type]) => any, + handler: (this: Element, event: HTMLElementEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -41,7 +41,7 @@ export function on( element: Element, type: Type, - handler: (this: Element, event: MediaQueryListEventMap[Type]) => any, + handler: (this: Element, event: MediaQueryListEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** diff --git a/packages/svelte/tests/types/events.ts b/packages/svelte/tests/types/events.ts index 77891952be..a334cb5493 100644 --- a/packages/svelte/tests/types/events.ts +++ b/packages/svelte/tests/types/events.ts @@ -8,6 +8,8 @@ on(window, 'click', (e) => e.button); on(document, 'click', (e) => e.button); +on(document.createElement('input'), 'input', (e) => e.currentTarget.value); + on( document.body, 'clidck', diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d276b2a628..c0b89d8736 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2810,7 +2810,7 @@ declare module 'svelte/events' { export function on( window: Window, type: Type, - handler: (this: Window, event: WindowEventMap[Type]) => any, + handler: (this: Window, event: WindowEventMap[Type] & { currentTarget: Window }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2821,7 +2821,7 @@ declare module 'svelte/events' { export function on( document: Document, type: Type, - handler: (this: Document, event: DocumentEventMap[Type]) => any, + handler: (this: Document, event: DocumentEventMap[Type] & { currentTarget: Document }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2832,7 +2832,7 @@ declare module 'svelte/events' { export function on( element: Element, type: Type, - handler: (this: Element, event: HTMLElementEventMap[Type]) => any, + handler: (this: Element, event: HTMLElementEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** @@ -2843,7 +2843,7 @@ declare module 'svelte/events' { export function on( element: Element, type: Type, - handler: (this: Element, event: MediaQueryListEventMap[Type]) => any, + handler: (this: Element, event: MediaQueryListEventMap[Type] & { currentTarget: Element }) => any, options?: AddEventListenerOptions | undefined ): () => void; /** From 31ac74fac7b1d6a1dc7aff1e462e657730aadfde Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 22 Dec 2025 15:09:30 -0500 Subject: [PATCH 010/142] fix: skip static optimisation for stateless deriveds after await (#17389) --- .changeset/deep-pears-juggle.md | 5 +++++ .../phases/3-transform/client/visitors/RegularElement.js | 4 +++- .../phases/3-transform/client/visitors/shared/utils.js | 2 +- .../samples/async-static-derived-after-await/_config.js | 9 +++++++++ .../samples/async-static-derived-after-await/main.svelte | 6 ++++++ 5 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 .changeset/deep-pears-juggle.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte diff --git a/.changeset/deep-pears-juggle.md b/.changeset/deep-pears-juggle.md new file mode 100644 index 0000000000..4cba6b7dee --- /dev/null +++ b/.changeset/deep-pears-juggle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip static optimisation for stateless deriveds after `await` diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 50cef67171..690715df84 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -336,7 +336,9 @@ export function RegularElement(node, context) { trimmed.every( (node) => node.type === 'Text' || - (!node.metadata.expression.has_state && !node.metadata.expression.has_await) + (!node.metadata.expression.has_state && + !node.metadata.expression.has_await && + !node.metadata.expression.has_blockers()) ) && trimmed.some((node) => node.type === 'ExpressionTag'); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 6145f66d87..7256073096 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -134,7 +134,7 @@ export function build_template_chunk( const evaluated = state.scope.evaluate(value); - has_await ||= node.metadata.expression.has_await; + has_await ||= node.metadata.expression.has_await || node.metadata.expression.has_blockers(); has_state ||= has_await || (node.metadata.expression.has_state && !evaluated.is_known); if (values.length === 1) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js new file mode 100644 index 0000000000..5c95b3149c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte new file mode 100644 index 0000000000..9ecc839c3c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-static-derived-after-await/main.svelte @@ -0,0 +1,6 @@ + + +

{message}

From d8085c24badd48bfcba4d5344f9327e3b2f8fae8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:09:13 +0100 Subject: [PATCH 011/142] Version Packages (#17388) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bumpy-radios-live.md | 5 ----- .changeset/deep-pears-juggle.md | 5 ----- .changeset/giant-gifts-mate.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/bumpy-radios-live.md delete mode 100644 .changeset/deep-pears-juggle.md delete mode 100644 .changeset/giant-gifts-mate.md diff --git a/.changeset/bumpy-radios-live.md b/.changeset/bumpy-radios-live.md deleted file mode 100644 index f5aa4129c7..0000000000 --- a/.changeset/bumpy-radios-live.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: type `currentTarget` in `on` function diff --git a/.changeset/deep-pears-juggle.md b/.changeset/deep-pears-juggle.md deleted file mode 100644 index 4cba6b7dee..0000000000 --- a/.changeset/deep-pears-juggle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: skip static optimisation for stateless deriveds after `await` diff --git a/.changeset/giant-gifts-mate.md b/.changeset/giant-gifts-mate.md deleted file mode 100644 index dc2486b698..0000000000 --- a/.changeset/giant-gifts-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent infinite loop when HMRing a component with an `await` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 86718d924c..dc0edbaf79 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.46.1 + +### Patch Changes + +- fix: type `currentTarget` in `on` function ([#17370](https://github.com/sveltejs/svelte/pull/17370)) + +- fix: skip static optimisation for stateless deriveds after `await` ([#17389](https://github.com/sveltejs/svelte/pull/17389)) + +- fix: prevent infinite loop when HMRing a component with an `await` ([#17380](https://github.com/sveltejs/svelte/pull/17380)) + ## 5.46.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 74c80536d3..7affd91207 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.46.0", + "version": "5.46.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4d102bbfbd..c28af94e19 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.46.0'; +export const VERSION = '5.46.1'; export const PUBLIC_VERSION = '5'; From fcdb28c8827288055c95d2e79fd13cfce31b2175 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Dec 2025 10:47:14 -0500 Subject: [PATCH 012/142] chore: disable HMR in playground (#17397) --- playgrounds/sandbox/svelte.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js index e1a6f1385b..68ac605385 100644 --- a/playgrounds/sandbox/svelte.config.js +++ b/playgrounds/sandbox/svelte.config.js @@ -1,6 +1,6 @@ export default { compilerOptions: { - hmr: true, + hmr: false, experimental: { async: true From 965bf6ceec0c6ee5347df6b04781fe9fe41aa5a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 26 Dec 2025 11:47:32 -0500 Subject: [PATCH 013/142] chore: better async setContext test (#17398) --- .../samples/async-set-context/A.svelte | 13 +++++++++++++ .../samples/async-set-context/B.svelte | 12 ++++++++++++ .../async-set-context/{Inner.svelte => C.svelte} | 3 ++- .../samples/async-set-context/Outer.svelte | 9 --------- .../samples/async-set-context/_config.js | 4 ++-- .../samples/async-set-context/main.svelte | 4 ++-- 6 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte rename packages/svelte/tests/runtime-runes/samples/async-set-context/{Inner.svelte => C.svelte} (60%) delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte new file mode 100644 index 0000000000..5b85a553d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/A.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte new file mode 100644 index 0000000000..2c93263ca5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/B.svelte @@ -0,0 +1,12 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte similarity index 60% rename from packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte rename to packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte index 2c7fd5d43d..2aebf7a64f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/C.svelte @@ -2,6 +2,7 @@ import { getContext } from "svelte"; let greeting = getContext("greeting"); + let recipient = getContext("recipient"); -

{greeting}

\ No newline at end of file +

{greeting} {recipient}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte deleted file mode 100644 index 9a493c5b75..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js index 041f67a39e..aebed211cc 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -3,9 +3,9 @@ import { test } from '../../test'; export default test({ mode: ['client', 'async-server'], - ssrHtml: `

hi

`, + ssrHtml: `

hello world

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

hi

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

hello world

'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte index 01b46bda93..e2189e7eb8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -1,7 +1,7 @@ - + From b1f44c46c3336df55ee6ebe38225ad746841af70 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Sat, 3 Jan 2026 23:43:31 +0800 Subject: [PATCH 014/142] fix: correctly compile server code when using `$inspect` after a top-level await expression (#17407) * add fix and test * changeset * Apply suggestion from @teemingc --- .changeset/spicy-years-stick.md | 5 +++++ .../phases/3-transform/shared/transform-async.js | 9 ++++++++- .../async-top-level-inspect-server/_config.js | 8 ++++++++ .../_expected/client/index.svelte.js | 16 ++++++++++++++++ .../_expected/server/index.svelte.js | 15 +++++++++++++++ .../async-top-level-inspect-server/index.svelte | 6 ++++++ 6 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/spicy-years-stick.md create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/index.svelte diff --git a/.changeset/spicy-years-stick.md b/.changeset/spicy-years-stick.md new file mode 100644 index 0000000000..76834e8381 --- /dev/null +++ b/.changeset/spicy-years-stick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove `$inspect` calls after await expressions when compiling for production server code diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js index 2b9c219d7d..8d3a6b2769 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -86,7 +86,14 @@ export function transform_body(instance_body, runner, transform) { } if (s.node.type === 'ExpressionStatement') { - const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression)); + // the expression may be a $inspect call, which will be transformed into an empty statement + const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ ( + transform(s.node.expression) + ); + + if (expression.type === 'EmptyStatement') { + return null; + } return expression.type === 'AwaitExpression' ? b.thunk(expression, true) diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_config.js b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_config.js new file mode 100644 index 0000000000..6214e6b429 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + experimental: { async: true }, + dev: false + } +}); diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/client/index.svelte.js new file mode 100644 index 0000000000..e8aa8dfa11 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/client/index.svelte.js @@ -0,0 +1,16 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +var root = $.from_html(`

`); + +export default function Async_top_level_inspect_server($$anchor) { + var data; + var $$promises = $.run([async () => data = await Promise.resolve(42),,]); + var p = root(); + var text = $.child(p, true); + + $.reset(p); + $.template_effect(() => $.set_text(text, data), void 0, void 0, [$$promises[1]]); + $.append($$anchor, p); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js new file mode 100644 index 0000000000..eb99a2fe1d --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/_expected/server/index.svelte.js @@ -0,0 +1,15 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_top_level_inspect_server($$renderer) { + var data; + var $$promises = $$renderer.run([async () => data = await Promise.resolve(42),,]); + + $$renderer.push(`

`); + + $$renderer.async([$$promises[1]], ($$renderer) => { + $$renderer.push(() => $.escape(data)); + }); + + $$renderer.push(`

`); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/index.svelte b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/index.svelte new file mode 100644 index 0000000000..06bb995b1e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-inspect-server/index.svelte @@ -0,0 +1,6 @@ + + +

{data}

From 37f08493c966401326b4bab9763e92c0a977f71a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Jan 2026 11:55:14 +0000 Subject: [PATCH 015/142] chore: eagerly assign labels to async deriveds (#17420) --- .../client/visitors/VariableDeclaration.js | 11 +++++------ .../svelte/src/internal/client/reactivity/deriveds.js | 5 ++++- 2 files changed, 9 insertions(+), 7 deletions(-) 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 57dba202ba..16aa164d84 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 @@ -209,11 +209,11 @@ export function VariableDeclaration(node, context) { let call = b.call( '$.async_derived', b.thunk(expression, true), + dev && b.literal(declarator.id.name), location ? b.literal(location) : undefined ); call = should_save ? save(call) : b.await(call); - if (dev) call = b.call('$.tag', call, b.literal(declarator.id.name)); declarations.push(b.declarator(declarator.id, call)); } else { @@ -244,17 +244,16 @@ export function VariableDeclaration(node, context) { call = b.call( '$.async_derived', b.thunk(expression, true), + dev && + b.literal( + `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]` + ), location ? b.literal(location) : undefined ); call = should_save ? save(call) : b.await(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)); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b2d5d59ae1..ee8ba29c21 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -93,11 +93,12 @@ export function derived(fn) { /** * @template V * @param {() => V | Promise} fn + * @param {string} [label] * @param {string} [location] If provided, print a warning if the value is not read immediately after update * @returns {Promise>} */ /*#__NO_SIDE_EFFECTS__*/ -export function async_derived(fn, location) { +export function async_derived(fn, label, location) { let parent = /** @type {Effect | null} */ (active_effect); if (parent === null) { @@ -109,6 +110,8 @@ export function async_derived(fn, location) { var promise = /** @type {Promise} */ (/** @type {unknown} */ (undefined)); var signal = source(/** @type {V} */ (UNINITIALIZED)); + if (DEV) signal.label = label; + // only suspend in async deriveds created on initialisation var should_suspend = !active_reaction; From 1482ae8726a40329fe540fb7f20e3517edd318fd Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Jan 2026 15:38:00 +0000 Subject: [PATCH 016/142] chore: remove `async_body` (#17425) * chore: remove unused async_body * unused --- packages/svelte/src/internal/client/index.js | 1 - .../src/internal/client/reactivity/async.js | 46 ------------------- 2 files changed, 47 deletions(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index a2add3ec59..a2666d8712 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -98,7 +98,6 @@ export { with_script } from './dom/template.js'; export { - async_body, for_await_track_reactivity_loss, run, save, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index e48aff3d7f..631af24807 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -25,7 +25,6 @@ import { set_from_async_derived } from './deriveds.js'; import { aborted } from './effects.js'; -import { hydrate_next, hydrating, set_hydrate_node, skip_nodes } from '../dom/hydration.js'; /** * @param {Array>} blockers @@ -211,51 +210,6 @@ export function unset_context() { } } -/** - * @param {TemplateNode} anchor - * @param {(target: TemplateNode) => Promise} fn - */ -export async function async_body(anchor, fn) { - var boundary = get_boundary(); - var batch = /** @type {Batch} */ (current_batch); - var blocking = !boundary.is_pending(); - - boundary.update_pending_count(1); - batch.increment(blocking); - - var active = /** @type {Effect} */ (active_effect); - - var was_hydrating = hydrating; - var next_hydrate_node = undefined; - - if (was_hydrating) { - hydrate_next(); - next_hydrate_node = skip_nodes(false); - } - - try { - var promise = fn(anchor); - } finally { - if (next_hydrate_node) { - set_hydrate_node(next_hydrate_node); - hydrate_next(); - } - } - - try { - await promise; - } catch (error) { - if (!aborted(active)) { - invoke_error_boundary(error, active); - } - } finally { - boundary.update_pending_count(-1); - batch.decrement(blocking); - - unset_context(); - } -} - /** * @param {Array<() => void | Promise>} thunks */ From 41e86438145d42c8ddf00fe1a44eaf3a7b8319c8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Jan 2026 16:39:10 +0000 Subject: [PATCH 017/142] fix: clear batch between runs (alternative approach) (#17424) * fix: clear batch between runs * changeset * add comment and note-to-self * associate effects with boundaries, albeit clumsily * remove unused async_body * dry out * fix * simplify * clarify * tweak * unused * unused * add an explanatory comment --- .changeset/tall-clocks-turn.md | 5 + .../src/internal/client/dom/blocks/async.js | 2 +- .../internal/client/dom/blocks/boundary.js | 65 ++++++--- .../src/internal/client/reactivity/async.js | 12 +- .../src/internal/client/reactivity/batch.js | 124 ++++++------------ .../internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/utils.js | 40 ++++++ .../Child.svelte | 15 +++ .../async-clear-batch-between-runs/_config.js | 32 +++++ .../main.svelte | 19 +++ 10 files changed, 209 insertions(+), 107 deletions(-) create mode 100644 .changeset/tall-clocks-turn.md create mode 100644 packages/svelte/src/internal/client/reactivity/utils.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte diff --git a/.changeset/tall-clocks-turn.md b/.changeset/tall-clocks-turn.md new file mode 100644 index 0000000000..22cefcff30 --- /dev/null +++ b/.changeset/tall-clocks-turn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: clear batch between runs diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 3d320d2ea9..3bc1eecba7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -21,7 +21,7 @@ import { get_boundary } from './boundary.js'; export function async(node, blockers = [], expressions = [], fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); - var blocking = !boundary.is_pending(); + var blocking = boundary.is_rendered(); boundary.update_pending_count(1); batch.increment(blocking); diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 140e876330..92f4c0a104 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -2,8 +2,10 @@ import { BOUNDARY_EFFECT, COMMENT_NODE, + DIRTY, EFFECT_PRESERVED, - EFFECT_TRANSPARENT + EFFECT_TRANSPARENT, + MAYBE_DIRTY } from '#client/constants'; import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; @@ -20,7 +22,8 @@ import { active_reaction, get, set_active_effect, - set_active_reaction + set_active_reaction, + set_signal_status } from '../../runtime.js'; import { hydrate_next, @@ -34,11 +37,12 @@ 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 } from '../../reactivity/batch.js'; +import { Batch, schedule_effect } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; import { create_text } from '../operations.js'; +import { defer_effect } from '../../reactivity/utils.js'; /** * @typedef {{ @@ -64,7 +68,7 @@ export class Boundary { /** @type {Boundary | null} */ parent; - #pending = false; + is_pending = false; /** @type {TemplateNode} */ #anchor; @@ -101,6 +105,12 @@ export class Boundary { #is_creating_fallback = false; + /** @type {Set} */ + #dirty_effects = new Set(); + + /** @type {Set} */ + #maybe_dirty_effects = new Set(); + /** * A source containing the number of pending async deriveds/expressions. * Only created if `$effect.pending()` is used inside the boundary, @@ -134,7 +144,7 @@ export class Boundary { this.parent = /** @type {Effect} */ (active_effect).b; - this.#pending = !!this.#props.pending; + this.is_pending = !!this.#props.pending; this.#effect = block(() => { /** @type {Effect} */ (active_effect).b = this; @@ -164,7 +174,7 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.#pending = false; + this.is_pending = false; } } @@ -187,7 +197,7 @@ export class Boundary { // Since server rendered resolved content, we never show pending state // Even if client-side async operations are still running, the content is already displayed - this.#pending = false; + this.is_pending = false; } #hydrate_pending_content() { @@ -212,7 +222,7 @@ export class Boundary { this.#pending_effect = null; }); - this.#pending = false; + this.is_pending = false; } }); } @@ -220,7 +230,7 @@ export class Boundary { #get_anchor() { var anchor = this.#anchor; - if (this.#pending) { + if (this.is_pending) { this.#pending_anchor = create_text(); this.#anchor.before(this.#pending_anchor); @@ -231,11 +241,19 @@ export class Boundary { } /** - * Returns `true` if the effect exists inside a boundary whose pending snippet is shown + * Defer an effect inside a pending boundary until the boundary resolves + * @param {Effect} effect + */ + defer_effect(effect) { + defer_effect(effect, this.#dirty_effects, this.#maybe_dirty_effects); + } + + /** + * Returns `false` if the effect exists inside a boundary whose pending snippet is shown * @returns {boolean} */ - is_pending() { - return this.#pending || (!!this.parent && this.parent.is_pending()); + is_rendered() { + return !this.is_pending && (!this.parent || this.parent.is_rendered()); } has_pending_snippet() { @@ -298,7 +316,24 @@ export class Boundary { this.#pending_count += d; if (this.#pending_count === 0) { - this.#pending = false; + this.is_pending = false; + + // any effects that were encountered and deferred during traversal + // should be rescheduled — after the next traversal (which will happen + // immediately, due to the same update that brought us here) + // the effects will be flushed + for (const e of this.#dirty_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); + } + + this.#dirty_effects.clear(); + this.#maybe_dirty_effects.clear(); if (this.#pending_effect) { pause_effect(this.#pending_effect, () => { @@ -394,7 +429,7 @@ export class Boundary { // we intentionally do not try to find the nearest pending boundary. If this boundary has one, we'll render it on reset // but it would be really weird to show the parent's boundary on a child reset. - this.#pending = this.has_pending_snippet(); + this.is_pending = this.has_pending_snippet(); this.#main_effect = this.#run(() => { this.#is_creating_fallback = false; @@ -404,7 +439,7 @@ export class Boundary { if (this.#pending_count > 0) { this.#show_pending_snippet(); } else { - this.#pending = false; + this.is_pending = false; } }; diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 631af24807..334bf8f145 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -218,7 +218,7 @@ export function run(thunks) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); - var blocking = !boundary.is_pending(); + var blocking = boundary.is_rendered(); boundary.update_pending_count(1); batch.increment(blocking); @@ -252,17 +252,13 @@ export function run(thunks) { throw STALE_REACTION; } - try { - restore(); - return fn(); - } finally { - // TODO do we need it here as well as below? - unset_context(); - } + restore(); + return fn(); }) .catch(handle_error) .finally(() => { unset_context(); + current_batch?.deactivate(); }); promises.push(promise); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6f941c7ff2..6dd90ba1b4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,5 +1,6 @@ /** @import { Fork } from 'svelte' */ /** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ +/** @import { Boundary } from '../dom/blocks/boundary' */ import { BLOCK_EFFECT, BRANCH_EFFECT, @@ -17,7 +18,6 @@ import { EAGER_EFFECT, HEAD_EFFECT, ERROR_VALUE, - WAS_MARKED, MANAGED_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; @@ -37,15 +37,7 @@ import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; - -/** - * @typedef {{ - * parent: EffectTarget | null; - * effect: Effect | null; - * effects: Effect[]; - * render_effects: Effect[]; - * }} EffectTarget - */ +import { defer_effect } from './utils.js'; /** @type {Set} */ const batches = new Set(); @@ -161,16 +153,14 @@ export class Batch { this.apply(); - /** @type {EffectTarget} */ - var target = { - parent: null, - effect: null, - effects: [], - render_effects: [] - }; + /** @type {Effect[]} */ + var effects = []; + + /** @type {Effect[]} */ + var render_effects = []; for (const root of root_effects) { - this.#traverse_effect_tree(root, target); + this.#traverse_effect_tree(root, effects, render_effects); // Note: #traverse_effect_tree runs block effects eagerly, which can schedule effects, // which means queued_root_effects now may be filled again. @@ -183,16 +173,16 @@ export class Batch { } if (this.is_deferred()) { - this.#defer_effects(target.effects); - this.#defer_effects(target.render_effects); + this.#defer_effects(render_effects); + this.#defer_effects(effects); } else { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. previous_batch = this; current_batch = null; - flush_queued_effects(target.render_effects); - flush_queued_effects(target.effects); + flush_queued_effects(render_effects); + flush_queued_effects(effects); previous_batch = null; @@ -206,13 +196,17 @@ export class Batch { * Traverse the effect tree, executing effects or stashing * them for later execution as appropriate * @param {Effect} root - * @param {EffectTarget} target + * @param {Effect[]} effects + * @param {Effect[]} render_effects */ - #traverse_effect_tree(root, target) { + #traverse_effect_tree(root, effects, render_effects) { root.f ^= CLEAN; var effect = root.first; + /** @type {Effect | null} */ + var pending_boundary = null; + while (effect !== null) { var flags = effect.f; var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; @@ -220,24 +214,32 @@ export class Batch { var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); - if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) { - target = { - parent: target, - effect, - effects: [], - render_effects: [] - }; + // Inside a `` with a pending snippet, + // all effects are deferred until the boundary resolves + // (except block/async effects, which run immediately) + if ( + async_mode_flag && + pending_boundary === null && + (flags & BOUNDARY_EFFECT) !== 0 && + effect.b?.is_pending + ) { + pending_boundary = effect; } if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; + } else if ( + pending_boundary !== null && + (flags & (EFFECT | RENDER_EFFECT | MANAGED_EFFECT)) !== 0 + ) { + /** @type {Boundary} */ (pending_boundary.b).defer_effect(effect); } else if ((flags & EFFECT) !== 0) { - target.effects.push(effect); + effects.push(effect); } else if (async_mode_flag && (flags & (RENDER_EFFECT | MANAGED_EFFECT)) !== 0) { - target.render_effects.push(effect); + render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect); + if ((flags & BLOCK_EFFECT) !== 0) this.#dirty_effects.add(effect); update_effect(effect); } @@ -253,14 +255,8 @@ export class Batch { effect = effect.next; while (effect === null && parent !== null) { - if (parent === target.effect) { - // TODO rather than traversing into pending boundaries and deferring the effects, - // could we just attach the effects _to_ the pending boundary and schedule them - // once the boundary is ready? - this.#defer_effects(target.effects); - this.#defer_effects(target.render_effects); - - target = /** @type {EffectTarget} */ (target.parent); + if (parent === pending_boundary) { + pending_boundary = null; } effect = parent.next; @@ -273,36 +269,8 @@ export class Batch { * @param {Effect[]} effects */ #defer_effects(effects) { - for (const e of effects) { - if ((e.f & DIRTY) !== 0) { - this.#dirty_effects.add(e); - } else if ((e.f & MAYBE_DIRTY) !== 0) { - this.#maybe_dirty_effects.add(e); - } - - // Since we're not executing these effects now, we need to clear any WAS_MARKED flags - // so that other batches can correctly reach these effects during their own traversal - this.#clear_marked(e.deps); - - // mark as clean so they get scheduled if they depend on pending async state - set_signal_status(e, CLEAN); - } - } - - /** - * @param {Value[] | null} deps - */ - #clear_marked(deps) { - if (deps === null) return; - - for (const dep of deps) { - if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) { - continue; - } - - dep.f ^= WAS_MARKED; - - this.#clear_marked(/** @type {Derived} */ (dep).deps); + for (var i = 0; i < effects.length; i += 1) { + defer_effect(effects[i], this.#dirty_effects, this.#maybe_dirty_effects); } } @@ -383,14 +351,6 @@ export class Batch { var previous_batch_values = batch_values; var is_earlier = true; - /** @type {EffectTarget} */ - var dummy_target = { - parent: null, - effect: null, - effects: [], - render_effects: [] - }; - for (const batch of batches) { if (batch === this) { is_earlier = false; @@ -439,10 +399,10 @@ export class Batch { batch.apply(); for (const root of queued_root_effects) { - batch.#traverse_effect_tree(root, dummy_target); + batch.#traverse_effect_tree(root, [], []); } - // TODO do we need to do anything with `target`? defer block effects? + // TODO do we need to do anything with the dummy effect arrays? batch.deactivate(); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index ee8ba29c21..9dc324d3f9 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -150,7 +150,7 @@ export function async_derived(fn, label, location) { var batch = /** @type {Batch} */ (current_batch); if (should_suspend) { - var blocking = !boundary.is_pending(); + var blocking = boundary.is_rendered(); boundary.update_pending_count(1); batch.increment(blocking); diff --git a/packages/svelte/src/internal/client/reactivity/utils.js b/packages/svelte/src/internal/client/reactivity/utils.js new file mode 100644 index 0000000000..0dd6b3212d --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/utils.js @@ -0,0 +1,40 @@ +/** @import { Derived, Effect, Value } from '#client' */ +import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants'; +import { set_signal_status } from '../runtime.js'; + +/** + * @param {Value[] | null} deps + */ +function clear_marked(deps) { + if (deps === null) return; + + for (const dep of deps) { + if ((dep.f & DERIVED) === 0 || (dep.f & WAS_MARKED) === 0) { + continue; + } + + dep.f ^= WAS_MARKED; + + clear_marked(/** @type {Derived} */ (dep).deps); + } +} + +/** + * @param {Effect} effect + * @param {Set} dirty_effects + * @param {Set} maybe_dirty_effects + */ +export function defer_effect(effect, dirty_effects, maybe_dirty_effects) { + if ((effect.f & DIRTY) !== 0) { + dirty_effects.add(effect); + } else if ((effect.f & MAYBE_DIRTY) !== 0) { + maybe_dirty_effects.add(effect); + } + + // Since we're not executing these effects now, we need to clear any WAS_MARKED flags + // so that other batches can correctly reach these effects during their own traversal + clear_marked(effect.deps); + + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(effect, CLEAN); +} diff --git a/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte new file mode 100644 index 0000000000..d156cc99af --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/Child.svelte @@ -0,0 +1,15 @@ + + +

x: {x}

+ + + {#snippet pending()} +

Loading...

+ {/snippet} + +

y: {y}

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

loading...

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

x: x2

+

y: y2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte new file mode 100644 index 0000000000..57ab32a6ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-clear-batch-between-runs/main.svelte @@ -0,0 +1,19 @@ + + + + + + + + + {#snippet pending()} +

loading...

+ {/snippet} +
From 044dce9da54cdf2dc1c8d268038364c4ac22b110 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 7 Jan 2026 18:37:01 +0000 Subject: [PATCH 018/142] fix: adjust `loc` property of `Program` nodes created from ` + +{#each items.filter(fn) as item} + +{/each} From 05a85d305a19f4c71e6dbfa5f967697234f180f1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Jan 2026 13:15:12 -0500 Subject: [PATCH 020/142] chore: use normalization helper for hydration tests (#17434) --- packages/svelte/tests/hydration/test.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index ba13d2c611..d7257c71d3 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -131,19 +131,12 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); - const normalize = (string: string) => - string - .trim() - .replaceAll('\r\n', '\n') - .replaceAll('/>', '>') - .replace(//g, ''); - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; - assert.equal(normalize(target.innerHTML), normalize(expected)); + assert_html_equal(target.innerHTML, expected); if (rendered.head) { const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head; - assert.equal(normalize(head.innerHTML), normalize(expected)); + assert_html_equal(head.innerHTML, expected); } if (config.snapshot) { From 4f49ea896129091175f1945f67fcb29d28ef8d0c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Jan 2026 15:15:13 -0500 Subject: [PATCH 021/142] fix: don't revert source to UNINITIALIZED state when time travelling (#17409) * add failing test * WIP * remove comment * changeset * fix: clear batch between runs * changeset * fix * unused --- .changeset/thick-islands-pull.md | 5 ++++ .../internal/client/dom/blocks/boundary.js | 8 +++--- .../src/internal/client/reactivity/batch.js | 3 ++- .../_config.js | 18 +++++++++++++ .../main.svelte | 25 +++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 .changeset/thick-islands-pull.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte diff --git a/.changeset/thick-islands-pull.md b/.changeset/thick-islands-pull.md new file mode 100644 index 0000000000..f987d72260 --- /dev/null +++ b/.changeset/thick-islands-pull.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't revert source to UNINITIALIZED state when time travelling diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 92f4c0a104..cc4bc34e7b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -161,6 +161,10 @@ export class Boundary { this.#hydrate_pending_content(); } else { this.#hydrate_resolved_content(); + + if (this.#pending_count === 0) { + this.is_pending = false; + } } } else { var anchor = this.#get_anchor(); @@ -194,10 +198,6 @@ export class Boundary { } catch (error) { this.error(error); } - - // Since server rendered resolved content, we never show pending state - // Even if client-side async operations are still running, the content is already displayed - this.is_pending = false; } #hydrate_pending_content() { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 6dd90ba1b4..d6d23dc9c5 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,7 @@ import { invoke_error_boundary } from '../error-handling.js'; import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; +import { UNINITIALIZED } from '../../../constants.js'; /** @type {Set} */ const batches = new Set(); @@ -281,7 +282,7 @@ export class Batch { * @param {any} value */ capture(source, value) { - if (!this.previous.has(source)) { + if (value !== UNINITIALIZED && !this.previous.has(source)) { this.previous.set(source, value); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js new file mode 100644 index 0000000000..49428f90ad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

baz: 69

+

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte new file mode 100644 index 0000000000..0e05d0c414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-with-effect-and-boundary/main.svelte @@ -0,0 +1,25 @@ + + +

baz: {baz}

+ + + {#snippet pending()} +

Loading...

+ {/snippet} + + {#if qux} +

+ {/if} +
From 7ad5772309dc27313ff044b183e44b67e8e14d2b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Jan 2026 17:28:30 -0500 Subject: [PATCH 022/142] fix: notify deriveds of changes to sources inside forks (#17437) * failing tests * update tests * update write versions when committing forks * remove second test, for now * changeset * fix: correctly update writable deriveds inside forks (#17438) * fix: correctly update writable deriveds inside forks * tweak * changeset * on second thoughts, minimise the diff, and revisit later * missed a spot --- .changeset/empty-paths-smile.md | 5 +++++ .changeset/seven-llamas-care.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 13 ++++++++--- .../async-fork-derived-writable/_config.js | 14 ++++++++++++ .../async-fork-derived-writable/main.svelte | 22 +++++++++++++++++++ .../samples/async-fork-derived/_config.js | 16 ++++++++++++++ .../samples/async-fork-derived/main.svelte | 16 ++++++++++++++ 7 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 .changeset/empty-paths-smile.md create mode 100644 .changeset/seven-llamas-care.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte diff --git a/.changeset/empty-paths-smile.md b/.changeset/empty-paths-smile.md new file mode 100644 index 0000000000..687ad19b24 --- /dev/null +++ b/.changeset/empty-paths-smile.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: notify deriveds of changes to sources inside forks diff --git a/.changeset/seven-llamas-care.md b/.changeset/seven-llamas-care.md new file mode 100644 index 0000000000..1b41b4848a --- /dev/null +++ b/.changeset/seven-llamas-care.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly update writable deriveds inside forks diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index d6d23dc9c5..9b80df34e4 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -25,6 +25,7 @@ import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, get, + increment_write_version, is_dirty, is_updating_effect, set_is_updating_effect, @@ -920,13 +921,18 @@ export function fork(fn) { flushSync(fn); - batch_values = null; - // revert state changes for (var [source, value] of batch.previous) { source.v = value; } + // make writable deriveds dirty, so they recalculate correctly + for (source of batch.current.keys()) { + if ((source.f & DERIVED) !== 0) { + set_signal_status(source, DIRTY); + } + } + return { commit: async () => { if (committed) { @@ -942,9 +948,10 @@ export function fork(fn) { batch.is_fork = false; - // apply changes + // apply changes and update write versions so deriveds see the change for (var [source, value] of batch.current) { source.v = value; + source.wv = increment_write_version(); } // trigger any `$state.eager(...)` expressions with the new state. diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js new file mode 100644 index 0000000000..b089b714ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + // d should be 10 (real-world: s=1, d=1*10) before commit, not 20 (fork: s=2, d=2*10) + // After commit, d should be 99 (the written value) + assert.deepEqual(logs, [10, 99]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte new file mode 100644 index 0000000000..bc118a558a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived-writable/main.svelte @@ -0,0 +1,22 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js new file mode 100644 index 0000000000..59ae376167 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [1, 2]); + + increment.click(); + await tick(); + assert.deepEqual(logs, [1, 2, 2, 3]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte new file mode 100644 index 0000000000..93761869d6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-derived/main.svelte @@ -0,0 +1,16 @@ + + + From c6ce1ffeffe1ecff45611b8ec4e653836f113b5d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 12:13:29 -0500 Subject: [PATCH 023/142] chore: move set_signal_status into separate module (#17441) --- .../src/internal/client/dom/blocks/boundary.js | 4 ++-- .../svelte/src/internal/client/reactivity/batch.js | 2 +- .../src/internal/client/reactivity/deriveds.js | 2 +- .../svelte/src/internal/client/reactivity/effects.js | 2 +- .../svelte/src/internal/client/reactivity/sources.js | 2 +- .../svelte/src/internal/client/reactivity/status.js | 12 ++++++++++++ .../svelte/src/internal/client/reactivity/utils.js | 2 +- packages/svelte/src/internal/client/runtime.js | 12 +----------- packages/svelte/src/legacy/legacy-client.js | 3 ++- 9 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 packages/svelte/src/internal/client/reactivity/status.js diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index cc4bc34e7b..61262a6576 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -22,8 +22,7 @@ import { active_reaction, get, set_active_effect, - set_active_reaction, - set_signal_status + set_active_reaction } from '../../runtime.js'; import { hydrate_next, @@ -43,6 +42,7 @@ import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; import { create_text } from '../operations.js'; import { defer_effect } from '../../reactivity/utils.js'; +import { set_signal_status } from '../../reactivity/status.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9b80df34e4..4c50781817 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -29,7 +29,6 @@ import { is_dirty, is_updating_effect, set_is_updating_effect, - set_signal_status, update_effect } from '../runtime.js'; import * as e from '../errors.js'; @@ -40,6 +39,7 @@ import { flush_eager_effects, old_values, set_eager_effects, source, update } fr import { eager_effect, unlink_effect } from './effects.js'; import { defer_effect } from './utils.js'; import { UNINITIALIZED } from '../../../constants.js'; +import { set_signal_status } from './status.js'; /** @type {Set} */ const batches = new Set(); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 9dc324d3f9..b8a0982b05 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -17,7 +17,6 @@ import { import { active_reaction, active_effect, - set_signal_status, update_reaction, increment_write_version, set_active_effect, @@ -37,6 +36,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; +import { set_signal_status } from './status.js'; /** @type {Effect | null} */ export let current_async_effect = null; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 717fc35006..80ce63e0cf 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -9,7 +9,6 @@ import { remove_reactions, set_active_reaction, set_is_destroying_effect, - set_signal_status, untrack, untracking } from '../runtime.js'; @@ -44,6 +43,7 @@ import { component_context, dev_current_component_function, dev_stack } from '.. import { Batch, current_batch, schedule_effect } from './batch.js'; import { flatten } from './async.js'; import { without_reactive_context } from '../dom/elements/bindings/shared.js'; +import { set_signal_status } from './status.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3f8d6cb09f..fcdd389fe4 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -6,7 +6,6 @@ import { untracked_writes, get, set_untracked_writes, - set_signal_status, untrack, increment_write_version, update_effect, @@ -40,6 +39,7 @@ import { component_context, is_runes } from '../context.js'; import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; +import { set_signal_status } from './status.js'; /** @type {Set} */ export let eager_effects = new Set(); diff --git a/packages/svelte/src/internal/client/reactivity/status.js b/packages/svelte/src/internal/client/reactivity/status.js new file mode 100644 index 0000000000..d3aaa35968 --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/status.js @@ -0,0 +1,12 @@ +/** @import { Signal } from '#client' */ +import { CLEAN, DIRTY, MAYBE_DIRTY } from '#client/constants'; + +const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); + +/** + * @param {Signal} signal + * @param {number} status + */ +export function set_signal_status(signal, status) { + signal.f = (signal.f & STATUS_MASK) | status; +} diff --git a/packages/svelte/src/internal/client/reactivity/utils.js b/packages/svelte/src/internal/client/reactivity/utils.js index 0dd6b3212d..0d27cb8b84 100644 --- a/packages/svelte/src/internal/client/reactivity/utils.js +++ b/packages/svelte/src/internal/client/reactivity/utils.js @@ -1,6 +1,6 @@ /** @import { Derived, Effect, Value } from '#client' */ import { CLEAN, DERIVED, DIRTY, MAYBE_DIRTY, WAS_MARKED } from '#client/constants'; -import { set_signal_status } from '../runtime.js'; +import { set_signal_status } from './status.js'; /** * @param {Value[] | null} deps diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 64c8409b8f..1b901268fc 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -56,6 +56,7 @@ import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js'; +import { set_signal_status } from './reactivity/status.js'; export let is_updating_effect = false; @@ -718,17 +719,6 @@ export function untrack(fn) { } } -const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); - -/** - * @param {Signal} signal - * @param {number} status - * @returns {void} - */ -export function set_signal_status(signal, status) { - signal.f = (signal.f & STATUS_MASK) | status; -} - /** * @param {Record} obj * @param {Array} keys diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index d4a053d1aa..ec90d2312c 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -3,7 +3,7 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j 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, get, set_signal_status } from '../internal/client/runtime.js'; +import { active_effect, get } from '../internal/client/runtime.js'; import { flushSync } from '../internal/client/reactivity/batch.js'; import { define_property, is_array } from '../internal/shared/utils.js'; import * as e from '../internal/client/errors.js'; @@ -12,6 +12,7 @@ import { DEV } from 'esm-env'; import { FILENAME } from '../constants.js'; import { component_context, dev_current_component_function } from '../internal/client/context.js'; import { async_mode_flag } from '../internal/flags/index.js'; +import { set_signal_status } from '../internal/client/reactivity/status.js'; /** * Takes the same options as a Svelte 4 component and the component function and returns a Svelte 4 compatible component. From f04f22ea4990c4d8336a205571cdbc05ecf819bb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 12:47:12 -0500 Subject: [PATCH 024/142] chore: use update_derived_status instead of set_signal_status where appropriate (#17442) Co-authored-by: David Roizenman --- .../src/internal/client/reactivity/deriveds.js | 5 ++--- .../src/internal/client/reactivity/sources.js | 8 +++++--- .../src/internal/client/reactivity/status.js | 17 +++++++++++++++-- packages/svelte/src/internal/client/runtime.js | 18 +++++++++++------- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b8a0982b05..48002d759f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -36,7 +36,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; -import { set_signal_status } from './status.js'; +import { update_derived_status } from './status.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -385,7 +385,6 @@ export function update_derived(derived) { batch_values.set(derived, value); } } else { - var status = (derived.f & CONNECTED) === 0 ? MAYBE_DIRTY : CLEAN; - set_signal_status(derived, status); + update_derived_status(derived); } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index fcdd389fe4..6333962f41 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -39,7 +39,7 @@ import { component_context, is_runes } from '../context.js'; import { Batch, batch_values, eager_block_effects, schedule_effect } from './batch.js'; import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; -import { set_signal_status } from './status.js'; +import { set_signal_status, update_derived_status } from './status.js'; /** @type {Set} */ export let eager_effects = new Set(); @@ -218,12 +218,14 @@ export function internal_set(source, value) { } if ((source.f & DERIVED) !== 0) { + const derived = /** @type {Derived} */ (source); + // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies if ((source.f & DIRTY) !== 0) { - execute_derived(/** @type {Derived} */ (source)); + execute_derived(derived); } - set_signal_status(source, (source.f & CONNECTED) !== 0 ? CLEAN : MAYBE_DIRTY); + update_derived_status(derived); } source.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/reactivity/status.js b/packages/svelte/src/internal/client/reactivity/status.js index d3aaa35968..024285e73a 100644 --- a/packages/svelte/src/internal/client/reactivity/status.js +++ b/packages/svelte/src/internal/client/reactivity/status.js @@ -1,5 +1,5 @@ -/** @import { Signal } from '#client' */ -import { CLEAN, DIRTY, MAYBE_DIRTY } from '#client/constants'; +/** @import { Derived, Signal } from '#client' */ +import { CLEAN, CONNECTED, DIRTY, MAYBE_DIRTY } from '#client/constants'; const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); @@ -10,3 +10,16 @@ const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); export function set_signal_status(signal, status) { signal.f = (signal.f & STATUS_MASK) | status; } + +/** + * Set a derived's status to CLEAN or MAYBE_DIRTY based on its connection state. + * @param {Derived} derived + */ +export function update_derived_status(derived) { + // Only mark as MAYBE_DIRTY if disconnected and has dependencies. + if ((derived.f & CONNECTED) !== 0 || derived.deps === null) { + set_signal_status(derived, CLEAN); + } else { + set_signal_status(derived, MAYBE_DIRTY); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 1b901268fc..e1a442830b 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -56,7 +56,7 @@ import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; import { without_reactive_context } from './dom/elements/bindings/shared.js'; -import { set_signal_status } from './reactivity/status.js'; +import { set_signal_status, update_derived_status } from './reactivity/status.js'; export let is_updating_effect = false; @@ -375,16 +375,20 @@ function remove_reaction(signal, dependency) { // allows us to skip the expensive work of disconnecting and immediately reconnecting it (new_deps === null || !new_deps.includes(dependency)) ) { - set_signal_status(dependency, MAYBE_DIRTY); + const derived = /** @type {Derived} */ (dependency); + // If we are working with a derived that is owned by an effect, then mark it as being // disconnected and remove the mark flag, as it cannot be reliably removed otherwise - if ((dependency.f & CONNECTED) !== 0) { - dependency.f ^= CONNECTED; - dependency.f &= ~WAS_MARKED; + if ((derived.f & CONNECTED) !== 0) { + derived.f ^= CONNECTED; + derived.f &= ~WAS_MARKED; } + + update_derived_status(derived); + // Disconnect any reactions owned by this reaction - destroy_derived_effects(/** @type {Derived} **/ (dependency)); - remove_reactions(/** @type {Derived} **/ (dependency), 0); + destroy_derived_effects(derived); + remove_reactions(derived, 0); } } From 359be81c094b35d12a2ce320127bfaa0ef431b6a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 12:52:21 -0500 Subject: [PATCH 025/142] chore: tidy up (#17443) * tidy up * unused --- packages/svelte/src/internal/client/reactivity/deriveds.js | 3 --- packages/svelte/src/internal/client/runtime.js | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 48002d759f..0fe83d7108 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -3,15 +3,12 @@ import { DEV } from 'esm-env'; import { ERROR_VALUE, - CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, - MAYBE_DIRTY, STALE_REACTION, ASYNC, WAS_MARKED, - CONNECTED, DESTROYED } from '#client/constants'; import { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e1a442830b..5f215b0d31 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Reaction, Signal, Source, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ import { DEV } from 'esm-env'; import { get_descriptors, get_prototype_of, index_of } from '../shared/utils.js'; import { @@ -28,7 +28,6 @@ import { old_values } from './reactivity/sources.js'; import { destroy_derived_effects, execute_derived, - current_async_effect, recent_async_deriveds, update_derived } from './reactivity/deriveds.js'; @@ -44,7 +43,6 @@ import { set_dev_current_component_function, set_dev_stack } from './context.js'; -import * as w from './warnings.js'; import { Batch, batch_values, From f8bdadc1bf5e1122494b0269f04634f46e8225bf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 13:23:47 -0500 Subject: [PATCH 026/142] chore: simplify is_dirty (#17444) * chore: simplify is_dirty * tidy up --- .../src/internal/client/reactivity/effects.js | 2 +- .../svelte/src/internal/client/runtime.js | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 80ce63e0cf..4cba446afc 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -328,7 +328,7 @@ export function legacy_pre_effect_reset() { // If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through // the effects dependencies and correctly ensure each dependency is up-to-date. - if ((effect.f & CLEAN) !== 0) { + if ((effect.f & CLEAN) !== 0 && effect.deps !== null) { set_signal_status(effect, MAYBE_DIRTY); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5f215b0d31..ffd61ed776 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -166,21 +166,18 @@ export function is_dirty(reaction) { } if ((flags & MAYBE_DIRTY) !== 0) { - var dependencies = reaction.deps; + var dependencies = /** @type {Value[]} */ (reaction.deps); + var length = dependencies.length; - if (dependencies !== null) { - var length = dependencies.length; + for (var i = 0; i < length; i++) { + var dependency = dependencies[i]; - for (var i = 0; i < length; i++) { - var dependency = dependencies[i]; - - if (is_dirty(/** @type {Derived} */ (dependency))) { - update_derived(/** @type {Derived} */ (dependency)); - } + if (is_dirty(/** @type {Derived} */ (dependency))) { + update_derived(/** @type {Derived} */ (dependency)); + } - if (dependency.wv > reaction.wv) { - return true; - } + if (dependency.wv > reaction.wv) { + return true; } } From 5588832d1792130eade828410dc3777f9a713bc8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 15:57:22 -0500 Subject: [PATCH 027/142] fix: prevent derives without dependencies from ever re-running (#17445) * fix: prevent derives without dependencies from ever re-running * tweak --- .changeset/plenty-candies-notice.md | 5 ++++ .../internal/client/reactivity/deriveds.js | 17 ++++++++++---- .../fork-derived-class-instance/_config.js | 22 ++++++++++++++++++ .../fork-derived-class-instance/main.svelte | 23 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 .changeset/plenty-candies-notice.md create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte diff --git a/.changeset/plenty-candies-notice.md b/.changeset/plenty-candies-notice.md new file mode 100644 index 0000000000..41c5fce12a --- /dev/null +++ b/.changeset/plenty-candies-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent derives without dependencies from ever re-running diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 0fe83d7108..59fb0f15aa 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,7 +9,8 @@ import { STALE_REACTION, ASYNC, WAS_MARKED, - DESTROYED + DESTROYED, + CLEAN } from '#client/constants'; import { active_reaction, @@ -33,7 +34,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; -import { update_derived_status } from './status.js'; +import { set_signal_status, update_derived_status } from './status.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -356,15 +357,21 @@ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { + derived.wv = increment_write_version(); + // in a fork, we don't update the underlying value, just `batch_values`. // the underlying value will be updated when the fork is committed. // otherwise, the next time we get here after a 'real world' state // change, `derived.equals` may incorrectly return `true` - if (!current_batch?.is_fork) { + if (!current_batch?.is_fork || derived.deps === null) { derived.v = value; - } - derived.wv = increment_write_version(); + // deriveds without dependencies should never be recomputed + if (derived.deps === null) { + set_signal_status(derived, CLEAN); + return; + } + } } // don't mark derived clean if we're reading it inside a diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js new file mode 100644 index 0000000000..feac447536 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + + const [, increment] = target.querySelectorAll('button'); + const p = target.querySelector('p'); + + assert.equal(p?.textContent, '0'); + + increment.click(); + await tick(); + + assert.equal(p?.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte new file mode 100644 index 0000000000..3623c1df66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if condition} + +

{counter.count}

+{/if} From 286b40c4526ce9970cb81ddd5e65b93b722fe468 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 15:57:22 -0500 Subject: [PATCH 028/142] fix: prevent derives without dependencies from ever re-running (#17445) * fix: prevent derives without dependencies from ever re-running * tweak Co-authored-by: David Roizenman --- .changeset/plenty-candies-notice.md | 5 ++++ .../internal/client/reactivity/deriveds.js | 17 ++++++++++---- .../fork-derived-class-instance/_config.js | 22 ++++++++++++++++++ .../fork-derived-class-instance/main.svelte | 23 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 .changeset/plenty-candies-notice.md create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte diff --git a/.changeset/plenty-candies-notice.md b/.changeset/plenty-candies-notice.md new file mode 100644 index 0000000000..41c5fce12a --- /dev/null +++ b/.changeset/plenty-candies-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent derives without dependencies from ever re-running diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 0fe83d7108..59fb0f15aa 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -9,7 +9,8 @@ import { STALE_REACTION, ASYNC, WAS_MARKED, - DESTROYED + DESTROYED, + CLEAN } from '#client/constants'; import { active_reaction, @@ -33,7 +34,7 @@ import { UNINITIALIZED } from '../../../constants.js'; import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; -import { update_derived_status } from './status.js'; +import { set_signal_status, update_derived_status } from './status.js'; /** @type {Effect | null} */ export let current_async_effect = null; @@ -356,15 +357,21 @@ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { + derived.wv = increment_write_version(); + // in a fork, we don't update the underlying value, just `batch_values`. // the underlying value will be updated when the fork is committed. // otherwise, the next time we get here after a 'real world' state // change, `derived.equals` may incorrectly return `true` - if (!current_batch?.is_fork) { + if (!current_batch?.is_fork || derived.deps === null) { derived.v = value; - } - derived.wv = increment_write_version(); + // deriveds without dependencies should never be recomputed + if (derived.deps === null) { + set_signal_status(derived, CLEAN); + return; + } + } } // don't mark derived clean if we're reading it inside a diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js new file mode 100644 index 0000000000..feac447536 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_no_async: true, + async test({ assert, target }) { + const [fork] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + + const [, increment] = target.querySelectorAll('button'); + const p = target.querySelector('p'); + + assert.equal(p?.textContent, '0'); + + increment.click(); + await tick(); + + assert.equal(p?.textContent, '1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte new file mode 100644 index 0000000000..3623c1df66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/fork-derived-class-instance/main.svelte @@ -0,0 +1,23 @@ + + + + +{#if condition} + +

{counter.count}

+{/if} From 2c2339028657fa52f38bd904bef88927288438c5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 9 Jan 2026 18:26:58 -0500 Subject: [PATCH 029/142] chore: const -> var --- packages/svelte/src/internal/client/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index ffd61ed776..52b24030fa 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -370,7 +370,7 @@ function remove_reaction(signal, dependency) { // allows us to skip the expensive work of disconnecting and immediately reconnecting it (new_deps === null || !new_deps.includes(dependency)) ) { - const derived = /** @type {Derived} */ (dependency); + var derived = /** @type {Derived} */ (dependency); // If we are working with a derived that is owned by an effect, then mark it as being // disconnected and remove the mark flag, as it cannot be reliably removed otherwise From ae224bea4937d1349366cb170a62cd438a1eb580 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 Jan 2026 14:59:07 -0500 Subject: [PATCH 030/142] fix: always reconnect deriveds in get, when appropriate (#17451) * failing test * fix: always reconnect deriveds in get, when appropriate * shuffle code around a bit * note to self --- .changeset/major-towns-grab.md | 5 ++++ .../svelte/src/internal/client/runtime.js | 25 +++++++++--------- .../samples/async-fork-snippet-dev/_config.js | 24 +++++++++++++++++ .../async-fork-snippet-dev/main.svelte | 26 +++++++++++++++++++ 4 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 .changeset/major-towns-grab.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte diff --git a/.changeset/major-towns-grab.md b/.changeset/major-towns-grab.md new file mode 100644 index 0000000000..de66b70313 --- /dev/null +++ b/.changeset/major-towns-grab.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always reconnect deriveds in get, when appropriate diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 52b24030fa..82c5f5a06f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -596,14 +596,14 @@ export function get(signal) { } } - if (is_destroying_effect) { - if (old_values.has(signal)) { - return old_values.get(signal); - } + if (is_destroying_effect && old_values.has(signal)) { + return old_values.get(signal); + } - if (is_derived) { - var derived = /** @type {Derived} */ (signal); + if (is_derived) { + var derived = /** @type {Derived} */ (signal); + if (is_destroying_effect) { var value = derived.v; // if the derived is dirty and has reactions, or depends on the values that just changed, re-execute @@ -619,14 +619,13 @@ export function get(signal) { return value; } - } else if ( - is_derived && - (!batch_values?.has(signal) || (current_batch?.is_fork && !effect_tracking())) - ) { - derived = /** @type {Derived} */ (signal); - if (is_dirty(derived)) { - update_derived(derived); + // TODO this should probably just be `!batch_values?.has(derived)` — the second bit + // should be taken care of by clearing `batch_values` in `mark_reactions`? + if (!batch_values?.has(derived) || (current_batch?.is_fork && !effect_tracking())) { + if (is_dirty(derived)) { + update_derived(derived); + } } if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js new file mode 100644 index 0000000000..2fb00562f5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target }) { + const [fork] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + + const [, toggle] = target.querySelectorAll('button'); + + toggle.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte new file mode 100644 index 0000000000..6945fcd153 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-snippet-dev/main.svelte @@ -0,0 +1,26 @@ + + + + +{#if condition} + + + {#snippet foo({ checked })} + {checked} + {/snippet} + + +{/if} From 7dbb5b478893056e4a9c43bdee5e34e64d86e588 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 12 Jan 2026 12:14:08 -0800 Subject: [PATCH 031/142] fix: derived reactivity and perf regressions (#17362) * add test sample * add test for sveltejs/kit#15059 * fix: reconnect deriveds inside branch effects * add changeset * fix: derived with no deps always set as MAYBE_DIRTY fixes #17342 * add test for #17342 * additional changeset * refactor: extract setting derived status to helper, apply to sources.js * add test case for #17352 * fix: reconnect child deriveds when evaluating connected parent derived fixes #17352 * fix import order causing Cannot read properties of undefined on dev load * remove duplicate iteration over deps * minor style tweaks * oops, fix merge * use update_derived_status, so that we never set a dep-less derived MAYBE_DIRTY * tweak * reaction.deps cannot be null for a MAYBE_DIRTY derived * make it such that reactions without deps are never MAYBE_DIRTY * since we no longer need to check reaction.deps === null, we can revert this bit * more explicit check * tidy up * more * gah whoops * move import * simplify test * make dep-less derived behaviour more explicit, move it above is_destroying_effect handling * remove test - this is adequately covered by #17445 * replace tricky unit test with component-based test * remove incorrect test * remove the BRANCH_EFFECT stuff * tidy up * DRY * tweak, add explanatory comment * tweak * explanatory comment * remove changeset * update changeset --------- Co-authored-by: Tee Ming Co-authored-by: Rich Harris --- .changeset/beige-sloths-cry.md | 5 ++ .../svelte/src/internal/client/runtime.js | 26 +++++++--- .../derived-read-outside-reaction/_config.js | 47 +++++++++++++++++++ .../derived-read-outside-reaction/main.svelte | 30 ++++++++++++ 4 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 .changeset/beige-sloths-cry.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte diff --git a/.changeset/beige-sloths-cry.md b/.changeset/beige-sloths-cry.md new file mode 100644 index 0000000000..013fd2f6cc --- /dev/null +++ b/.changeset/beige-sloths-cry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reconnect clean deriveds when they are read in a reactive context diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 82c5f5a06f..0e12324d72 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -620,15 +620,27 @@ export function get(signal) { return value; } - // TODO this should probably just be `!batch_values?.has(derived)` — the second bit - // should be taken care of by clearing `batch_values` in `mark_reactions`? - if (!batch_values?.has(derived) || (current_batch?.is_fork && !effect_tracking())) { - if (is_dirty(derived)) { - update_derived(derived); + // connect disconnected deriveds if we are reading them inside an effect, + // or inside another derived that is already connected + var should_connect = + (derived.f & CONNECTED) === 0 && + !untracking && + active_reaction !== null && + (is_updating_effect || (active_reaction.f & CONNECTED) !== 0); + + var is_new = derived.deps === null; + + if (is_dirty(derived)) { + if (should_connect) { + // set the flag before `update_derived`, so that the derived + // is added as a reaction to its dependencies + derived.f |= CONNECTED; } + + update_derived(derived); } - if (is_updating_effect && effect_tracking() && (derived.f & CONNECTED) === 0) { + if (should_connect && !is_new) { reconnect(derived); } } @@ -652,7 +664,7 @@ export function get(signal) { function reconnect(derived) { if (derived.deps === null) return; - derived.f ^= CONNECTED; + derived.f |= CONNECTED; for (const dep of derived.deps) { (dep.reactions ??= []).push(derived); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js new file mode 100644 index 0000000000..f3a71b93e6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/_config.js @@ -0,0 +1,47 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + +

1, 2, 3

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

2, 4, 6

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

2, 4, 6, 8

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

3, 6, 9, 12

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte new file mode 100644 index 0000000000..0b5096dfa4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-read-outside-reaction/main.svelte @@ -0,0 +1,30 @@ + + + + + + +

{products.join(', ')}

From 51c7f7152c244512067fa52312e15a596d1ae1c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:15:00 -0500 Subject: [PATCH 032/142] Version Packages (#17410) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/better-games-invite.md | 5 ----- .changeset/empty-paths-smile.md | 5 ----- .changeset/major-towns-grab.md | 5 ----- .changeset/plenty-candies-notice.md | 5 ----- .changeset/seven-llamas-care.md | 5 ----- .changeset/spicy-years-stick.md | 5 ----- .changeset/tall-clocks-turn.md | 5 ----- .changeset/tall-phones-fix.md | 5 ----- .changeset/thick-islands-pull.md | 5 ----- packages/svelte/CHANGELOG.md | 22 ++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 12 files changed, 24 insertions(+), 47 deletions(-) delete mode 100644 .changeset/better-games-invite.md delete mode 100644 .changeset/empty-paths-smile.md delete mode 100644 .changeset/major-towns-grab.md delete mode 100644 .changeset/plenty-candies-notice.md delete mode 100644 .changeset/seven-llamas-care.md delete mode 100644 .changeset/spicy-years-stick.md delete mode 100644 .changeset/tall-clocks-turn.md delete mode 100644 .changeset/tall-phones-fix.md delete mode 100644 .changeset/thick-islands-pull.md diff --git a/.changeset/better-games-invite.md b/.changeset/better-games-invite.md deleted file mode 100644 index 8361a1e9bb..0000000000 --- a/.changeset/better-games-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't transform references of function declarations in legacy mode diff --git a/.changeset/empty-paths-smile.md b/.changeset/empty-paths-smile.md deleted file mode 100644 index 687ad19b24..0000000000 --- a/.changeset/empty-paths-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: notify deriveds of changes to sources inside forks diff --git a/.changeset/major-towns-grab.md b/.changeset/major-towns-grab.md deleted file mode 100644 index de66b70313..0000000000 --- a/.changeset/major-towns-grab.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always reconnect deriveds in get, when appropriate diff --git a/.changeset/plenty-candies-notice.md b/.changeset/plenty-candies-notice.md deleted file mode 100644 index 41c5fce12a..0000000000 --- a/.changeset/plenty-candies-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent derives without dependencies from ever re-running diff --git a/.changeset/seven-llamas-care.md b/.changeset/seven-llamas-care.md deleted file mode 100644 index 1b41b4848a..0000000000 --- a/.changeset/seven-llamas-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly update writable deriveds inside forks diff --git a/.changeset/spicy-years-stick.md b/.changeset/spicy-years-stick.md deleted file mode 100644 index 76834e8381..0000000000 --- a/.changeset/spicy-years-stick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: remove `$inspect` calls after await expressions when compiling for production server code diff --git a/.changeset/tall-clocks-turn.md b/.changeset/tall-clocks-turn.md deleted file mode 100644 index 22cefcff30..0000000000 --- a/.changeset/tall-clocks-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: clear batch between runs diff --git a/.changeset/tall-phones-fix.md b/.changeset/tall-phones-fix.md deleted file mode 100644 index 29e57c060f..0000000000 --- a/.changeset/tall-phones-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: adjust `loc` property of `Program` nodes created from `' + }, + + async test() { + // this test will fail when evaluating the `head` script if the vulnerability is present + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte new file mode 100644 index 0000000000..1327492190 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydratable-script-escape/main.svelte @@ -0,0 +1,9 @@ + + +

{value}

From cfc7e0a8c3b1846f0c17388735eb0263b53b93a2 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 15 Jan 2026 09:15:26 -0700 Subject: [PATCH 036/142] chore: Upgrade devalue (#17475) --- packages/svelte/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d7c25b28f1..8bd600545f 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -174,7 +174,7 @@ "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.5.0", + "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05298fe8e3..b616fc0577 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 devalue: - specifier: ^5.5.0 - version: 5.5.0 + specifier: ^5.6.2 + version: 5.6.2 esm-env: specifier: ^1.2.1 version: 1.2.1 @@ -1219,8 +1219,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.2: + resolution: {integrity: sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -3500,7 +3500,7 @@ snapshots: detect-libc@1.0.3: optional: true - devalue@5.5.0: {} + devalue@5.6.2: {} dir-glob@3.0.1: dependencies: From 0718a26b435b927428dee57a71a91c1a77dc19cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:22:53 -0700 Subject: [PATCH 037/142] Version Packages (#17476) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/tasty-vans-repeat.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/tasty-vans-repeat.md diff --git a/.changeset/tasty-vans-repeat.md b/.changeset/tasty-vans-repeat.md deleted file mode 100644 index d95e8de7e7..0000000000 --- a/.changeset/tasty-vans-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `devalue.uneval` to serialize `hydratable` keys diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index bf22761869..a767760641 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.46.4 + +### Patch Changes + +- fix: use `devalue.uneval` to serialize `hydratable` keys ([`ef81048e238844b729942441541d6dcfe6c8ccca`](https://github.com/sveltejs/svelte/commit/ef81048e238844b729942441541d6dcfe6c8ccca)) + ## 5.46.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8bd600545f..1723e3d00a 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.46.3", + "version": "5.46.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 9f84875407..96c48a055c 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.46.3'; +export const VERSION = '5.46.4'; export const PUBLIC_VERSION = '5'; From 17c22969d8f7b1723eae58075fbb32b530518e22 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 16 Jan 2026 22:33:11 +0100 Subject: [PATCH 038/142] fix: don't reset static elements with debug/snippets (#17477) --- .changeset/little-aliens-pick.md | 5 +++++ .../phases/3-transform/client/visitors/RegularElement.js | 7 ++++--- .../phases/3-transform/client/visitors/shared/fragment.js | 5 ++--- .../tests/hydration/samples/no-reset-debug/_config.js | 3 +++ .../tests/hydration/samples/no-reset-debug/main.svelte | 8 ++++++++ .../tests/hydration/samples/no-reset-snippet/_config.js | 3 +++ .../tests/hydration/samples/no-reset-snippet/main.svelte | 6 ++++++ 7 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 .changeset/little-aliens-pick.md create mode 100644 packages/svelte/tests/hydration/samples/no-reset-debug/_config.js create mode 100644 packages/svelte/tests/hydration/samples/no-reset-debug/main.svelte create mode 100644 packages/svelte/tests/hydration/samples/no-reset-snippet/_config.js create mode 100644 packages/svelte/tests/hydration/samples/no-reset-snippet/main.svelte diff --git a/.changeset/little-aliens-pick.md b/.changeset/little-aliens-pick.md new file mode 100644 index 0000000000..1433625551 --- /dev/null +++ b/.changeset/little-aliens-pick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't reset static elements with debug/snippets diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 690715df84..c780350d3e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -21,7 +21,7 @@ import { build_set_class, build_set_style } from './shared/element.js'; -import { process_children } from './shared/fragment.js'; +import { process_children, is_static_element } from './shared/fragment.js'; import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; @@ -356,8 +356,9 @@ export function RegularElement(node, context) { let arg = context.state.node; // If `hydrate_node` is set inside the element, we need to reset it - // after the element has been hydrated - let needs_reset = trimmed.some((node) => node.type !== 'Text'); + // after the element has been hydrated. We need to check if any child + // would actually advance the hydrate_node cursor - static elements don't. + let needs_reset = trimmed.some((node) => node.type !== 'Text' && !is_static_element(node)); // The same applies if it's a `