From cd8d40af1ae755ca7c889f3aa1b6c3d8d0c14dd7 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:40:14 +0100 Subject: [PATCH 01/23] chore: robustify playground decode (#17606) For some reason LLMs like to percent-encode stuff, this makes the download script still run in that case --- playgrounds/sandbox/scripts/download.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/playgrounds/sandbox/scripts/download.js b/playgrounds/sandbox/scripts/download.js index e3c862cb61..538b4956fe 100644 --- a/playgrounds/sandbox/scripts/download.js +++ b/playgrounds/sandbox/scripts/download.js @@ -635,7 +635,19 @@ if (is_local) { } else if (url && url.origin === 'https://svelte.dev' && url.pathname.startsWith('/playground/')) { // Svelte playground URL handling (existing logic) if (url.hash.length > 1) { - const decoded = atob(url.hash.slice(1).replaceAll('-', '+').replaceAll('_', '/')); + // Decode percent-encoded characters (e.g., %5F => _), and replace base64 chars. + let decoded; + try { + // First, decode URI components to handle %xx encodings (e.g. %5F -> _) (LLMs calling this script sometimes encode them for some reason) + decoded = url.hash.slice(1); + decoded = decodeURIComponent(decoded); + + // Now, restore for base64 (replace -/+, _/ /) + decoded = atob(decoded.replaceAll('-', '+').replaceAll('_', '/')); + } catch (e) { + console.error('Failed to decode URL hash:', e); + process.exit(1); + } // putting it directly into the blob gives a corrupted file const u8 = new Uint8Array(decoded.length); for (let i = 0; i < decoded.length; i++) { From bc4dc1d10d54ecbc61398a248f8a45a4f4bcfa13 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:28:32 +0100 Subject: [PATCH 02/23] fix: avoid erroneous async derived expressions for blocks (#17604) fixes #17595 When an if/key/etc block has an expression that depends on an async blocker (e.g., is inside a component with top level `await`), the compiler incorrectly treats the expression as async - even when the expression itself contains no `await`. This causes the expression to be added to `$.async`'s `expressions` array, which wraps it in an `async_derived`. This is not only unnecessary but also buggy: it breaks the direct reactive connection between the source and its dependent effects, causing inconsistent effect executions. The fix is to only add expressions to `$.async`'s `expressions` array when they actually contain an `await`. When a branch is speculatively marked for destruction (condition temporarily falsy), its child effects are reset to `CLEAN` to prevent them running in a doomed branch (as of #17581). However, if the branch survives (condition becomes truthy again), those effects remain `CLEAN` and never run - the source was already marked dirty before the reset, so no new dirty marking occurs. The fix is to change `skipped_effects` from a `Set` to a `Map` that tracks which child effects were dirty/maybe_dirty before being reset. When a branch is unskipped (survives), restore their status and reschedule them. --------- Co-authored-by: Rich Harris --- .changeset/some-teams-pay.md | 5 ++ .changeset/yummy-insects-wonder.md | 5 ++ .../3-transform/client/visitors/EachBlock.js | 15 +++-- .../3-transform/client/visitors/HtmlTag.js | 14 ++-- .../3-transform/client/visitors/IfBlock.js | 14 ++-- .../3-transform/client/visitors/KeyBlock.js | 32 +++++---- .../client/visitors/SvelteElement.js | 14 ++-- .../internal/client/dom/blocks/branches.js | 8 +-- .../src/internal/client/dom/blocks/each.js | 4 +- .../src/internal/client/reactivity/batch.js | 65 +++++++++++++++---- .../svelte/tests/runtime-legacy/shared.ts | 4 +- .../Child.svelte | 9 +++ .../_config.js | 16 +++++ .../main.svelte | 14 ++++ 14 files changed, 166 insertions(+), 53 deletions(-) create mode 100644 .changeset/some-teams-pay.md create mode 100644 .changeset/yummy-insects-wonder.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte diff --git a/.changeset/some-teams-pay.md b/.changeset/some-teams-pay.md new file mode 100644 index 0000000000..d7ae69566d --- /dev/null +++ b/.changeset/some-teams-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid erroneous async derived expressions for blocks diff --git a/.changeset/yummy-insects-wonder.md b/.changeset/yummy-insects-wonder.md new file mode 100644 index 0000000000..6e705cd8cf --- /dev/null +++ b/.changeset/yummy-insects-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reschedule effects inside unskipped branches diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index c0bfe272e5..1dbc34fdc3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -312,10 +312,10 @@ export function EachBlock(node, context) { declarations.push(b.let(node.index, index)); } - const is_async = node.metadata.expression.is_async(); + const has_await = node.metadata.expression.has_await; - const get_collection = b.thunk(collection, node.metadata.expression.has_await); - const thunk = is_async ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection; + const get_collection = b.thunk(collection, has_await); + const thunk = has_await ? b.thunk(b.call('$.get', b.id('$$collection'))) : get_collection; const render_args = [b.id('$$anchor'), item]; if (uses_index || collection_id) render_args.push(index); @@ -342,15 +342,18 @@ export function EachBlock(node, context) { statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function))); } - if (is_async) { + if (node.metadata.expression.is_async()) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, node.metadata.expression.blockers(), - b.array([get_collection]), - b.arrow([context.state.node, b.id('$$collection')], b.block(statements)) + has_await ? b.array([get_collection]) : b.void0, + b.arrow( + has_await ? [context.state.node, b.id('$$collection')] : [context.state.node], + b.block(statements) + ) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 0567edc610..2706cf7f0a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -11,10 +11,11 @@ import { build_expression } from './shared/utils.js'; export function HtmlTag(node, context) { context.state.template.push_comment(); - const is_async = node.metadata.expression.is_async(); + const has_await = node.metadata.expression.has_await; + const has_blockers = node.metadata.expression.has_blockers(); const expression = build_expression(context, node.expression, node.metadata.expression); - const html = is_async ? b.call('$.get', b.id('$$html')) : expression; + const html = has_await ? b.call('$.get', b.id('$$html')) : expression; const is_svg = context.state.metadata.namespace === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; @@ -31,15 +32,18 @@ export function HtmlTag(node, context) { ); // push into init, so that bindings run afterwards, which might trigger another run and override hydration - if (is_async) { + if (has_await || has_blockers) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, node.metadata.expression.blockers(), - b.array([b.thunk(expression, node.metadata.expression.has_await)]), - b.arrow([context.state.node, b.id('$$html')], b.block([statement])) + has_await ? b.array([b.thunk(expression, true)]) : b.void0, + b.arrow( + has_await ? [context.state.node, b.id('$$html')] : [context.state.node], + b.block([statement]) + ) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index fcbb59ba74..c0e66635df 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -25,10 +25,11 @@ export function IfBlock(node, context) { statements.push(b.var(alternate_id, b.arrow([b.id('$$anchor')], alternate))); } - const is_async = node.metadata.expression.is_async(); + const has_await = node.metadata.expression.has_await; + const has_blockers = node.metadata.expression.has_blockers(); const expression = build_expression(context, node.test, node.metadata.expression); - const test = is_async ? b.call('$.get', b.id('$$condition')) : expression; + const test = has_await ? b.call('$.get', b.id('$$condition')) : expression; /** @type {Expression[]} */ const args = [ @@ -72,15 +73,18 @@ export function IfBlock(node, context) { statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if')); - if (is_async) { + if (has_await || has_blockers) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, node.metadata.expression.blockers(), - b.array([b.thunk(expression, node.metadata.expression.has_await)]), - b.arrow([context.state.node, b.id('$$condition')], b.block(statements)) + has_await ? b.array([b.thunk(expression, true)]) : b.void0, + b.arrow( + has_await ? [context.state.node, b.id('$$condition')] : [context.state.node], + b.block(statements) + ) ) ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index d050155e8b..143a4e8edd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -11,29 +11,35 @@ import { build_expression, add_svelte_meta } from './shared/utils.js'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const is_async = node.metadata.expression.is_async(); + const has_await = node.metadata.expression.has_await; + const has_blockers = node.metadata.expression.has_blockers(); const expression = build_expression(context, node.expression, node.metadata.expression); - const key = b.thunk(is_async ? b.call('$.get', b.id('$$key')) : expression); + const key = b.thunk(has_await ? b.call('$.get', b.id('$$key')) : expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); - let statement = add_svelte_meta( + const statement = add_svelte_meta( b.call('$.key', context.state.node, key, b.arrow([b.id('$$anchor')], body)), node, 'key' ); - if (is_async) { - statement = b.stmt( - b.call( - '$.async', - context.state.node, - node.metadata.expression.blockers(), - b.array([b.thunk(expression, node.metadata.expression.has_await)]), - b.arrow([context.state.node, b.id('$$key')], b.block([statement])) + if (has_await || has_blockers) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + node.metadata.expression.blockers(), + has_await ? b.array([b.thunk(expression, true)]) : b.void0, + b.arrow( + has_await ? [context.state.node, b.id('$$key')] : [context.state.node], + b.block([statement]) + ) + ) ) ); + } else { + context.state.init.push(statement); } - - context.state.init.push(statement); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index c8192cf00a..10024298fa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -93,10 +93,11 @@ export function SvelteElement(node, context) { ); } - const is_async = node.metadata.expression.is_async(); + const has_await = node.metadata.expression.has_await; + const has_blockers = node.metadata.expression.has_blockers(); const expression = /** @type {Expression} */ (context.visit(node.tag)); - const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression); + const get_tag = b.thunk(has_await ? b.call('$.get', b.id('$$tag')) : expression); /** @type {Statement[]} */ const inner = inner_context.state.init; @@ -139,15 +140,18 @@ export function SvelteElement(node, context) { ) ); - if (is_async) { + if (has_await || has_blockers) { context.state.init.push( b.stmt( b.call( '$.async', context.state.node, node.metadata.expression.blockers(), - b.array([b.thunk(expression, node.metadata.expression.has_await)]), - b.arrow([context.state.node, b.id('$$tag')], b.block(statements)) + has_await ? b.array([b.thunk(expression, true)]) : b.void0, + b.arrow( + has_await ? [context.state.node, b.id('$$tag')] : [context.state.node], + b.block(statements) + ) ) ) ); diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 527f0b0a8f..6b77903574 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -200,17 +200,17 @@ export class BranchManager { if (defer) { for (const [k, effect] of this.#onscreen) { if (k === key) { - batch.skipped_effects.delete(effect); + batch.unskip_effect(effect); } else { - batch.skipped_effects.add(effect); + batch.skip_effect(effect); } } for (const [k, branch] of this.#offscreen) { if (k === key) { - batch.skipped_effects.delete(branch.effect); + batch.unskip_effect(branch.effect); } else { - batch.skipped_effects.add(branch.effect); + batch.skip_effect(branch.effect); } } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 232656ec11..6eaeac0f38 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -257,7 +257,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f if (item.i) internal_set(item.i, index); if (defer) { - batch.skipped_effects.delete(item.e); + batch.unskip_effect(item.e); } } else { item = create_item( @@ -299,7 +299,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f if (defer) { for (const [key, item] of items) { if (!keys.has(key)) { - batch.skipped_effects.add(item.e); + batch.skip_effect(item.e); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2b6e84889b..cef2df4716 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -130,11 +130,13 @@ export class Batch { #maybe_dirty_effects = new Set(); /** - * A set of branches that still exist, but will be destroyed when this batch - * is committed — we skip over these during `process` - * @type {Set} + * A map of branches that still exist, but will be destroyed when this batch + * is committed — we skip over these during `process`. + * The value contains child effects that were dirty/maybe_dirty before being reset, + * so they can be rescheduled if the branch survives. + * @type {Map} */ - skipped_effects = new Set(); + #skipped_branches = new Map(); is_fork = false; @@ -144,6 +146,38 @@ export class Batch { return this.is_fork || this.#blocking_pending > 0; } + /** + * Add an effect to the #skipped_branches map and reset its children + * @param {Effect} effect + */ + skip_effect(effect) { + if (!this.#skipped_branches.has(effect)) { + this.#skipped_branches.set(effect, { d: [], m: [] }); + } + } + + /** + * Remove an effect from the #skipped_branches map and reschedule + * any tracked dirty/maybe_dirty child effects + * @param {Effect} effect + */ + unskip_effect(effect) { + var tracked = this.#skipped_branches.get(effect); + if (tracked) { + this.#skipped_branches.delete(effect); + + for (var e of tracked.d) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (e of tracked.m) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); + } + } + } + /** * * @param {Effect[]} root_effects @@ -172,8 +206,8 @@ export class Batch { this.#defer_effects(render_effects); this.#defer_effects(effects); - for (const e of this.skipped_effects) { - reset_branch(e); + for (const [e, t] of this.#skipped_branches) { + reset_branch(e, t); } } else { // append/remove branches @@ -220,7 +254,7 @@ export class Batch { var is_branch = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) !== 0; var is_skippable_branch = is_branch && (flags & CLEAN) !== 0; - var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); + var skip = is_skippable_branch || (flags & INERT) !== 0 || this.#skipped_branches.has(effect); // Inside a `` with a pending snippet, // all effects are deferred until the boundary resolves @@ -807,7 +841,8 @@ export function schedule_effect(signal) { var flags = effect.f; // if the effect is being scheduled because a parent (each/await/etc) block - // updated an internal source, bail out or we'll cause a second flush + // updated an internal source, or because a branch is being unskipped, + // bail out or we'll cause a second flush if ( is_flushing && effect === active_effect && @@ -887,20 +922,28 @@ export function eager(fn) { /** * Mark all the effects inside a skipped branch CLEAN, so that - * they can be correctly rescheduled later + * they can be correctly rescheduled later. Tracks dirty and maybe_dirty + * effects so they can be rescheduled if the branch survives. * @param {Effect} effect + * @param {{ d: Effect[], m: Effect[] }} tracked */ -function reset_branch(effect) { +function reset_branch(effect, tracked) { // clean branch = nothing dirty inside, no need to traverse further if ((effect.f & BRANCH_EFFECT) !== 0 && (effect.f & CLEAN) !== 0) { return; } + if ((effect.f & DIRTY) !== 0) { + tracked.d.push(effect); + } else if ((effect.f & MAYBE_DIRTY) !== 0) { + tracked.m.push(effect); + } + set_signal_status(effect, CLEAN); var e = effect.first; while (e !== null) { - reset_branch(e); + reset_branch(e, tracked); e = e.next; } } diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 13975c68ee..8c29a6ada2 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -47,9 +47,9 @@ export interface RuntimeTest = Record; /** Temporarily skip specific modes, without skipping the entire test */ skip_mode?: Array<'server' | 'async-server' | 'client' | 'hydrate'>; - /** Skip if running with process.env.NO_ASYNC */ + /** Skip if running with process.env.SVELTE_NO_ASYNC */ skip_no_async?: boolean; - /** Skip if running without process.env.NO_ASYNC */ + /** Skip if running without process.env.SVELTE_NO_ASYNC */ skip_async?: boolean; html?: string; ssrHtml?: string; diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte new file mode 100644 index 0000000000..fecbe222e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js new file mode 100644 index 0000000000..2f371bc6b7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + assert.deepEqual(logs, ['promise resolved with:', 'some-id']); + + const button = target.querySelector('button'); + button?.click(); + await tick(); + + assert.deepEqual(logs, ['promise resolved with:', 'some-id']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte new file mode 100644 index 0000000000..d5cdd5967e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-unmount-undefined-props/main.svelte @@ -0,0 +1,14 @@ + + +{#if active} + +{/if} + + From baba15ab981a3b090eaabefe3425f5f4ea9a8074 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Thu, 5 Feb 2026 00:31:33 +0800 Subject: [PATCH 03/23] fix: avoid 'node:crypto' cloudflare warnings (#17612) * fix: avoid 'node:crypto' cloudflare warnings * format * changeset * Apply suggestion from @teemingc --- .changeset/violet-pans-know.md | 5 +++++ packages/svelte/src/internal/server/crypto.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/violet-pans-know.md diff --git a/.changeset/violet-pans-know.md b/.changeset/violet-pans-know.md new file mode 100644 index 0000000000..59bf1dcaaf --- /dev/null +++ b/.changeset/violet-pans-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid Cloudflare warnings about not having the "node:crypto" module diff --git a/packages/svelte/src/internal/server/crypto.js b/packages/svelte/src/internal/server/crypto.js index 8727635481..9bb6ecdd39 100644 --- a/packages/svelte/src/internal/server/crypto.js +++ b/packages/svelte/src/internal/server/crypto.js @@ -12,7 +12,8 @@ export async function sha256(data) { crypto ??= globalThis.crypto?.subtle?.digest ? globalThis.crypto : // @ts-ignore - we don't install node types in the prod build - (await import('node:crypto')).webcrypto; + // don't use 'node:crypto' because static analysers will think we rely on node when we don't + (await import('node:' + 'crypto')).webcrypto; const hash_buffer = await crypto.subtle.digest('SHA-256', text_encoder.encode(data)); From 05229d96823cc88d11c0b4a81b357cdb67f69326 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Thu, 5 Feb 2026 00:32:33 +0800 Subject: [PATCH 04/23] chore: remove SvelteKit data attributes from elements.d.ts (#17613) * Remove SvelteKit data attributes from elements.d.ts Removed SvelteKit specific data attributes from elements.d.ts. * Remove SvelteKit data attributes from elements.d.ts Removed SvelteKit data attributes from elements.d.ts. --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/quiet-buttons-bathe.md | 5 +++++ packages/svelte/elements.d.ts | 17 ----------------- 2 files changed, 5 insertions(+), 17 deletions(-) create mode 100644 .changeset/quiet-buttons-bathe.md diff --git a/.changeset/quiet-buttons-bathe.md b/.changeset/quiet-buttons-bathe.md new file mode 100644 index 0000000000..419a5569b4 --- /dev/null +++ b/.changeset/quiet-buttons-bathe.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +chore: remove SvelteKit data attributes from elements.d.ts diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index fa74124472..885004dd2a 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -851,23 +851,6 @@ export interface HTMLAttributes extends AriaAttributes, D readonly 'bind:offsetWidth'?: number | undefined | null; readonly 'bind:offsetHeight'?: number | undefined | null; - // SvelteKit - 'data-sveltekit-keepfocus'?: true | '' | 'off' | undefined | null; - 'data-sveltekit-noscroll'?: true | '' | 'off' | undefined | null; - 'data-sveltekit-preload-code'?: - | true - | '' - | 'eager' - | 'viewport' - | 'hover' - | 'tap' - | 'off' - | undefined - | null; - 'data-sveltekit-preload-data'?: true | '' | 'hover' | 'tap' | 'off' | undefined | null; - 'data-sveltekit-reload'?: true | '' | 'off' | undefined | null; - 'data-sveltekit-replacestate'?: true | '' | 'off' | undefined | null; - // allow any data- attribute [key: `data-${string}`]: any; From eb63a6bbbaffec07890ae181b02edf4c52e2586a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:52:30 -0500 Subject: [PATCH 05/23] Version Packages (#17614) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/quiet-buttons-bathe.md | 5 ----- .changeset/some-teams-pay.md | 5 ----- .changeset/violet-pans-know.md | 5 ----- .changeset/yummy-insects-wonder.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/quiet-buttons-bathe.md delete mode 100644 .changeset/some-teams-pay.md delete mode 100644 .changeset/violet-pans-know.md delete mode 100644 .changeset/yummy-insects-wonder.md diff --git a/.changeset/quiet-buttons-bathe.md b/.changeset/quiet-buttons-bathe.md deleted file mode 100644 index 419a5569b4..0000000000 --- a/.changeset/quiet-buttons-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -chore: remove SvelteKit data attributes from elements.d.ts diff --git a/.changeset/some-teams-pay.md b/.changeset/some-teams-pay.md deleted file mode 100644 index d7ae69566d..0000000000 --- a/.changeset/some-teams-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid erroneous async derived expressions for blocks diff --git a/.changeset/violet-pans-know.md b/.changeset/violet-pans-know.md deleted file mode 100644 index 59bf1dcaaf..0000000000 --- a/.changeset/violet-pans-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: avoid Cloudflare warnings about not having the "node:crypto" module diff --git a/.changeset/yummy-insects-wonder.md b/.changeset/yummy-insects-wonder.md deleted file mode 100644 index 6e705cd8cf..0000000000 --- a/.changeset/yummy-insects-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reschedule effects inside unskipped branches diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c7c38302e8..a71effd5dd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.49.2 + +### Patch Changes + +- chore: remove SvelteKit data attributes from elements.d.ts ([#17613](https://github.com/sveltejs/svelte/pull/17613)) + +- fix: avoid erroneous async derived expressions for blocks ([#17604](https://github.com/sveltejs/svelte/pull/17604)) + +- fix: avoid Cloudflare warnings about not having the "node:crypto" module ([#17612](https://github.com/sveltejs/svelte/pull/17612)) + +- fix: reschedule effects inside unskipped branches ([#17604](https://github.com/sveltejs/svelte/pull/17604)) + ## 5.49.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index fd4b98679e..48f492783f 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.49.1", + "version": "5.49.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 557b1ff156..31538c3fe7 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.49.1'; +export const VERSION = '5.49.2'; export const PUBLIC_VERSION = '5'; From 4f41e816baa007fb6a4e31164da49576e87e342e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 4 Feb 2026 13:07:46 -0500 Subject: [PATCH 06/23] fix: ensure infinite effect loops are cleared after flushing (#17601) * failing effect-loop-infinite test * fix --- .changeset/chatty-mammals-find.md | 5 +++++ .../svelte/src/internal/client/reactivity/batch.js | 3 ++- packages/svelte/tests/runtime-legacy/shared.ts | 2 ++ .../samples/effect-loop-infinite/_config.js | 11 ++++++----- 4 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .changeset/chatty-mammals-find.md diff --git a/.changeset/chatty-mammals-find.md b/.changeset/chatty-mammals-find.md new file mode 100644 index 0000000000..373dc0059a --- /dev/null +++ b/.changeset/chatty-mammals-find.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure infinite effect loops are cleared after flushing diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index cef2df4716..9bf93c873f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -647,8 +647,9 @@ function flush_effects() { } } } finally { - is_flushing = false; + queued_root_effects = []; + is_flushing = false; last_scheduled_effect = null; if (DEV) { diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 8c29a6ada2..c5317f822e 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -521,6 +521,8 @@ async function run_test_variant( errors, hydrate: hydrate_fn }); + + flushSync(); } if (config.runtime_error && !unhandled_rejection) { diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js index 57f60c2b44..44cf5b09e2 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -11,11 +11,12 @@ export default test({ test({ assert, errors }) { const [button] = document.querySelectorAll('button'); - try { + assert.throws(() => { flushSync(() => button.click()); - } catch (e) { - assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at' - assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); - } + }, /effect_update_depth_exceeded/); + + assert.equal(errors.length, 1); + + assert.doesNotThrow(flushSync); } }); From d7a8e3d1300fbb802b84a6ebf0f3f71ed734a556 Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 4 Feb 2026 21:51:50 +0200 Subject: [PATCH 07/23] fix: emit `each_key_duplicate` error in production (#16724) * fix: emit `each_key_duplicate` error in production * fix: preserve key * Update packages/svelte/src/internal/client/dom/blocks/each.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/dom/blocks/each.js Co-authored-by: Rich Harris * fix: ensure keys are validated * fix silly test name * fix: cover other case of duplicate keys * emit error on hydration * ensure the error is handled * drop useless tests * unused * finish merge * add lost check back * chore: bump playwright (#17565) * chore: bump playwright * maybe this will help somehow? * err whatever * fix * chore: allow testing in production env 2 (#17590) * Revert "chore: allow testing in production env (#16840)" This reverts commit ffd65e90febc29feaca48e142126a4087fcaca9f. * new approach * fix: handle renderer.run rejections (#17591) * fix: handle renderer run rejections * add test * changeset * simplify * explanatory comment --------- Co-authored-by: Antonio Bennett Co-authored-by: Rich Harris * fix: only create async functions in SSR output when necessary (#17593) * fix: only create async functions in SSR output when necessary * actually... * simplify generated code a bit more * simplify * fix: merge consecutive text nodes during hydration for large text content (#17587) * fix: merge consecutive text nodes during hydration for large text content Fixes #17582 Browsers automatically split text nodes exceeding 65536 characters into multiple consecutive text nodes during HTML parsing. This causes hydration mismatches when Svelte expects a single text node. The fix merges consecutive text nodes during hydration by: - Detecting when the current node is a text node - Finding all consecutive text node siblings - Merging their content into the first text node - Removing the extra text nodes This restores correct hydration behavior for large text content. * add test, fix * fix * fix * changeset --------- Co-authored-by: Miner Co-authored-by: Rich Harris * Version Packages (#17585) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Revert "drop useless tests" This reverts commit 65f77ef8409d6e8c91163f36853d36b05ec01ae8. * update tests * fix test * we don't need to expose this function any more * figured it out... we cant have errors during reconcile * simplify * tweak * unused * revert no-longer-needed change * unused --------- Co-authored-by: Rich Harris Co-authored-by: Antonio Bennett <31296212+Antonio-Bennett@users.noreply.github.com> Co-authored-by: Antonio Bennett Co-authored-by: FORMI <239411042+Richman018@users.noreply.github.com> Co-authored-by: Miner Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/sharp-snakes-poke.md | 5 +++ .../3-transform/client/visitors/EachBlock.js | 4 -- .../src/internal/client/dom/blocks/each.js | 37 +++++++++++++++++++ packages/svelte/src/internal/client/index.js | 2 +- .../svelte/src/internal/client/validate.js | 34 ----------------- .../samples/keyed-each-unique-2/_config.js | 12 ++++++ .../samples/keyed-each-unique-2/main.svelte | 8 ++++ .../samples/keyed-each-unique-3/_config.js | 5 +++ .../samples/keyed-each-unique-3/main.svelte | 7 ++++ .../samples/keyed-each-unique/_config.js | 12 ++++++ .../samples/keyed-each-unique/main.svelte | 8 ++++ 11 files changed, 95 insertions(+), 39 deletions(-) create mode 100644 .changeset/sharp-snakes-poke.md create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js create mode 100644 packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte diff --git a/.changeset/sharp-snakes-poke.md b/.changeset/sharp-snakes-poke.md new file mode 100644 index 0000000000..7f7f8aa7b2 --- /dev/null +++ b/.changeset/sharp-snakes-poke.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: emit `each_key_duplicate` error in production diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 1dbc34fdc3..b2724fa90f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -338,10 +338,6 @@ export function EachBlock(node, context) { const statements = [add_svelte_meta(b.call('$.each', ...args), node, 'each')]; - if (dev && node.metadata.keyed) { - statements.unshift(b.stmt(b.call('$.validate_each_keys', thunk, key_function))); - } - if (node.metadata.expression.is_async()) { context.state.init.push( b.stmt( diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 6eaeac0f38..25f7cf91eb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -40,6 +40,7 @@ import { get } from '../../runtime.js'; import { DEV } from 'esm-env'; import { derived_safe_equal } from '../../reactivity/deriveds.js'; import { current_batch } from '../../reactivity/batch.js'; +import * as e from '../../errors.js'; // When making substantive changes to this file, validate them with the each block stress test: // https://svelte.dev/playground/1972b2cf46564476ad8c8c6405b23b7b @@ -290,6 +291,15 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } + if (length > keys.size) { + if (DEV) { + validate_each_keys(array, get_key); + } else { + // in prod, the additional information isn't printed, so don't bother computing it + e.each_key_duplicate('', '', ''); + } + } + // remove excess nodes if (hydrating && length > 0) { set_hydrate_node(skip_nodes()); @@ -676,3 +686,30 @@ function link(state, prev, next) { next.prev = prev; } } + +/** + * @param {Array} array + * @param {(item: any, index: number) => string} key_fn + * @returns {void} + */ +function validate_each_keys(array, key_fn) { + const keys = new Map(); + const length = array.length; + + for (let i = 0; i < length; i++) { + const key = key_fn(array[i], i); + + if (keys.has(key)) { + const a = String(keys.get(key)); + const b = String(i); + + /** @type {string | null} */ + let k = String(key); + if (k.startsWith('[object ')) k = null; + + e.each_key_duplicate(a, b, k); + } + + keys.set(key, i); + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 1d9f7dfff7..7fcaf77dc5 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -158,7 +158,7 @@ export { deep_read_state, active_effect } from './runtime.js'; -export { validate_binding, validate_each_keys } from './validate.js'; +export { validate_binding } from './validate.js'; export { raf } from './timing.js'; export { proxy } from './proxy.js'; export { create_custom_element } from './dom/elements/custom-element.js'; diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 48a44db304..a169225f1e 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,45 +1,11 @@ /** @import { Blocker } from '#client' */ import { dev_current_component_function } from './context.js'; -import { is_array } from '../shared/utils.js'; -import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; import { render_effect } from './reactivity/effects.js'; import * as w from './warnings.js'; import { capture_store_binding } from './reactivity/store.js'; import { run_after_blockers } from './reactivity/async.js'; -/** - * @param {() => any} collection - * @param {(item: any, index: number) => string} key_fn - * @returns {void} - */ -export function validate_each_keys(collection, key_fn) { - render_effect(() => { - const keys = new Map(); - const maybe_array = collection(); - const array = is_array(maybe_array) - ? maybe_array - : maybe_array == null - ? [] - : Array.from(maybe_array); - const length = array.length; - for (let i = 0; i < length; i++) { - const key = key_fn(array[i], i); - if (keys.has(key)) { - const a = String(keys.get(key)); - const b = String(i); - - /** @type {string | null} */ - let k = String(key); - if (k.startsWith('[object ')) k = null; - - e.each_key_duplicate(a, b, k); - } - keys.set(key, i); - } - }); -} - /** * @param {string} binding * @param {Blocker[]} blockers diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js new file mode 100644 index 0000000000..bc945876ea --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + let button = target.querySelector('button'); + + button?.click(); + + assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte new file mode 100644 index 0000000000..f8ba50d866 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-2/main.svelte @@ -0,0 +1,8 @@ + + + +{#each data as d (d)} + {d} +{/each} diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js new file mode 100644 index 0000000000..7e1840200a --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + error: 'each_key_duplicate' +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte new file mode 100644 index 0000000000..a05781bcb9 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique-3/main.svelte @@ -0,0 +1,7 @@ + + +{#each data as d (d)} + {d} +{/each} diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js new file mode 100644 index 0000000000..bc945876ea --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + let button = target.querySelector('button'); + + button?.click(); + + assert.throws(flushSync, 'https://svelte.dev/e/each_key_duplicate'); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte new file mode 100644 index 0000000000..3d52179372 --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/keyed-each-unique/main.svelte @@ -0,0 +1,8 @@ + + + +{#each data as d (d)} + {d} +{/each} From f05f946feafd804de3416742e5d2379651087b2e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Feb 2026 09:57:16 -0500 Subject: [PATCH 08/23] fix: add vite-ignore comment inside dynamic crypto import (#17623) --- .changeset/forty-worlds-attack.md | 5 +++++ packages/svelte/src/internal/server/crypto.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/forty-worlds-attack.md diff --git a/.changeset/forty-worlds-attack.md b/.changeset/forty-worlds-attack.md new file mode 100644 index 0000000000..52bb3644c0 --- /dev/null +++ b/.changeset/forty-worlds-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add vite-ignore comment inside dynamic crypto import diff --git a/packages/svelte/src/internal/server/crypto.js b/packages/svelte/src/internal/server/crypto.js index 9bb6ecdd39..76d66b6d1a 100644 --- a/packages/svelte/src/internal/server/crypto.js +++ b/packages/svelte/src/internal/server/crypto.js @@ -13,7 +13,7 @@ export async function sha256(data) { ? globalThis.crypto : // @ts-ignore - we don't install node types in the prod build // don't use 'node:crypto' because static analysers will think we rely on node when we don't - (await import('node:' + 'crypto')).webcrypto; + (await import(/* @vite-ignore */ 'node:' + 'crypto')).webcrypto; const hash_buffer = await crypto.subtle.digest('SHA-256', text_encoder.encode(data)); From fcbd3e325a62c4a2c83ff51f721fdfe26b7451d9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 5 Feb 2026 09:59:07 -0500 Subject: [PATCH 09/23] feat: allow use of createContext when instantiating components programmatically (#17575) * feat: allow use of createContext when instantiating components programmatically * docs --- .changeset/orange-ants-greet.md | 5 ++++ documentation/docs/06-runtime/02-context.md | 26 +++++++++++++++++++ packages/svelte/src/internal/client/render.js | 12 +++------ .../svelte/src/internal/server/renderer.js | 12 +++------ .../create-context-programmatic/Child.svelte | 7 +++++ .../create-context-programmatic/_config.js | 8 ++++++ .../create-context-programmatic/main.svelte | 20 ++++++++++++++ 7 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 .changeset/orange-ants-greet.md create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte diff --git a/.changeset/orange-ants-greet.md b/.changeset/orange-ants-greet.md new file mode 100644 index 0000000000..6f7c684fee --- /dev/null +++ b/.changeset/orange-ants-greet.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow use of createContext when instantiating components programmatically diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 0dfb996164..61b203f93e 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -97,6 +97,32 @@ import { createContext } from 'svelte'; export const [getUserContext, setUserContext] = createContext(); ``` +When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: + +```js +import { mount, unmount } from 'svelte'; +import { expect, test } from 'vitest'; +import { setUserContext } from './context'; +import MyComponent from './MyComponent.svelte'; + +test('MyComponent', () => { + function Wrapper(...args) { + setUserContext({ name: 'Bob' }); + return MyComponent(...args); + } + + const component = mount(Wrapper, { + target: document.body + }); + + expect(document.body.innerHTML).toBe('

Hello Bob!

'); + + unmount(component); +}); +``` + +This approach also works with [`hydrate`](imperative-component-api#hydrate) and [`render`](imperative-component-api#render). + ## Replacing global state When you have state shared by many different components, you might be tempted to put it in its own module and just import it wherever it's needed: diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index c76f0b1ce7..c09f5fdd05 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -208,11 +208,9 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro pending: () => {} }, (anchor_node) => { - if (context) { - push({}); - var ctx = /** @type {ComponentContext} */ (component_context); - ctx.c = context; - } + push({}); + var ctx = /** @type {ComponentContext} */ (component_context); + if (context) ctx.c = context; if (events) { // We can't spread the object or else we'd lose the state proxy stuff, if it is one @@ -241,9 +239,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro } } - if (context) { - pop(); - } + pop(); } ); diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 65f9bcae6a..49f4c1b7d2 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -617,18 +617,14 @@ export class Renderer { renderer.push(BLOCK_OPEN); - if (options.context) { - push(); - /** @type {SSRContext} */ (ssr_context).c = options.context; - /** @type {SSRContext} */ (ssr_context).r = renderer; - } + push(); + if (options.context) /** @type {SSRContext} */ (ssr_context).c = options.context; + /** @type {SSRContext} */ (ssr_context).r = renderer; // @ts-expect-error component(renderer, options.props ?? {}); - if (options.context) { - pop(); - } + pop(); renderer.push(BLOCK_CLOSE); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte new file mode 100644 index 0000000000..3e39d5043e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte @@ -0,0 +1,7 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js new file mode 100644 index 0000000000..f4374c8759 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: `
`, + html: `

hello

`, + + test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte new file mode 100644 index 0000000000..3c42dbf180 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte @@ -0,0 +1,20 @@ + + +
{ + mount(Wrapper(Child), { target }); +}}>
From 3970e7a302e29fa09eb3c0351301f260401e32f0 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 5 Feb 2026 13:58:50 -0700 Subject: [PATCH 10/23] chore: fix pkg.pr.new workflow (#17633) --- .github/workflows/pkg.pr.new-comment.yml | 115 ------------- .github/workflows/pkg.pr.new.yml | 205 +++++++++++++++++++++-- 2 files changed, 190 insertions(+), 130 deletions(-) delete mode 100644 .github/workflows/pkg.pr.new-comment.yml diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml deleted file mode 100644 index 64495cc5c8..0000000000 --- a/.github/workflows/pkg.pr.new-comment.yml +++ /dev/null @@ -1,115 +0,0 @@ -name: Update pkg.pr.new comment - -on: - workflow_run: - workflows: ['Publish Any Commit'] - types: - - completed - -permissions: - pull-requests: write - -jobs: - build: - name: 'Update comment' - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v7 - with: - name: output - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - run: ls -R . - - name: 'Post or update comment' - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - - const bot_comment_identifier = ``; - - const body = (number) => `${bot_comment_identifier} - - [Playground](https://svelte.dev/playground?version=pr-${number}) - - \`\`\` - ${output.packages.map((p) => `pnpm add https://pkg.pr.new/${p.name}@${number}`).join('\n')} - \`\`\` - `; - - async function find_bot_comment(issue_number) { - if (!issue_number) return null; - const comments = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: issue_number, - }); - return comments.data.find((comment) => - comment.body.includes(bot_comment_identifier) - ); - } - - async function create_or_update_comment(issue_number) { - if (!issue_number) { - console.log('No issue number provided. Cannot post or update comment.'); - return; - } - - const existing_comment = await find_bot_comment(issue_number); - if (existing_comment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing_comment.id, - body: body(issue_number), - }); - } else { - await github.rest.issues.createComment({ - issue_number: issue_number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body(issue_number), - }); - } - } - - async function log_publish_info() { - const svelte_package = output.packages.find(p => p.name === 'svelte'); - const svelte_sha = svelte_package.url.replace(/^.+@([^@]+)$/, '$1'); - console.log('\n' + '='.repeat(50)); - console.log('Publish Information'); - console.log('='.repeat(50)); - console.log('\nPublished Packages:'); - console.log(output.packages.map((p) => `${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.url.replace(/^.+@([^@]+)$/, '$1')}`).join('\n')); - if(svelte_sha){ - console.log('\nPlayground URL:'); - console.log(`\nhttps://svelte.dev/playground?version=commit-${svelte_sha}`) - } - console.log('\n' + '='.repeat(50)); - } - - if (output.event_name === 'pull_request') { - if (output.number) { - await create_or_update_comment(output.number); - } - } else if (output.event_name === 'push') { - const pull_requests = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - head: `${context.repo.owner}:${output.ref.replace('refs/heads/', '')}`, - }); - - if (pull_requests.data.length > 0) { - await create_or_update_comment(pull_requests.data[0].number); - } else { - console.log( - 'No open pull request found for this push. Logging publish information to console:' - ); - await log_publish_info(); - } - } diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 252cbed769..51b7472911 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -1,16 +1,51 @@ -name: Publish Any Commit -on: [push, pull_request] +name: pkg.pr.new +on: + pull_request_target: + types: [opened, synchronize] + push: + branches: [main] permissions: {} jobs: - build: - permissions: {} + # This job determines the environment to use for the build job. It ensures that: + # - For pushes to main, we use the "Publish pkg.pr.new (maintainers)" environment. + # - For PRs from the same repository, we also use the "Publish pkg.pr.new (maintainers)" environment, since these are trusted. + # - For PRs from forks, we use the "Publish pkg.pr.new (external contributors)" environment, which requires manual approval by a maintainer before the build job can run. + # This protects us from running untrusted code while still allowing external contributors to use pkg.pr.new. + resolve-env: + runs-on: ubuntu-latest + outputs: + environment: ${{ steps.resolve.outputs.environment }} + steps: + - name: Determine environment + id: resolve + run: | + if [[ "${{ github.event_name }}" == "push" ]]; then + echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT" + elif [[ "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then + echo "environment=Publish pkg.pr.new (maintainers)" >> "$GITHUB_OUTPUT" + else + echo "environment=Publish pkg.pr.new (external contributors)" >> "$GITHUB_OUTPUT" + fi + build: + needs: resolve-env runs-on: ubuntu-latest + # This is the line that ensures forks require manual approval before running the build job + environment: ${{ needs.resolve-env.outputs.environment }} + + # No permissions — this job runs user-controlled code + permissions: {} steps: - uses: actions/checkout@v6 + with: + # For pull_request_target, we must explicitly check out the PR head. + # This is safe because the environment gate above has already fired — + # an org member has approved this specific commit for external PRs. + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: @@ -24,21 +59,161 @@ jobs: run: pnpm build - run: pnpx pkg-pr-new publish --comment=off --json output.json --compact --no-template './packages/svelte' - - name: Add metadata to output + + - name: Upload output + uses: actions/upload-artifact@v4 + with: + name: output + path: ./output.json + + # Sanitizes the untrusted output from the build job before it's consumed by + # jobs with elevated permissions. This ensures that only known package names + # and valid SHA prefixes make it through. + sanitize: + needs: build + runs-on: ubuntu-latest + + permissions: {} + + steps: + - name: Download artifact + uses: actions/download-artifact@v7 + with: + name: output + + - name: Sanitize output + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const raw = JSON.parse(fs.readFileSync('output.json', 'utf8')); + + const ALLOWED_PACKAGES = new Set(['svelte']); + const SHA_PATTERN = /^[0-9a-f]{6}$/; + + const packages = (raw.packages || []) + .filter(p => { + if (!ALLOWED_PACKAGES.has(p.name)) { + console.log(`Skipping unexpected package: ${JSON.stringify(p.name)}`); + return false; + } + const sha = p.url?.replace(/^.+@([^@]+)$/, '$1'); + if (!sha || !SHA_PATTERN.test(sha)) { + console.log(`Skipping package with invalid SHA: ${JSON.stringify(p.url)}`); + return false; + } + return true; + }) + .map(p => ({ + name: p.name, + sha: p.url.replace(/^.+@([^@]+)$/, '$1'), + })); + + fs.writeFileSync('sanitized-output.json', JSON.stringify({ packages }), 'utf8'); + + - name: Upload sanitized output + uses: actions/upload-artifact@v4 + with: + name: sanitized-output + path: ./sanitized-output.json + + comment: + needs: sanitize + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + + permissions: + pull-requests: write + + steps: + - name: Download sanitized artifact + uses: actions/download-artifact@v7 + with: + name: sanitized-output + + - name: Post or update comment uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - output.number = context.issue.number; - output.event_name = context.eventName; - output.ref = context.ref; - fs.writeFileSync('output.json', JSON.stringify(output), 'utf8'); - - name: Upload output - uses: actions/upload-artifact@v6 + const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); + + if (packages.length === 0) { + console.log('No valid packages found. Skipping comment.'); + return; + } + + // Issue number from trusted event context, never from the artifact + const issue_number = context.issue.number; + + const bot_comment_identifier = ``; + + const body = `${bot_comment_identifier} + + [Playground](https://svelte.dev/playground?version=pr-${issue_number}) + + \`\`\` + ${packages.map(p => `pnpm add https://pkg.pr.new/${p.name}@${issue_number}`).join('\n')} + \`\`\` + `; + + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + }); + const existing = comments.data.find(c => c.body.includes(bot_comment_identifier)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + body, + }); + } + + log: + needs: sanitize + if: github.event_name == 'push' + runs-on: ubuntu-latest + + permissions: {} + + steps: + - name: Download sanitized artifact + uses: actions/download-artifact@v7 with: - name: output - path: ./output.json + name: sanitized-output + + - name: Log publish info + uses: actions/github-script@v8 + with: + script: | + const fs = require('fs'); + const { packages } = JSON.parse(fs.readFileSync('sanitized-output.json', 'utf8')); + + if (packages.length === 0) { + console.log('No valid packages found.'); + return; + } - - run: ls -R . + console.log('\n' + '='.repeat(50)); + console.log('Publish Information'); + console.log('='.repeat(50)); + for (const p of packages) { + console.log(`${p.name} - pnpm add https://pkg.pr.new/${p.name}@${p.sha}`); + } + const svelte = packages.find(p => p.name === 'svelte'); + if (svelte) { + console.log(`\nPlayground: https://svelte.dev/playground?version=commit-${svelte.sha}`); + } + console.log('='.repeat(50)); From 660c4c12b1e1c50d067dfd800465bea21d29651f Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Thu, 5 Feb 2026 14:04:56 -0700 Subject: [PATCH 11/23] chore: off-by-one in workflow (#17634) --- .github/workflows/pkg.pr.new.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkg.pr.new.yml b/.github/workflows/pkg.pr.new.yml index 51b7472911..0f0c433361 100644 --- a/.github/workflows/pkg.pr.new.yml +++ b/.github/workflows/pkg.pr.new.yml @@ -89,7 +89,7 @@ jobs: const raw = JSON.parse(fs.readFileSync('output.json', 'utf8')); const ALLOWED_PACKAGES = new Set(['svelte']); - const SHA_PATTERN = /^[0-9a-f]{6}$/; + const SHA_PATTERN = /^[0-9a-f]{7}$/; const packages = (raw.packages || []) .filter(p => { From a75866f34d5b64e0c40b2888a5f89a69ec1151f9 Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:03:40 +0100 Subject: [PATCH 12/23] fix: detect store in each block expression regardless of AST shape (#17636) The store invalidation detection in each blocks only checked for Identifier and MemberExpression AST node types. This caused bind: on iteration variables to silently fail when the expression used logical operators (e.g. `{#each $store.items ?? [] as item}`). Use expression metadata dependencies instead of AST type checking to find store_sub bindings, which correctly handles all expression shapes. Fixes #14625 --- .changeset/fix-each-bind-store-logical.md | 5 +++++ .../3-transform/client/visitors/EachBlock.js | 12 ++++------ .../store-each-binding-logical/_config.js | 22 +++++++++++++++++++ .../store-each-binding-logical/main.svelte | 13 +++++++++++ 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-each-bind-store-logical.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte diff --git a/.changeset/fix-each-bind-store-logical.md b/.changeset/fix-each-bind-store-logical.md new file mode 100644 index 0000000000..1327015124 --- /dev/null +++ b/.changeset/fix-each-bind-store-logical.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: detect store in each block expression regardless of AST shape diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index b2724fa90f..a1371b516a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -101,15 +101,11 @@ export function EachBlock(node, context) { } // If the array is a store expression, we need to invalidate it when the array is changed. - // This doesn't catch all cases, but all the ones that Svelte 4 catches, too. let store_to_invalidate = ''; - if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') { - const id = object(node.expression); - if (id) { - const binding = context.state.scope.get(id.name); - if (binding?.kind === 'store_sub') { - store_to_invalidate = id.name; - } + for (const binding of node.metadata.expression.dependencies) { + if (binding.kind === 'store_sub') { + store_to_invalidate = binding.node.name; + break; } } diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js new file mode 100644 index 0000000000..1134b20d0c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + test({ assert, target, window }) { + const input = target.querySelector('input'); + ok(input); + + const event = new window.Event('input'); + input.value = 'changed'; + input.dispatchEvent(event); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` + +

changed

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte new file mode 100644 index 0000000000..77731ea5a4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/store-each-binding-logical/main.svelte @@ -0,0 +1,13 @@ + + +{#each $items ?? [] as item} + +{/each} + +

{$items[0].text}

From f5304ec8c944b1893e8150449d26cf07af910d7b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Feb 2026 04:11:26 -0500 Subject: [PATCH 13/23] chore: simplify SSR code (#17639) creates a few abstractions in the compiler code around common "if-else" scenarios, as well as introducing a new runtime render helper --------- Co-authored-by: Simon Holthausen Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .../3-transform/client/visitors/Fragment.js | 40 +++--- .../3-transform/server/transform-server.js | 11 +- .../phases/3-transform/server/types.d.ts | 3 +- .../3-transform/server/visitors/AwaitBlock.js | 15 ++- .../3-transform/server/visitors/EachBlock.js | 30 ++--- .../3-transform/server/visitors/Fragment.js | 2 +- .../3-transform/server/visitors/HtmlTag.js | 25 ++-- .../3-transform/server/visitors/IfBlock.js | 21 ++-- .../server/visitors/RegularElement.js | 55 ++------ .../3-transform/server/visitors/RenderTag.js | 14 +-- .../server/visitors/SlotElement.js | 11 +- .../server/visitors/SvelteElement.js | 53 +++----- .../server/visitors/shared/component.js | 41 ++---- .../server/visitors/shared/utils.js | 118 +++++++++--------- .../svelte/src/internal/server/renderer.js | 9 ++ .../_expected/server/index.svelte.js | 6 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 6 +- 21 files changed, 184 insertions(+), 284 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 79a443967c..f463111c4d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -120,34 +120,32 @@ export function Fragment(node, context) { state.init.unshift(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + } else if (is_standalone) { + // no need to create a template, we can just use the existing block's anchor + process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); } else { - if (is_standalone) { - // no need to create a template, we can just use the existing block's anchor - process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); - } else { - /** @type {(is_text: boolean) => Expression} */ - const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); - - process_children(trimmed, expression, false, { ...context, state }); + /** @type {(is_text: boolean) => Expression} */ + const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); - let flags = TEMPLATE_FRAGMENT; + process_children(trimmed, expression, false, { ...context, state }); - if (state.template.needs_import_node) { - flags |= TEMPLATE_USE_IMPORT_NODE; - } + let flags = TEMPLATE_FRAGMENT; - if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { - // special case — we can use `$.comment` instead of creating a unique template - state.init.unshift(b.var(id, b.call('$.comment'))); - } else { - const template = transform_template(state, namespace, flags); - state.hoisted.push(b.var(template_name, template)); + if (state.template.needs_import_node) { + flags |= TEMPLATE_USE_IMPORT_NODE; + } - state.init.unshift(b.var(id, b.call(template_name))); - } + if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'comment') { + // special case — we can use `$.comment` instead of creating a unique template + state.init.unshift(b.var(id, b.call('$.comment'))); + } else { + const template = transform_template(state, namespace, flags); + state.hoisted.push(b.var(template_name, template)); - close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + state.init.unshift(b.var(id, b.call(template_name))); } + + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 0e2f68d0f5..b9f1441bff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -41,7 +41,6 @@ import { TitleElement } from './visitors/TitleElement.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; import { VariableDeclaration } from './visitors/VariableDeclaration.js'; import { SvelteBoundary } from './visitors/SvelteBoundary.js'; -import { call_component_renderer } from './visitors/shared/utils.js'; /** @type {Visitors} */ const global_visitors = { @@ -105,7 +104,7 @@ export function server_component(analysis, options) { namespace: options.namespace, preserve_whitespace: options.preserveWhitespace, state_fields: new Map(), - skip_hydration_boundaries: false, + is_standalone: false, is_instance: false }; @@ -260,7 +259,13 @@ export function server_component(analysis, options) { if (should_inject_context) { component_block = b.block([ - call_component_renderer(component_block, dev && b.id(component_name)) + b.stmt( + b.call( + '$$renderer.component', + b.arrow([b.id('$$renderer')], component_block, false), + dev && b.id(component_name) + ) + ) ]); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index e7a72fb8ad..4912728a1e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -26,7 +26,8 @@ export interface ComponentServerTransformState extends ServerTransformState { readonly template: Array; readonly namespace: Namespace; readonly preserve_whitespace: boolean; - readonly skip_hydration_boundaries: boolean; + /** True if the current node is a) a component or render tag and b) the sole child of a block */ + readonly is_standalone: boolean; /** Transformed async `{@const }` declarations (if any) and those coming after them */ async_consts?: { id: Identifier; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js index b8d2e42144..84c2a81612 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, create_async_block } from './shared/utils.js'; +import { block_close, create_child_block } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -25,13 +25,12 @@ export function AwaitBlock(node, context) { ) ); - if (node.metadata.expression.is_async()) { - statement = create_async_block( - b.block([statement]), + context.state.template.push( + ...create_child_block( + [statement], node.metadata.expression.blockers(), node.metadata.expression.has_await - ); - } - - context.state.template.push(statement, block_close); + ), + block_close + ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js index 3c0a8c1676..cb5a61d52f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -18,8 +18,8 @@ export function EachBlock(node, context) { const array_id = state.scope.root.unique('each_array'); - /** @type {Statement} */ - let block = b.block([b.const(array_id, b.call('$.ensure_array_like', collection))]); + /** @type {Statement[]} */ + let statements = [b.const(array_id, b.call('$.ensure_array_like', collection))]; /** @type {Statement[]} */ const each = []; @@ -53,7 +53,7 @@ export function EachBlock(node, context) { fallback.body.unshift(b.stmt(b.call(b.id('$$renderer.push'), block_open_else))); - block.body.push( + statements.push( b.if( b.binary('!==', b.member(array_id, 'length'), b.literal(0)), b.block([open, for_loop]), @@ -62,19 +62,15 @@ export function EachBlock(node, context) { ); } else { state.template.push(block_open); - block.body.push(for_loop); + statements.push(for_loop); } - if (node.metadata.expression.is_async()) { - state.template.push( - create_async_block( - block, - node.metadata.expression.blockers(), - node.metadata.expression.has_await - ), - block_close - ); - } else { - state.template.push(...block.body, block_close); - } + state.template.push( + ...create_child_block( + statements, + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ), + block_close + ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js index ef5bd985ae..a6ff33c7ab 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js @@ -28,7 +28,7 @@ export function Fragment(node, context) { init: [], template: [], namespace, - skip_hydration_boundaries: is_standalone, + is_standalone, async_consts: undefined }; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js index 3f423fa60d..ee790c3e7c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/HtmlTag.js @@ -2,25 +2,24 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, create_push } from './shared/utils.js'; +import { create_child_block } from './shared/utils.js'; /** * @param {AST.HtmlTag} node * @param {ComponentContext} context */ export function HtmlTag(node, context) { - const expression = /** @type {Expression} */ (context.visit(node.expression)); - const call = b.call('$.html', expression); + const expression = b.call('$.html', /** @type {Expression} */ (context.visit(node.expression))); - const has_await = node.metadata.expression.has_await; - - if (has_await) { - context.state.template.push(block_open); - } - - context.state.template.push(create_push(call, node.metadata.expression, true)); - - if (has_await) { - context.state.template.push(block_close); + if (node.metadata.expression.is_async()) { + context.state.template.push( + ...create_child_block( + [b.stmt(b.call('$$renderer.push', expression))], + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ) + ); + } else { + context.state.template.push(expression); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js index e8418343be..06b1b1e966 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/IfBlock.js @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; -import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js'; +import { block_close, block_open, block_open_else, create_child_block } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -23,17 +23,12 @@ export function IfBlock(node, context) { /** @type {Statement} */ let statement = b.if(test, consequent, alternate); - const is_async = node.metadata.expression.is_async(); - - const has_await = node.metadata.expression.has_await; - - if (is_async || has_await) { - statement = create_async_block( - b.block([statement]), + context.state.template.push( + ...create_child_block( + [statement], node.metadata.expression.blockers(), - !!has_await - ); - } - - context.state.template.push(statement, block_close); + node.metadata.expression.has_await + ), + block_close + ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 5b905752f4..1c6bb0a198 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -1,5 +1,4 @@ /** @import { Expression } from 'estree' */ -/** @import { Location } from 'locate-character' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */ /** @import { Scope } from '../../../scope.js' */ @@ -8,13 +7,7 @@ import { dev, locator } from '../../../../state.js'; import * as b from '#compiler/builders'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { build_element_attributes, prepare_element_spread_object } from './shared/element.js'; -import { - process_children, - build_template, - create_child_block, - PromiseOptimiser, - create_async_block -} from './shared/utils.js'; +import { process_children, build_template, PromiseOptimiser } from './shared/utils.js'; import { is_customizable_select_element } from '../../../nodes.js'; /** @@ -66,17 +59,9 @@ export function RegularElement(node, context) { b.literal(``) ); - // TODO this is a real edge case, would be good to DRY this out - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block( - b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]) - ) - ); - } else { - context.state.init.push(...state.init); - context.state.template.push(...state.template); - } + context.state.template.push( + ...optimiser.render([...state.init, ...build_template(state.template)]) + ); return; } @@ -130,13 +115,7 @@ export function RegularElement(node, context) { const statement = b.stmt(b.call('$$renderer.select', attributes, fn, ...rest)); - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block(b.block([optimiser.apply(), ...state.init, statement])) - ); - } else { - context.state.template.push(...state.init, statement); - } + context.state.template.push(...optimiser.render([...state.init, statement])); return; } @@ -183,13 +162,7 @@ export function RegularElement(node, context) { const statement = b.stmt(b.call('$$renderer.option', attributes, body, ...rest)); - if (optimiser.expressions.length > 0) { - context.state.template.push( - create_child_block(b.block([optimiser.apply(), ...state.init, statement])) - ); - } else { - context.state.template.push(...state.init, statement); - } + context.state.template.push(...optimiser.render([...state.init, statement])); return; } @@ -235,19 +208,9 @@ export function RegularElement(node, context) { } if (optimiser.is_async()) { - let statements = [...state.init, ...build_template(state.template)]; - - if (optimiser.has_await) { - statements = [create_child_block(b.block([optimiser.apply(), ...statements]))]; - } - - const blockers = optimiser.blockers(); - - if (blockers.elements.length > 0) { - statements = [create_async_block(b.block(statements), blockers, false, false)]; - } - - context.state.template.push(...statements); + context.state.template.push( + ...optimiser.render([...state.init, ...build_template(state.template)]) + ); } else { context.state.init.push(...state.init); context.state.template.push(...state.template); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js index 6d7cef0d95..fe20e85e1b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RenderTag.js @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types.js' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_async_block, empty_comment, PromiseOptimiser } from './shared/utils.js'; +import { empty_comment, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -35,17 +35,9 @@ export function RenderTag(node, context) { ) ); - if (optimiser.is_async()) { - statement = create_async_block( - b.block([optimiser.apply(), statement]), - optimiser.blockers(), - optimiser.has_await - ); - } - - context.state.template.push(statement); + context.state.template.push(...optimiser.render_block([statement])); - if (!context.state.skip_hydration_boundaries) { + if (!context.state.is_standalone) { context.state.template.push(empty_comment); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js index d0f8e25d02..3cebdf4541 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js @@ -5,7 +5,6 @@ import * as b from '#compiler/builders'; import { build_attribute_value, PromiseOptimiser, - create_async_block, block_open, block_close } from './shared/utils.js'; @@ -65,13 +64,5 @@ export function SlotElement(node, context) { fallback ); - const statement = optimiser.is_async() - ? create_async_block( - b.block([optimiser.apply(), b.stmt(slot)]), - optimiser.blockers(), - optimiser.has_await - ) - : b.stmt(slot); - - context.state.template.push(block_open, statement, block_close); + context.state.template.push(block_open, ...optimiser.render_block([b.stmt(slot)]), block_close); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js index 75ba323e20..398ecc68a2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js @@ -1,4 +1,3 @@ -/** @import { Location } from 'locate-character' */ /** @import { BlockStatement, Expression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ @@ -6,12 +5,7 @@ import { dev, locator } from '../../../../state.js'; import * as b from '#compiler/builders'; import { determine_namespace_for_children } from '../../utils.js'; import { build_element_attributes } from './shared/element.js'; -import { - build_template, - create_async_block, - create_child_block, - PromiseOptimiser -} from './shared/utils.js'; +import { build_template, create_child_block, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -67,36 +61,29 @@ export function SvelteElement(node, context) { const attributes = b.block([...state.init, ...build_template(state.template)]); const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state)); - /** @type {Statement} */ - let statement = b.stmt( - b.call( - '$.element', - b.id('$$renderer'), - tag, - attributes.body.length > 0 && b.thunk(attributes), - children.body.length > 0 && b.thunk(children) - ) + statements.push( + ...optimiser.render([ + b.stmt( + b.call( + '$.element', + b.id('$$renderer'), + tag, + attributes.body.length > 0 && b.thunk(attributes), + children.body.length > 0 && b.thunk(children) + ) + ) + ]) ); - if (optimiser.expressions.length > 0) { - statement = create_child_block(b.block([optimiser.apply(), statement])); - } - - statements.push(statement); - if (dev) { statements.push(b.stmt(b.call('$.pop_element'))); } - if (node.metadata.expression.is_async()) { - statements = [ - create_async_block( - b.block(statements), - node.metadata.expression.blockers(), - node.metadata.expression.has_await - ) - ]; - } - - context.state.template.push(...statements); + context.state.template.push( + ...create_child_block( + statements, + node.metadata.expression.blockers(), + node.metadata.expression.has_await + ) + ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index cfd9a27d7b..fe49a67b28 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -1,12 +1,7 @@ /** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types.js' */ -import { - empty_comment, - build_attribute_value, - create_async_block, - PromiseOptimiser -} from './utils.js'; +import { empty_comment, build_attribute_value, PromiseOptimiser } from './utils.js'; import * as b from '#compiler/builders'; import { is_element_node } from '../../../../nodes.js'; import { dev } from '../../../../../state.js'; @@ -325,32 +320,16 @@ export function build_inline_component(node, expression, context) { optimiser.check_blockers(node.metadata.expression); } - const is_async = optimiser.is_async(); - - if (is_async) { - statement = create_async_block( - b.block([ - optimiser.apply(), - dynamic && custom_css_props.length === 0 - ? b.stmt(b.call('$$renderer.push', empty_comment)) - : b.empty, - statement - ]), - optimiser.blockers(), - optimiser.has_await - ); - } else if (dynamic && custom_css_props.length === 0) { - context.state.template.push(empty_comment); - } - - context.state.template.push(statement); + context.state.template.push( + ...optimiser.render_block([ + dynamic && custom_css_props.length === 0 + ? b.stmt(b.call('$$renderer.push', empty_comment)) + : b.empty, + statement + ]) + ); - if ( - !is_async && - !context.state.skip_hydration_boundaries && - custom_css_props.length === 0 && - optimiser.expressions.length === 0 - ) { + if (!optimiser.is_async() && !context.state.is_standalone && custom_css_props.length === 0) { context.state.template.push(empty_comment); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index b02a935ed2..ee14a4d135 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -81,7 +81,19 @@ export function process_children(nodes, { visit, state }) { flush(); const expression = /** @type {Expression} */ (visit(node.expression)); - state.template.push(create_push(b.call('$.escape', expression), node.metadata.expression)); + + let call = b.call( + '$$renderer.push', + b.thunk(b.call('$.escape', expression), node.metadata.expression.has_await) + ); + + const blockers = node.metadata.expression.blockers(); + + if (blockers.elements.length > 0) { + call = b.call('$$renderer.async', blockers, b.arrow([b.id('$$renderer')], call)); + } + + state.template.push(b.stmt(call)); } else if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') { sequence.push(node); } else { @@ -262,72 +274,20 @@ export function build_getter(node, state) { } /** - * Creates a `$$renderer.child(...)` expression statement - * @param {BlockStatement | Expression} body - * @returns {Statement} - */ -export function create_child_block(body) { - return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, true))); -} - -/** - * Creates a `$$renderer.async(...)` expression statement - * @param {BlockStatement | Expression} body + * @param {Statement[]} statements * @param {ArrayExpression} blockers * @param {boolean} has_await - * @param {boolean} needs_hydration_markers */ -export function create_async_block( - body, - blockers = b.array([]), - has_await = true, - needs_hydration_markers = true -) { - return b.stmt( - b.call( - needs_hydration_markers ? '$$renderer.async_block' : '$$renderer.async', - blockers, - b.arrow([b.id('$$renderer')], body, has_await) - ) - ); -} - -/** - * @param {Expression} expression - * @param {ExpressionMetadata} metadata - * @param {boolean} needs_hydration_markers - * @returns {Expression | Statement} - */ -export function create_push(expression, metadata, needs_hydration_markers = false) { - if (metadata.is_async()) { - let statement = b.stmt(b.call('$$renderer.push', b.thunk(expression, metadata.has_await))); - - const blockers = metadata.blockers(); - - if (blockers.elements.length > 0) { - statement = create_async_block( - b.block([statement]), - blockers, - false, - needs_hydration_markers - ); - } - - return statement; +export function create_child_block(statements, blockers, has_await) { + if (blockers.elements.length === 0 && !has_await) { + return statements; } - return expression; -} + const fn = b.arrow([b.id('$$renderer')], b.block(statements), has_await); -/** - * @param {BlockStatement | Expression} body - * @param {Identifier | false} component_fn_id - * @returns {Statement} - */ -export function call_component_renderer(body, component_fn_id) { - return b.stmt( - b.call('$$renderer.component', b.arrow([b.id('$$renderer')], body, false), component_fn_id) - ); + return blockers.elements.length > 0 + ? [b.stmt(b.call('$$renderer.async_block', blockers, fn))] + : [b.stmt(b.call('$$renderer.child_block', fn))]; } /** @@ -373,7 +333,7 @@ export class PromiseOptimiser { } } - apply() { + #apply() { if (this.expressions.length === 0) { return b.empty; } @@ -403,4 +363,38 @@ export class PromiseOptimiser { is_async() { return this.expressions.length > 0 || this.#blockers.size > 0; } + + /** + * @param {Statement[]} statements + * @returns {Statement[]} + */ + render(statements) { + if (!this.is_async()) { + return statements; + } + + const fn = b.arrow( + [b.id('$$renderer')], + b.block([this.#apply(), ...statements]), + this.has_await + ); + + const blockers = this.blockers(); + + return blockers.elements.length > 0 + ? [b.stmt(b.call('$$renderer.async', blockers, fn))] + : [b.stmt(b.call('$$renderer.child', fn))]; + } + + /** + * @param {Statement[]} statements + * @returns {Statement[]} + */ + render_block(statements) { + if (!this.is_async()) { + return statements; + } + + return create_child_block([this.#apply(), ...statements], this.blockers(), this.has_await); + } } diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 49f4c1b7d2..62196350bf 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -171,6 +171,15 @@ export class Renderer { return promises; } + /** + * @param {(renderer: Renderer) => MaybePromise} fn + */ + child_block(fn) { + this.#out.push(BLOCK_OPEN); + this.child(fn); + this.#out.push(BLOCK_CLOSE); + } + /** * Create a child renderer. The child renderer inherits the state from the parent, * but has its own content. diff --git a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js index a726b903bc..03bbc5ba88 100644 --- a/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-const/_expected/server/index.svelte.js @@ -19,11 +19,7 @@ export default function Async_const($$renderer) { ]); $$renderer.push(`

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

`); } else { $$renderer.push(''); diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index 7249fd6e4f..b331875d04 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async_block([], async ($$renderer) => { + $$renderer.child_block(async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index 43fe9414eb..86948b4a3a 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async_block([], async ($$renderer) => { + $$renderer.child_block(async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index 1e7330429a..c69c038973 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async_block([], async ($$renderer) => { + $$renderer.child_block(async ($$renderer) => { if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 1ca24cf81a..1355ba34f0 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async_block([], async ($$renderer) => { + $$renderer.child_block(async ($$renderer) => { if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); 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 index eb99a2fe1d..cff5f2d569 100644 --- 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 @@ -6,10 +6,6 @@ export default function Async_top_level_inspect_server($$renderer) { var $$promises = $$renderer.run([async () => data = await Promise.resolve(42),,]); $$renderer.push(`

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

`); } \ No newline at end of file From bc449758695ec0b5cd34b4cdfa7aaf1ccbc5105b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Feb 2026 04:19:14 -0500 Subject: [PATCH 14/23] fix: properly hydrate already-resolved async blocks (alternative) (#17641) This is basically #17611, minus #17640, plus #17639. We need to add the $.next() call after render tags as well as components; rather than duplicating the logic, we can use is_standalone to determine when this is necessary (since this is what prevents $.append(...) from being used). Fixes #17261 Fixes #17608 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/poor-students-nail.md | 5 ++++ .../3-transform/client/transform-client.js | 1 + .../phases/3-transform/client/types.d.ts | 3 +++ .../3-transform/client/visitors/Fragment.js | 5 +++- .../3-transform/client/visitors/RenderTag.js | 4 ++++ .../client/visitors/shared/component.js | 24 ++++++++++++------- .../server/visitors/shared/component.js | 8 ++++++- .../Inner.svelte | 4 ++++ .../Outer.svelte | 4 ++++ .../Trigger.svelte | 7 ++++++ .../_config.js | 10 ++++++++ .../main.svelte | 16 +++++++++++++ .../Component.svelte | 5 ++++ .../async-each-item-duplication/_config.js | 14 +++++++++++ .../async-each-item-duplication/main.svelte | 10 ++++++++ 15 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 .changeset/poor-students-nail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte diff --git a/.changeset/poor-students-nail.md b/.changeset/poor-students-nail.md new file mode 100644 index 0000000000..cee650c002 --- /dev/null +++ b/.changeset/poor-students-nail.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: properly hydrate already-resolved async blocks 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 d5ce3caaa9..b50a73b8b6 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 @@ -166,6 +166,7 @@ export function client_component(analysis, options) { in_constructor: false, instance_level_snippets: [], module_level_snippets: [], + is_standalone: false, // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 4438ec015b..287bf24ac6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -83,6 +83,9 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly instance_level_snippets: VariableDeclaration[]; /** Snippets hoisted to the module */ readonly module_level_snippets: VariableDeclaration[]; + + /** True if the current node is a) a component or render tag and b) the sole child of a block */ + readonly is_standalone: boolean; } export type Context = import('zimmerframe').Context; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index f463111c4d..00b0cfaa2e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -122,7 +122,10 @@ export function Fragment(node, context) { close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (is_standalone) { // no need to create a template, we can just use the existing block's anchor - process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); + process_children(trimmed, () => b.id('$$anchor'), false, { + ...context, + state: { ...state, is_standalone } + }); } else { /** @type {(is_text: boolean) => Expression} */ const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index d14336bb7e..5d39cf2216 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -85,6 +85,10 @@ export function RenderTag(node, context) { ) ) ); + + if (context.state.is_standalone) { + context.state.init.push(b.stmt(b.call('$.next'))); + } } else { context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index bb72794af8..1d6d3413bf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -461,7 +461,7 @@ export function build_component(node, component_name, loc, context) { memoizer.check_blockers(node.metadata.expression); } - const statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)]; + let statements = [...snippet_declarations, ...memoizer.deriveds(context.state.analysis.runes)]; if (is_component_dynamic) { const prev = fn; @@ -515,15 +515,21 @@ export function build_component(node, component_name, loc, context) { const blockers = memoizer.blockers(); if (async_values || blockers) { - return b.stmt( - b.call( - '$.async', - anchor, - blockers, - async_values, - b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) + statements = [ + b.stmt( + b.call( + '$.async', + anchor, + blockers, + async_values, + b.arrow([b.id('$$anchor'), ...memoizer.async_ids()], b.block(statements)) + ) ) - ); + ]; + + if (context.state.is_standalone) { + statements.push(b.stmt(b.call('$.next'))); + } } return statements.length > 1 ? b.block(statements) : statements[0]; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index fe49a67b28..6a2c6eb0be 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -101,10 +101,16 @@ export function build_inline_component(node, expression, context) { } push_prop(b.prop('init', b.key(attribute.name), value)); - } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { + } else if (attribute.type === 'BindDirective') { // Bindings are a bit special: we don't want to add them to (async) deriveds but we need to check if they have blockers optimiser.check_blockers(attribute.metadata.expression); + if (attribute.name === 'this') { + // bind:this is client-only, but we still need to check for blockers to ensure + // the server generates matching hydration markers if the client wraps in $.async + continue; + } + if (attribute.expression.type === 'SequenceExpression') { const [get, set] = /** @type {SequenceExpression} */ (context.visit(attribute.expression)) .expressions; diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Inner.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte new file mode 100644 index 0000000000..99f885189b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Outer.svelte @@ -0,0 +1,4 @@ + +
{@render children?.()}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte new file mode 100644 index 0000000000..fc434d748e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/Trigger.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js new file mode 100644 index 0000000000..d77ba45ae4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/_config.js @@ -0,0 +1,10 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '
foo
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte new file mode 100644 index 0000000000..6cfc73ca25 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attach-hydration-mismatch/main.svelte @@ -0,0 +1,16 @@ + + + + + foo + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte new file mode 100644 index 0000000000..9f4e638629 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/Component.svelte @@ -0,0 +1,5 @@ + + +

{message}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js new file mode 100644 index 0000000000..2e20f83f7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + ssrHtml: `

item 1

item 2

item 3

`, + html: `

item 1

item 2

item 3

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

item 1

item 2

item 3

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte new file mode 100644 index 0000000000..ae54b63414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-each-item-duplication/main.svelte @@ -0,0 +1,10 @@ + + +{#each messages as message} + +{/each} From 6e5f2b157a02df4dd1f4e2ad3275655bc52cd023 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Feb 2026 10:54:15 -0500 Subject: [PATCH 15/23] fix: exit resolved async blocks on correct node when hydrating (#17640) * fix: exit resolved async blocks on correct node when hydrating * expand test + fix * tweak, add note to self --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/tiny-owls-pay.md | 5 +++++ .../src/internal/client/dom/blocks/async.js | 15 ++++++++++++++- .../samples/async-if-hydration/Child.svelte | 5 +++++ .../samples/async-if-hydration/_config.js | 11 +++++++++++ .../samples/async-if-hydration/main.svelte | 18 ++++++++++++++++++ 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/tiny-owls-pay.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte diff --git a/.changeset/tiny-owls-pay.md b/.changeset/tiny-owls-pay.md new file mode 100644 index 0000000000..ac25500258 --- /dev/null +++ b/.changeset/tiny-owls-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: exit resolved async blocks on correct node when hydrating diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 0e3ab33dda..e8c9cf0643 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -20,13 +20,27 @@ import { get_boundary } from './boundary.js'; */ export function async(node, blockers = [], expressions = [], fn) { var was_hydrating = hydrating; + var end = null; if (was_hydrating) { hydrate_next(); + end = skip_nodes(false); } if (expressions.length === 0 && blockers.every((b) => b.settled)) { fn(node); + + // This is necessary because it is not guaranteed that the render function will + // advance the hydration node to $.async's end marker: it may stop at an inner + // block's end marker (in case of an inner if block for example), but it also may + // stop at the correct $.async end marker (in case of component child) - hence + // we can't just use hydrate_next() + // TODO this feels indicative of a bug elsewhere; ideally we wouldn't need + // to double-traverse in the already-resolved case + if (was_hydrating) { + set_hydrate_node(end); + } + return; } @@ -39,7 +53,6 @@ export function async(node, blockers = [], expressions = [], fn) { if (was_hydrating) { var previous_hydrate_node = hydrate_node; - var end = skip_nodes(false); set_hydrate_node(end); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte new file mode 100644 index 0000000000..02ef294d99 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/Child.svelte @@ -0,0 +1,5 @@ + + +{b} diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js new file mode 100644 index 0000000000..8132e9c522 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, `

hello

true
`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte new file mode 100644 index 0000000000..3b08d41640 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-hydration/main.svelte @@ -0,0 +1,18 @@ + + +{#if a} +
+ {#if b} +

hello

+ {/if} +
+
+ +
+{/if} From 989492f0572cb6dd4beaf621dfbba0bcd409913c Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:01:41 +0100 Subject: [PATCH 16/23] fix: use "set up" (verb) instead of "setup" (noun) in comments (#17632) "Setup" is a noun/adjective, while "set up" is the verb form. --- documentation/docs/07-misc/02-testing.md | 2 +- packages/svelte/tests/signals/test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 85db7fc01f..c1bc69c1c2 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -181,7 +181,7 @@ export default defineConfig({ /* ... */ ], test: { - // If you are testing components client-side, you need to setup a DOM environment. + // If you are testing components client-side, you need to set up a DOM environment. // If not all your files should have this environment, you can use a // `// @vitest-environment jsdom` comment at the top of the test files instead. environment: 'jsdom' diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 23c4bb42f9..5486ccdb45 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -21,7 +21,7 @@ import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/inter /** * @param runes runes mode - * @param fn A function that returns a function because we first need to setup all the signals + * @param fn A function that returns a function because we first need to set up all the signals * and then execute the test in order to simulate a real component */ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { From 01db7b8c2a0dbc7eb1fb8f1eff116f2b5b5b3ba9 Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:03:14 +0100 Subject: [PATCH 17/23] fix: update migration guide to reflect current passive events (#17631) Remove `onwheel` and `onmousewheel` from the list of passive events in the v5 migration guide. Since #13322, only `ontouchstart` and `ontouchmove` are passive by default, matching the basic-markup docs. Closes #17430 --- documentation/docs/07-misc/07-v5-migration-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 40cbc3bd9e..5a80734b7a 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -778,9 +778,9 @@ In Svelte 4, doing the following triggered reactivity: This is because the Svelte compiler treated the assignment to `foo.value` as an instruction to update anything that referenced `foo`. In Svelte 5, reactivity is determined at runtime rather than compile time, so you should define `value` as a reactive `$state` field on the `Foo` class. Wrapping `new Foo()` with `$state(...)` will have no effect — only vanilla objects and arrays are made deeply reactive. -### Touch and wheel events are passive +### Touch events are passive -When using `onwheel`, `onmousewheel`, `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`. +When using `ontouchstart` and `ontouchmove` event attributes, the handlers are [passive](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners) to align with browser defaults. This greatly improves responsiveness by allowing the browser to scroll the document immediately, rather than waiting to see if the event handler calls `event.preventDefault()`. In the very rare cases that you need to prevent these event defaults, you should use [`on`](/docs/svelte/svelte-events#on) instead (for example inside an action). From 0c715afdab2c3077b32793a41a7ddccb3be65c64 Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:03:58 +0100 Subject: [PATCH 18/23] fix: correct spelling errors in test files (#17630) Fix "wich" -> "which", "aswell" -> "as well" in CSS global-block test, and "occurence" -> "occurrence" in sourcemaps test type definition. --- packages/svelte/tests/css/samples/global-block/expected.css | 4 ++-- packages/svelte/tests/css/samples/global-block/input.svelte | 4 ++-- packages/svelte/tests/sourcemaps/test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css index be1838fd98..ef68280093 100644 --- a/packages/svelte/tests/css/samples/global-block/expected.css +++ b/packages/svelte/tests/css/samples/global-block/expected.css @@ -39,7 +39,7 @@ /*}*/ } - /* ...wich is equivalent to `div :global { &.x { ...} }` ... */ + /* ...which is equivalent to `div :global { &.x { ...} }` ... */ div.svelte-xyz { &.x { color: green; @@ -51,7 +51,7 @@ color: green; } - /* ...and therefore `div { :global.x { ... }` aswell */ + /* ...and therefore `div { :global.x { ... }` as well */ div.svelte-xyz { &.x { color: green; diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte index 86d438031a..1a9993bfe0 100644 --- a/packages/svelte/tests/css/samples/global-block/input.svelte +++ b/packages/svelte/tests/css/samples/global-block/input.svelte @@ -41,7 +41,7 @@ } } - /* ...wich is equivalent to `div :global { &.x { ...} }` ... */ + /* ...which is equivalent to `div :global { &.x { ...} }` ... */ div :global { &.x { color: green; @@ -53,7 +53,7 @@ color: green; } - /* ...and therefore `div { :global.x { ... }` aswell */ + /* ...and therefore `div { :global.x { ... }` as well */ div { :global.x { color: green; diff --git a/packages/svelte/tests/sourcemaps/test.ts b/packages/svelte/tests/sourcemaps/test.ts index 0ac0e6f905..6749483079 100644 --- a/packages/svelte/tests/sourcemaps/test.ts +++ b/packages/svelte/tests/sourcemaps/test.ts @@ -8,9 +8,9 @@ import { decode } from '@jridgewell/sourcemap-codec'; type SourceMapEntry = | string | { - /** If not the first occurence, but the nth should be found */ + /** If not the first occurrence, but the nth should be found */ idxOriginal?: number; - /** If not the first occurence, but the nth should be found */ + /** If not the first occurrence, but the nth should be found */ idxGenerated?: number; /** The original string to find */ str: string; From 4453e4895d407df6895f481257b675a43ed702c6 Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:04:42 +0100 Subject: [PATCH 19/23] fix: fix spelling errors in media-query and test file (#17628) - media-query.js: parentehesis -> parenthesis - error-boundary-12/main.svelte: occured -> occurred --- packages/svelte/src/reactivity/media-query.js | 2 +- .../tests/runtime-runes/samples/error-boundary-12/main.svelte | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/reactivity/media-query.js b/packages/svelte/src/reactivity/media-query.js index d286709719..6d1d084298 100644 --- a/packages/svelte/src/reactivity/media-query.js +++ b/packages/svelte/src/reactivity/media-query.js @@ -7,7 +7,7 @@ const parenthesis_regex = /\(.+\)/; // // eg: new MediaQuery('screen') // -// however because of the auto-parenthesis logic in the constructor since there's no parentehesis +// however because of the auto-parenthesis logic in the constructor since there's no parenthesis // in the media query they'll be surrounded by parenthesis // // however we can check if the media query is only composed of these keywords diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte index d9dee1e2b0..748acab91e 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-12/main.svelte @@ -15,6 +15,6 @@ {d} {#snippet failed()} -

Error occured

+

Error occurred

{/snippet}
From f71a6813b7bf9c53b1dce0b5f3a1232f55b1f26a Mon Sep 17 00:00:00 2001 From: Artyom Alekseevich <47069814+FrankFMY@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:13:54 +0100 Subject: [PATCH 20/23] fix: treat menu element like ul/ol for a11y role checks (#17638) * treat menu element like ul/ol for a11y role checks The element has the same implicit role (list) as
    and
      , so it should receive the same treatment in a11y checks: - Allow without redundant role warning (CSS list-style:none can remove semantics, role restores them) - Allow with interactive roles like menu, menubar, radiogroup, tablist, tree, treegrid (same exceptions as ul/ol) Fixes #8529 * changeset --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/fix-menu-element-a11y-roles.md | 5 +++++ .../2-analyze/visitors/shared/a11y/constants.js | 1 + .../phases/2-analyze/visitors/shared/a11y/index.js | 2 +- .../input.svelte | 7 +++++++ .../samples/a11y-no-redundant-roles/input.svelte | 4 ++-- .../samples/a11y-no-redundant-roles/warnings.json | 12 ------------ 6 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 .changeset/fix-menu-element-a11y-roles.md diff --git a/.changeset/fix-menu-element-a11y-roles.md b/.changeset/fix-menu-element-a11y-roles.md new file mode 100644 index 0000000000..75cfe8d310 --- /dev/null +++ b/.changeset/fix-menu-element-a11y-roles.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat `` like `
        `/`
          ` for a11y role checks diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js index 684ea92094..49099c2cb1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/constants.js @@ -174,6 +174,7 @@ export const input_type_to_implicit_role = new Map([ export const a11y_non_interactive_element_to_interactive_role_exceptions = { ul: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'], ol: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'], + menu: ['listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid'], li: ['menuitem', 'option', 'row', 'tab', 'treeitem'], table: ['grid'], td: ['gridcell'], diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js index f5f4982ed2..45de8b10a1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js @@ -167,7 +167,7 @@ export function check_element(node, context) { if ( current_role === get_implicit_role(node.name, attribute_map) && //
            is ok because CSS list-style:none removes the semantics and this is a way to bring them back - !['ul', 'ol', 'li'].includes(node.name) && + !['ul', 'ol', 'li', 'menu'].includes(node.name) && // is ok because without href the a tag doesn't have a role of link !(node.name === 'a' && !attribute_map.has('href')) ) { diff --git a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte index e5db8719b0..edfa3eeada 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-noninteractive-element-to-interactive-role/input.svelte @@ -69,6 +69,13 @@
          • + + + + + + +
            diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte index 2ccd765aa2..9e06ebe420 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte @@ -22,7 +22,7 @@
            - +
            @@ -44,5 +44,5 @@
            - + diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json index 861f000aef..e041037d71 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json @@ -275,18 +275,6 @@ "line": 24 } }, - { - "code": "a11y_no_redundant_roles", - "end": { - "column": 17, - "line": 25 - }, - "message": "Redundant role 'list'", - "start": { - "column": 6, - "line": 25 - } - }, { "code": "a11y_no_redundant_roles", "end": { From bd7b8aa10807be8884f5e7224340f8d980d971ec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Feb 2026 11:37:25 -0500 Subject: [PATCH 21/23] chore: fix broken css test (#17644) --- packages/svelte/tests/css/samples/global-block/_config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js index 18a56e9a97..84b4989f76 100644 --- a/packages/svelte/tests/css/samples/global-block/_config.js +++ b/packages/svelte/tests/css/samples/global-block/_config.js @@ -9,12 +9,12 @@ export default test({ start: { line: 73, column: 1, - character: 964 + character: 966 }, end: { line: 73, column: 16, - character: 979 + character: 981 } }, { @@ -23,12 +23,12 @@ export default test({ start: { line: 104, column: 29, - character: 1270 + character: 1272 }, end: { line: 104, column: 43, - character: 1284 + character: 1286 } } ] From 57cb393796841538c1b38ef99dd2be48815604a5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 6 Feb 2026 11:57:49 -0500 Subject: [PATCH 22/23] fix: allow NaN in key blocks (#17642) * fix: allow NaN in key blocks * lol whoops * Update packages/svelte/src/internal/client/dom/blocks/key.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/clear-olives-share.md | 5 +++++ .../src/internal/client/dom/blocks/key.js | 7 +++++++ .../runtime-runes/samples/key-nan/_config.js | 17 +++++++++++++++++ .../runtime-runes/samples/key-nan/main.svelte | 10 ++++++++++ 4 files changed, 39 insertions(+) create mode 100644 .changeset/clear-olives-share.md create mode 100644 packages/svelte/tests/runtime-runes/samples/key-nan/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte diff --git a/.changeset/clear-olives-share.md b/.changeset/clear-olives-share.md new file mode 100644 index 0000000000..975b2b21b6 --- /dev/null +++ b/.changeset/clear-olives-share.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow `{#key NaN}` diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 849b1c2447..dd40d8a1b8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -4,6 +4,8 @@ import { block } from '../../reactivity/effects.js'; import { hydrate_next, hydrating } from '../hydration.js'; import { BranchManager } from './branches.js'; +const NAN = Symbol('NaN'); + /** * @template V * @param {TemplateNode} node @@ -23,6 +25,11 @@ export function key(node, get_key, render_fn) { block(() => { var key = get_key(); + // NaN !== NaN, hence we do this workaround to not trigger remounts unnecessarily + if (key !== key) { + key = /** @type {any} */ (NAN); + } + // key blocks in Svelte <5 had stupid semantics if (legacy && key !== null && typeof key === 'object') { key = /** @type {V} */ ({}); diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js new file mode 100644 index 0000000000..bd0dd06761 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/key-nan/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: '

            it rendered

            ', + + test({ assert, target, logs }) { + assert.deepEqual(logs, ['rendering']); + + const btn = target.querySelector('button'); + flushSync(() => btn?.click()); + + // should not re-render + assert.deepEqual(logs, ['rendering']); + assert.htmlEqual(target.innerHTML, '

            it rendered

            '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte new file mode 100644 index 0000000000..24f7f2d179 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/key-nan/main.svelte @@ -0,0 +1,10 @@ + + + + +{#key x} + {console.log('rendering')} +

            it rendered

            +{/key} From 863c9d6875470ad7699614fe278c0d99903d30f8 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Fri, 6 Feb 2026 10:27:13 -0800 Subject: [PATCH 23/23] chore: wrap JSDoc URLs in @see and @link tags (#17617) * docs: wrap JSDoc URLs in @see and @link tags * fix: move curly brace to end of URL * chore: add changeset * add link text * regenerate --------- Co-authored-by: Rich Harris --- .changeset/loud-bottles-own.md | 5 ++++ packages/svelte/src/ambient.d.ts | 28 +++++++++---------- .../src/compiler/phases/1-parse/read/style.js | 2 +- .../client/dom/elements/bindings/size.js | 5 ++-- .../samples/transition-component/_config.js | 2 +- packages/svelte/types/index.d.ts | 28 +++++++++---------- 6 files changed, 37 insertions(+), 33 deletions(-) create mode 100644 .changeset/loud-bottles-own.md diff --git a/.changeset/loud-bottles-own.md b/.changeset/loud-bottles-own.md new file mode 100644 index 0000000000..493dc6c47f --- /dev/null +++ b/.changeset/loud-bottles-own.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: wrap JSDoc URLs in `@see` and `@link` tags diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index ec5b799470..159a568477 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -16,7 +16,7 @@ declare module '*.svelte' { * let count = $state(0); * ``` * - * https://svelte.dev/docs/svelte/$state + * @see {@link https://svelte.dev/docs/svelte/$state Documentation} * * @param initial The initial value */ @@ -126,7 +126,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.raw + * @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation} * * @param initial The initial value */ @@ -147,7 +147,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.snapshot + * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation} * * @param state The value to snapshot */ @@ -187,7 +187,7 @@ declare namespace $state { * let double = $derived(count * 2); * ``` * - * https://svelte.dev/docs/svelte/$derived + * @see {@link https://svelte.dev/docs/svelte/$derived Documentation} * * @param expression The derived state expression */ @@ -209,7 +209,7 @@ declare namespace $derived { * }); * ``` * - * https://svelte.dev/docs/svelte/$derived#$derived.by + * @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation} */ export function by(fn: () => T): T; @@ -251,7 +251,7 @@ declare namespace $derived { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect + * @see {@link https://svelte.dev/docs/svelte/$effect Documentation} * @param fn The function to execute */ declare function $effect(fn: () => void | (() => void)): void; @@ -270,7 +270,7 @@ declare namespace $effect { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect#$effect.pre + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation} * @param fn The function to execute */ export function pre(fn: () => void | (() => void)): void; @@ -278,7 +278,7 @@ declare namespace $effect { /** * Returns the number of promises that are pending in the current boundary, not including child boundaries. * - * https://svelte.dev/docs/svelte/$effect#$effect.pending + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation} */ export function pending(): number; @@ -300,7 +300,7 @@ declare namespace $effect { * * This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. * - * https://svelte.dev/docs/svelte/$effect#$effect.tracking + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation} */ export function tracking(): boolean; @@ -328,7 +328,7 @@ declare namespace $effect { * * ``` * - * https://svelte.dev/docs/svelte/$effect#$effect.root + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation} */ export function root(fn: () => void | (() => void)): () => void; @@ -364,7 +364,7 @@ declare namespace $effect { * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$props + * @see {@link https://svelte.dev/docs/svelte/$props Documentation} */ declare function $props(): any; @@ -410,7 +410,7 @@ declare namespace $props { * let { propName = $bindable() }: { propName: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$bindable + * @see {@link https://svelte.dev/docs/svelte/$bindable Documentation} */ declare function $bindable(fallback?: T): T; @@ -456,7 +456,7 @@ declare namespace $bindable { * $inspect(x, y).with(() => { debugger; }); * ``` * - * https://svelte.dev/docs/svelte/$inspect + * @see {@link https://svelte.dev/docs/svelte/$inspect Documentation} */ declare function $inspect( ...values: T @@ -522,7 +522,7 @@ declare namespace $inspect { * * Only available inside custom element components, and only on the client-side. * - * https://svelte.dev/docs/svelte/$host + * @see {@link https://svelte.dev/docs/svelte/$host Documentation} */ declare function $host(): El; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 55647b2c94..4f2db349cd 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -568,7 +568,7 @@ function read_attribute_value(parser) { } /** - * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + * @see {@link https://www.w3.org/TR/css-syntax-3/#ident-token-diagram CSS Syntax Module Level 3} * @param {Parser} parser */ function read_identifier(parser) { diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/size.js b/packages/svelte/src/internal/client/dom/elements/bindings/size.js index a76c70aab1..016ee5a547 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/size.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/size.js @@ -2,9 +2,8 @@ import { effect, teardown } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; /** - * Resize observer singleton. - * One listener per element only! - * https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ + * We create one listener for all elements + * @see {@link https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ Explanation} */ class ResizeObserverSingleton { /** */ diff --git a/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js b/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js index 414a8b0cc2..1bdf3a5a2e 100644 --- a/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/transition-component/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; /** * $.component() should not break transition - * https://github.com/sveltejs/svelte/issues/13645 + * @see {@link https://github.com/sveltejs/svelte/issues/13645} */ export default test({ test({ assert, raf, target }) { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 730e0ff655..62c0e210be 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3188,7 +3188,7 @@ declare module 'svelte/types/compiler/interfaces' { * let count = $state(0); * ``` * - * https://svelte.dev/docs/svelte/$state + * @see {@link https://svelte.dev/docs/svelte/$state Documentation} * * @param initial The initial value */ @@ -3298,7 +3298,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.raw + * @see {@link https://svelte.dev/docs/svelte/$state#$state.raw Documentation} * * @param initial The initial value */ @@ -3319,7 +3319,7 @@ declare namespace $state { * * ``` * - * https://svelte.dev/docs/svelte/$state#$state.snapshot + * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation} * * @param state The value to snapshot */ @@ -3359,7 +3359,7 @@ declare namespace $state { * let double = $derived(count * 2); * ``` * - * https://svelte.dev/docs/svelte/$derived + * @see {@link https://svelte.dev/docs/svelte/$derived Documentation} * * @param expression The derived state expression */ @@ -3381,7 +3381,7 @@ declare namespace $derived { * }); * ``` * - * https://svelte.dev/docs/svelte/$derived#$derived.by + * @see {@link https://svelte.dev/docs/svelte/$derived#$derived.by Documentation} */ export function by(fn: () => T): T; @@ -3423,7 +3423,7 @@ declare namespace $derived { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect + * @see {@link https://svelte.dev/docs/svelte/$effect Documentation} * @param fn The function to execute */ declare function $effect(fn: () => void | (() => void)): void; @@ -3442,7 +3442,7 @@ declare namespace $effect { * * Does not run during server-side rendering. * - * https://svelte.dev/docs/svelte/$effect#$effect.pre + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pre Documentation} * @param fn The function to execute */ export function pre(fn: () => void | (() => void)): void; @@ -3450,7 +3450,7 @@ declare namespace $effect { /** * Returns the number of promises that are pending in the current boundary, not including child boundaries. * - * https://svelte.dev/docs/svelte/$effect#$effect.pending + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.pending Documentation} */ export function pending(): number; @@ -3472,7 +3472,7 @@ declare namespace $effect { * * This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. * - * https://svelte.dev/docs/svelte/$effect#$effect.tracking + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.tracking Documentation} */ export function tracking(): boolean; @@ -3500,7 +3500,7 @@ declare namespace $effect { * * ``` * - * https://svelte.dev/docs/svelte/$effect#$effect.root + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.root Documentation} */ export function root(fn: () => void | (() => void)): () => void; @@ -3536,7 +3536,7 @@ declare namespace $effect { * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$props + * @see {@link https://svelte.dev/docs/svelte/$props Documentation} */ declare function $props(): any; @@ -3582,7 +3582,7 @@ declare namespace $props { * let { propName = $bindable() }: { propName: boolean } = $props(); * ``` * - * https://svelte.dev/docs/svelte/$bindable + * @see {@link https://svelte.dev/docs/svelte/$bindable Documentation} */ declare function $bindable(fallback?: T): T; @@ -3628,7 +3628,7 @@ declare namespace $bindable { * $inspect(x, y).with(() => { debugger; }); * ``` * - * https://svelte.dev/docs/svelte/$inspect + * @see {@link https://svelte.dev/docs/svelte/$inspect Documentation} */ declare function $inspect( ...values: T @@ -3694,7 +3694,7 @@ declare namespace $inspect { * * Only available inside custom element components, and only on the client-side. * - * https://svelte.dev/docs/svelte/$host + * @see {@link https://svelte.dev/docs/svelte/$host Documentation} */ declare function $host(): El;