From e4ae9e9f1dd23d47eb056f4be3a5301de5bce5dd Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 10 Oct 2025 17:20:13 +0200 Subject: [PATCH 01/20] runtime-first approach --- .../phases/3-transform/client/types.d.ts | 2 - .../client/visitors/CallExpression.js | 18 +++------ .../3-transform/client/visitors/Fragment.js | 5 --- .../client/visitors/RegularElement.js | 7 +--- .../svelte/src/compiler/types/template.d.ts | 1 - .../internal/client/dom/blocks/boundary.js | 39 +++++++++---------- .../src/internal/client/reactivity/batch.js | 14 +++++++ .../svelte/src/internal/client/runtime.js | 30 ++++++++++++-- 8 files changed, 65 insertions(+), 51 deletions(-) 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 248158992278..932d35367162 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 @@ -24,8 +24,6 @@ export interface ClientTransformState extends TransformState { /** `true` if we're transforming the contents of ` + + + +{count} | {x} From e8330ee7bfff1eef61d5990e99c75dc6e225b5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20R=C3=BCger?= Date: Tue, 14 Oct 2025 15:56:41 +0200 Subject: [PATCH 10/20] fix: svg `radialGradient` `fr` attribute missing in types (#16943) * fix(svg radialGradient): fr attribute missing in types * chore: add changeset --- .changeset/grumpy-towns-stop.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/grumpy-towns-stop.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md new file mode 100644 index 000000000000..2b146818f5ed --- /dev/null +++ b/.changeset/grumpy-towns-stop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index b0c2fae2de69..17ff10072998 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1658,6 +1658,7 @@ export interface SVGAttributes extends AriaAttributes, DO 'font-variant'?: number | string | undefined | null; 'font-weight'?: number | string | undefined | null; format?: number | string | undefined | null; + fr?: number | string | undefined | null; from?: number | string | undefined | null; fx?: number | string | undefined | null; fy?: number | string | undefined | null; From 2a951391dc31326fd5df14bf1bfcf425bd13d5ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 09:59:09 -0400 Subject: [PATCH 11/20] Version Packages (#16940) * Version Packages * Update packages/svelte/CHANGELOG.md --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/grumpy-towns-stop.md | 5 ----- .changeset/major-beans-fry.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/grumpy-towns-stop.md delete mode 100644 .changeset/major-beans-fry.md diff --git a/.changeset/grumpy-towns-stop.md b/.changeset/grumpy-towns-stop.md deleted file mode 100644 index 2b146818f5ed..000000000000 --- a/.changeset/grumpy-towns-stop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -add missing type for `fr` attribute for `radialGradient` tags in svg diff --git a/.changeset/major-beans-fry.md b/.changeset/major-beans-fry.md deleted file mode 100644 index 8f35683cd623..000000000000 --- a/.changeset/major-beans-fry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: unset context on stale promises diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 70f549ce2964..b3af39eb4cb6 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.39.13 + +### Patch Changes + +- fix: add missing type for `fr` attribute for `radialGradient` tags in svg ([#16943](https://github.com/sveltejs/svelte/pull/16943)) + +- fix: unset context on stale promises ([#16935](https://github.com/sveltejs/svelte/pull/16935)) + ## 5.39.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a2d5a6e40148..55b44fb2b65c 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.39.12", + "version": "5.39.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e520d1248a49..536a2260c9e2 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.39.12'; +export const VERSION = '5.39.13'; export const PUBLIC_VERSION = '5'; From a7c958a2a5a89e1d22fa530e2bacd2b0e3ba7ba6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:12:07 -0400 Subject: [PATCH 12/20] chore: simplify `batch.apply()` (#16945) * chore: simplify `batch.apply()` * belt and braces * note to self --- .changeset/pretty-llamas-explode.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 +++++++------------ .../internal/client/reactivity/deriveds.js | 8 ++- .../svelte/src/internal/client/runtime.js | 10 ++- 4 files changed, 41 insertions(+), 49 deletions(-) create mode 100644 .changeset/pretty-llamas-explode.md diff --git a/.changeset/pretty-llamas-explode.md b/.changeset/pretty-llamas-explode.md new file mode 100644 index 000000000000..00109112de60 --- /dev/null +++ b/.changeset/pretty-llamas-explode.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify `batch.apply()` diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 102d0670b664..0dc149260a82 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -44,12 +44,12 @@ export let current_batch = null; export let previous_batch = null; /** - * When time travelling, we re-evaluate deriveds based on the temporary - * values of their dependencies rather than their actual values, and cache - * the results in this map rather than on the deriveds themselves - * @type {Map | null} + * When time travelling (i.e. working in one batch, while other batches + * still have ongoing work), we ignore the real values of affected + * signals in favour of their values within the batch + * @type {Map | null} */ -export let batch_deriveds = null; +export let batch_values = null; /** @type {Set<() => void>} */ export let effect_pending_updates = new Set(); @@ -152,7 +152,7 @@ export class Batch { previous_batch = null; - var revert = Batch.apply(this); + this.apply(); for (const root of root_effects) { this.#traverse_effect_tree(root); @@ -161,6 +161,10 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit if (this.#pending === 0) { + // TODO we need this because we commit _then_ flush effects... + // maybe there's a way we can reverse the order? + var previous_batch_sources = batch_values; + this.#commit(); var render_effects = this.#render_effects; @@ -175,6 +179,7 @@ export class Batch { previous_batch = this; current_batch = null; + batch_values = previous_batch_sources; flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -187,7 +192,7 @@ export class Batch { this.#defer_effects(this.#block_effects); } - revert(); + batch_values = null; for (const effect of this.#boundary_async_effects) { update_effect(effect); @@ -274,6 +279,7 @@ export class Batch { } this.current.set(source, source.v); + batch_values?.set(source, source.v); } activate() { @@ -282,6 +288,7 @@ export class Batch { deactivate() { current_batch = null; + batch_values = null; } flush() { @@ -352,14 +359,14 @@ export class Batch { if (queued_root_effects.length > 0) { current_batch = batch; - const revert = Batch.apply(batch); + batch.apply(); for (const root of queued_root_effects) { batch.#traverse_effect_tree(root); } queued_root_effects = []; - revert(); + batch.deactivate(); } } @@ -423,49 +430,23 @@ export class Batch { queue_micro_task(task); } - /** - * @param {Batch} current_batch - */ - static apply(current_batch) { - if (!async_mode_flag || batches.size === 1) { - return noop; - } + apply() { + if (!async_mode_flag || batches.size === 1) return; // if there are multiple batches, we are 'time travelling' — - // we need to undo the changes belonging to any batch - // other than the current one - - /** @type {Map} */ - var current_values = new Map(); - batch_deriveds = new Map(); - - for (const [source, current] of current_batch.current) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = current; - } + // we need to override values with the ones in this batch... + batch_values = new Map(this.current); + // ...and undo changes belonging to other batches for (const batch of batches) { - if (batch === current_batch) continue; + if (batch === this) continue; for (const [source, previous] of batch.#previous) { - if (!current_values.has(source)) { - current_values.set(source, { v: source.v, wv: source.wv }); - source.v = previous; + if (!batch_values.has(source)) { + batch_values.set(source, previous); } } } - - return () => { - for (const [source, { v, wv }] of current_values) { - // reset the source to the current value (unless - // it got a newer value as a result of effects running) - if (source.wv <= wv) { - source.v = v; - } - } - - batch_deriveds = null; - }; } } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 076a91923680..bf8733cfe508 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -33,7 +33,7 @@ import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_deriveds, current_batch } from './batch.js'; +import { batch_values, current_batch } from './batch.js'; import { unset_context } from './async.js'; import { deferred } from '../../shared/utils.js'; @@ -336,6 +336,8 @@ export function update_derived(derived) { var value = execute_derived(derived); if (!derived.equals(value)) { + // TODO can we avoid setting `derived.v` when `batch_values !== null`, + // without causing the value to be stale later? derived.v = value; derived.wv = increment_write_version(); } @@ -346,8 +348,8 @@ export function update_derived(derived) { return; } - if (batch_deriveds !== null) { - batch_deriveds.set(derived, derived.v); + if (batch_values !== null) { + batch_values.set(derived, derived.v); } else { var status = (skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null ? MAYBE_DIRTY : CLEAN; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b8f5f5ffc999..a146659bf688 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -42,7 +42,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; +import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -671,8 +671,8 @@ export function get(signal) { } else if (is_derived) { derived = /** @type {Derived} */ (signal); - if (batch_deriveds?.has(derived)) { - return batch_deriveds.get(derived); + if (batch_values?.has(derived)) { + return batch_values.get(derived); } if (is_dirty(derived)) { @@ -680,6 +680,10 @@ export function get(signal) { } } + if (batch_values?.has(signal)) { + return batch_values.get(signal); + } + if ((signal.f & ERROR_VALUE) !== 0) { throw signal.v; } From d50701d277e5c57204f1e51314926b0d73b0cbf9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 11:36:55 -0400 Subject: [PATCH 13/20] unused --- packages/svelte/src/internal/client/reactivity/batch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 0dc149260a82..2edfc1343a50 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,7 +14,7 @@ import { DERIVED } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; -import { deferred, define_property, noop } from '../../shared/utils.js'; +import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, is_dirty, From 28765f846e956c0da0e41e634de8c1066da9e6d5 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:59:02 +0200 Subject: [PATCH 14/20] fix: don't rerun async effects unnecessarily (#16944) Since #16866, when an async effect runs multiple times, we rebase older batches and rerun those effects. This can have unintended consequences: In a case where an async effect only depends on a single source, and that single source was updated in a later batch, we know that we don't need to / should not rerun the older batch. This PR makes it so: We collect all the sources of older batches that are not part of the current batch that just committed, and then only mark those async effects as dirty which depend on one of those other sources. Fixes the bug I noticed while working on #16935 --- .changeset/wild-mirrors-take.md | 5 ++ .../src/internal/client/reactivity/batch.js | 67 ++++++++++++++----- .../internal/client/reactivity/deriveds.js | 7 ++ .../samples/async-resolve-stale/_config.js | 3 +- 4 files changed, 65 insertions(+), 17 deletions(-) create mode 100644 .changeset/wild-mirrors-take.md diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md new file mode 100644 index 000000000000..faf28e7695c5 --- /dev/null +++ b/.changeset/wild-mirrors-take.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't rerun async effects unnecessarily diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2edfc1343a50..2956e7ed6afe 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,4 +1,4 @@ -/** @import { Derived, Effect, Source, Value } from '#client' */ +/** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ import { BLOCK_EFFECT, BRANCH_EFFECT, @@ -342,31 +342,46 @@ export class Batch { continue; } + /** @type {Source[]} */ + const sources = []; + for (const [source, value] of this.current) { if (batch.current.has(source)) { - if (is_earlier) { + if (is_earlier && value !== batch.current.get(source)) { // bring the value up to date batch.current.set(source, value); } else { - // later batch has more recent value, + // same value or later batch has more recent value, // no need to re-run these effects continue; } } - mark_effects(source); + sources.push(source); } - if (queued_root_effects.length > 0) { - current_batch = batch; - batch.apply(); + if (sources.length === 0) { + continue; + } - for (const root of queued_root_effects) { - batch.#traverse_effect_tree(root); + // Re-run async/block effects that depend on distinct values changed in both batches + const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); + if (others.length > 0) { + for (const source of sources) { + mark_effects(source, others); } - queued_root_effects = []; - batch.deactivate(); + if (queued_root_effects.length > 0) { + current_batch = batch; + batch.apply(); + + for (const root of queued_root_effects) { + batch.#traverse_effect_tree(root); + } + + queued_root_effects = []; + batch.deactivate(); + } } } @@ -621,17 +636,19 @@ function flush_queued_effects(effects) { /** * This is similar to `mark_reactions`, but it only marks async/block effects - * so that these can re-run after another batch has been committed + * depending on `value` and at least one of the other `sources`, so that + * these effects can re-run after another batch has been committed * @param {Value} value + * @param {Source[]} sources */ -function mark_effects(value) { +function mark_effects(value, sources) { if (value.reactions !== null) { for (const reaction of value.reactions) { const flags = reaction.f; if ((flags & DERIVED) !== 0) { - mark_effects(/** @type {Derived} */ (reaction)); - } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0) { + mark_effects(/** @type {Derived} */ (reaction), sources); + } else if ((flags & (ASYNC | BLOCK_EFFECT)) !== 0 && depends_on(reaction, sources)) { set_signal_status(reaction, DIRTY); schedule_effect(/** @type {Effect} */ (reaction)); } @@ -639,6 +656,26 @@ function mark_effects(value) { } } +/** + * @param {Reaction} reaction + * @param {Source[]} sources + */ +function depends_on(reaction, sources) { + if (reaction.deps !== null) { + for (const dep of reaction.deps) { + if (sources.includes(dep)) { + return true; + } + + if ((dep.f & DERIVED) !== 0 && depends_on(/** @type {Derived} */ (dep), sources)) { + return true; + } + } + } + + return false; +} + /** * @param {Effect} signal * @returns {void} diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index bf8733cfe508..fa780013e15b 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -171,6 +171,13 @@ export function async_derived(fn, location) { internal_set(signal, value); + // All prior async derived runs are now stale + for (const [b, d] of deferreds) { + deferreds.delete(b); + if (b === batch) break; + d.reject(STALE_REACTION); + } + if (DEV && location !== undefined) { recent_async_deriveds.add(signal); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js index bccf12562ad3..50bb414afc8b 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -20,7 +20,6 @@ export default test({ input.value = '12'; input.dispatchEvent(new Event('input', { bubbles: true })); await macrotask(6); - // TODO this is wrong (separate bug), this should be 3 | 12 - assert.htmlEqual(target.innerHTML, ' 5 | 12'); + assert.htmlEqual(target.innerHTML, ' 3 | 12'); } }); From 99711d582263ce3dc0103baecf579e995363db86 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:25:52 +0200 Subject: [PATCH 15/20] fix: ensure map iteration order is correct (#16947) quick follow-up to #16944 Resetting a map entry does not change its position in the map when iterating. We need to make sure that reset makes that batch jump "to the front" for the "reject all stale batches" logic below. Edge case for which I can't come up with a test case but it _is_ a possibility. --- packages/svelte/src/internal/client/reactivity/deriveds.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fa780013e15b..6aa9a1d9d920 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -144,6 +144,7 @@ export function async_derived(fn, location) { batch.increment(); deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below deferreds.set(batch, d); } } From f3c55e8e6c26bc8c006c84a656c845584abf3eb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 15:48:29 -0400 Subject: [PATCH 16/20] feat: add `createContext` utility for type-safe context (#16948) * feat: add `createContext` utility for type-safe context * regenerate --- .changeset/neat-melons-cheer.md | 5 +++++ .../98-reference/.generated/shared-errors.md | 8 ++++++++ .../svelte/messages/shared-errors/errors.md | 6 ++++++ packages/svelte/src/index-client.js | 8 +++++++- packages/svelte/src/index-server.js | 8 +++++++- .../svelte/src/internal/client/context.js | 20 +++++++++++++++++++ .../svelte/src/internal/server/context.js | 9 +++++++++ packages/svelte/src/internal/shared/errors.js | 16 +++++++++++++++ .../samples/create-context/Child.svelte | 7 +++++++ .../samples/create-context/_config.js | 5 +++++ .../samples/create-context/main.svelte | 16 +++++++++++++++ packages/svelte/types/index.d.ts | 4 ++++ 12 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 .changeset/neat-melons-cheer.md create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/create-context/main.svelte diff --git a/.changeset/neat-melons-cheer.md b/.changeset/neat-melons-cheer.md new file mode 100644 index 000000000000..1107d7ef20a0 --- /dev/null +++ b/.changeset/neat-melons-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `createContext` utility for type-safe context diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index 6c31aaafd0df..07e13dea459b 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -60,6 +60,14 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +### missing_context + +``` +Context was not set in a parent component +``` + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ### snippet_without_render_tag ``` diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 4b4d3322028d..e3959034a3c3 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -52,6 +52,12 @@ Certain lifecycle methods can only be used during component initialisation. To f ``` +## missing_context + +> Context was not set in a parent component + +The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in a parent component. + ## snippet_without_render_tag > Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 85eeab7de989..337cbb500b39 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -242,7 +242,13 @@ function init_update_callbacks(context) { } export { flushSync } from './internal/client/reactivity/batch.js'; -export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; +export { + createContext, + getContext, + getAllContexts, + hasContext, + setContext +} from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack, settled } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index f193c4689474..223ce6a4cde1 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -39,6 +39,12 @@ export async function settled() {} export { getAbortSignal } from './internal/server/abort-signal.js'; -export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; +export { + createContext, + getAllContexts, + getContext, + hasContext, + setContext +} from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index cad75546d4b4..ea63072a377f 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -69,6 +69,26 @@ export function set_dev_current_component_function(fn) { dev_current_component_function = fn; } +/** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + + return [ + () => { + if (!hasContext(key)) { + e.missing_context(); + } + + return getContext(key); + }, + (context) => setContext(key, context) + ]; +} + /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index c59b2d260afb..1813bfbf7848 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -10,6 +10,15 @@ export function set_ssr_context(v) { ssr_context = v; } +/** + * @template T + * @returns {[() => T, (context: T) => T]} + */ +export function createContext() { + const key = {}; + return [() => getContext(key), (context) => setContext(key, context)]; +} + /** * @template T * @param {any} key diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 6bcc35016a70..669cdd96a7f3 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -51,6 +51,22 @@ export function lifecycle_outside_component(name) { } } +/** + * Context was not set in a parent component + * @returns {never} + */ +export function missing_context() { + if (DEV) { + const error = new Error(`missing_context\nContext was not set in a parent component\nhttps://svelte.dev/e/missing_context`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/missing_context`); + } +} + /** * Attempted to render a snippet without a `{@render}` block. This would cause the snippet code to be stringified instead of its content being rendered to the DOM. To fix this, change `{snippet}` to `{@render snippet()}`. * @returns {never} diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte new file mode 100644 index 000000000000..3e39d5043eff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte @@ -0,0 +1,7 @@ + + +

{message}

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

hello

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte new file mode 100644 index 000000000000..8d3c50ba5539 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 6dc6629faad5..58e3285e4ad1 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -448,6 +448,10 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * */ + export function createContext(): [() => T, (context: T) => T]; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. From 005895d9940eea9006b1d36f2b0d7260a15a91a9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 15:58:42 -0400 Subject: [PATCH 17/20] Version Packages (#16946) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/neat-melons-cheer.md | 5 ----- .changeset/pretty-llamas-explode.md | 5 ----- .changeset/wild-mirrors-take.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/neat-melons-cheer.md delete mode 100644 .changeset/pretty-llamas-explode.md delete mode 100644 .changeset/wild-mirrors-take.md diff --git a/.changeset/neat-melons-cheer.md b/.changeset/neat-melons-cheer.md deleted file mode 100644 index 1107d7ef20a0..000000000000 --- a/.changeset/neat-melons-cheer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `createContext` utility for type-safe context diff --git a/.changeset/pretty-llamas-explode.md b/.changeset/pretty-llamas-explode.md deleted file mode 100644 index 00109112de60..000000000000 --- a/.changeset/pretty-llamas-explode.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify `batch.apply()` diff --git a/.changeset/wild-mirrors-take.md b/.changeset/wild-mirrors-take.md deleted file mode 100644 index faf28e7695c5..000000000000 --- a/.changeset/wild-mirrors-take.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't rerun async effects unnecessarily diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index b3af39eb4cb6..5d23dddd1582 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.40.0 + +### Minor Changes + +- feat: add `createContext` utility for type-safe context ([#16948](https://github.com/sveltejs/svelte/pull/16948)) + +### Patch Changes + +- chore: simplify `batch.apply()` ([#16945](https://github.com/sveltejs/svelte/pull/16945)) + +- fix: don't rerun async effects unnecessarily ([#16944](https://github.com/sveltejs/svelte/pull/16944)) + ## 5.39.13 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 55b44fb2b65c..8c049e0f8575 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.39.13", + "version": "5.40.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 536a2260c9e2..2fcca0bd8d2e 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.39.13'; +export const VERSION = '5.40.0'; export const PUBLIC_VERSION = '5'; From 9cdd76e3a3666b6c041fc2eb2c4498db31c795e5 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Tue, 14 Oct 2025 14:56:49 -0600 Subject: [PATCH 18/20] chore: Remove annoying sync-async warning (#16949) --- .changeset/eager-cups-argue.md | 5 +++++ .../docs/98-reference/.generated/server-warnings.md | 9 --------- .../svelte/messages/server-warnings/warnings.md | 5 ----- packages/svelte/src/internal/server/renderer.js | 2 -- packages/svelte/src/internal/server/warnings.js | 13 +------------ packages/svelte/src/legacy/legacy-server.js | 1 - 6 files changed, 6 insertions(+), 29 deletions(-) create mode 100644 .changeset/eager-cups-argue.md delete mode 100644 documentation/docs/98-reference/.generated/server-warnings.md delete mode 100644 packages/svelte/messages/server-warnings/warnings.md diff --git a/.changeset/eager-cups-argue.md b/.changeset/eager-cups-argue.md new file mode 100644 index 000000000000..74f03bc1dabe --- /dev/null +++ b/.changeset/eager-cups-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: Remove sync-in-async warning for server rendering diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md deleted file mode 100644 index 26b3628be9d1..000000000000 --- a/documentation/docs/98-reference/.generated/server-warnings.md +++ /dev/null @@ -1,9 +0,0 @@ - - -### experimental_async_ssr - -``` -Attempted to use asynchronous rendering without `experimental.async` enabled -``` - -Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously. diff --git a/packages/svelte/messages/server-warnings/warnings.md b/packages/svelte/messages/server-warnings/warnings.md deleted file mode 100644 index 4df89d017621..000000000000 --- a/packages/svelte/messages/server-warnings/warnings.md +++ /dev/null @@ -1,5 +0,0 @@ -## experimental_async_ssr - -> Attempted to use asynchronous rendering without `experimental.async` enabled - -Set `experimental.async: true` in your compiler options (usually in `svelte.config.js`) to use async server rendering. This render ran synchronously. diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index bbb43a6f3b35..602c680c08ff 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -4,7 +4,6 @@ import { async_mode_flag } from '../flags/index.js'; import { abort } from './abort-signal.js'; import { pop, push, set_ssr_context, ssr_context } from './context.js'; import * as e from './errors.js'; -import * as w from './warnings.js'; import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { attributes } from './index.js'; @@ -361,7 +360,6 @@ export class Renderer { */ (onfulfilled, onrejected) => { if (!async_mode_flag) { - w.experimental_async_ssr(); const result = (sync ??= Renderer.#render(component, options)); const user_result = onfulfilled({ head: result.head, diff --git a/packages/svelte/src/internal/server/warnings.js b/packages/svelte/src/internal/server/warnings.js index d8d9cd6d4325..d4ee7a86c220 100644 --- a/packages/svelte/src/internal/server/warnings.js +++ b/packages/svelte/src/internal/server/warnings.js @@ -3,15 +3,4 @@ import { DEV } from 'esm-env'; var bold = 'font-weight: bold'; -var normal = 'font-weight: normal'; - -/** - * Attempted to use asynchronous rendering without `experimental.async` enabled - */ -export function experimental_async_ssr() { - if (DEV) { - console.warn(`%c[svelte] experimental_async_ssr\n%cAttempted to use asynchronous rendering without \`experimental.async\` enabled\nhttps://svelte.dev/e/experimental_async_ssr`, bold, normal); - } else { - console.warn(`https://svelte.dev/e/experimental_async_ssr`); - } -} \ No newline at end of file +var normal = 'font-weight: normal'; \ No newline at end of file diff --git a/packages/svelte/src/legacy/legacy-server.js b/packages/svelte/src/legacy/legacy-server.js index b7d3e673bce5..a50d961751d8 100644 --- a/packages/svelte/src/legacy/legacy-server.js +++ b/packages/svelte/src/legacy/legacy-server.js @@ -53,7 +53,6 @@ export function asClassComponent(component) { */ value: (onfulfilled, onrejected) => { if (!async_mode_flag) { - w.experimental_async_ssr(); const user_result = onfulfilled({ css: munged.css, head: munged.head, From 323a6419ec3ff92ae20146edcb2656d919f5a65b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 17:43:08 -0400 Subject: [PATCH 19/20] fix --- .../src/internal/client/reactivity/batch.js | 12 ------------ .../svelte/src/internal/client/runtime.js | 19 ++++--------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9801d2b5b33c..2956e7ed6afe 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -63,18 +63,6 @@ let last_scheduled_effect = null; let is_flushing = false; export let is_flushing_sync = false; -/** - * A map of signal -> pending value - * - * A signal will appear in here if there's pending async work and a signal - * cannot be updated to its new value until that work completes. - * - * Related: `read_pending` in `runtime.js` - * - * @type {Map} - */ -export let pending_values = new Map(); - export class Batch { /** * The current values of any sources that are updated in this batch diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 53b177adcf2a..38f63f2f74a9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -42,13 +42,7 @@ import { set_dev_stack } from './context.js'; import * as w from './warnings.js'; -import { - Batch, - batch_values, - flushSync, - pending_values, - schedule_effect -} from './reactivity/batch.js'; +import { Batch, batch_values, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; import { captured_signals } from './legacy.js'; @@ -697,20 +691,15 @@ export function get(signal) { } } - if (batch_values?.has(signal)) { + if (!read_pending && batch_values?.has(signal)) { return batch_values.get(signal); } - var value = signal.v; - if (read_pending && pending_values.has(signal)) { - value = /** @type {{v: any}} */ (pending_values.get(signal)).v; - } - if ((signal.f & ERROR_VALUE) !== 0) { - throw value; + throw signal.v; } - return value; + return signal.v; } /** @param {Derived} derived */ From 61adc3d82f1f835fab85ee842ae61e2f5fbc2b69 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 14 Oct 2025 17:59:45 -0400 Subject: [PATCH 20/20] use `$state.eager(value)` instead of `$effect.pending(value)` --- packages/svelte/src/ambient.d.ts | 18 +++++++++++++++--- .../2-analyze/visitors/CallExpression.js | 7 +++++++ .../client/visitors/CallExpression.js | 13 +++++++------ .../server/visitors/CallExpression.js | 6 +++++- packages/svelte/src/utils.js | 1 + packages/svelte/types/index.d.ts | 18 +++++++++++++++--- 6 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index d655fb648a2f..64cdcc93b2d2 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -95,12 +95,24 @@ declare namespace $state { : never : never; + /** + * Returns the latest `value`, even if the rest of the UI is suspending + * while async work (such as data loading) completes. + * + * ```svelte + * + * ``` + */ + export function eager(value: T): T; /** * Declares state that is _not_ made deeply reactive — instead of mutating it, * you must reassign it. * * Example: - * ```ts + * ```svelte * * - * * ``` @@ -124,7 +136,7 @@ declare namespace $state { * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * * Example: - * ```ts + * ```svelte * * - * * ``` @@ -3214,7 +3226,7 @@ declare namespace $state { * To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snapshot`: * * Example: - * ```ts + * ```svelte *