From 7c8be602be7c179a7f248aea078448413815e70a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 11:49:51 -0400 Subject: [PATCH 001/367] move config into svelte.config.js (#16265) --- playgrounds/sandbox/svelte.config.js | 5 +++++ playgrounds/sandbox/vite.config.js | 9 +-------- 2 files changed, 6 insertions(+), 8 deletions(-) create mode 100644 playgrounds/sandbox/svelte.config.js diff --git a/playgrounds/sandbox/svelte.config.js b/playgrounds/sandbox/svelte.config.js new file mode 100644 index 0000000000..65f739bdb6 --- /dev/null +++ b/playgrounds/sandbox/svelte.config.js @@ -0,0 +1,5 @@ +export default { + compilerOptions: { + hmr: true + } +}; diff --git a/playgrounds/sandbox/vite.config.js b/playgrounds/sandbox/vite.config.js index 51bfd0a212..5ce0204217 100644 --- a/playgrounds/sandbox/vite.config.js +++ b/playgrounds/sandbox/vite.config.js @@ -7,14 +7,7 @@ export default defineConfig({ minify: false }, - plugins: [ - inspect(), - svelte({ - compilerOptions: { - hmr: true - } - }) - ], + plugins: [inspect(), svelte()], optimizeDeps: { // svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change From b6731456595ffc43620d76f2afb115a19bb7553f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 15:47:25 -0400 Subject: [PATCH 002/367] feat: add `getAbortSignal()` (#16266) * WIP getAbortSignal * add test * regenerate * add error code * changeset * regenerate * try this * { stale: true } * fix test * lint * abort synchronously in SSR * make STALE_REACTION a `StaleReactionError extends Error` * make non-optional * Update packages/svelte/src/internal/server/abort-signal.js Co-authored-by: Elliott Johnson --------- Co-authored-by: Elliott Johnson --- .changeset/short-fireants-flow.md | 5 ++ .../98-reference/.generated/client-errors.md | 6 ++ .../svelte/messages/client-errors/errors.md | 4 ++ packages/svelte/src/index-client.js | 33 ++++++++- packages/svelte/src/index-server.js | 2 + .../svelte/src/internal/client/constants.js | 6 ++ packages/svelte/src/internal/client/errors.js | 16 +++++ .../internal/client/reactivity/deriveds.js | 3 +- .../src/internal/client/reactivity/effects.js | 9 ++- .../src/internal/client/reactivity/types.d.ts | 2 + .../svelte/src/internal/client/runtime.js | 8 ++- .../src/internal/server/abort-signal.js | 13 ++++ packages/svelte/src/internal/server/index.js | 71 ++++++++++--------- .../samples/get-abort-signal/_config.js | 34 +++++++++ .../samples/get-abort-signal/main.svelte | 33 +++++++++ packages/svelte/types/index.d.ts | 24 +++++++ 16 files changed, 231 insertions(+), 38 deletions(-) create mode 100644 .changeset/short-fireants-flow.md create mode 100644 packages/svelte/src/internal/server/abort-signal.js create mode 100644 packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte diff --git a/.changeset/short-fireants-flow.md b/.changeset/short-fireants-flow.md new file mode 100644 index 0000000000..b9955ff577 --- /dev/null +++ b/.changeset/short-fireants-flow.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `getAbortSignal()` diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 111b0b8940..3f33e37d2e 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -74,6 +74,12 @@ Effect cannot be created inside a `$derived` value that was not itself created i Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops ``` +### get_abort_signal_outside_reaction + +``` +`getAbortSignal()` can only be called inside an effect or derived +``` + ### hydration_failed ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 6d96770eba..47c2038d70 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -48,6 +48,10 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops +## get_abort_signal_outside_reaction + +> `getAbortSignal()` can only be called inside an effect or derived + ## hydration_failed > Failed to hydrate the application diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index efd5628ae9..0d962aacd1 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */ /** @import { EventDispatcher } from './index.js' */ /** @import { NotFunction } from './internal/types.js' */ -import { untrack } from './internal/client/runtime.js'; +import { active_reaction, untrack } from './internal/client/runtime.js'; import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; @@ -44,6 +44,37 @@ if (DEV) { throw_rune_error('$bindable'); } +/** + * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed. + * + * Must be called while a derived or effect is running. + * + * ```svelte + * + * ``` + */ +export function getAbortSignal() { + if (active_reaction === null) { + e.get_abort_signal_outside_reaction(); + } + + return (active_reaction.ac ??= new AbortController()).signal; +} + /** * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. * Unlike `$effect`, the provided function only runs once. diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5a..e5039cf150 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -35,6 +35,8 @@ export function unmount() { export async function tick() {} +export { getAbortSignal } from './internal/server/abort-signal.js'; + export { 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/constants.js b/packages/svelte/src/internal/client/constants.js index 3ca915f98e..dd3d1b2df6 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -27,6 +27,12 @@ export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); +// allow users to ignore aborted signal errors if `reason.stale` +export const STALE_REACTION = new (class StaleReactionError extends Error { + name = 'StaleReactionError'; + message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; +})(); + export const ELEMENT_NODE = 1; export const TEXT_NODE = 3; export const COMMENT_NODE = 8; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index a0ac021c03..5c3f5340e1 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -195,6 +195,22 @@ export function effect_update_depth_exceeded() { } } +/** + * `getAbortSignal()` can only be called inside an effect or derived + * @returns {never} + */ +export function get_abort_signal_outside_reaction() { + if (DEV) { + const error = new Error(`get_abort_signal_outside_reaction\n\`getAbortSignal()\` can only be called inside an effect or derived\nhttps://svelte.dev/e/get_abort_signal_outside_reaction`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/get_abort_signal_outside_reaction`); + } +} + /** * Failed to hydrate the application * @returns {never} diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index e9cea0df3e..d3123d24a1 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -53,7 +53,8 @@ export function derived(fn) { rv: 0, v: /** @type {V} */ (null), wv: 0, - parent: parent_derived ?? active_effect + parent: parent_derived ?? active_effect, + ac: null }; if (DEV && tracing_mode_flag) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 03d073781d..a2806bde81 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -32,7 +32,8 @@ import { HEAD_EFFECT, MAYBE_DIRTY, EFFECT_HAS_DERIVED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + STALE_REACTION } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -106,7 +107,8 @@ function create_effect(type, fn, sync, push = true) { prev: null, teardown: null, transitions: null, - wv: 0 + wv: 0, + ac: null }; if (DEV) { @@ -397,6 +399,8 @@ export function destroy_effect_children(signal, remove_dom = false) { signal.first = signal.last = null; while (effect !== null) { + effect.ac?.abort(STALE_REACTION); + var next = effect.next; if ((effect.f & ROOT_EFFECT) !== 0) { @@ -478,6 +482,7 @@ export function destroy_effect(effect, remove_dom = true) { effect.fn = effect.nodes_start = effect.nodes_end = + effect.ac = null; } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 756bb98f09..88c84f27fe 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -40,6 +40,8 @@ export interface Reaction extends Signal { fn: null | Function; /** Signals that this signal reads from */ deps: null | Value[]; + /** An AbortController that aborts when the signal is destroyed */ + ac: null | AbortController; } export interface Derived extends Value, Reaction { diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d057bfdf0d..3e70537d7c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - EFFECT_IS_UPDATING + EFFECT_IS_UPDATING, + STALE_REACTION } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -276,6 +277,11 @@ export function update_reaction(reaction) { reaction.f |= EFFECT_IS_UPDATING; + if (reaction.ac !== null) { + reaction.ac.abort(STALE_REACTION); + reaction.ac = null; + } + try { var result = /** @type {Function} */ (0, reaction.fn)(); var deps = reaction.deps; diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js new file mode 100644 index 0000000000..da579b2592 --- /dev/null +++ b/packages/svelte/src/internal/server/abort-signal.js @@ -0,0 +1,13 @@ +import { STALE_REACTION } from '#client/constants'; + +/** @type {AbortController | null} */ +export let controller = null; + +export function abort() { + controller?.abort(STALE_REACTION); + controller = null; +} + +export function getAbortSignal() { + return (controller ??= new AbortController()).signal; +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 2ca85fff44..ceb516ebb0 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -18,6 +18,7 @@ import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; import { Payload } from './payload.js'; +import { abort } from './abort-signal.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -66,50 +67,54 @@ export let on_destroy = []; * @returns {RenderOutput} */ export function render(component, options = {}) { - const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); + try { + const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); - const prev_on_destroy = on_destroy; - on_destroy = []; - payload.out += BLOCK_OPEN; + const prev_on_destroy = on_destroy; + on_destroy = []; + payload.out += BLOCK_OPEN; - let reset_reset_element; + let reset_reset_element; - if (DEV) { - // prevent parent/child element state being corrupted by a bad render - reset_reset_element = reset_elements(); - } + if (DEV) { + // prevent parent/child element state being corrupted by a bad render + reset_reset_element = reset_elements(); + } - if (options.context) { - push(); - /** @type {Component} */ (current_component).c = options.context; - } + if (options.context) { + push(); + /** @type {Component} */ (current_component).c = options.context; + } - // @ts-expect-error - component(payload, options.props ?? {}, {}, {}); + // @ts-expect-error + component(payload, options.props ?? {}, {}, {}); - if (options.context) { - pop(); - } + if (options.context) { + pop(); + } - if (reset_reset_element) { - reset_reset_element(); - } + if (reset_reset_element) { + reset_reset_element(); + } - payload.out += BLOCK_CLOSE; - for (const cleanup of on_destroy) cleanup(); - on_destroy = prev_on_destroy; + payload.out += BLOCK_CLOSE; + for (const cleanup of on_destroy) cleanup(); + on_destroy = prev_on_destroy; - let head = payload.head.out + payload.head.title; + let head = payload.head.out + payload.head.title; - for (const { hash, code } of payload.css) { - head += ``; - } + for (const { hash, code } of payload.css) { + head += ``; + } - return { - head, - html: payload.out, - body: payload.out - }; + return { + head, + html: payload.out, + body: payload.out + }; + } finally { + abort(); + } } /** diff --git a/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js new file mode 100644 index 0000000000..6a85e2615a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/_config.js @@ -0,0 +1,34 @@ +import { test } from '../../test'; + +export default test({ + html: `

loading...

`, + + async test({ assert, target, variant, logs }) { + await new Promise((f) => setTimeout(f, 50)); + + if (variant === 'hydrate') { + assert.deepEqual(logs, [ + 'aborted', + 'StaleReactionError', + 'The reaction that called `getAbortSignal()` was re-run or destroyed' + ]); + } + + logs.length = 0; + + const [button] = target.querySelectorAll('button'); + + await new Promise((f) => setTimeout(f, 50)); + assert.htmlEqual(target.innerHTML, '

0

'); + + button.click(); + await new Promise((f) => setTimeout(f, 50)); + assert.htmlEqual(target.innerHTML, '

2

'); + + assert.deepEqual(logs, [ + 'aborted', + 'StaleReactionError', + 'The reaction that called `getAbortSignal()` was re-run or destroyed' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte new file mode 100644 index 0000000000..be5625125b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/get-abort-signal/main.svelte @@ -0,0 +1,33 @@ + + + + +{#await delayed_count} +

loading...

+{:then count} +

{count}

+{:catch error} + {console.log('this should never be rendered')} +{/await} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 3401efac04..432171ae0d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -348,6 +348,30 @@ declare module 'svelte' { */ props: Props; }); + /** + * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed. + * + * Must be called while a derived or effect is running. + * + * ```svelte + * + * ``` + */ + export function getAbortSignal(): AbortSignal; /** * `onMount`, like [`$effect`](https://svelte.dev/docs/svelte/$effect), schedules a function to run as soon as the component has been mounted to the DOM. * Unlike `$effect`, the provided function only runs once. From eb530c82c4e8870194cb919be4bc76600f0e0231 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 16:04:44 -0400 Subject: [PATCH 003/367] chore: move stuff from `analysis` into global compiler state (#16268) * break out locator stuff from the rest of global state * move some stuff around * tighten up * make runes globally available * make component_name globally available * unused --- packages/svelte/src/compiler/index.js | 14 ++++---- packages/svelte/src/compiler/migrate/index.js | 5 ++- .../src/compiler/phases/1-parse/index.js | 3 ++ .../src/compiler/phases/2-analyze/index.js | 17 +++++++++ .../svelte/src/compiler/phases/types.d.ts | 3 ++ packages/svelte/src/compiler/state.js | 35 +++++++++++++------ 6 files changed, 57 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 429c6b4a6f..9ba23c1485 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -20,9 +20,8 @@ export { default as preprocess } from './preprocess/index.js'; */ export function compile(source, options) { source = remove_bom(source); - state.reset_warning_filter(options.warningFilter); + state.reset_warnings(options.warningFilter); const validated = validate_component_options(options, ''); - state.reset(source, validated); let parsed = _parse(source); @@ -64,9 +63,8 @@ export function compile(source, options) { */ export function compileModule(source, options) { source = remove_bom(source); - state.reset_warning_filter(options.warningFilter); + state.reset_warnings(options.warningFilter); const validated = validate_module_options(options, ''); - state.reset(source, validated); const analysis = analyze_module(source, validated); return transform_module(analysis, source, validated); @@ -96,6 +94,7 @@ export function compileModule(source, options) { * @returns {Record} */ +// TODO 6.0 remove unused `filename` /** * The parse function parses a component, returning only its abstract syntax tree. * @@ -104,14 +103,15 @@ export function compileModule(source, options) { * * The `loose` option, available since 5.13.0, tries to always return an AST even if the input will not successfully compile. * + * The `filename` option is unused and will be removed in Svelte 6.0. + * * @param {string} source * @param {{ filename?: string; rootDir?: string; modern?: boolean; loose?: boolean }} [options] * @returns {AST.Root | LegacyRoot} */ -export function parse(source, { filename, rootDir, modern, loose } = {}) { +export function parse(source, { modern, loose } = {}) { source = remove_bom(source); - state.reset_warning_filter(() => false); - state.reset(source, { filename: filename ?? '(unknown)', rootDir }); + state.reset_warnings(() => false); const ast = _parse(source, loose); return to_public_ast(source, ast, modern); diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 5ca9adb98b..fdc9734f85 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -9,7 +9,7 @@ import { parse } from '../phases/1-parse/index.js'; import { regex_valid_component_name } from '../phases/1-parse/state/element.js'; import { analyze_component } from '../phases/2-analyze/index.js'; import { get_rune } from '../phases/scope.js'; -import { reset, reset_warning_filter } from '../state.js'; +import { reset, reset_warnings } from '../state.js'; import { extract_identifiers, extract_all_identifiers_from_expression, @@ -134,8 +134,7 @@ export function migrate(source, { filename, use_ts } = {}) { return start + style_placeholder + end; }); - reset_warning_filter(() => false); - reset(source, { filename: filename ?? '(unknown)' }); + reset_warnings(() => false); let parsed = parse(source); diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index b8ae8199eb..77cc2bf3fa 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -9,6 +9,7 @@ import { create_fragment } from './utils/create.js'; import read_options from './read/options.js'; import { is_reserved } from '../../../utils.js'; import { disallow_children } from '../2-analyze/visitors/shared/special-element.js'; +import * as state from '../../state.js'; const regex_position_indicator = / \(\d+:\d+\)$/; @@ -301,6 +302,8 @@ export class Parser { * @returns {AST.Root} */ export function parse(template, loose = false) { + state.set_source(template); + const parser = new Parser(template, loose); return parser.root; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index be1f1ee5bb..d73e273ec2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -76,6 +76,7 @@ import { UseDirective } from './visitors/UseDirective.js'; import { VariableDeclarator } from './visitors/VariableDeclarator.js'; import is_reference from 'is-reference'; import { mark_subtree_dynamic } from './visitors/shared/fragment.js'; +import * as state from '../../state.js'; /** * @type {Visitors} @@ -240,6 +241,7 @@ export function analyze_module(source, options) { /** @type {AST.JSComment[]} */ const comments = []; + state.set_source(source); const ast = parse(source, comments, false, false); const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null); @@ -269,6 +271,13 @@ export function analyze_module(source, options) { classes: new Map() }; + state.reset({ + dev: options.dev, + filename: options.filename, + rootDir: options.rootDir, + runes: true + }); + walk( /** @type {Node} */ (ast), { @@ -506,6 +515,14 @@ export function analyze_component(root, source, options) { snippets: new Set() }; + state.reset({ + component_name: analysis.name, + dev: options.dev, + filename: options.filename, + rootDir: options.rootDir, + runes: true + }); + if (!runes) { // every exported `let` or `var` declaration becomes a prop, everything else becomes an export for (const node of instance.ast.body) { diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 661f363991..dba2559a17 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -34,7 +34,9 @@ export interface ReactiveStatement { */ export interface Analysis { module: Js; + /** @deprecated use `component_name` from `state.js` instead */ name: string; // TODO should this be filename? it's used in `compileModule` as well as `compile` + /** @deprecated use `runes` from `state.js` instead */ runes: boolean; immutable: boolean; tracing: boolean; @@ -90,6 +92,7 @@ export interface ComponentAnalysis extends Analysis { keyframes: string[]; has_global: boolean; }; + /** @deprecated use `source` from `state.js` instead */ source: string; undefined_exports: Map; /** diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 1db3db917f..9095651ced 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,8 @@ export let warnings = []; */ export let filename; +export let component_name = ''; + /** * The original source code * @type {string} @@ -28,8 +30,16 @@ export let source; */ export let dev; +export let runes = false; + export let locator = getLocator('', { offsetLine: 1 }); +/** @param {string} value */ +export function set_source(value) { + source = value; + locator = getLocator(source, { offsetLine: 1 }); +} + /** * @param {AST.SvelteNode & { start?: number | undefined }} node */ @@ -71,8 +81,9 @@ export function pop_ignore() { * * @param {(warning: Warning) => boolean} fn */ -export function reset_warning_filter(fn = () => true) { +export function reset_warnings(fn = () => true) { warning_filter = fn; + warnings = []; } /** @@ -85,23 +96,27 @@ export function is_ignored(node, code) { } /** - * @param {string} _source - * @param {{ dev?: boolean; filename: string; rootDir?: string }} options + * @param {{ + * dev: boolean; + * filename: string; + * component_name?: string; + * rootDir?: string; + * runes: boolean; + * }} state */ -export function reset(_source, options) { - source = _source; - const root_dir = options.rootDir?.replace(/\\/g, '/'); - filename = options.filename.replace(/\\/g, '/'); +export function reset(state) { + const root_dir = state.rootDir?.replace(/\\/g, '/'); + filename = state.filename.replace(/\\/g, '/'); - dev = !!options.dev; + dev = state.dev; + runes = state.runes; + component_name = state.component_name ?? '(unknown)'; if (typeof root_dir === 'string' && filename.startsWith(root_dir)) { // make filename relative to rootDir filename = filename.replace(root_dir, '').replace(/^[/\\]/, ''); } - locator = getLocator(source, { offsetLine: 1 }); - warnings = []; ignore_stack = []; ignore_map.clear(); } From d427ffd8b96c5a5509ee602bd77d58a90f24e784 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Jul 2025 10:37:37 -0400 Subject: [PATCH 004/367] chore: encapsulate expression memoization (#16269) * chore: encapsulate expression memoization * add comment * tweak * use b.id --- .../3-transform/client/transform-client.js | 4 +- .../phases/3-transform/client/types.d.ts | 5 +- .../3-transform/client/visitors/Fragment.js | 4 +- .../client/visitors/RegularElement.js | 61 ++++++++++--------- .../client/visitors/SvelteElement.js | 6 +- .../client/visitors/shared/element.js | 37 +++++------ .../client/visitors/shared/utils.js | 52 ++++++++++++---- 7 files changed, 99 insertions(+), 70 deletions(-) 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 e85a35cf8e..a9c0651e0f 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 @@ -168,9 +168,9 @@ export function client_component(analysis, options) { // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), update: /** @type {any} */ (null), - expressions: /** @type {any} */ (null), after_update: /** @type {any} */ (null), - template: /** @type {any} */ (null) + template: /** @type {any} */ (null), + memoizer: /** @type {any} */ (null) }; const module = /** @type {ESTree.Program} */ ( 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 2388ee1b00..cf5c942268 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 @@ -12,6 +12,7 @@ import type { AST, Namespace, ValidatedCompileOptions } from '#compiler'; import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; import type { Template } from './transform-template/template.js'; +import type { Memoizer } from './visitors/shared/utils.js'; export interface ClientTransformState extends TransformState { /** @@ -49,8 +50,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly update: Statement[]; /** Stuff that happens after the render effect (control blocks, dynamic elements, bindings, actions, etc) */ readonly after_update: Statement[]; - /** Expressions used inside the render effect */ - readonly expressions: Expression[]; + /** Memoized expressions */ + readonly memoizer: Memoizer; /** The HTML template string */ readonly template: Template; readonly metadata: { 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 4825184d31..7cd2bd90ed 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 @@ -6,7 +6,7 @@ import * as b from '#compiler/builders'; import { clean_nodes, infer_namespace } from '../../utils.js'; import { transform_template } from '../transform-template/index.js'; import { process_children } from './shared/fragment.js'; -import { build_render_statement } from './shared/utils.js'; +import { build_render_statement, Memoizer } from './shared/utils.js'; import { Template } from '../transform-template/template.js'; /** @@ -64,8 +64,8 @@ export function Fragment(node, context) { ...context.state, init: [], update: [], - expressions: [], after_update: [], + memoizer: new Memoizer(), template: new Template(), transform: { ...context.state.transform }, metadata: { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index ae8680f594..eed2a75506 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -22,7 +22,7 @@ import { build_set_style } from './shared/element.js'; import { process_children } from './shared/fragment.js'; -import { build_render_statement, build_template_chunk, get_expression_id } from './shared/utils.js'; +import { build_render_statement, build_template_chunk, Memoizer } from './shared/utils.js'; import { visit_event_attribute } from './shared/events.js'; /** @@ -253,8 +253,7 @@ export function RegularElement(node, context) { const { value, has_state } = build_attribute_value( attribute.value, context, - (value, metadata) => - metadata.has_call ? get_expression_id(context.state.expressions, value) : value + (value, metadata) => (metadata.has_call ? context.state.memoizer.add(value) : value) ); const update = build_element_attribute_update(node, node_id, name, value, attributes); @@ -455,11 +454,15 @@ function setup_select_synchronization(value_binding, context) { /** * @param {AST.ClassDirective[]} class_directives - * @param {Expression[]} expressions * @param {ComponentContext} context + * @param {Memoizer} memoizer * @return {ObjectExpression | Identifier} */ -export function build_class_directives_object(class_directives, expressions, context) { +export function build_class_directives_object( + class_directives, + context, + memoizer = context.state.memoizer +) { let properties = []; let has_call_or_state = false; @@ -471,38 +474,40 @@ export function build_class_directives_object(class_directives, expressions, con const directives = b.object(properties); - return has_call_or_state ? get_expression_id(expressions, directives) : directives; + return has_call_or_state ? memoizer.add(directives) : directives; } /** * @param {AST.StyleDirective[]} style_directives - * @param {Expression[]} expressions * @param {ComponentContext} context - * @return {ObjectExpression | ArrayExpression}} + * @param {Memoizer} memoizer + * @return {ObjectExpression | ArrayExpression | Identifier}} */ -export function build_style_directives_object(style_directives, expressions, context) { - let normal_properties = []; - let important_properties = []; +export function build_style_directives_object( + style_directives, + context, + memoizer = context.state.memoizer +) { + const normal = b.object([]); + const important = b.object([]); - for (const directive of style_directives) { + let has_call_or_state = false; + + for (const d of style_directives) { const expression = - directive.value === true - ? build_getter({ name: directive.name, type: 'Identifier' }, context.state) - : build_attribute_value(directive.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(expressions, value) : value - ).value; - const property = b.init(directive.name, expression); - - if (directive.modifiers.includes('important')) { - important_properties.push(property); - } else { - normal_properties.push(property); - } + d.value === true + ? build_getter(b.id(d.name), context.state) + : build_attribute_value(d.value, context).value; + + const object = d.modifiers.includes('important') ? important : normal; + object.properties.push(b.init(d.name, expression)); + + has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; } - return important_properties.length - ? b.array([b.object(normal_properties), b.object(important_properties)]) - : b.object(normal_properties); + const directives = important.properties.length ? b.array([normal, important]) : normal; + + return has_call_or_state ? memoizer.add(directives) : directives; } /** @@ -624,7 +629,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(state.expressions, value) : value + metadata.has_call ? state.memoizer.add(value) : value ); const evaluated = context.state.scope.evaluate(value); 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 7381553dbe..6af5f5770e 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 @@ -10,7 +10,7 @@ import { build_attribute_effect, build_set_class } from './shared/element.js'; -import { build_render_statement } from './shared/utils.js'; +import { build_render_statement, Memoizer } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -46,8 +46,8 @@ export function SvelteElement(node, context) { node: element_id, init: [], update: [], - expressions: [], - after_update: [] + after_update: [], + memoizer: new Memoizer() } }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 10f942b7d4..d119b0457b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, Memoizer } from './utils.js'; /** * @param {Array} attributes @@ -28,18 +28,12 @@ export function build_attribute_effect( /** @type {ObjectExpression['properties']} */ const values = []; - /** @type {Expression[]} */ - const expressions = []; - - /** @param {Expression} value */ - function memoize(value) { - return b.id(`$${expressions.push(value) - 1}`); - } + const memoizer = new Memoizer(); for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { value } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? memoize(value) : value + metadata.has_call ? memoizer.add(value) : value ); if ( @@ -57,7 +51,7 @@ export function build_attribute_effect( let value = /** @type {Expression} */ (context.visit(attribute)); if (attribute.metadata.expression.has_call) { - value = memoize(value); + value = memoizer.add(value); } values.push(b.spread(value)); @@ -69,7 +63,7 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.CLASS')]), - build_class_directives_object(class_directives, expressions, context) + build_class_directives_object(class_directives, context, memoizer) ) ); } @@ -79,21 +73,20 @@ export function build_attribute_effect( b.prop( 'init', b.array([b.id('$.STYLE')]), - build_style_directives_object(style_directives, expressions, context) + build_style_directives_object(style_directives, context, memoizer) ) ); } + const ids = memoizer.apply(); + context.state.init.push( b.stmt( b.call( '$.attribute_effect', element_id, - b.arrow( - expressions.map((_, i) => b.id(`$${i}`)), - b.object(values) - ), - expressions.length > 0 && b.array(expressions.map((expression) => b.thunk(expression))), + b.arrow(ids, b.object(values)), + memoizer.sync_values(), element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), @@ -158,7 +151,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c value = b.call('$.clsx', value); } - return metadata.has_call ? get_expression_id(context.state.expressions, value) : value; + return metadata.has_call ? context.state.memoizer.add(value) : value; }); /** @type {Identifier | undefined} */ @@ -171,7 +164,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c let next; if (class_directives.length) { - next = build_class_directives_object(class_directives, context.state.expressions, context); + next = build_class_directives_object(class_directives, context); has_state ||= class_directives.some((d) => d.metadata.expression.has_state); if (has_state) { @@ -226,7 +219,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c */ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? get_expression_id(context.state.expressions, value) : value + metadata.has_call ? context.state.memoizer.add(value) : value ); /** @type {Identifier | undefined} */ @@ -235,11 +228,11 @@ export function build_set_style(node_id, attribute, style_directives, context) { /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ArrayExpression | ObjectExpression | undefined} */ + /** @type {Expression | undefined} */ let next; if (style_directives.length) { - next = build_style_directives_object(style_directives, context.state.expressions, context); + next = build_style_directives_object(style_directives, context); has_state ||= style_directives.some((d) => d.metadata.expression.has_state); if (has_state) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 0bd9bfb128..b80466ccc9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -21,12 +21,41 @@ export function memoize_expression(state, value) { } /** - * Pushes `value` into `expressions` and returns a new id - * @param {Expression[]} expressions - * @param {Expression} value + * A utility for extracting complex expressions (such as call expressions) + * from templates and replacing them with `$0`, `$1` etc */ -export function get_expression_id(expressions, value) { - return b.id(`$${expressions.push(value) - 1}`); +export class Memoizer { + /** @type {Array<{ id: Identifier, expression: Expression }>} */ + #sync = []; + + /** + * @param {Expression} expression + */ + add(expression) { + const id = b.id('#'); // filled in later + + this.#sync.push({ id, expression }); + + return id; + } + + apply() { + return this.#sync.map((memo, i) => { + memo.id.name = `$${i}`; + return memo.id; + }); + } + + deriveds(runes = true) { + return this.#sync.map((memo) => + b.let(memo.id, b.call(runes ? '$.derived' : '$.derived_safe_equal', b.thunk(memo.expression))) + ); + } + + sync_values() { + if (this.#sync.length === 0) return; + return b.array(this.#sync.map((memo) => b.thunk(memo.expression))); + } } /** @@ -40,8 +69,7 @@ export function build_template_chunk( values, context, state = context.state, - memoize = (value, metadata) => - metadata.has_call ? get_expression_id(state.expressions, value) : value + memoize = (value, metadata) => (metadata.has_call ? state.memoizer.add(value) : value) ) { /** @type {Expression[]} */ const expressions = []; @@ -128,18 +156,20 @@ export function build_template_chunk( * @param {ComponentClientTransformState} state */ export function build_render_statement(state) { + const ids = state.memoizer.apply(); + const values = state.memoizer.sync_values(); + return b.stmt( b.call( '$.template_effect', b.arrow( - state.expressions.map((_, i) => b.id(`$${i}`)), + ids, state.update.length === 1 && state.update[0].type === 'ExpressionStatement' ? state.update[0].expression : b.block(state.update) ), - state.expressions.length > 0 && - b.array(state.expressions.map((expression) => b.thunk(expression))), - state.expressions.length > 0 && !state.analysis.runes && b.id('$.derived_safe_equal') + values, + values && !state.analysis.runes && b.id('$.derived_safe_equal') ) ); } From 11ec907fd57fec85e090d6a620743bd447f9d2ef Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Jul 2025 14:20:19 -0400 Subject: [PATCH 005/367] chore: simplify props (#16270) * simplify props * simplify * tweak * reorder a bit * simplify * unused * more * more * tweak * also appears to be unnecessary * changeset * apparently this is also unnecessary * explanatory comment --- .changeset/new-trees-behave.md | 5 + .../svelte/src/internal/client/constants.js | 2 - .../src/internal/client/reactivity/props.js | 175 +++++++----------- .../svelte/src/internal/client/runtime.js | 13 +- 4 files changed, 73 insertions(+), 122 deletions(-) create mode 100644 .changeset/new-trees-behave.md diff --git a/.changeset/new-trees-behave.md b/.changeset/new-trees-behave.md new file mode 100644 index 0000000000..d5fab30f3e --- /dev/null +++ b/.changeset/new-trees-behave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify props diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index dd3d1b2df6..cd5e0d2244 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -15,8 +15,6 @@ export const DESTROYED = 1 << 14; export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ export const EFFECT_TRANSPARENT = 1 << 16; -/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 17; export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f3111361c0..f51291b1cc 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -8,12 +8,11 @@ import { PROPS_IS_UPDATED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; -import { mutable_source, set, source, update } from './sources.js'; +import { set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { get, captured_signals, untrack } from '../runtime.js'; -import { safe_equals } from './equality.js'; +import { get, untrack } from '../runtime.js'; import * as e from '../errors.js'; -import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; +import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -260,89 +259,92 @@ function has_destroyed_component_ctx(current_value) { * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} */ export function prop(props, key, flags, fallback) { - var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0; var bindable = (flags & PROPS_IS_BINDABLE) !== 0; var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0; - var is_store_sub = false; - var prop_value; - - if (bindable) { - [prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key])); - } else { - prop_value = /** @type {V} */ (props[key]); - } - - // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` - // or `createClassComponent(Component, props)` - var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; - - var setter = - (bindable && - (get_descriptor(props, key)?.set ?? - (is_entry_props && key in props && ((v) => (props[key] = v))))) || - undefined; var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; - var fallback_used = false; var get_fallback = () => { - fallback_used = true; if (fallback_dirty) { fallback_dirty = false; - if (lazy) { - fallback_value = untrack(/** @type {() => V} */ (fallback)); - } else { - fallback_value = /** @type {V} */ (fallback); - } + + fallback_value = lazy + ? untrack(/** @type {() => V} */ (fallback)) + : /** @type {V} */ (fallback); } return fallback_value; }; - if (prop_value === undefined && fallback !== undefined) { - if (setter && runes) { - e.props_invalid_value(key); - } + /** @type {((v: V) => void) | undefined} */ + var setter; - prop_value = get_fallback(); - if (setter) setter(prop_value); + if (bindable) { + // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` + // or `createClassComponent(Component, props)` + var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; + + setter = + get_descriptor(props, key)?.set ?? + (is_entry_props && key in props ? (v) => (props[key] = v) : undefined); + } + + var initial_value; + var is_store_sub = false; + + if (bindable) { + [initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key])); + } else { + initial_value = /** @type {V} */ (props[key]); + } + + if (initial_value === undefined && fallback !== undefined) { + initial_value = get_fallback(); + + if (setter) { + if (runes) e.props_invalid_value(key); + setter(initial_value); + } } /** @type {() => V} */ var getter; + if (runes) { getter = () => { var value = /** @type {V} */ (props[key]); if (value === undefined) return get_fallback(); fallback_dirty = true; - fallback_used = false; return value; }; } else { - // Svelte 4 did not trigger updates when a primitive value was updated to the same value. - // Replicate that behavior through using a derived - var derived_getter = (immutable ? derived : derived_safe_equal)( - () => /** @type {V} */ (props[key]) - ); - derived_getter.f |= LEGACY_DERIVED_PROP; getter = () => { - var value = get(derived_getter); - if (value !== undefined) fallback_value = /** @type {V} */ (undefined); + var value = /** @type {V} */ (props[key]); + + if (value !== undefined) { + // in legacy mode, we don't revert to the fallback value + // if the prop goes from defined to undefined. The easiest + // way to model this is to make the fallback undefined + // as soon as the prop has a value + fallback_value = /** @type {V} */ (undefined); + } + return value === undefined ? fallback_value : value; }; } - // easy mode — prop is never written to - if ((flags & PROPS_IS_UPDATED) === 0 && runes) { + // prop is never written to — we only need a getter + if ((flags & PROPS_IS_UPDATED) === 0) { return getter; } - // intermediate mode — prop is written to, but the parent component had - // `bind:foo` which means we can just call `$$props.foo = value` directly + // prop is written to, but the parent component had `bind:foo` which + // means we can just call `$$props.foo = value` directly if (setter) { var legacy_parent = props.$$legacy; + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { // We don't want to notify if the value was mutated and the parent is in runes mode. @@ -352,82 +354,39 @@ export function prop(props, key, flags, fallback) { if (!runes || !mutation || legacy_parent || is_store_sub) { /** @type {Function} */ (setter)(mutation ? getter() : value); } + return value; - } else { - return getter(); } + + return getter(); }; } - // hard mode. this is where it gets ugly — the value in the child should - // synchronize with the parent, but it should also be possible to temporarily - // set the value to something else locally. - var from_child = false; - var was_from_child = false; - - // The derived returns the current value. The underlying mutable - // source is written to from various places to persist this value. - var inner_current_value = mutable_source(prop_value); - var current_value = derived(() => { - var parent_value = getter(); - var child_value = get(inner_current_value); - - if (from_child) { - from_child = false; - was_from_child = true; - return child_value; - } - - was_from_child = false; - return (inner_current_value.v = parent_value); - }); - - // Ensure we eagerly capture the initial value if it's bindable - if (bindable) { - get(current_value); - } + // prop is written to, but there's no binding, which means we + // create a derived that we can write to locally + var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); - if (!immutable) current_value.equals = safe_equals; + // Capture the initial value if it's bindable + if (bindable) get(d); return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { - // legacy nonsense — need to ensure the source is invalidated when necessary - // also needed for when handling inspect logic so we can inspect the correct source signal - if (captured_signals !== null) { - // set this so that we don't reset to the parent value if `d` - // is invalidated because of `invalidate_inner_signals` (rather - // than because the parent or child value changed) - from_child = was_from_child; - // invoke getters so that signals are picked up by `invalidate_inner_signals` - getter(); - get(inner_current_value); - } - if (arguments.length > 0) { - const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; - - if (!current_value.equals(new_value)) { - from_child = true; - set(inner_current_value, new_value); - // To ensure the fallback value is consistent when used with proxies, we - // update the local fallback_value, but only if the fallback is actively used - if (fallback_used && fallback_value !== undefined) { - fallback_value = new_value; - } + const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; - if (has_destroyed_component_ctx(current_value)) { - return value; - } + set(d, new_value); - untrack(() => get(current_value)); // force a synchronisation immediately + if (fallback_value !== undefined) { + fallback_value = new_value; } return value; } - if (has_destroyed_component_ctx(current_value)) { - return current_value.v; + // TODO is this still necessary post-#16263? + if (has_destroyed_component_ctx(d)) { + return d.v; } - return get(current_value); + return get(d); }; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e70537d7c..5a798ba3e9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -20,7 +20,6 @@ import { STATE_SYMBOL, BLOCK_EFFECT, ROOT_EFFECT, - LEGACY_DERIVED_PROP, DISCONNECTED, EFFECT_IS_UPDATING, STALE_REACTION @@ -863,17 +862,7 @@ export function invalidate_inner_signals(fn) { var captured = capture_signals(() => untrack(fn)); for (var signal of captured) { - // Go one level up because derived signals created as part of props in legacy mode - if ((signal.f & LEGACY_DERIVED_PROP) !== 0) { - for (const dep of /** @type {Derived} */ (signal).deps || []) { - if ((dep.f & DERIVED) === 0) { - // Use internal_set instead of set here and below to avoid mutation validation - internal_set(dep, dep.v); - } - } - } else { - internal_set(signal, signal.v); - } + internal_set(signal, signal.v); } } From 2f68131e9a780f139d6bdcf1b27854d850542671 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 1 Jul 2025 23:10:16 +0200 Subject: [PATCH 006/367] fix: revert props legacy mode regression (#16279) #16270 removed a condition which seemed to keep passing the corresponding test, but it actually introduced a regression since the PROPS_IS_UPDATED is always set when accessors should be created, which is the case by default in legacy mode tests. Setting accessors to false in the test reveals the regression, so this reverts that part of the refactoring --- packages/svelte/src/internal/client/reactivity/props.js | 8 +++++--- .../runtime-legacy/samples/prop-no-change/_config.js | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f51291b1cc..3501bcd3c7 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -336,7 +336,7 @@ export function prop(props, key, flags, fallback) { } // prop is never written to — we only need a getter - if ((flags & PROPS_IS_UPDATED) === 0) { + if (runes && (flags & PROPS_IS_UPDATED) === 0) { return getter; } @@ -362,8 +362,10 @@ export function prop(props, key, flags, fallback) { }; } - // prop is written to, but there's no binding, which means we - // create a derived that we can write to locally + // Either prop is written to, but there's no binding, which means we + // create a derived that we can write to locally. + // Or we are in legacy mode where we always create a derived to replicate that + // Svelte 4 did not trigger updates when a primitive value was updated to the same value. var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); // Capture the initial value if it's bindable diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js index 905c2a6226..84658336e2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/prop-no-change/_config.js @@ -2,6 +2,7 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + accessors: false, test({ assert, logs, target }) { assert.deepEqual(logs, ['primitive', 'object']); target.querySelector('button')?.click(); From 9dddb31b4a9260a520d19e2955393da0aa1d7f3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 08:07:08 -0400 Subject: [PATCH 007/367] Version Packages (#16276) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/new-trees-behave.md | 5 ----- .changeset/short-fireants-flow.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/new-trees-behave.md delete mode 100644 .changeset/short-fireants-flow.md diff --git a/.changeset/new-trees-behave.md b/.changeset/new-trees-behave.md deleted file mode 100644 index d5fab30f3e..0000000000 --- a/.changeset/new-trees-behave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify props diff --git a/.changeset/short-fireants-flow.md b/.changeset/short-fireants-flow.md deleted file mode 100644 index b9955ff577..0000000000 --- a/.changeset/short-fireants-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: add `getAbortSignal()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ef64091ca3..7589eda728 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.35.0 + +### Minor Changes + +- feat: add `getAbortSignal()` ([#16266](https://github.com/sveltejs/svelte/pull/16266)) + +### Patch Changes + +- chore: simplify props ([#16270](https://github.com/sveltejs/svelte/pull/16270)) + ## 5.34.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2d88d2a051..e8c28aeaa1 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.34.9", + "version": "5.35.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1c0aed6e87..baa2dbd046 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.34.9'; +export const VERSION = '5.35.0'; export const PUBLIC_VERSION = '5'; From 32882a956bc1283063d6da52401ac02b0aedefdb Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:12:25 +0200 Subject: [PATCH 008/367] feat: add parent hierarchy to `__svelte_meta` objects at dev time (#16255) * feat: add parent hierarchy to `__svelte_meta` objects at dev time This adds a `parent` property to the `__svelte_meta` properties that are added to elements at dev time. This property represents the closest non-element parent the element is related to. For example for `{#if ...}
foo
{/if}` the `parent` of the div would be the line/column of the if block. The parent is recursive and goes upwards (through component boundaries) until the root component is reached, which has no parent. part of #11389 * oops * Apply suggestions from code review Co-authored-by: Rich Harris * tweak * original component tag * make render appear in tree, keep tree in sync when rerenders occur --------- Co-authored-by: Rich Harris --- .changeset/hot-buses-end.md | 5 + .../3-transform/client/visitors/AwaitBlock.js | 8 +- .../3-transform/client/visitors/EachBlock.js | 4 +- .../3-transform/client/visitors/IfBlock.js | 4 +- .../3-transform/client/visitors/KeyBlock.js | 8 +- .../3-transform/client/visitors/RenderTag.js | 14 +- .../client/visitors/shared/component.js | 10 +- .../client/visitors/shared/utils.js | 35 +++- packages/svelte/src/compiler/state.js | 3 + .../svelte/src/internal/client/context.js | 40 +++- .../src/internal/client/dev/elements.js | 2 + .../src/internal/client/dom/blocks/await.js | 16 +- .../client/dom/blocks/svelte-element.js | 3 +- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/effects.js | 8 +- .../src/internal/client/reactivity/types.d.ts | 10 +- .../svelte/src/internal/client/runtime.js | 9 +- .../svelte/src/internal/client/types.d.ts | 9 + .../samples/svelte-meta-parent/_config.js | 186 ++++++++++++++++++ .../samples/svelte-meta-parent/child.svelte | 1 + .../samples/svelte-meta-parent/main.svelte | 50 +++++ .../svelte-meta-parent/passthrough.svelte | 9 + 22 files changed, 408 insertions(+), 28 deletions(-) create mode 100644 .changeset/hot-buses-end.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md new file mode 100644 index 0000000000..dd5a28fca8 --- /dev/null +++ b/.changeset/hot-buses-end.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add parent hierarchy to `__svelte_meta` objects diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 7873cf3ddb..c550c8e17b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -5,7 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; -import { build_expression } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -54,7 +54,7 @@ export function AwaitBlock(node, context) { } context.state.init.push( - b.stmt( + add_svelte_meta( b.call( '$.await', context.state.node, @@ -64,7 +64,9 @@ export function AwaitBlock(node, context) { : b.null, then_block, catch_block - ) + ), + node, + 'await' ) ); } 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 201c4b278f..353927b865 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 @@ -13,7 +13,7 @@ import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { get_value } from './shared/declarations.js'; -import { build_expression } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -337,7 +337,7 @@ export function EachBlock(node, context) { ); } - context.state.init.push(b.stmt(b.call('$.each', ...args))); + context.state.init.push(add_svelte_meta(b.call('$.each', ...args), node, 'each')); } /** 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 deab040e50..cfd2bb7b09 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 @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -74,7 +74,7 @@ export function IfBlock(node, context) { args.push(b.id('$$elseif')); } - statements.push(b.stmt(b.call('$.if', ...args))); + statements.push(add_svelte_meta(b.call('$.if', ...args), node, 'if')); context.state.init.push(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 2f17479c7e..3add1fbe93 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 @@ -2,7 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { build_expression, add_svelte_meta } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -15,6 +15,10 @@ export function KeyBlock(node, context) { const body = /** @type {Expression} */ (context.visit(node.fragment)); context.state.init.push( - b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) + add_svelte_meta( + b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body)), + node, + 'key' + ) ); } 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 c3615d9d50..7f1a4ae7ba 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 @@ -3,7 +3,7 @@ /** @import { ComponentContext } from '../types' */ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_expression } from './shared/utils.js'; +import { add_svelte_meta, build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -48,16 +48,22 @@ export function RenderTag(node, context) { } context.state.init.push( - b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) + add_svelte_meta( + b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args), + node, + 'render' + ) ); } else { context.state.init.push( - b.stmt( + add_svelte_meta( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, context.state.node, ...args - ) + ), + node, + 'render' ) ); } 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 cb6e4de478..aa3704b50b 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 @@ -4,7 +4,12 @@ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; +import { + build_bind_this, + memoize_expression, + validate_binding, + add_svelte_meta +} from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; @@ -483,7 +488,8 @@ export function build_component(node, component_name, context) { ); } else { context.state.template.push_comment(); - statements.push(b.stmt(fn(anchor))); + + statements.push(add_svelte_meta(fn(anchor), node, 'component', { componentTag: node.name })); } return statements.length > 1 ? b.block(statements) : statements[0]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index b80466ccc9..de74fede0c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; @@ -7,7 +7,7 @@ import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; -import { dev, is_ignored, locator } from '../../../../../state.js'; +import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { build_getter, create_derived } from '../../utils.js'; /** @@ -424,3 +424,34 @@ export function build_expression(context, expression, metadata, state = context. return sequence; } + +/** + * Wraps a statement/expression with dev stack tracking in dev mode + * @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.) + * @param {{ start?: number }} node - AST node for location info + * @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component + * @param {Record} [additional] - Any additional properties to add to the dev stack entry + * @returns {ExpressionStatement} - Statement with or without dev stack wrapping + */ +export function add_svelte_meta(expression, node, type, additional) { + if (!dev) { + return b.stmt(expression); + } + + const location = node.start !== undefined && locator(node.start); + if (!location) { + return b.stmt(expression); + } + + return b.stmt( + b.call( + '$.add_svelte_meta', + b.arrow([], expression), + b.literal(type), + b.id(component_name), + b.literal(location.line), + b.literal(location.column), + additional && b.object(Object.entries(additional).map(([k, v]) => b.init(k, b.literal(v)))) + ) + ); +} diff --git a/packages/svelte/src/compiler/state.js b/packages/svelte/src/compiler/state.js index 9095651ced..5eb25dd6bb 100644 --- a/packages/svelte/src/compiler/state.js +++ b/packages/svelte/src/compiler/state.js @@ -16,6 +16,9 @@ export let warnings = []; */ export let filename; +/** + * The name of the component that is used in the `export default function ...` statement. + */ export let component_name = ''; /** diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 7c7213b7a2..e4220149ab 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,4 +1,4 @@ -/** @import { ComponentContext } from '#client' */ +/** @import { ComponentContext, DevStackEntry } from '#client' */ import { DEV } from 'esm-env'; import { lifecycle_outside_component } from '../shared/errors.js'; @@ -11,6 +11,7 @@ import { } from './runtime.js'; import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; +import { FILENAME } from '../../constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -20,6 +21,43 @@ export function set_component_context(context) { component_context = context; } +/** @type {DevStackEntry | null} */ +export let dev_stack = null; + +/** @param {DevStackEntry | null} stack */ +export function set_dev_stack(stack) { + dev_stack = stack; +} + +/** + * Execute a callback with a new dev stack entry + * @param {() => any} callback - Function to execute + * @param {DevStackEntry['type']} type - Type of block/component + * @param {any} component - Component function + * @param {number} line - Line number + * @param {number} column - Column number + * @param {Record} [additional] - Any additional properties to add to the dev stack entry + * @returns {any} + */ +export function add_svelte_meta(callback, type, component, line, column, additional) { + const parent = dev_stack; + + dev_stack = { + type, + file: component[FILENAME], + line, + column, + parent, + ...additional + }; + + try { + return callback(); + } finally { + dev_stack = parent; + } +} + /** * The current component function. Different from current component context: * ```html diff --git a/packages/svelte/src/internal/client/dev/elements.js b/packages/svelte/src/internal/client/dev/elements.js index f70f893d1e..8dd54e0a2a 100644 --- a/packages/svelte/src/internal/client/dev/elements.js +++ b/packages/svelte/src/internal/client/dev/elements.js @@ -2,6 +2,7 @@ import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, ELEMENT_NODE } from '#client/constants'; import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; import { hydrating } from '../dom/hydration.js'; +import { dev_stack } from '../context.js'; /** * @param {any} fn @@ -28,6 +29,7 @@ export function add_locations(fn, filename, locations) { function assign_location(element, filename, location) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], column: location[1] } }; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 47df5fc9a5..325224fff2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -16,9 +16,11 @@ import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { component_context, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from '../../context.js'; const PENDING = 0; @@ -45,6 +47,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { /** @type {any} */ var component_function = DEV ? component_context?.function : null; + var dev_original_stack = DEV ? dev_stack : null; /** @type {V | Promise | typeof UNINITIALIZED} */ var input = UNINITIALIZED; @@ -75,7 +78,10 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_active_effect(effect); set_active_reaction(effect); // TODO do we need both? set_component_context(active_component_context); - if (DEV) set_dev_current_component_function(component_function); + if (DEV) { + set_dev_current_component_function(component_function); + set_dev_stack(dev_original_stack); + } } try { @@ -107,7 +113,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } } finally { if (restore) { - if (DEV) set_dev_current_component_function(null); + if (DEV) { + set_dev_current_component_function(null); + set_dev_stack(null); + } + set_component_context(null); set_active_reaction(null); set_active_effect(null); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index ffa57b2d8b..231a3621b1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -18,7 +18,7 @@ import { import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; -import { component_context } from '../../context.js'; +import { component_context, dev_stack } from '../../context.js'; import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; @@ -107,6 +107,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio if (DEV && location) { // @ts-expect-error element.__svelte_meta = { + parent: dev_stack, loc: { file: filename, line: location[0], diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 60f9af9120..576a30fa77 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,6 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; -export { push, pop } from './context.js'; +export { push, pop, add_svelte_meta } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; export { add_locations } from './dev/elements.js'; diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index a2806bde81..a2de8940a6 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -41,7 +41,7 @@ import { DEV } from 'esm-env'; import { define_property } from '../../shared/utils.js'; import { get_next_sibling } from '../dom/operations.js'; import { derived } from './deriveds.js'; -import { component_context, dev_current_component_function } from '../context.js'; +import { component_context, dev_current_component_function, dev_stack } from '../context.js'; /** * @param {'$effect' | '$effect.pre' | '$inspect'} rune @@ -359,7 +359,11 @@ export function template_effect(fn, thunks = [], d = derived) { * @param {number} flags */ export function block(fn, flags = 0) { - return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + if (DEV) { + effect.dev_stack = dev_stack; + } + return effect; } /** diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 88c84f27fe..80c4155705 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,10 @@ -import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; +import type { + ComponentContext, + DevStackEntry, + Equals, + TemplateNode, + TransitionManager +} from '#client'; export interface Signal { /** Flags bitmask */ @@ -80,6 +86,8 @@ export interface Effect extends Reaction { parent: Effect | null; /** Dev only */ component_function?: any; + /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */ + dev_stack?: DevStackEntry | null; } export type Source = Value; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5a798ba3e9..8e6242447e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -34,12 +34,13 @@ import { tracing_expressions, get_stack } from './dev/tracing.js'; import { component_context, dev_current_component_function, + dev_stack, is_runes, set_component_context, - set_dev_current_component_function + set_dev_current_component_function, + set_dev_stack } from './context.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; -import { snapshot } from '../shared/clone.js'; let is_flushing = false; @@ -444,6 +445,9 @@ export function update_effect(effect) { if (DEV) { var previous_component_fn = dev_current_component_function; set_dev_current_component_function(effect.component_function); + var previous_stack = /** @type {any} */ (dev_stack); + // only block effects have a dev stack, keep the current one otherwise + set_dev_stack(effect.dev_stack ?? dev_stack); } try { @@ -478,6 +482,7 @@ export function update_effect(effect) { if (DEV) { set_dev_current_component_function(previous_component_fn); + set_dev_stack(previous_stack); } } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 9703c2aac1..0b7310e172 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -187,4 +187,13 @@ export type SourceLocation = | [line: number, column: number] | [line: number, column: number, SourceLocation[]]; +export interface DevStackEntry { + file: string; + type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render'; + line: number; + column: number; + parent: DevStackEntry | null; + componentTag?: string; +} + export * from './reactivity/types'; diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js new file mode 100644 index 0000000000..eba85d5098 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/_config.js @@ -0,0 +1,186 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + compileOptions: { + dev: true + }, + html: ` +

no parent

+ +

if

+

each

+

loading

+

key

+

hi

+

hi

+

hi

+

hi

+

hi

+ `, + + async test({ target, assert }) { + function check() { + const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] = + target.querySelectorAll('p'); + + // @ts-expect-error + assert.deepEqual(main.__svelte_meta.parent, null); + + // @ts-expect-error + assert.deepEqual(if_.__svelte_meta.parent, { + file: 'main.svelte', + type: 'if', + line: 12, + column: 0, + parent: null + }); + + // @ts-expect-error + assert.deepEqual(each.__svelte_meta.parent, { + file: 'main.svelte', + type: 'each', + line: 16, + column: 0, + parent: null + }); + + // @ts-expect-error + assert.deepEqual(await_.__svelte_meta.parent, { + file: 'main.svelte', + type: 'await', + line: 20, + column: 0, + parent: null + }); + + // @ts-expect-error + assert.deepEqual(key.__svelte_meta.parent, { + file: 'main.svelte', + type: 'key', + line: 26, + column: 0, + parent: null + }); + + // @ts-expect-error + assert.deepEqual(child1.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 30, + column: 0, + parent: null + }); + + // @ts-expect-error + assert.deepEqual(child2.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 33, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 32, + column: 0, + parent: null + } + } + }); + + // @ts-expect-error + assert.deepEqual(child3.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'Child', + line: 38, + column: 2, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 37, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'render', + line: 5, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 36, + column: 0, + parent: null + } + } + } + } + }); + + // @ts-expect-error + assert.deepEqual(child4.__svelte_meta.parent, { + file: 'passthrough.svelte', + type: 'render', + line: 8, + column: 1, + parent: { + file: 'passthrough.svelte', + type: 'if', + line: 7, + column: 0, + parent: { + file: 'main.svelte', + type: 'component', + componentTag: 'Passthrough', + line: 43, + column: 1, + parent: { + file: 'main.svelte', + type: 'if', + line: 42, + column: 0, + parent: null + } + } + } + }); + + // @ts-expect-error + assert.deepEqual(dynamic.__svelte_meta.parent, { + file: 'main.svelte', + type: 'component', + componentTag: 'x.y', + line: 50, + column: 0, + parent: null + }); + } + + await tick(); + check(); + + // Test that stack is kept when re-rendering + const button = target.querySelector('button'); + button?.click(); + await tick(); + button?.click(); + await tick(); + check(); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte new file mode 100644 index 0000000000..0df6def593 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/child.svelte @@ -0,0 +1 @@ +

hi

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte new file mode 100644 index 0000000000..b9bd46d8f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/main.svelte @@ -0,0 +1,50 @@ + + +

no parent

+ + +{#if true} +

if

+{/if} + +{#each [1]} +

each

+{/each} + +{#await Promise.resolve()} +

loading

+{:then} +

await

+{/await} + +{#key key} +

key

+{/key} + + + + + + + + + + + + + +{#if show} + + {#snippet named()} +

hi

+ {/snippet} +
+{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte new file mode 100644 index 0000000000..70ba81a4c1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-meta-parent/passthrough.svelte @@ -0,0 +1,9 @@ + + +{@render children?.()} + +{#if true} + {@render named?.()} +{/if} From 49cba86e9b604f66bcd037e5eeb7a27788510b3b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Jul 2025 11:19:51 -0400 Subject: [PATCH 009/367] chore: rename EFFECT_HAS_DERIVED to EFFECT_PRESERVED (#16283) --- packages/svelte/src/internal/client/constants.js | 2 +- packages/svelte/src/internal/client/reactivity/deriveds.js | 4 ++-- packages/svelte/src/internal/client/reactivity/effects.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index cd5e0d2244..38db28fecf 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -17,7 +17,7 @@ export const EFFECT_RAN = 1 << 15; export const EFFECT_TRANSPARENT = 1 << 16; export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; -export const EFFECT_HAS_DERIVED = 1 << 20; +export const EFFECT_PRESERVED = 1 << 20; export const EFFECT_IS_UPDATING = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index d3123d24a1..fdfc13c0e2 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,6 +1,6 @@ /** @import { Derived, Effect } from '#client' */ import { DEV } from 'esm-env'; -import { CLEAN, DERIVED, DIRTY, EFFECT_HAS_DERIVED, MAYBE_DIRTY, UNOWNED } from '#client/constants'; +import { CLEAN, DERIVED, DIRTY, EFFECT_PRESERVED, MAYBE_DIRTY, UNOWNED } from '#client/constants'; import { active_reaction, active_effect, @@ -38,7 +38,7 @@ export function derived(fn) { } else { // Since deriveds are evaluated lazily, any effects created inside them are // created too late to ensure that the parent effect is added to the tree - active_effect.f |= EFFECT_HAS_DERIVED; + active_effect.f |= EFFECT_PRESERVED; } /** @type {Derived} */ diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index a2de8940a6..702c410ce8 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -31,7 +31,7 @@ import { INSPECT_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, - EFFECT_HAS_DERIVED, + EFFECT_PRESERVED, BOUNDARY_EFFECT, STALE_REACTION } from '#client/constants'; @@ -135,7 +135,7 @@ function create_effect(type, fn, sync, push = true) { effect.first === null && effect.nodes_start === null && effect.teardown === null && - (effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0; + (effect.f & (EFFECT_PRESERVED | BOUNDARY_EFFECT)) === 0; if (!inert && push) { if (parent !== null) { From 7914cb183554b16627bec40c8e91b163beaf0480 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Jul 2025 12:16:53 -0400 Subject: [PATCH 010/367] chore: convert boundary implementation to a class (#16284) * WIP class boundaries * WIP * WIP * WIP * unused * unused * unused --- .../internal/client/dom/blocks/boundary.js | 233 +++++++++++------- .../src/internal/client/error-handling.js | 4 +- .../src/internal/client/reactivity/effects.js | 1 + .../src/internal/client/reactivity/types.d.ts | 3 + 4 files changed, 149 insertions(+), 92 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d7a53da1d1..03cec4fd2f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -1,6 +1,5 @@ /** @import { Effect, TemplateNode, } from '#client' */ - -import { BOUNDARY_EFFECT, EFFECT_TRANSPARENT } from '#client/constants'; +import { BOUNDARY_EFFECT, EFFECT_PRESERVED, EFFECT_TRANSPARENT } from '#client/constants'; import { component_context, set_component_context } from '../../context.js'; import { invoke_error_boundary } from '../../error-handling.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; @@ -21,116 +20,170 @@ import { import { queue_micro_task } from '../task.js'; /** - * @param {Effect} boundary - * @param {() => void} fn + * @typedef {{ + * onerror?: (error: unknown, reset: () => void) => void; + * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void; + * }} BoundaryProps */ -function with_boundary(boundary, fn) { - var previous_effect = active_effect; - var previous_reaction = active_reaction; - var previous_ctx = component_context; - - set_active_effect(boundary); - set_active_reaction(boundary); - set_component_context(boundary.ctx); - - try { - fn(); - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - set_component_context(previous_ctx); - } -} + +var flags = EFFECT_TRANSPARENT | EFFECT_PRESERVED | BOUNDARY_EFFECT; /** * @param {TemplateNode} node - * @param {{ - * onerror?: (error: unknown, reset: () => void) => void, - * failed?: (anchor: Node, error: () => unknown, reset: () => () => void) => void - * }} props - * @param {((anchor: Node) => void)} boundary_fn + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children * @returns {void} */ -export function boundary(node, props, boundary_fn) { - var anchor = node; +export function boundary(node, props, children) { + new Boundary(node, props, children); +} + +export class Boundary { + /** @type {TemplateNode} */ + #anchor; + + /** @type {TemplateNode} */ + #hydrate_open; + + /** @type {BoundaryProps} */ + #props; + + /** @type {((anchor: Node) => void)} */ + #children; /** @type {Effect} */ - var boundary_effect; - - block(() => { - var boundary = /** @type {Effect} */ (active_effect); - var hydrate_open = hydrate_node; - var is_creating_fallback = false; - - // We re-use the effect's fn property to avoid allocation of an additional field - boundary.fn = (/** @type {unknown}} */ error) => { - var onerror = props.onerror; - let failed = props.failed; - - // If we have nothing to capture the error, or if we hit an error while - // rendering the fallback, re-throw for another boundary to handle - if ((!onerror && !failed) || is_creating_fallback) { - throw error; - } + #effect; - var reset = () => { - pause_effect(boundary_effect); + /** @type {Effect | null} */ + #main_effect = null; - with_boundary(boundary, () => { - is_creating_fallback = false; - boundary_effect = branch(() => boundary_fn(anchor)); - }); - }; + /** @type {Effect | null} */ + #failed_effect = null; - var previous_reaction = active_reaction; + #is_creating_fallback = false; - try { - set_active_reaction(null); - onerror?.(error, reset); - } finally { - set_active_reaction(previous_reaction); + /** + * @param {TemplateNode} node + * @param {BoundaryProps} props + * @param {((anchor: Node) => void)} children + */ + constructor(node, props, children) { + this.#anchor = node; + this.#props = props; + this.#children = children; + + this.#hydrate_open = hydrate_node; + + this.#effect = block(() => { + /** @type {Effect} */ (active_effect).b = this; + + if (hydrating) { + hydrate_next(); } - if (boundary_effect) { - destroy_effect(boundary_effect); - } else if (hydrating) { - set_hydrate_node(hydrate_open); - next(); - set_hydrate_node(remove_nodes()); + try { + this.#main_effect = branch(() => children(this.#anchor)); + } catch (error) { + this.error(error); } + }, flags); - if (failed) { - // Render the `failed` snippet in a microtask - queue_micro_task(() => { - with_boundary(boundary, () => { - is_creating_fallback = true; - - try { - boundary_effect = branch(() => { - failed( - anchor, - () => error, - () => reset - ); - }); - } catch (error) { - invoke_error_boundary(error, /** @type {Effect} */ (boundary.parent)); - } - - is_creating_fallback = false; - }); + if (hydrating) { + this.#anchor = hydrate_node; + } + } + + /** + * @param {() => Effect | null} fn + */ + #run(fn) { + var previous_effect = active_effect; + var previous_reaction = active_reaction; + var previous_ctx = component_context; + + set_active_effect(this.#effect); + set_active_reaction(this.#effect); + set_component_context(this.#effect.ctx); + + try { + return fn(); + } finally { + set_active_effect(previous_effect); + set_active_reaction(previous_reaction); + set_component_context(previous_ctx); + } + } + + /** @param {unknown} error */ + error(error) { + var onerror = this.#props.onerror; + let failed = this.#props.failed; + + const reset = () => { + if (this.#failed_effect !== null) { + pause_effect(this.#failed_effect, () => { + this.#failed_effect = null; }); } + + this.#main_effect = this.#run(() => { + this.#is_creating_fallback = false; + return branch(() => this.#children(this.#anchor)); + }); }; - if (hydrating) { - hydrate_next(); + // If we have nothing to capture the error, or if we hit an error while + // rendering the fallback, re-throw for another boundary to handle + if (this.#is_creating_fallback || (!onerror && !failed)) { + throw error; + } + + var previous_reaction = active_reaction; + + try { + set_active_reaction(null); + onerror?.(error, reset); + } finally { + set_active_reaction(previous_reaction); } - boundary_effect = branch(() => boundary_fn(anchor)); - }, EFFECT_TRANSPARENT | BOUNDARY_EFFECT); + if (this.#main_effect) { + destroy_effect(this.#main_effect); + this.#main_effect = null; + } - if (hydrating) { - anchor = hydrate_node; + if (this.#failed_effect) { + destroy_effect(this.#failed_effect); + this.#failed_effect = null; + } + + if (hydrating) { + set_hydrate_node(this.#hydrate_open); + next(); + set_hydrate_node(remove_nodes()); + } + + if (failed) { + queue_micro_task(() => { + this.#failed_effect = this.#run(() => { + this.#is_creating_fallback = true; + + try { + return branch(() => { + failed( + this.#anchor, + () => error, + () => reset + ); + }); + } catch (error) { + invoke_error_boundary(error, /** @type {Effect} */ (this.#effect.parent)); + return null; + } finally { + this.#is_creating_fallback = false; + } + }); + }); + } } } diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 99f3ed6cd4..378f7408ef 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -1,4 +1,5 @@ /** @import { Effect } from '#client' */ +/** @import { Boundary } from './dom/blocks/boundary.js' */ import { DEV } from 'esm-env'; import { FILENAME } from '../../constants.js'; import { is_firefox } from './dom/operations.js'; @@ -39,8 +40,7 @@ export function invoke_error_boundary(error, effect) { while (effect !== null) { if ((effect.f & BOUNDARY_EFFECT) !== 0) { try { - // @ts-expect-error - effect.fn(error); + /** @type {Boundary} */ (effect.b).error(error); return; } catch {} } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 702c410ce8..7570064c37 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -104,6 +104,7 @@ function create_effect(type, fn, sync, push = true) { last: null, next: null, parent, + b: parent && parent.b, prev: null, teardown: null, transitions: null, diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 80c4155705..90f922df68 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -5,6 +5,7 @@ import type { TemplateNode, TransitionManager } from '#client'; +import type { Boundary } from '../dom/blocks/boundary'; export interface Signal { /** Flags bitmask */ @@ -84,6 +85,8 @@ export interface Effect extends Reaction { last: null | Effect; /** Parent effect */ parent: Effect | null; + /** The boundary this effect belongs to */ + b: Boundary | null; /** Dev only */ component_function?: any; /** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */ From dde791311d3c6a7b28e276548e6beb45a9f2e798 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 2 Jul 2025 13:06:45 -0400 Subject: [PATCH 011/367] chore: minor tweaks (#16285) --- packages/svelte/src/internal/client/constants.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 38db28fecf..1cffa43940 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -15,17 +15,17 @@ export const DESTROYED = 1 << 14; export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ export const EFFECT_TRANSPARENT = 1 << 16; -export const INSPECT_EFFECT = 1 << 18; -export const HEAD_EFFECT = 1 << 19; -export const EFFECT_PRESERVED = 1 << 20; -export const EFFECT_IS_UPDATING = 1 << 21; +export const INSPECT_EFFECT = 1 << 17; +export const HEAD_EFFECT = 1 << 18; +export const EFFECT_PRESERVED = 1 << 19; +export const EFFECT_IS_UPDATING = 1 << 20; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); export const LOADING_ATTR_SYMBOL = Symbol(''); export const PROXY_PATH_SYMBOL = Symbol('proxy path'); -// allow users to ignore aborted signal errors if `reason.stale` +/** allow users to ignore aborted signal errors if `reason.name === 'StaleReactionError` */ export const STALE_REACTION = new (class StaleReactionError extends Error { name = 'StaleReactionError'; message = 'The reaction that called `getAbortSignal()` was re-run or destroyed'; From 5ebbedf2051715f1df10a884976d7d23b1d7f5fc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:18:28 +0200 Subject: [PATCH 012/367] Version Packages (#16281) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hot-buses-end.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/hot-buses-end.md diff --git a/.changeset/hot-buses-end.md b/.changeset/hot-buses-end.md deleted file mode 100644 index dd5a28fca8..0000000000 --- a/.changeset/hot-buses-end.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -feat: add parent hierarchy to `__svelte_meta` objects diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 7589eda728..fb5d43c9dd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.35.1 + +### Patch Changes + +- feat: add parent hierarchy to `__svelte_meta` objects ([#16255](https://github.com/sveltejs/svelte/pull/16255)) + ## 5.35.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e8c28aeaa1..b1e57b9829 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.0", + "version": "5.35.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index baa2dbd046..2efcf03eb1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.0'; +export const VERSION = '5.35.1'; export const PUBLIC_VERSION = '5'; From eeb32a50d14d2ac69970b0e207746092a34d2fc3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 11:47:26 -0400 Subject: [PATCH 013/367] fix: bump esrap (#16295) --- .changeset/fifty-hats-watch.md | 5 +++++ packages/svelte/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/fifty-hats-watch.md diff --git a/.changeset/fifty-hats-watch.md b/.changeset/fifty-hats-watch.md new file mode 100644 index 0000000000..83bb72e735 --- /dev/null +++ b/.changeset/fifty-hats-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: bump esrap diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b1e57b9829..b6776a8fbc 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -171,7 +171,7 @@ "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", - "esrap": "^2.0.0", + "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b7fb655b2d..87fb2b2667 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,8 +87,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 esrap: - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.1.0 + version: 2.1.0 is-reference: specifier: ^3.0.3 version: 3.0.3 @@ -1261,8 +1261,8 @@ packages: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} - esrap@2.0.0: - resolution: {integrity: sha512-zhw1TDqno99Ld5wOpe0t47rzVyxfGc1fvxNzPsqk4idUf5dcAePkAyfTceLJaSTytjiWDu26S5tI+grjvymXJA==} + esrap@2.1.0: + resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -3622,7 +3622,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.0.0: + esrap@2.1.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 From 220e901a825a23df7c310cd5978d3141e573e645 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 11:50:58 -0400 Subject: [PATCH 014/367] Version Packages (#16297) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fifty-hats-watch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fifty-hats-watch.md diff --git a/.changeset/fifty-hats-watch.md b/.changeset/fifty-hats-watch.md deleted file mode 100644 index 83bb72e735..0000000000 --- a/.changeset/fifty-hats-watch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: bump esrap diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index fb5d43c9dd..ad72bfabb0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.35.2 + +### Patch Changes + +- fix: bump esrap ([#16295](https://github.com/sveltejs/svelte/pull/16295)) + ## 5.35.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b6776a8fbc..872c21a179 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.1", + "version": "5.35.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2efcf03eb1..47c354c7a2 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.1'; +export const VERSION = '5.35.2'; export const PUBLIC_VERSION = '5'; From 432763a03ebcb4c03e7c5084cb52aac6f1afaa22 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 3 Jul 2025 12:11:28 -0400 Subject: [PATCH 015/367] chore: `reaction_sources` -> `source_ownership` (#16287) * reaction_sources -> source_ownership * put equality statements first, it's faster --- .../src/internal/client/reactivity/sources.js | 4 +-- .../svelte/src/internal/client/runtime.js | 28 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 0db3530232..a86ccfee4f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,7 +11,7 @@ import { untrack, increment_write_version, update_effect, - reaction_sources, + source_ownership, check_dirtiness, untracking, is_destroying_effect, @@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) { (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 && - !(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) + !(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source)) ) { e.state_unsafe_mutation(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8e6242447e..fce6c78b56 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -86,17 +86,17 @@ export function set_active_effect(effect) { /** * When sources are created within a reaction, reading and writing * them within that reaction should not cause a re-run - * @type {null | [active_reaction: Reaction, sources: Source[]]} + * @type {null | { reaction: Reaction, sources: Source[] }} */ -export let reaction_sources = null; +export let source_ownership = null; /** @param {Value} value */ export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { - if (reaction_sources === null) { - reaction_sources = [active_reaction, [value]]; + if (source_ownership === null) { + source_ownership = { reaction: active_reaction, sources: [value] }; } else { - reaction_sources[1].push(value); + source_ownership.sources.push(value); } } } @@ -235,7 +235,12 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; - if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue; + if ( + source_ownership?.reaction === active_reaction && + source_ownership.sources.includes(signal) + ) { + continue; + } if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); @@ -257,7 +262,7 @@ export function update_reaction(reaction) { var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; var previous_skip_reaction = skip_reaction; - var previous_reaction_sources = reaction_sources; + var previous_reaction_sources = source_ownership; var previous_component_context = component_context; var previous_untracking = untracking; @@ -270,7 +275,7 @@ export function update_reaction(reaction) { (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - reaction_sources = null; + source_ownership = null; set_component_context(reaction.ctx); untracking = false; read_version++; @@ -358,7 +363,7 @@ export function update_reaction(reaction) { untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; skip_reaction = previous_skip_reaction; - reaction_sources = previous_reaction_sources; + source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; @@ -739,7 +744,10 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - if (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) { + if ( + source_ownership?.reaction !== active_reaction || + !source_ownership?.sources.includes(signal) + ) { var deps = active_reaction.deps; if (signal.rv < read_version) { signal.rv = read_version; From c3348aea06b5dcc13159b96053351f36d6cb6d71 Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 4 Jul 2025 17:54:37 +0300 Subject: [PATCH 016/367] fix: do not proxify the value assigned to a derived (#16302) --- .changeset/new-wolves-punch.md | 5 ++++ .../client/visitors/AssignmentExpression.js | 1 + .../samples/writable-derived-4/_config.js | 23 +++++++++++++++++++ .../samples/writable-derived-4/main.svelte | 9 ++++++++ 4 files changed, 38 insertions(+) create mode 100644 .changeset/new-wolves-punch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-4/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived-4/main.svelte diff --git a/.changeset/new-wolves-punch.md b/.changeset/new-wolves-punch.md new file mode 100644 index 0000000000..c856038cbc --- /dev/null +++ b/.changeset/new-wolves-punch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: do not proxify the value assigned to a derived diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index ce190814f8..7d64d60bca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -137,6 +137,7 @@ function build_assignment(operator, left, right, context) { binding.kind !== 'prop' && binding.kind !== 'bindable_prop' && binding.kind !== 'raw_state' && + binding.kind !== 'derived' && binding.kind !== 'store_sub' && context.state.analysis.runes && should_proxy(right, context.state.scope) && diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-4/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived-4/_config.js new file mode 100644 index 0000000000..8d312ca73a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-4/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [btn1, btn2, btn3] = target.getElementsByTagName('button'); + const [div] = target.getElementsByTagName('div'); + + flushSync(() => btn1.click()); + assert.equal(div.textContent, '1'); + flushSync(() => btn2.click()); + assert.equal(div.textContent, '1'); + flushSync(() => btn3.click()); + assert.equal(div.textContent, '2'); + + flushSync(() => btn1.click()); + assert.equal(div.textContent, '2'); + flushSync(() => btn2.click()); + assert.equal(div.textContent, '2'); + flushSync(() => btn3.click()); + assert.equal(div.textContent, '3'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived-4/main.svelte new file mode 100644 index 0000000000..a24f69ba77 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived-4/main.svelte @@ -0,0 +1,9 @@ + + + + + + +
{count.value}
From 834cd91b367172859b26d35a23aad4e87800b881 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sun, 6 Jul 2025 14:57:45 +0200 Subject: [PATCH 017/367] fix: account for mounting when `select_option` in `attribute_effect` (#16309) --- .changeset/curvy-carrots-prove.md | 5 +++++ .../src/internal/client/dom/elements/attributes.js | 5 +++-- .../samples/select-spread-option-selected/_config.js | 9 +++++++++ .../samples/select-spread-option-selected/main.svelte | 8 ++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 .changeset/curvy-carrots-prove.md create mode 100644 packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/main.svelte diff --git a/.changeset/curvy-carrots-prove.md b/.changeset/curvy-carrots-prove.md new file mode 100644 index 0000000000..7723990179 --- /dev/null +++ b/.changeset/curvy-carrots-prove.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: account for mounting when `select_option` in `attribute_effect` diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 5db685cf3e..c96338778f 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -514,9 +514,10 @@ export function attribute_effect( if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); - + var mounting = true; effect(() => { - select_option(select, /** @type {Record} */ (prev).value); + select_option(select, /** @type {Record} */ (prev).value, mounting); + mounting = false; init_select(select); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/_config.js b/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/_config.js new file mode 100644 index 0000000000..50da3ac095 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/_config.js @@ -0,0 +1,9 @@ +import { ok, test } from '../../test'; + +export default test({ + async test({ assert, target, instance }) { + const select = target.querySelector('select'); + ok(select); + assert.equal(select.selectedIndex, 1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/main.svelte b/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/main.svelte new file mode 100644 index 0000000000..e5fc6c5535 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/select-spread-option-selected/main.svelte @@ -0,0 +1,8 @@ + + + From 4a26edf54576152affe8649eca8c9b3d1a5b8015 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 10:17:17 -0400 Subject: [PATCH 018/367] chore: remove unused `mounting` variable from `select_option` call (#16312) --- .../svelte/src/internal/client/dom/elements/attributes.js | 7 +++---- .../src/internal/client/dom/elements/bindings/select.js | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index c96338778f..66f484a59a 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -491,7 +491,7 @@ export function attribute_effect( var current = set_attributes(element, prev, next, css_hash, skip_warning); if (inited && is_select && 'value' in next) { - select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); + select_option(/** @type {HTMLSelectElement} */ (element), next.value); } for (let symbol of Object.getOwnPropertySymbols(effects)) { @@ -514,10 +514,9 @@ export function attribute_effect( if (is_select) { var select = /** @type {HTMLSelectElement} */ (element); - var mounting = true; + effect(() => { - select_option(select, /** @type {Record} */ (prev).value, mounting); - mounting = false; + select_option(select, /** @type {Record} */ (prev).value, true); init_select(select); }); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index 5e89686d86..e39fb865cd 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -9,9 +9,9 @@ import * as w from '../../../warnings.js'; * @template V * @param {HTMLSelectElement} select * @param {V} value - * @param {boolean} [mounting] + * @param {boolean} mounting */ -export function select_option(select, value, mounting) { +export function select_option(select, value, mounting = false) { if (select.multiple) { // If value is null or undefined, keep the selection as is if (value == undefined) { From 63e48365fbad844f5a82cf5e5a994fdfd13f419b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 10:53:41 -0400 Subject: [PATCH 019/367] Version Packages (#16303) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/curvy-carrots-prove.md | 5 ----- .changeset/new-wolves-punch.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/curvy-carrots-prove.md delete mode 100644 .changeset/new-wolves-punch.md diff --git a/.changeset/curvy-carrots-prove.md b/.changeset/curvy-carrots-prove.md deleted file mode 100644 index 7723990179..0000000000 --- a/.changeset/curvy-carrots-prove.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: account for mounting when `select_option` in `attribute_effect` diff --git a/.changeset/new-wolves-punch.md b/.changeset/new-wolves-punch.md deleted file mode 100644 index c856038cbc..0000000000 --- a/.changeset/new-wolves-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: do not proxify the value assigned to a derived diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ad72bfabb0..4f1be5e299 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.3 + +### Patch Changes + +- fix: account for mounting when `select_option` in `attribute_effect` ([#16309](https://github.com/sveltejs/svelte/pull/16309)) + +- fix: do not proxify the value assigned to a derived ([#16302](https://github.com/sveltejs/svelte/pull/16302)) + ## 5.35.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 872c21a179..30a8f8833a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.2", + "version": "5.35.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 47c354c7a2..d98b622908 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.2'; +export const VERSION = '5.35.3'; export const PUBLIC_VERSION = '5'; From cf2ff5e8dc908395853e3a7cc2022fe3552f7062 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 11:46:57 -0400 Subject: [PATCH 020/367] fix: abort and reschedule effect processing after state change in user effect (#16280) * fix: abort and reschedule effect processing after state change in user effect * failing test * skip for now --- .changeset/cuddly-walls-tan.md | 5 ++++ .../svelte/src/internal/client/constants.js | 1 + .../svelte/src/internal/client/context.js | 4 ++-- .../src/internal/client/reactivity/effects.js | 15 ++++++++---- .../svelte/src/internal/client/runtime.js | 15 +++++++++++- .../samples/effect-order-6/A.svelte | 11 +++++++++ .../samples/effect-order-6/B.svelte | 9 ++++++++ .../samples/effect-order-6/Child.svelte | 20 ++++++++++++++++ .../samples/effect-order-6/_config.js | 13 +++++++++++ .../samples/effect-order-6/main.svelte | 23 +++++++++++++++++++ .../samples/effect-order-7/A.svelte | 9 ++++++++ .../samples/effect-order-7/B.svelte | 9 ++++++++ .../samples/effect-order-7/Child.svelte | 20 ++++++++++++++++ .../samples/effect-order-7/_config.js | 15 ++++++++++++ .../samples/effect-order-7/main.svelte | 22 ++++++++++++++++++ 15 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 .changeset/cuddly-walls-tan.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte diff --git a/.changeset/cuddly-walls-tan.md b/.changeset/cuddly-walls-tan.md new file mode 100644 index 0000000000..feececc052 --- /dev/null +++ b/.changeset/cuddly-walls-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: abort and reschedule effect processing after state change in user effect diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 1cffa43940..a5a8b42972 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -19,6 +19,7 @@ export const INSPECT_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const EFFECT_IS_UPDATING = 1 << 20; +export const USER_EFFECT = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const LEGACY_PROPS = Symbol('legacy props'); diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index e4220149ab..736b81c172 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -9,7 +9,7 @@ import { set_active_effect, set_active_reaction } from './runtime.js'; -import { effect, teardown } from './reactivity/effects.js'; +import { create_user_effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; @@ -191,7 +191,7 @@ export function pop(component) { var component_effect = component_effects[i]; set_active_effect(component_effect.effect); set_active_reaction(component_effect.reaction); - effect(component_effect.fn); + create_user_effect(component_effect.fn); } } finally { set_active_effect(previous_effect); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 7570064c37..df2afd3e38 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -33,7 +33,8 @@ import { MAYBE_DIRTY, EFFECT_PRESERVED, BOUNDARY_EFFECT, - STALE_REACTION + STALE_REACTION, + USER_EFFECT } from '#client/constants'; import { set } from './sources.js'; import * as e from '../errors.js'; @@ -200,11 +201,17 @@ export function user_effect(fn) { reaction: active_reaction }); } else { - var signal = effect(fn); - return signal; + return create_user_effect(fn); } } +/** + * @param {() => void | (() => void)} fn + */ +export function create_user_effect(fn) { + return create_effect(EFFECT | USER_EFFECT, fn, false); +} + /** * Internal representation of `$effect.pre(...)` * @param {() => void | (() => void)} fn @@ -217,7 +224,7 @@ export function user_pre_effect(fn) { value: '$effect.pre' }); } - return render_effect(fn); + return create_effect(RENDER_EFFECT | USER_EFFECT, fn, true); } /** @param {() => void | (() => void)} fn */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fce6c78b56..c00ade5587 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, DISCONNECTED, EFFECT_IS_UPDATING, - STALE_REACTION + STALE_REACTION, + USER_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -581,6 +582,8 @@ function flush_queued_effects(effects) { if ((effect.f & (DESTROYED | INERT)) === 0) { if (check_dirtiness(effect)) { + var wv = write_version; + update_effect(effect); // Effects with no dependencies or teardown do not get added to the effect tree. @@ -597,9 +600,19 @@ function flush_queued_effects(effects) { effect.fn = null; } } + + // if state is written in a user effect, abort and re-schedule, lest we run + // effects that should be removed as a result of the state change + if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + break; + } } } } + + for (; i < length; i += 1) { + schedule_effect(effects[i]); + } } /** diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte new file mode 100644 index 0000000000..2e789a0460 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/A.svelte @@ -0,0 +1,11 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte new file mode 100644 index 0000000000..1fad19bc15 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/B.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte new file mode 100644 index 0000000000..b905b4b4d7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/Child.svelte @@ -0,0 +1,20 @@ + + + + +{#if object?.boolean} + + {@render children(object.boolean)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js new file mode 100644 index 0000000000..8f9077e954 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [open, close] = target.querySelectorAll('button'); + + flushSync(() => open.click()); + flushSync(() => close.click()); + + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte new file mode 100644 index 0000000000..eee487fa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-6/main.svelte @@ -0,0 +1,23 @@ + + + + + + +
+ + + {#snippet children(boolean)} + + {/snippet} + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte new file mode 100644 index 0000000000..54f4869d62 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/A.svelte @@ -0,0 +1,9 @@ + + +{boolean} + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte new file mode 100644 index 0000000000..2a2e634db1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/B.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte new file mode 100644 index 0000000000..9606fd8602 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/Child.svelte @@ -0,0 +1,20 @@ + + + + +{#if object?.nested} + + {@render children(object.nested)} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js new file mode 100644 index 0000000000..29c33c7b18 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, + + async test({ assert, target, logs }) { + const [open, close] = target.querySelectorAll('button'); + + flushSync(() => open.click()); + flushSync(() => close.click()); + + assert.deepEqual(logs, [true]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte new file mode 100644 index 0000000000..c9c45c50cf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/main.svelte @@ -0,0 +1,22 @@ + + + + + + +
+ + + {#snippet children(nested)} +
+ {/snippet} +
From eb094ba059d262e5583108a85b0e549b95dbd344 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 12:37:51 -0400 Subject: [PATCH 021/367] Version Packages (#16313) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cuddly-walls-tan.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/cuddly-walls-tan.md diff --git a/.changeset/cuddly-walls-tan.md b/.changeset/cuddly-walls-tan.md deleted file mode 100644 index feececc052..0000000000 --- a/.changeset/cuddly-walls-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: abort and reschedule effect processing after state change in user effect diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4f1be5e299..89ae380840 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.35.4 + +### Patch Changes + +- fix: abort and reschedule effect processing after state change in user effect ([#16280](https://github.com/sveltejs/svelte/pull/16280)) + ## 5.35.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 30a8f8833a..aaf8f26fec 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.3", + "version": "5.35.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d98b622908..c4111b9e8d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.3'; +export const VERSION = '5.35.4'; export const PUBLIC_VERSION = '5'; From 140462374a759d7c38262e9823a7db6c73bcc067 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 7 Jul 2025 13:32:15 -0400 Subject: [PATCH 022/367] fix: re-evaluate derived props during teardown (#16278) * Store deriveds old value before updating them for consistency with directly assigned sources when reading in teardown functions * Add tests for derived old values in teardown functions * Add tests for props old values in onDestroy hooks to prevent regressions * Add changeset * Update comment * remove extraneous tests - these pass with or without the src change * revert * WIP * simplify props * simplify * tweak * reorder a bit * simplify * unused * more * more * tweak * also appears to be unnecessary * changeset * fix * Update .changeset/light-rivers-jump.md * easier debugging * fix * WIP * fix --------- Co-authored-by: raythurnvoid <53383860+raythurnvoid@users.noreply.github.com> --- .changeset/light-rivers-jump.md | 5 ++ .../internal/client/reactivity/deriveds.js | 3 +- .../src/internal/client/reactivity/props.js | 80 ++++++++++++++----- .../svelte/src/internal/client/runtime.js | 44 +++++++++- .../derived-cleanup-old-value/_config.js | 22 +++++ .../derived-cleanup-old-value/main.svelte | 20 +++++ .../effect-teardown-derived/Component.svelte | 15 ++++ .../effect-teardown-derived/_config.js | 17 ++++ .../effect-teardown-derived/main.svelte | 13 +++ 9 files changed, 194 insertions(+), 25 deletions(-) create mode 100644 .changeset/light-rivers-jump.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md new file mode 100644 index 0000000000..2454d57156 --- /dev/null +++ b/.changeset/light-rivers-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: re-evaluate derived props during teardown diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fdfc13c0e2..e563c77534 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -19,6 +19,7 @@ import { inspect_effects, set_inspect_effects } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { tracing_mode_flag } from '../../flags/index.js'; import { component_context } from '../context.js'; +import { UNINITIALIZED } from '../../../constants.js'; /** * @template V @@ -51,7 +52,7 @@ export function derived(fn) { fn, reactions: null, rv: 0, - v: /** @type {V} */ (null), + v: /** @type {V} */ (UNINITIALIZED), wv: 0, parent: parent_derived ?? active_effect, ac: null diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 3501bcd3c7..03daad5251 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,18 +1,26 @@ -/** @import { Derived, Source } from './types.js' */ +/** @import { ComponentContext } from '#client' */ +/** @import { Derived, Effect, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED + PROPS_IS_UPDATED, + UNINITIALIZED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { get, untrack } from '../runtime.js'; +import { + active_effect, + get, + is_destroying_effect, + set_active_effect, + untrack +} from '../runtime.js'; import * as e from '../errors.js'; -import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; +import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -92,7 +100,7 @@ export function rest_props(props, exclude, name) { /** * The proxy handler for legacy $$restProps and $$props - * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source }>}} + * @type {ProxyHandler<{ props: Record, exclude: Array, special: Record unknown>, version: Source, parent_effect: Effect }>}} */ const legacy_rest_props_handler = { get(target, key) { @@ -102,17 +110,25 @@ const legacy_rest_props_handler = { }, set(target, key, value) { if (!(key in target.special)) { - // Handle props that can temporarily get out of sync with the parent - /** @type {Record unknown>} */ - target.special[key] = prop( - { - get [key]() { - return target.props[key]; - } - }, - /** @type {string} */ (key), - PROPS_IS_UPDATED - ); + var previous_effect = active_effect; + + try { + set_active_effect(target.parent_effect); + + // Handle props that can temporarily get out of sync with the parent + /** @type {Record unknown>} */ + target.special[key] = prop( + { + get [key]() { + return target.props[key]; + } + }, + /** @type {string} */ (key), + PROPS_IS_UPDATED + ); + } finally { + set_active_effect(previous_effect); + } } target.special[key](value); @@ -151,7 +167,19 @@ const legacy_rest_props_handler = { * @returns {Record} */ export function legacy_rest_props(props, exclude) { - return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler); + return new Proxy( + { + props, + exclude, + special: {}, + version: source(0), + // TODO this is only necessary because we need to track component + // destruction inside `prop`, because of `bind:this`, but it + // seems likely that we can simplify `bind:this` instead + parent_effect: /** @type {Effect} */ (active_effect) + }, + legacy_rest_props_handler + ); } /** @@ -366,16 +394,24 @@ export function prop(props, key, flags, fallback) { // create a derived that we can write to locally. // Or we are in legacy mode where we always create a derived to replicate that // Svelte 4 did not trigger updates when a primitive value was updated to the same value. - var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); + var overridden = false; + + var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => { + overridden = false; + return getter(); + }); // Capture the initial value if it's bindable if (bindable) get(d); + var parent_effect = /** @type {Effect} */ (active_effect); + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; set(d, new_value); + overridden = true; if (fallback_value !== undefined) { fallback_value = new_value; @@ -384,8 +420,12 @@ export function prop(props, key, flags, fallback) { return value; } - // TODO is this still necessary post-#16263? - if (has_destroyed_component_ctx(d)) { + // special case — avoid recalculating the derived if we're in a + // teardown function and the prop was overridden locally, or the + // component was already destroyed (this latter part is necessary + // because `bind:this` can read props after the component has + // been destroyed. TODO simplify `bind:this` + if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) { return d.v; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c00ade5587..28fcf26e83 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; -import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; +import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { tracing_mode_flag } from '../flags/index.js'; @@ -42,6 +42,7 @@ import { set_dev_stack } from './context.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; +import { UNINITIALIZED } from '../../constants.js'; let is_flushing = false; @@ -795,7 +796,7 @@ export function get(signal) { } } - if (is_derived) { + if (is_derived && !is_destroying_effect) { derived = /** @type {Derived} */ (signal); if (check_dirtiness(derived)) { @@ -836,13 +837,48 @@ export function get(signal) { } } - if (is_destroying_effect && old_values.has(signal)) { - return old_values.get(signal); + if (is_destroying_effect) { + if (old_values.has(signal)) { + return old_values.get(signal); + } + + if (is_derived) { + derived = /** @type {Derived} */ (signal); + + var value = derived.v; + + // if the derived is dirty, or depends on the values that just changed, re-execute + if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) { + value = execute_derived(derived); + } + + old_values.set(derived, value); + + return value; + } } return signal.v; } +/** @param {Derived} derived */ +function depends_on_old_values(derived) { + if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst + if (derived.deps === null) return false; + + for (const dep of derived.deps) { + if (old_values.has(dep)) { + return true; + } + + if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) { + return true; + } + } + + return false; +} + /** * Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared * @template V diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js new file mode 100644 index 0000000000..694dccdcf8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs, target }) { + const [increment] = target.querySelectorAll('button'); + + flushSync(() => increment.click()); + flushSync(() => increment.click()); + flushSync(() => increment.click()); + + assert.deepEqual(logs, [ + 'count: 1', + 'squared: 1', + 'count: 2', + 'squared: 4', + 'count: 3', + 'squared: 9', + 'count: 4' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte new file mode 100644 index 0000000000..a4c58a8e99 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-cleanup-old-value/main.svelte @@ -0,0 +1,20 @@ + + + + +

count: {count}

+ +{#if count % 2 === 0} +

squared: {squared}

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte new file mode 100644 index 0000000000..3c863c7fc9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/Component.svelte @@ -0,0 +1,15 @@ + + +

{count}

+ + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js new file mode 100644 index 0000000000..1f6e5d1f7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/_config.js @@ -0,0 +1,17 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [increment, toggle] = target.querySelectorAll('button'); + + flushSync(() => toggle.click()); + assert.deepEqual(logs, [0, 'hello']); + + flushSync(() => toggle.click()); + flushSync(() => increment.click()); + flushSync(() => increment.click()); + + assert.deepEqual(logs, [0, 'hello', 1, 'hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte new file mode 100644 index 0000000000..1a97d3e760 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-teardown-derived/main.svelte @@ -0,0 +1,13 @@ + + + + + +{#if count < 2 && message === 'hello'} + +{/if} From c9098bcaa0cf7accf7de4a16f8b72671023e69e7 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 9 Jul 2025 15:32:18 +0200 Subject: [PATCH 023/367] fix: use `state` instead of `source` in reactive classes (#16239) * fix: use `state` instead of `source` in reactive classes * fix: use `active_reaction` as indication to use `source` or `state` * fix: cleanup `#initial_reaction` on `teardown` to free memory * fix: use `#source` in `set` too * unused * chore: use WeakRef * use update_version instead of WeakRef in SvelteSet/SvelteMap (#16324) * tidy up * tweak comment to remove active_reaction reference --------- Co-authored-by: Rich Harris --- .../svelte/src/internal/client/runtime.js | 6 +- packages/svelte/src/motion/spring.js | 12 +- packages/svelte/src/motion/tweened.js | 6 +- packages/svelte/src/reactivity/map.js | 25 ++++- packages/svelte/src/reactivity/set.js | 18 ++- .../side-effect-derived-map/_config.js | 33 +++++- .../side-effect-derived-map/main.svelte | 106 +++++++++++++++--- .../side-effect-derived-set/_config.js | 12 +- .../side-effect-derived-set/main.svelte | 51 ++++++--- .../side-effect-derived-spring/_config.js | 23 ++++ .../side-effect-derived-spring/main.svelte | 26 +++++ .../side-effect-derived-tween/_config.js | 23 ++++ .../side-effect-derived-tween/main.svelte | 26 +++++ 13 files changed, 318 insertions(+), 49 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 28fcf26e83..cd7170d326 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -134,6 +134,8 @@ let write_version = 1; /** @type {number} Used to version each read of a source of derived to avoid duplicating depedencies inside a reaction */ let read_version = 0; +export let update_version = read_version; + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -267,6 +269,7 @@ export function update_reaction(reaction) { var previous_reaction_sources = source_ownership; var previous_component_context = component_context; var previous_untracking = untracking; + var previous_update_version = update_version; var flags = reaction.f; @@ -280,7 +283,7 @@ export function update_reaction(reaction) { source_ownership = null; set_component_context(reaction.ctx); untracking = false; - read_version++; + update_version = ++read_version; reaction.f |= EFFECT_IS_UPDATING; @@ -368,6 +371,7 @@ export function update_reaction(reaction) { source_ownership = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + update_version = previous_update_version; reaction.f ^= EFFECT_IS_UPDATING; } diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 0f3bc6fb9f..44be1a501b 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -5,7 +5,7 @@ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { render_effect } from '../internal/client/reactivity/effects.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; @@ -170,9 +170,9 @@ export function spring(value, opts = {}) { * @since 5.8.0 */ export class Spring { - #stiffness = source(0.15); - #damping = source(0.8); - #precision = source(0.01); + #stiffness = state(0.15); + #damping = state(0.8); + #precision = state(0.01); #current; #target; @@ -194,8 +194,8 @@ export class Spring { * @param {SpringOpts} [options] */ constructor(value, options = {}) { - this.#current = DEV ? tag(source(value), 'Spring.current') : source(value); - this.#target = DEV ? tag(source(value), 'Spring.target') : source(value); + this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); + this.#target = DEV ? tag(state(value), 'Spring.target') : state(value); if (typeof options.stiffness === 'number') this.#stiffness.v = clamp(options.stiffness, 0, 1); if (typeof options.damping === 'number') this.#damping.v = clamp(options.damping, 0, 1); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 09bd06c325..437c22ec3b 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -6,7 +6,7 @@ import { raf } from '../internal/client/timing.js'; import { loop } from '../internal/client/loop.js'; import { linear } from '../easing/index.js'; import { is_date } from './utils.js'; -import { set, source } from '../internal/client/reactivity/sources.js'; +import { set, state } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get, render_effect } from 'svelte/internal/client'; import { DEV } from 'esm-env'; @@ -191,8 +191,8 @@ export class Tween { * @param {TweenedOptions} options */ constructor(value, options = {}) { - this.#current = source(value); - this.#target = source(value); + this.#current = state(value); + this.#target = state(value); this.#defaults = options; if (DEV) { diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index cd2fac163f..4fa2dfd7b2 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { set, source, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; /** @@ -56,6 +56,7 @@ export class SvelteMap extends Map { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -79,6 +80,19 @@ export class SvelteMap extends Map { } } + /** + * If the source is being created inside the same reaction as the SvelteMap instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + /** @param {K} key */ has(key) { var sources = this.#sources; @@ -87,7 +101,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -123,7 +137,7 @@ export class SvelteMap extends Map { if (s === undefined) { var ret = super.get(key); if (ret !== undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -154,7 +168,7 @@ export class SvelteMap extends Map { var version = this.#version; if (s === undefined) { - s = source(0); + s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); @@ -219,8 +233,7 @@ export class SvelteMap extends Map { if (this.#size.v !== sources.size) { for (var key of super.keys()) { if (!sources.has(key)) { - var s = source(0); - + var s = this.#source(0); if (DEV) { tag(s, `SvelteMap get(${label(key)})`); } diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 8a656c2bc1..9e3c330beb 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,7 +2,7 @@ import { DEV } from 'esm-env'; import { source, set, state } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; -import { get } from '../internal/client/runtime.js'; +import { get, update_version } from '../internal/client/runtime.js'; import { increment } from './utils.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; @@ -50,6 +50,7 @@ export class SvelteSet extends Set { #sources = new Map(); #version = state(0); #size = state(0); + #update_version = update_version || -1; /** * @param {Iterable | null | undefined} [value] @@ -75,6 +76,19 @@ export class SvelteSet extends Set { if (!inited) this.#init(); } + /** + * If the source is being created inside the same reaction as the SvelteSet instance, + * we use `state` so that it will not be a dependency of the reaction. Otherwise we + * use `source` so it will be. + * + * @template T + * @param {T} value + * @returns {Source} + */ + #source(value) { + return update_version === this.#update_version ? state(value) : source(value); + } + // We init as part of the first instance so that we can treeshake this class #init() { inited = true; @@ -116,7 +130,7 @@ export class SvelteSet extends Set { return false; } - s = source(true); + s = this.#source(true); if (DEV) { tag(s, `SvelteSet has(${label(value)})`); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js index 5ad6f57e31..c10dc7fb55 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/_config.js @@ -8,7 +8,8 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4, button5, button6, button7, button8] = + target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +20,35 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); + + assert.throws(() => { + button5?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button6?.click(); + flushSync(); + }); + + assert.throws(() => { + button7?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button8?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte index bdd5ccb75c..c37f37ceb6 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-map/main.svelte @@ -1,27 +1,101 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throw_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has} + {throw_has} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has} + {works_has} {/if} + +{#if outside_get} + {throw_get} +{/if} + +{#if inside_get} + {works_get} +{/if} + + +{#if outside_values} + {throw_values} +{/if} + +{#if inside_values} + {works_values} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js index 5ad6f57e31..5cf066fb8a 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/_config.js @@ -8,7 +8,7 @@ export default test({ }, test({ assert, target }) { - const [button1, button2] = target.querySelectorAll('button'); + const [button1, button2, button3, button4] = target.querySelectorAll('button'); assert.throws(() => { button1?.click(); @@ -19,5 +19,15 @@ export default test({ button2?.click(); flushSync(); }); + + assert.throws(() => { + button3?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button4?.click(); + flushSync(); + }); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte index 8564f6e7c4..1d6735ba64 100644 --- a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-set/main.svelte @@ -1,27 +1,52 @@ - -{#if visibleExternal} - {throws} + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} + + +{#if outside_has_delete} + {throws_has_delete} {/if} - -{#if visibleInternal} - {works} + +{#if inside_has_delete} + {works_has_delete} {/if} diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js new file mode 100644 index 0000000000..5ad6f57e31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte new file mode 100644 index 0000000000..b0818deca9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-spring/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js new file mode 100644 index 0000000000..5ad6f57e31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/_config.js @@ -0,0 +1,23 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true, + runes: true + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + + assert.throws(() => { + button1?.click(); + flushSync(); + }, /state_unsafe_mutation/); + + assert.doesNotThrow(() => { + button2?.click(); + flushSync(); + }); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte new file mode 100644 index 0000000000..bd007f2b50 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/side-effect-derived-tween/main.svelte @@ -0,0 +1,26 @@ + + + +{#if outside_basic} + {throws_basic} +{/if} + +{#if inside_basic} + {works_basic} +{/if} \ No newline at end of file From 9d176b50080ed075d97ed96a15e2109b3db69886 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 09:37:29 -0400 Subject: [PATCH 024/367] chore: changeset for #16239 (#16325) --- .changeset/gorgeous-birds-brake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/gorgeous-birds-brake.md diff --git a/.changeset/gorgeous-birds-brake.md b/.changeset/gorgeous-birds-brake.md new file mode 100644 index 0000000000..8365cc0e6a --- /dev/null +++ b/.changeset/gorgeous-birds-brake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction From fab2091743d48f682d470cccb1a99590ce6bac32 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:41:08 -0400 Subject: [PATCH 025/367] Version Packages (#16314) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/gorgeous-birds-brake.md | 5 ----- .changeset/light-rivers-jump.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/gorgeous-birds-brake.md delete mode 100644 .changeset/light-rivers-jump.md diff --git a/.changeset/gorgeous-birds-brake.md b/.changeset/gorgeous-birds-brake.md deleted file mode 100644 index 8365cc0e6a..0000000000 --- a/.changeset/gorgeous-birds-brake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction diff --git a/.changeset/light-rivers-jump.md b/.changeset/light-rivers-jump.md deleted file mode 100644 index 2454d57156..0000000000 --- a/.changeset/light-rivers-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: re-evaluate derived props during teardown diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 89ae380840..19aa1466c0 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.5 + +### Patch Changes + +- fix: associate sources in Spring/Tween/SvelteMap/SvelteSet with correct reaction ([#16325](https://github.com/sveltejs/svelte/pull/16325)) + +- fix: re-evaluate derived props during teardown ([#16278](https://github.com/sveltejs/svelte/pull/16278)) + ## 5.35.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index aaf8f26fec..4ac497ed41 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.4", + "version": "5.35.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c4111b9e8d..eb68753d71 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.4'; +export const VERSION = '5.35.5'; export const PUBLIC_VERSION = '5'; From 71ed9e4648705ac79f1f834b8530d0d3ee06ffa2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 9 Jul 2025 20:56:33 -0400 Subject: [PATCH 026/367] chore: simplify/optimize source_ownership occurrence (#16328) --- packages/svelte/src/internal/client/runtime.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index cd7170d326..d6e7325ba3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -236,16 +236,13 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) var reactions = signal.reactions; if (reactions === null) return; + if (source_ownership?.reaction === active_reaction && source_ownership.sources.includes(signal)) { + return; + } + for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; - if ( - source_ownership?.reaction === active_reaction && - source_ownership.sources.includes(signal) - ) { - continue; - } - if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { From 86b06cb6945816ec8182544ba101cd38679f96a1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 01:58:42 -0400 Subject: [PATCH 027/367] chore: dry out transition `get_options` (#16329) --- .../src/internal/client/dom/elements/transitions.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 38100e982c..00fad9ffdb 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -209,21 +209,14 @@ export function transition(flags, element, get_fn, get_params) { var outro; function get_options() { - var previous_reaction = active_reaction; - var previous_effect = active_effect; - set_active_reaction(null); - set_active_effect(null); - try { + return without_reactive_context(() => { // If a transition is still ongoing, we use the existing options rather than generating // new ones. This ensures that reversible transitions reverse smoothly, rather than // jumping to a new spot because (for example) a different `duration` was used return (current_options ??= get_fn()(element, get_params?.() ?? /** @type {P} */ ({}), { direction })); - } finally { - set_active_reaction(previous_reaction); - set_active_effect(previous_effect); - } + }); } /** @type {TransitionManager} */ From c3361bfdb7adc3f2df6ec831fb54f2ca614880e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 01:59:57 -0400 Subject: [PATCH 028/367] chore: remove component_context.d (#16330) --- packages/svelte/src/internal/client/context.js | 9 ++------- packages/svelte/src/internal/client/reactivity/props.js | 8 -------- packages/svelte/src/internal/client/types.d.ts | 2 -- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 736b81c172..d79d827fee 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -139,16 +139,15 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - var ctx = (component_context = { + component_context = { p: component_context, c: null, - d: false, e: null, m: false, s: props, x: null, l: null - }); + }; if (legacy_mode_flag && !runes) { component_context.l = { @@ -159,10 +158,6 @@ export function push(props, runes = false, fn) { }; } - teardown(() => { - /** @type {ComponentContext} */ (ctx).d = true; - }); - if (DEV) { // component function component_context.function = fn; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 03daad5251..f39d45bb04 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -268,14 +268,6 @@ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } -/** - * @param {Derived} current_value - * @returns {boolean} - */ -function has_destroyed_component_ctx(current_value) { - return current_value.ctx?.d ?? false; -} - /** * This function is responsible for synchronizing a possibly bound prop with the inner component state. * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 0b7310e172..bcbd0ac8a3 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -14,8 +14,6 @@ export type ComponentContext = { p: null | ComponentContext; /** context */ c: null | Map; - /** destroyed */ - d: boolean; /** deferred effects */ e: null | Array<{ fn: () => void | (() => void); From 0cafe34c92dd21b532702bf6bc27cce108e25ea0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 07:44:07 -0400 Subject: [PATCH 029/367] chore: DRY out increment logic (#16332) --- packages/svelte/src/internal/client/proxy.js | 16 ++++------------ .../src/internal/client/reactivity/sources.js | 8 ++++++++ .../svelte/src/reactivity/create-subscriber.js | 3 +-- packages/svelte/src/reactivity/map.js | 3 +-- packages/svelte/src/reactivity/set.js | 3 +-- .../svelte/src/reactivity/url-search-params.js | 3 +-- packages/svelte/src/reactivity/utils.js | 7 ------- 7 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 packages/svelte/src/reactivity/utils.js diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index d9063aee34..97c8da9d33 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -8,7 +8,7 @@ import { is_array, object_prototype } from '../shared/utils.js'; -import { state as source, set } from './reactivity/sources.js'; +import { state as source, set, increment } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; @@ -118,7 +118,7 @@ export function proxy(value) { if (prop in target) { const s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(prop, s); - update_version(version); + increment(version); if (DEV) { tag(s, get_label(path, prop)); @@ -136,7 +136,7 @@ export function proxy(value) { } } set(s, UNINITIALIZED); - update_version(version); + increment(version); } return true; @@ -304,7 +304,7 @@ export function proxy(value) { } } - update_version(version); + increment(version); } return true; @@ -343,14 +343,6 @@ function get_label(path, prop) { return /^\d+$/.test(prop) ? `${path}[${prop}]` : `${path}['${prop}']`; } -/** - * @param {Source} signal - * @param {1 | -1} [d] - */ -function update_version(signal, d = 1) { - set(signal, signal.v + d); -} - /** * @param {any} value */ diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index a86ccfee4f..9f08354cc0 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -259,6 +259,14 @@ export function update_pre(source, d = 1) { return set(source, d === 1 ? ++value : --value); } +/** + * Silently (without using `get`) increment a source + * @param {Source} source + */ +export function increment(source) { + set(source, source.v + 1); +} + /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 491ffb45cb..67a92ceb7b 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -1,8 +1,7 @@ import { get, tick, untrack } from '../internal/client/runtime.js'; import { effect_tracking, render_effect } from '../internal/client/reactivity/effects.js'; -import { source } from '../internal/client/reactivity/sources.js'; +import { source, increment } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; -import { increment } from './utils.js'; import { DEV } from 'esm-env'; /** diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 4fa2dfd7b2..014b5e7c7c 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -1,9 +1,8 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { set, source, state } from '../internal/client/reactivity/sources.js'; +import { set, source, state, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; import { get, update_version } from '../internal/client/runtime.js'; -import { increment } from './utils.js'; /** * A reactive version of the built-in [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object. diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 9e3c330beb..d7c2deeaae 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -1,9 +1,8 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { source, set, state } from '../internal/client/reactivity/sources.js'; +import { source, set, state, increment } from '../internal/client/reactivity/sources.js'; import { label, tag } from '../internal/client/dev/tracing.js'; import { get, update_version } from '../internal/client/runtime.js'; -import { increment } from './utils.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; diff --git a/packages/svelte/src/reactivity/url-search-params.js b/packages/svelte/src/reactivity/url-search-params.js index 389da7cdb6..2381e11875 100644 --- a/packages/svelte/src/reactivity/url-search-params.js +++ b/packages/svelte/src/reactivity/url-search-params.js @@ -1,9 +1,8 @@ import { DEV } from 'esm-env'; -import { state } from '../internal/client/reactivity/sources.js'; +import { state, increment } from '../internal/client/reactivity/sources.js'; import { tag } from '../internal/client/dev/tracing.js'; import { get } from '../internal/client/runtime.js'; import { get_current_url } from './url.js'; -import { increment } from './utils.js'; export const REPLACE = Symbol(); diff --git a/packages/svelte/src/reactivity/utils.js b/packages/svelte/src/reactivity/utils.js deleted file mode 100644 index cd55e0e0ba..0000000000 --- a/packages/svelte/src/reactivity/utils.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @import { Source } from '#client' */ -import { set } from '../internal/client/reactivity/sources.js'; - -/** @param {Source} source */ -export function increment(source) { - set(source, source.v + 1); -} From 8da222f46021fd2bd3e69048557a229c20a2fe94 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 08:57:33 -0400 Subject: [PATCH 030/367] chore: simplify component pop (#16331) * chore: simplify pop * shuffle * remove .m flag on component context * unused * shuffle * simplify * context is never null in pop * changeset --- .changeset/new-candles-marry.md | 5 ++ .../svelte/src/internal/client/context.js | 50 ++++++++----------- .../src/internal/client/reactivity/effects.js | 18 ++----- .../svelte/src/internal/client/types.d.ts | 8 +-- 4 files changed, 30 insertions(+), 51 deletions(-) create mode 100644 .changeset/new-candles-marry.md diff --git a/.changeset/new-candles-marry.md b/.changeset/new-candles-marry.md new file mode 100644 index 0000000000..4d55980c72 --- /dev/null +++ b/.changeset/new-candles-marry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify internal component `pop()` diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index d79d827fee..6876a89f57 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -143,7 +143,6 @@ export function push(props, runes = false, fn) { p: component_context, c: null, e: null, - m: false, s: props, x: null, l: null @@ -171,37 +170,28 @@ export function push(props, runes = false, fn) { * @returns {T} */ export function pop(component) { - const context_stack_item = component_context; - if (context_stack_item !== null) { - if (component !== undefined) { - context_stack_item.x = component; - } - const component_effects = context_stack_item.e; - if (component_effects !== null) { - var previous_effect = active_effect; - var previous_reaction = active_reaction; - context_stack_item.e = null; - try { - for (var i = 0; i < component_effects.length; i++) { - var component_effect = component_effects[i]; - set_active_effect(component_effect.effect); - set_active_reaction(component_effect.reaction); - create_user_effect(component_effect.fn); - } - } finally { - set_active_effect(previous_effect); - set_active_reaction(previous_reaction); - } - } - component_context = context_stack_item.p; - if (DEV) { - dev_current_component_function = context_stack_item.p?.function ?? null; + var context = /** @type {ComponentContext} */ (component_context); + var effects = context.e; + + if (effects !== null) { + context.e = null; + + for (var fn of effects) { + create_user_effect(fn); } - context_stack_item.m = true; } - // Micro-optimization: Don't set .a above to the empty object - // so it can be garbage-collected when the return here is unused - return component || /** @type {T} */ ({}); + + if (component !== undefined) { + context.x = component; + } + + component_context = context.p; + + if (DEV) { + dev_current_component_function = component_context?.function ?? null; + } + + return component ?? /** @type {T} */ ({}); } /** @returns {boolean} */ diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index df2afd3e38..51c6805529 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -179,28 +179,18 @@ export function teardown(fn) { export function user_effect(fn) { validate_effect('$effect'); - // Non-nested `$effect(...)` in a component should be deferred - // until the component is mounted - var defer = - active_effect !== null && - (active_effect.f & BRANCH_EFFECT) !== 0 && - component_context !== null && - !component_context.m; - if (DEV) { define_property(fn, 'name', { value: '$effect' }); } - if (defer) { + if (!active_reaction && active_effect && (active_effect.f & BRANCH_EFFECT) !== 0) { + // Top-level `$effect(...)` in a component — defer until mount var context = /** @type {ComponentContext} */ (component_context); - (context.e ??= []).push({ - fn, - effect: active_effect, - reaction: active_reaction - }); + (context.e ??= []).push(fn); } else { + // Everything else — create immediately return create_user_effect(fn); } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index bcbd0ac8a3..a42f91343c 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -15,13 +15,7 @@ export type ComponentContext = { /** context */ c: null | Map; /** deferred effects */ - e: null | Array<{ - fn: () => void | (() => void); - effect: null | Effect; - reaction: null | Reaction; - }>; - /** mounted */ - m: boolean; + e: null | Array<() => void | (() => void)>; /** * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` * @deprecated remove in 6.0 From 0e7e873a1b9a4b5c999ccc2525172d658fc73e25 Mon Sep 17 00:00:00 2001 From: "Dominik G." Date: Thu, 10 Jul 2025 15:41:47 +0200 Subject: [PATCH 031/367] chore: update svelte-ecosystem-ci trigger (#16315) --- .github/workflows/ecosystem-ci-trigger.yml | 43 +++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 71df3242e8..7c6b740370 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -8,9 +8,17 @@ jobs: trigger: runs-on: ubuntu-latest if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') + permissions: + issues: write # to add / delete reactions + pull-requests: read # to read PR data + actions: read # to check workflow status + contents: read # to clone the repo steps: - - uses: GitHubSecurityLab/actions-permissions/monitor@v1 - - uses: actions/github-script@v6 + - name: monitor action permissions + uses: GitHubSecurityLab/actions-permissions/monitor@v1 + - name: check user authorization # user needs triage permission + uses: actions/github-script@v7 + id: check-permissions with: script: | const user = context.payload.sender.login @@ -29,7 +37,7 @@ jobs: } if (hasTriagePermission) { - console.log('Allowed') + console.log('User is allowed. Adding +1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, @@ -37,16 +45,18 @@ jobs: content: '+1', }) } else { - console.log('Not allowed') + console.log('User is not allowed. Adding -1 reaction.') await github.rest.reactions.createForIssueComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: context.payload.comment.id, content: '-1', }) - throw new Error('not allowed') + throw new Error('User does not have the necessary permissions.') } - - uses: actions/github-script@v6 + + - name: Get PR Data + uses: actions/github-script@v7 id: get-pr-data with: script: | @@ -59,21 +69,27 @@ jobs: return { num: context.issue.number, branchName: pr.head.ref, + commit: pr.head.sha, repo: pr.head.repo.full_name } - - id: generate-token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 #keep pinned for security reasons, currently 1.8.0 + + - name: Generate Token + id: generate-token + uses: actions/create-github-app-token@v2 with: - app_id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} - private_key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} - repository: '${{ github.repository_owner }}/svelte-ecosystem-ci' - - uses: actions/github-script@v6 + app-id: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_ID }} + private-key: ${{ secrets.ECOSYSTEM_CI_GITHUB_APP_PRIVATE_KEY }} + repositories: | + svelte + svelte-ecosystem-ci + + - name: Trigger Downstream Workflow + uses: actions/github-script@v7 id: trigger env: COMMENT: ${{ github.event.comment.body }} with: github-token: ${{ steps.generate-token.outputs.token }} - result-encoding: string script: | const comment = process.env.COMMENT.trim() const prData = ${{ steps.get-pr-data.outputs.result }} @@ -89,6 +105,7 @@ jobs: prNumber: '' + prData.num, branchName: prData.branchName, repo: prData.repo, + commit: prData.commit, suite: suite === '' ? '-' : suite } }) From 443e76e4baa6b57739b263d4dbb74868508ec2e1 Mon Sep 17 00:00:00 2001 From: "Dominik G." Date: Thu, 10 Jul 2025 16:56:41 +0200 Subject: [PATCH 032/367] fix: set write permission for PR in ecosystem-ci-trigger workflow (#16337) * fix: set write permission for PR in ecosystem-ci-trigger workflow * chore: match wording with vites trigger --- .github/workflows/ecosystem-ci-trigger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ecosystem-ci-trigger.yml b/.github/workflows/ecosystem-ci-trigger.yml index 7c6b740370..9be1f00104 100644 --- a/.github/workflows/ecosystem-ci-trigger.yml +++ b/.github/workflows/ecosystem-ci-trigger.yml @@ -10,7 +10,7 @@ jobs: if: github.repository == 'sveltejs/svelte' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/ecosystem-ci run') permissions: issues: write # to add / delete reactions - pull-requests: read # to read PR data + pull-requests: write # to read PR data, and to add labels actions: read # to check workflow status contents: read # to clone the repo steps: From 6f0aec527177d4c3ec56b94d1e8b21d28266bbb7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 10 Jul 2025 12:36:10 -0400 Subject: [PATCH 033/367] chore: simplify source ownership (#16333) * simplify source ownership * rename * changeset * make it unnecessary to hand onto `current_sources` past the initial update --- .changeset/eight-walls-mate.md | 5 ++++ packages/svelte/src/internal/client/proxy.js | 29 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 4 +-- .../svelte/src/internal/client/runtime.js | 28 +++++++++--------- 4 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 .changeset/eight-walls-mate.md diff --git a/.changeset/eight-walls-mate.md b/.changeset/eight-walls-mate.md new file mode 100644 index 0000000000..a7de4e6278 --- /dev/null +++ b/.changeset/eight-walls-mate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify reaction/source ownership tracking diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 97c8da9d33..5da1b7e188 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,13 @@ /** @import { Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; +import { + get, + active_effect, + update_version, + active_reaction, + set_update_version, + set_active_reaction +} from './runtime.js'; import { array_prototype, get_descriptor, @@ -41,7 +48,7 @@ export function proxy(value) { var version = source(0); var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; - var reaction = active_reaction; + var parent_version = update_version; /** * Executes the proxy in the context of the reaction it was originally created in, if any @@ -49,13 +56,23 @@ export function proxy(value) { * @param {() => T} fn */ var with_parent = (fn) => { - var previous_reaction = active_reaction; - set_active_reaction(reaction); + if (update_version === parent_version) { + return fn(); + } + + // child source is being created after the initial proxy — + // prevent it from being associated with the current reaction + var reaction = active_reaction; + var version = update_version; + + set_active_reaction(null); + set_update_version(parent_version); - /** @type {T} */ var result = fn(); - set_active_reaction(previous_reaction); + set_active_reaction(reaction); + set_update_version(version); + return result; }; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9f08354cc0..f84312e31c 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,7 +11,7 @@ import { untrack, increment_write_version, update_effect, - source_ownership, + current_sources, check_dirtiness, untracking, is_destroying_effect, @@ -140,7 +140,7 @@ export function set(source, value, should_proxy = false) { (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | INSPECT_EFFECT)) !== 0 && - !(source_ownership?.reaction === active_reaction && source_ownership.sources.includes(source)) + !current_sources?.includes(source) ) { e.state_unsafe_mutation(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d6e7325ba3..6477e2942a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -88,17 +88,17 @@ export function set_active_effect(effect) { /** * When sources are created within a reaction, reading and writing * them within that reaction should not cause a re-run - * @type {null | { reaction: Reaction, sources: Source[] }} + * @type {null | Source[]} */ -export let source_ownership = null; +export let current_sources = null; /** @param {Value} value */ export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { - if (source_ownership === null) { - source_ownership = { reaction: active_reaction, sources: [value] }; + if (current_sources === null) { + current_sources = [value]; } else { - source_ownership.sources.push(value); + current_sources.push(value); } } } @@ -136,6 +136,11 @@ let read_version = 0; export let update_version = read_version; +/** @param {number} value */ +export function set_update_version(value) { + update_version = value; +} + // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; @@ -236,7 +241,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) var reactions = signal.reactions; if (reactions === null) return; - if (source_ownership?.reaction === active_reaction && source_ownership.sources.includes(signal)) { + if (current_sources?.includes(signal)) { return; } @@ -263,7 +268,7 @@ export function update_reaction(reaction) { var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; var previous_skip_reaction = skip_reaction; - var previous_reaction_sources = source_ownership; + var previous_sources = current_sources; var previous_component_context = component_context; var previous_untracking = untracking; var previous_update_version = update_version; @@ -277,7 +282,7 @@ export function update_reaction(reaction) { (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - source_ownership = null; + current_sources = null; set_component_context(reaction.ctx); untracking = false; update_version = ++read_version; @@ -365,7 +370,7 @@ export function update_reaction(reaction) { untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; skip_reaction = previous_skip_reaction; - source_ownership = previous_reaction_sources; + current_sources = previous_sources; set_component_context(previous_component_context); untracking = previous_untracking; update_version = previous_update_version; @@ -759,10 +764,7 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - if ( - source_ownership?.reaction !== active_reaction || - !source_ownership?.sources.includes(signal) - ) { + if (!current_sources?.includes(signal)) { var deps = active_reaction.deps; if (signal.rv < read_version) { signal.rv = read_version; From ca1eb55e970243dbed1c032e038860218325d63a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:41:02 -0400 Subject: [PATCH 034/367] Version Packages (#16334) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eight-walls-mate.md | 5 ----- .changeset/new-candles-marry.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/eight-walls-mate.md delete mode 100644 .changeset/new-candles-marry.md diff --git a/.changeset/eight-walls-mate.md b/.changeset/eight-walls-mate.md deleted file mode 100644 index a7de4e6278..0000000000 --- a/.changeset/eight-walls-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify reaction/source ownership tracking diff --git a/.changeset/new-candles-marry.md b/.changeset/new-candles-marry.md deleted file mode 100644 index 4d55980c72..0000000000 --- a/.changeset/new-candles-marry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: simplify internal component `pop()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 19aa1466c0..8f158c528f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.35.6 + +### Patch Changes + +- chore: simplify reaction/source ownership tracking ([#16333](https://github.com/sveltejs/svelte/pull/16333)) + +- chore: simplify internal component `pop()` ([#16331](https://github.com/sveltejs/svelte/pull/16331)) + ## 5.35.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4ac497ed41..4eed145f3f 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.35.5", + "version": "5.35.6", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index eb68753d71..266b0b9491 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.35.5'; +export const VERSION = '5.35.6'; export const PUBLIC_VERSION = '5'; From e802d3b2cc0b6b5f96f853833fc6852705554a6c Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Fri, 11 Jul 2025 20:31:22 +0300 Subject: [PATCH 035/367] chore: replace inline regex with variable (#16340) * chore: replace inline regex with variable * Update packages/svelte/src/compiler/phases/patterns.js Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> * Update a11y.js * Update a11y.js --------- Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> --- .changeset/tiny-news-whisper.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/a11y.js | 6 ++++-- packages/svelte/src/compiler/phases/patterns.js | 2 ++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/tiny-news-whisper.md diff --git a/.changeset/tiny-news-whisper.md b/.changeset/tiny-news-whisper.md new file mode 100644 index 0000000000..8bf877085d --- /dev/null +++ b/.changeset/tiny-news-whisper.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: replace inline regex with variable diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index 1f58a28cad..e103e3eb80 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -6,7 +6,9 @@ import { roles as roles_map, aria, elementRoles } from 'aria-query'; import { AXObjects, AXObjectRoles, elementAXObjects } from 'axobject-query'; import { regex_heading_tags, + regex_js_prefix, regex_not_whitespace, + regex_redundant_img_alt, regex_starts_with_vowel, regex_whitespaces } from '../../../patterns.js'; @@ -1011,7 +1013,7 @@ export function check_element(node, context) { if (href) { const href_value = get_static_text_value(href); if (href_value !== null) { - if (href_value === '' || href_value === '#' || /^\W*javascript:/i.test(href_value)) { + if (href_value === '' || href_value === '#' || regex_js_prefix.test(href_value)) { w.a11y_invalid_attribute(href, href_value, href.name); } } @@ -1061,7 +1063,7 @@ export function check_element(node, context) { const alt_attribute = get_static_text_value(attribute_map.get('alt')); const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); if (alt_attribute && !aria_hidden && !has_spread) { - if (/\b(image|picture|photo)\b/i.test(alt_attribute)) { + if (regex_redundant_img_alt.test(alt_attribute)) { w.a11y_img_redundant_alt(node); } } diff --git a/packages/svelte/src/compiler/phases/patterns.js b/packages/svelte/src/compiler/phases/patterns.js index 2bee717131..448be7f949 100644 --- a/packages/svelte/src/compiler/phases/patterns.js +++ b/packages/svelte/src/compiler/phases/patterns.js @@ -23,3 +23,5 @@ export const regex_heading_tags = /^h[1-6]$/; export const regex_illegal_attribute_character = /(^[0-9-.])|[\^$@%&#?!|()[\]{}^*+~;]/; export const regex_bidirectional_control_characters = /[\u202a\u202b\u202c\u202d\u202e\u2066\u2067\u2068\u2069]+/g; +export const regex_js_prefix = /^\W*javascript:/i; +export const regex_redundant_img_alt = /\b(image|picture|photo)\b/i; From 96c1a10042475c4ef8b55dfdca8538a25e792626 Mon Sep 17 00:00:00 2001 From: "Ahmad S." Date: Fri, 11 Jul 2025 20:41:59 +0300 Subject: [PATCH 036/367] fix: silence autofocus a11y warning inside `` (#16341) --- .changeset/cuddly-humans-end.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/a11y.js | 2 +- .../tests/validator/samples/a11y-no-autofocus/input.svelte | 7 ++++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .changeset/cuddly-humans-end.md diff --git a/.changeset/cuddly-humans-end.md b/.changeset/cuddly-humans-end.md new file mode 100644 index 0000000000..d02e08de7a --- /dev/null +++ b/.changeset/cuddly-humans-end.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: silence autofocus a11y warning inside `` diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index e103e3eb80..152e679bf5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -877,7 +877,7 @@ export function check_element(node, context) { } // no-autofocus - if (name === 'autofocus') { + if (name === 'autofocus' && node.name !== 'dialog' && !is_parent(context.path, ['dialog'])) { w.a11y_autofocus(attribute); } diff --git a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte index 769dbe8c5b..7b1dccd1e8 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-autofocus/input.svelte @@ -1 +1,6 @@ -
\ No newline at end of file +
+ + + + + From 58baf80a70433a27a58bf5a642b4ccfd0c23349b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 13 Jul 2025 02:40:26 -0700 Subject: [PATCH 037/367] docs: add note about proxying state proxies (#16354) --- documentation/docs/02-runes/02-$state.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 8e6c91fad7..7c4571e575 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -50,7 +50,7 @@ todos.push({ }); ``` -> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. +> [!NOTE] When you update properties of proxies, the original object is _not_ mutated. If you desire to use your own proxy handlers in a state proxy, [you should wrap the object _after_ wrapping it in `$state`](https://svelte.dev/playground/hello-world?version=latest#H4sIAAAAAAAACpWR3WoDIRCFX2UqhWyIJL3erAulL9C7XnQLMe5ksbUqOpsfln33YuyGFNJC8UKdc2bOhw7Myk9kJXsJ0nttO9jcR5KEG9AWJDwHdzwxznbaYGTl68Do5JM_FRifuh-9X8Y9Gkq1rYx4q66cJbQUWcmqqIL2VDe2IYMEbvuOikBADi-GJDSkXG-phId0G-frye2DO2psQYDFQ0Ys8gQO350dUkEydEg82T0GOs0nsSG9g2IqgxACZueo2ZUlpdvoDC6N64qsg1QKY8T2bpZp8gpIfbCQ85Zn50Ud82HkeY83uDjspenxv3jXcSDyjPWf9L1vJf0GH666J-jLu1ery4dV257IWXBWGa0-xFDMQdTTn2ScxWKsn86ROsLwQxqrVR5QM84Ij8TKFD2-cUZSm4O2LSt30kQcvwCgCmfZnAIAAA==). Note that if you destructure a reactive value, the references are not reactive — as in normal JavaScript, they are evaluated at the point of destructuring: From 61f75651d6a4f1106a55c2cd357751d473729e55 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 12:48:08 -0400 Subject: [PATCH 038/367] chore: re-export shared errors (#16356) --- .../process-messages/templates/client-errors.js | 2 ++ .../process-messages/templates/server-errors.js | 2 ++ packages/svelte/src/index-client.js | 11 +++++------ packages/svelte/src/internal/client/context.js | 4 ++-- packages/svelte/src/internal/client/dev/validation.js | 7 ++++--- packages/svelte/src/internal/client/errors.js | 2 ++ packages/svelte/src/internal/server/context.js | 2 +- packages/svelte/src/internal/server/dev.js | 4 ++-- packages/svelte/src/internal/server/errors.js | 2 +- packages/svelte/src/legacy/legacy-client.js | 4 ++-- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/svelte/scripts/process-messages/templates/client-errors.js b/packages/svelte/scripts/process-messages/templates/client-errors.js index c72e9f9d5e..ef749b4ba3 100644 --- a/packages/svelte/scripts/process-messages/templates/client-errors.js +++ b/packages/svelte/scripts/process-messages/templates/client-errors.js @@ -1,5 +1,7 @@ import { DEV } from 'esm-env'; +export * from '../shared/errors.js'; + /** * MESSAGE * @param {string} PARAMETER diff --git a/packages/svelte/scripts/process-messages/templates/server-errors.js b/packages/svelte/scripts/process-messages/templates/server-errors.js index 6fb7924564..0bbe801abc 100644 --- a/packages/svelte/scripts/process-messages/templates/server-errors.js +++ b/packages/svelte/scripts/process-messages/templates/server-errors.js @@ -1,3 +1,5 @@ +export * from '../shared/errors.js'; + /** * MESSAGE * @param {string} PARAMETER diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 0d962aacd1..ae1caf16d9 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -5,7 +5,6 @@ import { active_reaction, untrack } from './internal/client/runtime.js'; import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; -import { lifecycle_outside_component } from './internal/shared/errors.js'; import { legacy_mode_flag } from './internal/flags/index.js'; import { component_context } from './internal/client/context.js'; import { DEV } from 'esm-env'; @@ -91,7 +90,7 @@ export function getAbortSignal() { */ export function onMount(fn) { if (component_context === null) { - lifecycle_outside_component('onMount'); + e.lifecycle_outside_component('onMount'); } if (legacy_mode_flag && component_context.l !== null) { @@ -115,7 +114,7 @@ export function onMount(fn) { */ export function onDestroy(fn) { if (component_context === null) { - lifecycle_outside_component('onDestroy'); + e.lifecycle_outside_component('onDestroy'); } onMount(() => () => untrack(fn)); @@ -158,7 +157,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false export function createEventDispatcher() { const active_component_context = component_context; if (active_component_context === null) { - lifecycle_outside_component('createEventDispatcher'); + e.lifecycle_outside_component('createEventDispatcher'); } return (type, detail, options) => { @@ -196,7 +195,7 @@ export function createEventDispatcher() { */ export function beforeUpdate(fn) { if (component_context === null) { - lifecycle_outside_component('beforeUpdate'); + e.lifecycle_outside_component('beforeUpdate'); } if (component_context.l === null) { @@ -219,7 +218,7 @@ export function beforeUpdate(fn) { */ export function afterUpdate(fn) { if (component_context === null) { - lifecycle_outside_component('afterUpdate'); + e.lifecycle_outside_component('afterUpdate'); } if (component_context.l === null) { diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 6876a89f57..eae326f9bb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, DevStackEntry } from '#client' */ import { DEV } from 'esm-env'; -import { lifecycle_outside_component } from '../shared/errors.js'; +import * as e from './errors.js'; import { source } from './reactivity/sources.js'; import { active_effect, @@ -205,7 +205,7 @@ export function is_runes() { */ function get_or_init_context_map(name) { if (component_context === null) { - lifecycle_outside_component(name); + e.lifecycle_outside_component(name); } return (component_context.c ??= new Map(get_parent_context(component_context) || undefined)); diff --git a/packages/svelte/src/internal/client/dev/validation.js b/packages/svelte/src/internal/client/dev/validation.js index e41e4c4628..60d140c718 100644 --- a/packages/svelte/src/internal/client/dev/validation.js +++ b/packages/svelte/src/internal/client/dev/validation.js @@ -1,15 +1,16 @@ -import { invalid_snippet_arguments } from '../../shared/errors.js'; +import * as e from '../errors.js'; /** * @param {Node} anchor * @param {...(()=>any)[]} args */ export function validate_snippet_args(anchor, ...args) { if (typeof anchor !== 'object' || !(anchor instanceof Node)) { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } + for (let arg of args) { if (typeof arg !== 'function') { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } } } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 5c3f5340e1..64dc34e8d0 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -2,6 +2,8 @@ import { DEV } from 'esm-env'; +export * from '../shared/errors.js'; + /** * Using `bind:value` together with a checkbox input is not allowed. Use `bind:checked` instead * @returns {never} diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index 4e547f48cb..bae93beb53 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -1,7 +1,7 @@ /** @import { Component } from '#server' */ import { DEV } from 'esm-env'; import { on_destroy } from './index.js'; -import * as e from '../shared/errors.js'; +import * as e from './errors.js'; /** @type {Component | null} */ export var current_component = null; diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js index efc761d7c5..3c320f9698 100644 --- a/packages/svelte/src/internal/server/dev.js +++ b/packages/svelte/src/internal/server/dev.js @@ -5,7 +5,7 @@ import { is_tag_valid_with_parent } from '../../html-tree-validation.js'; import { current_component } from './context.js'; -import { invalid_snippet_arguments } from '../shared/errors.js'; +import * as e from './errors.js'; import { HeadPayload, Payload } from './payload.js'; /** @@ -102,6 +102,6 @@ export function validate_snippet_args(payload) { // for some reason typescript consider the type of payload as never after the first instanceof !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) ) { - invalid_snippet_arguments(); + e.invalid_snippet_arguments(); } } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index e47530c9aa..458937218f 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -1,6 +1,6 @@ /* This file is generated by scripts/process-messages/index.js. Do not edit! */ - +export * from '../shared/errors.js'; /** * `%name%(...)` is not available on the server diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 45c478ecab..61acbeaa28 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -4,8 +4,8 @@ import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; -import { lifecycle_outside_component } from '../internal/shared/errors.js'; import { define_property, is_array } from '../internal/shared/utils.js'; +import * as e from '../internal/client/errors.js'; import * as w from '../internal/client/warnings.js'; import { DEV } from 'esm-env'; import { FILENAME } from '../constants.js'; @@ -245,7 +245,7 @@ export function handlers(...handlers) { export function createBubbler() { const active_component_context = component_context; if (active_component_context === null) { - lifecycle_outside_component('createBubbler'); + e.lifecycle_outside_component('createBubbler'); } return (/**@type {string}*/ type) => (/**@type {Event}*/ event) => { From 4ef53a75a9f7428efe169ab2e037e87508c3ac87 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 15:15:02 -0400 Subject: [PATCH 039/367] chore: tidy up some stuff (#16357) * chore: tidy up some stuff * shut up dumbass --- packages/svelte/knip.json | 4 ---- packages/svelte/src/compiler/phases/scope.js | 6 +++--- packages/svelte/src/internal/client/context.js | 9 +-------- packages/svelte/src/internal/server/abort-signal.js | 2 +- packages/svelte/src/internal/shared/validate.js | 2 -- packages/svelte/src/utils.js | 2 +- 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/svelte/knip.json b/packages/svelte/knip.json index 7a27a64a91..0d1bf17e9f 100644 --- a/packages/svelte/knip.json +++ b/packages/svelte/knip.json @@ -1,10 +1,6 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", "entry": [ - "src/*/index.js", - "src/index-client.ts", - "src/index-server.ts", - "src/index.d.ts", "tests/**/*.js", "tests/**/*.ts", "!tests/**/*.svelte", diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8a2cc39ba7..662924b52c 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -18,9 +18,9 @@ import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; const UNKNOWN = Symbol('unknown'); /** Includes `BigInt` */ -export const NUMBER = Symbol('number'); -export const STRING = Symbol('string'); -export const FUNCTION = Symbol('string'); +const NUMBER = Symbol('number'); +const STRING = Symbol('string'); +const FUNCTION = Symbol('string'); /** @type {Record} */ const globals = { diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index eae326f9bb..a9ceafcd11 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -1,15 +1,8 @@ /** @import { ComponentContext, DevStackEntry } from '#client' */ - import { DEV } from 'esm-env'; import * as e from './errors.js'; import { source } from './reactivity/sources.js'; -import { - active_effect, - active_reaction, - set_active_effect, - set_active_reaction -} from './runtime.js'; -import { create_user_effect, teardown } from './reactivity/effects.js'; +import { create_user_effect } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; diff --git a/packages/svelte/src/internal/server/abort-signal.js b/packages/svelte/src/internal/server/abort-signal.js index da579b2592..a769a46e3d 100644 --- a/packages/svelte/src/internal/server/abort-signal.js +++ b/packages/svelte/src/internal/server/abort-signal.js @@ -1,7 +1,7 @@ import { STALE_REACTION } from '#client/constants'; /** @type {AbortController | null} */ -export let controller = null; +let controller = null; export function abort() { controller?.abort(STALE_REACTION); diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index bbb237594b..8f3e2807e7 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -1,5 +1,3 @@ -/** @import { TemplateNode } from '#client' */ -/** @import { Getters } from '#shared' */ import { is_void } from '../../utils.js'; import * as w from './warnings.js'; import * as e from './errors.js'; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 921eaec57c..2fc21220f0 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -428,7 +428,7 @@ export function is_mathml(name) { return MATHML_ELEMENTS.includes(name); } -export const STATE_CREATION_RUNES = /** @type {const} */ ([ +const STATE_CREATION_RUNES = /** @type {const} */ ([ '$state', '$state.raw', '$derived', From a23599a196380ce7fc3fb374dac4daee173776b7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 13 Jul 2025 18:20:29 -0400 Subject: [PATCH 040/367] fix: don't show adjusted error messages in boundaries (#16360) --- .changeset/large-balloons-agree.md | 5 ++ .../src/internal/client/error-handling.js | 47 ++++++++++++------- .../samples/error-boundary-3/_config.js | 2 +- .../samples/error-boundary-3/main.svelte | 6 +-- 4 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 .changeset/large-balloons-agree.md diff --git a/.changeset/large-balloons-agree.md b/.changeset/large-balloons-agree.md new file mode 100644 index 0000000000..9355336862 --- /dev/null +++ b/.changeset/large-balloons-agree.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't show adjusted error messages in boundaries diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 378f7408ef..b12f21adfc 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -7,14 +7,16 @@ import { BOUNDARY_EFFECT, EFFECT_RAN } from './constants.js'; import { define_property, get_descriptor } from '../shared/utils.js'; import { active_effect } from './runtime.js'; +const adjustments = new WeakMap(); + /** * @param {unknown} error */ export function handle_error(error) { var effect = /** @type {Effect} */ (active_effect); - if (DEV && error instanceof Error) { - adjust_error(error, effect); + if (DEV && error instanceof Error && !adjustments.has(error)) { + adjustments.set(error, get_adjustments(error, effect)); } if ((effect.f & EFFECT_RAN) === 0) { @@ -48,21 +50,19 @@ export function invoke_error_boundary(error, effect) { effect = effect.parent; } + if (error instanceof Error) { + apply_adjustments(error); + } + throw error; } -/** @type {WeakSet} */ -const adjusted_errors = new WeakSet(); - /** * Add useful information to the error message/stack in development * @param {Error} error * @param {Effect} effect */ -function adjust_error(error, effect) { - if (adjusted_errors.has(error)) return; - adjusted_errors.add(error); - +function get_adjustments(error, effect) { const message_descriptor = get_descriptor(error, 'message'); // if the message was already changed and it's not configurable we can't change it @@ -78,17 +78,28 @@ function adjust_error(error, effect) { context = context.p; } - define_property(error, 'message', { - value: error.message + `\n${component_stack}\n` - }); + return { + message: error.message + `\n${component_stack}\n`, + stack: error.stack + ?.split('\n') + .filter((line) => !line.includes('svelte/src/internal')) + .join('\n') + }; +} + +/** + * @param {Error} error + */ +function apply_adjustments(error) { + const adjusted = adjustments.get(error); + + if (adjusted) { + define_property(error, 'message', { + value: adjusted.message + }); - if (error.stack) { - // Filter out internal modules define_property(error, 'stack', { - value: error.stack - .split('\n') - .filter((line) => !line.includes('svelte/src/internal')) - .join('\n') + value: adjusted.stack }); } } diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js index 040e13676e..06da8f667c 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/_config.js @@ -9,6 +9,6 @@ export default test({ flushSync(); assert.deepEqual(logs, ['error caught']); - assert.htmlEqual(target.innerHTML, `
Fallback!
`); + assert.htmlEqual(target.innerHTML, `
oh no!
`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte index bad84666c0..bc7fe072c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-3/main.svelte @@ -1,6 +1,6 @@ + + + + + +

{await load(deferred)}

+ + {#snippet pending()} +

pending

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

pending

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

hello

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

hello

+ + {#snippet pending()} +

pending

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

pending

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

hello

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

hello

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

hello

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

hello

+ + {#snippet pending()} +

pending

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

false

+

1

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

true

+

{await push()}

+ {:else} +

false

+

{await push()}

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

loading...

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

false

+

1

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

true

+ {#each await push() as count}

{count}

{/each} + {:else} +

false

+ {#each await push() as count}

{count}

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

loading...

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

before

+

before

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

during

+

during

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

after

+

after

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

{entry}

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

{entry}

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

pending...

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

loading

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

A

+

a

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

AA

+

aa

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

AAA

+

aaa

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

{await push(input.toUpperCase())}

+ + {#if true} +

{input}

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

loading

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

{n}

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

1

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

pending

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

{(await d).value}

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

pending

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

two

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

pending

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

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

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

pending

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

42

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

84

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

84

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

86

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

pending

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

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

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

pending...

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

0: 0

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

pending...

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

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

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

pending

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

pending

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

pending

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

a

b

c

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

a

b

c

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

b

c

d

e

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

{await deferred.promise}

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

pending

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

pending

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

a

b

c

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

a

b

c

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

d

e

f

g

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

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

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

{item}

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

{e.message}

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

pending

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

pending

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

a

b

c

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

a

b

c

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

d

e

f

g

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

{item}

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

pending

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

pending...

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

0

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

1

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

2

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

4

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

5

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

{await process(count)}

+ + {#snippet pending()} +

pending...

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

pending

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

oops!

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

pending

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

wheee

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

{await deferred.promise}

+ + {#snippet pending()} +

pending

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

{error.message}

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

pending

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

hello

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

hello

+

updating...

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

goodbye

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

{await deferred.promise}

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

updating...

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

pending

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

pending

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

hello

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

hello

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

goodbye

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

{@html await deferred.promise}

+ + {#snippet pending()} +

pending

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

pending

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

yes

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

yes

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

no

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

yes

+ {:else} +

no

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

pending

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

pending

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

hello

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

hello

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

pending

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

loading...

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

1 * 2 = 2

+

2 * 2 = 4

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

2 * 2 = 4

+

4 * 2 = 8

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

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

+

{b} * 2 = {b * 2}

+ + {#snippet pending()} +

loading...

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

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

+ + {#snippet pending()} +

loading...

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

{indirect}

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

0

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

1

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

pending

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

{value}

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

pending

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

hello

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

hello

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

hello again

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

pending

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

pending

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

3

3

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

{await a_plus_b()}

+

{await a + await b}

+ + {#snippet pending()} +

pending

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

b

+ + + + +

pending...

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

c

+ + + + +

c

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

b

+ + + + +

b

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

{route}

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

a

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

b

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

c

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

pending...

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

a

+ + + + +

a

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

c

+ + + + +

c

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

b

+ + + + +

b

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

{route}

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

a

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

b

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

c

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

pending...

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

pending

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

hello

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

hello

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

wheee

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

{message}

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

pending

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

loading...

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

hello

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

{message}

+
+ + {#snippet pending()} +

loading...

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

a: 0

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

a: 0

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

b: 0

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

a: {await push(a, count)}

+ {:else} +

b: {await push(b, count)}

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

loading...

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

0

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

2

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

delayed: 3

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

delayed: {await push()}

+ {:else} +

{await count}

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

loading...

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

pending

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

hello

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

hello

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

hello

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

pending

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

{value}

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

condition is true

+

hello

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

condition is {condition}

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

pending

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

{value}

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

pending

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

hello

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

pending

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

0

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

2

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

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

+ {:else} +

{await count}

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

loading...

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

pending

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

pending

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

true

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

{await d2.promise}

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

pending

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

loading...

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

1

+

1

+

1

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

1

+

1

+

1

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

2

+

2

+

2

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

{x}

+

{await x}

+

{y}

+ + {#snippet pending()} +

loading...

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

value = 2

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

value = 1

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

value = {value}

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

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

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

pending promises: {$effect.pending()}

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

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

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

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

2 + 2 = 3

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

{await one()}

+

{await two()}

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

{await push(value)}

+

{await push(value)}

+

{await push(value)}

+ +

pending: {$effect.pending()}

+ + {#snippet pending()} +

loading...

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

pending

`, + html: `

pending

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

3

3

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

6

6

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

{await a_plus_b()}

-

{await a + await b}

+

{await a_plus_b_plus_c()}

+

{await a + await b + await c}

{#snippet pending()}

pending

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

hello

invalid

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

oops! {e.message}

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

oops! {e.message}

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

yikes!

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

yikes!

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

hello!

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

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

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

{error.message}

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

index: ${$.escape(i)}

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

index: ${$.escape(i)}

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

hello

child element

another child element

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

hello

child element

another child element

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

hello world

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

hello world

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

hello world

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

hello world

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

Hello, world!

123

Hello, world

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

Hello, world!

123

Hello, world

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

0

${$.escape(location.href)}

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

0

${$.escape(location.href)}

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

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

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

${$.escape(title)}

we don't need to traverse these nodes

or

these

ones

${$.html(content)}

these

trailing

nodes

can

be

completely

ignored

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

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

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

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

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

loading...

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

{count} ** 2 = {squared}

+

{count} ** 3 = {cubed}

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

1 ** 2 = 1

+

1 ** 3 = 1

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

2 ** 2 = 4

+

2 ** 3 = 8

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

pending

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

a

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

b

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

b

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

a

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

a

+ {:else} +

b

+ {/if} + + {#snippet pending()}{/snippet} +
+ From 1f99914a16e03209cb7c0aac8ce9d3d72bb32f15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Jul 2025 17:22:01 -0400 Subject: [PATCH 101/367] chore: move `capture_signals` to legacy module (#16456) --- .changeset/swift-cherries-know.md | 5 ++ .../svelte/src/internal/client/dev/tracing.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/client/legacy.js | 46 ++++++++++++++++ .../svelte/src/internal/client/runtime.js | 54 ++----------------- 5 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 .changeset/swift-cherries-know.md create mode 100644 packages/svelte/src/internal/client/legacy.js diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md new file mode 100644 index 0000000000..d8bbb1256a --- /dev/null +++ b/.changeset/swift-cherries-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: move `capture_signals` to legacy module diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 1b26c702fb..673a710fac 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -4,7 +4,7 @@ import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; -import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; +import { active_reaction, untrack } from '../runtime.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cddb432a98..90f0f9baac 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -138,11 +138,11 @@ export { mark_store_binding } from './reactivity/store.js'; export { boundary, pending } from './dom/blocks/boundary.js'; +export { invalidate_inner_signals } from './legacy.js'; export { set_text } from './render.js'; export { get, safe_get, - invalidate_inner_signals, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/legacy.js b/packages/svelte/src/internal/client/legacy.js new file mode 100644 index 0000000000..97ad7244c0 --- /dev/null +++ b/packages/svelte/src/internal/client/legacy.js @@ -0,0 +1,46 @@ +/** @import { Value } from '#client' */ +import { internal_set } from './reactivity/sources.js'; +import { untrack } from './runtime.js'; + +/** + * @type {Set | null} + * @deprecated + */ +export let captured_signals = null; + +/** + * Capture an array of all the signals that are read when `fn` is called + * @template T + * @param {() => T} fn + */ +function capture_signals(fn) { + var previous_captured_signals = captured_signals; + + try { + captured_signals = new Set(); + + untrack(fn); + + if (previous_captured_signals !== null) { + for (var signal of captured_signals) { + previous_captured_signals.add(signal); + } + } + + return captured_signals; + } finally { + captured_signals = previous_captured_signals; + } +} + +/** + * Invokes a function and captures all signals that are read during the invocation, + * then invalidates them. + * @param {() => any} fn + * @deprecated + */ +export function invalidate_inner_signals(fn) { + for (var signal of capture_signals(fn)) { + internal_set(signal, signal.v); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3d3c89975c..e86866af2a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,7 @@ import { STALE_REACTION, ERROR_VALUE } from './constants.js'; -import { internal_set, old_values } from './reactivity/sources.js'; +import { old_values } from './reactivity/sources.js'; import { destroy_derived_effects, execute_derived, @@ -45,6 +45,7 @@ import * as w from './warnings.js'; import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; +import { captured_signals } from './legacy.js'; export let is_updating_effect = false; @@ -137,14 +138,6 @@ export function set_update_version(value) { // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; -// Handle collecting all signals which are read during a specific time frame -/** @type {Set | null} */ -export let captured_signals = null; - -/** @param {Set | null} value */ -export function set_captured_signals(value) { - captured_signals = value; -} export function increment_write_version() { return ++write_version; @@ -531,9 +524,7 @@ export function get(signal) { var flags = signal.f; var is_derived = (flags & DERIVED) !== 0; - if (captured_signals !== null) { - captured_signals.add(signal); - } + captured_signals?.add(signal); // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { @@ -713,45 +704,6 @@ export function safe_get(signal) { return signal && get(signal); } -/** - * Capture an array of all the signals that are read when `fn` is called - * @template T - * @param {() => T} fn - */ -function capture_signals(fn) { - var previous_captured_signals = captured_signals; - captured_signals = new Set(); - - var captured = captured_signals; - var signal; - - try { - untrack(fn); - if (previous_captured_signals !== null) { - for (signal of captured_signals) { - previous_captured_signals.add(signal); - } - } - } finally { - captured_signals = previous_captured_signals; - } - - return captured; -} - -/** - * Invokes a function and captures all signals that are read during the invocation, - * then invalidates them. - * @param {() => any} fn - */ -export function invalidate_inner_signals(fn) { - var captured = capture_signals(() => untrack(fn)); - - for (var signal of captured) { - internal_set(signal, signal.v); - } -} - /** * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), * any state read inside `fn` will not be treated as a dependency. From 8afe5ec0c5a2e38b9b3a55b0dec94b99aa10447c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:23:10 -0400 Subject: [PATCH 102/367] Version Packages (#16453) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/angry-hornets-hug.md | 5 ----- .changeset/four-spiders-type.md | 5 ----- .changeset/silent-rockets-tease.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/angry-hornets-hug.md delete mode 100644 .changeset/four-spiders-type.md delete mode 100644 .changeset/silent-rockets-tease.md diff --git a/.changeset/angry-hornets-hug.md b/.changeset/angry-hornets-hug.md deleted file mode 100644 index ffe59db100..0000000000 --- a/.changeset/angry-hornets-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always mark reactions of deriveds diff --git a/.changeset/four-spiders-type.md b/.changeset/four-spiders-type.md deleted file mode 100644 index 9a4056c50a..0000000000 --- a/.changeset/four-spiders-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add labels to `@const` tags and props diff --git a/.changeset/silent-rockets-tease.md b/.changeset/silent-rockets-tease.md deleted file mode 100644 index 1a708c1d69..0000000000 --- a/.changeset/silent-rockets-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: tag stores for `$inspect.trace()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c20618af11..472a2b4a98 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.36.11 + +### Patch Changes + +- fix: always mark reactions of deriveds ([#16457](https://github.com/sveltejs/svelte/pull/16457)) + +- fix: add labels to `@const` tags and props ([#16454](https://github.com/sveltejs/svelte/pull/16454)) + +- fix: tag stores for `$inspect.trace()` ([#16452](https://github.com/sveltejs/svelte/pull/16452)) + ## 5.36.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 28ccb2884a..4b22911e6e 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.10", + "version": "5.36.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8c59098e0c..07cf3cdf4d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.10'; +export const VERSION = '5.36.11'; export const PUBLIC_VERSION = '5'; From ce4a99ed6d0b4b53c7abb7a8763e8e4bc4de5431 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:11:26 -0400 Subject: [PATCH 103/367] Version Packages (#16459) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/swift-cherries-know.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/swift-cherries-know.md diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md deleted file mode 100644 index d8bbb1256a..0000000000 --- a/.changeset/swift-cherries-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: move `capture_signals` to legacy module diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 472a2b4a98..1234efa0d5 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.12 + +### Patch Changes + +- chore: move `capture_signals` to legacy module ([#16456](https://github.com/sveltejs/svelte/pull/16456)) + ## 5.36.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4b22911e6e..629ec99af8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.11", + "version": "5.36.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 07cf3cdf4d..465bd73f0f 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.11'; +export const VERSION = '5.36.12'; export const PUBLIC_VERSION = '5'; From aabd333d89a4c9241bc3ed57752cb2616dd66b7c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:22:13 +0200 Subject: [PATCH 104/367] fix: ensure subscriptions are picked up correctly by deriveds (#16466) Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. If we didn't do this then the comparison of write versions would determine that the derived has a later version than the subscriber, and it would not be re-run. Fixes #16311 Fixes #15888 --- .changeset/fresh-penguins-impress.md | 5 +++ .../src/reactivity/create-subscriber.js | 4 ++ .../samples/store-inside-derived/_config.js | 45 +++++++++++++++++++ .../samples/store-inside-derived/main.svelte | 36 +++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .changeset/fresh-penguins-impress.md create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md new file mode 100644 index 0000000000..35ff4f0aaa --- /dev/null +++ b/.changeset/fresh-penguins-impress.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 4dcac4e6f6..6f9313eb0a 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -82,6 +82,10 @@ export function createSubscriber(start) { if (subscribers === 0) { stop?.(); stop = undefined; + // Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. + // If we didn't do this then the comparison of write versions would determine that the derived has a later version than + // the subscriber, and it would not be re-run. + increment(version); } }); }; diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js new file mode 100644 index 0000000000..de078b1e75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test: ({ assert, target }) => { + const [loading, increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +
$value: 0
+
valueFromStore.current: 0
+
valueDerivedCurrent: 0
+ + + ` + ); + + loading.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: Loading...
+
valueFromStore.current: Loading...
+
valueDerivedCurrent: Loading...
+ + + ` + ); + + increment.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: 1
+
valueFromStore.current: 1
+
valueDerivedCurrent: 1
+ + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte new file mode 100644 index 0000000000..06d0a0d4b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte @@ -0,0 +1,36 @@ + + +
+ $value: {isLoading ? 'Loading...' : $value} +
+ +
+ valueFromStore.current: {isLoading ? 'Loading...' : valueFromStore.current} +
+ +
+ valueDerivedCurrent: {isLoading ? 'Loading...' : valueDerivedCurrent} +
+ + + + From 28403beaeb360fd7b4096fe7c8ef98e43d0676c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:32:47 -0400 Subject: [PATCH 105/367] Version Packages (#16467) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fresh-penguins-impress.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fresh-penguins-impress.md diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md deleted file mode 100644 index 35ff4f0aaa..0000000000 --- a/.changeset/fresh-penguins-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1234efa0d5..5a5e532a08 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.13 + +### Patch Changes + +- fix: ensure subscriptions are picked up correctly by deriveds ([#16466](https://github.com/sveltejs/svelte/pull/16466)) + ## 5.36.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 629ec99af8..4bf9a5df22 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.12", + "version": "5.36.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 465bd73f0f..7d47fbc5f1 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.12'; +export const VERSION = '5.36.13'; export const PUBLIC_VERSION = '5'; From 9412c5861c365610d7c6c0e4ecd3572ac3fe5860 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Jul 2025 22:17:25 -0400 Subject: [PATCH 106/367] chore: log effect functions in log_effect_tree (#16468) --- packages/svelte/src/internal/client/dev/debug.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index c47080ed2f..2714a3af1f 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -63,6 +63,13 @@ export function log_effect_tree(effect, depth = 0) { // eslint-disable-next-line no-console console.log(callsite); + } else { + // eslint-disable-next-line no-console + console.groupCollapsed(`%cfn`, `font-weight: normal`); + // eslint-disable-next-line no-console + console.log(effect.fn); + // eslint-disable-next-line no-console + console.groupEnd(); } if (effect.deps !== null) { From 1deb31082a383fb5f4f9ae86f1ff12657823bcd7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 22 Jul 2025 12:56:32 -0400 Subject: [PATCH 107/367] fix: abort and reschedule `$effect.pre` when necessary (#16335) * unskip failing test * fix * tidy up * skip_no_async * add comment --- .../runtime-runes/samples/effect-order-7/_config.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js index 29c33c7b18..f0a9c2e867 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js @@ -2,14 +2,18 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - skip: true, + // For this to work in non-async mode, we would need to abort + // inside `#traverse_effect_tree`, which would be very + // complicated and annoying. Since this hasn't been + // a real issue (AFAICT), we ignore it + skip_no_async: true, - async test({ assert, target, logs }) { + async test({ target }) { const [open, close] = target.querySelectorAll('button'); flushSync(() => open.click()); - flushSync(() => close.click()); - assert.deepEqual(logs, [true]); + // if the effect queue isn't aborted after the state change, this will throw + flushSync(() => close.click()); } }); From 68372460e999e48ac09b1c2bd4935ceaea8c33e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 22 Jul 2025 13:29:15 -0400 Subject: [PATCH 108/367] chore: small tidy up (#16476) --- .../src/internal/client/reactivity/batch.js | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ec082bb595..c452211894 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -193,6 +193,8 @@ 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.#async_effects.length === 0 && this.#pending === 0) { + this.#commit(); + var render_effects = this.#render_effects; var effects = this.#effects; @@ -200,8 +202,6 @@ export class Batch { this.#effects = []; this.#block_effects = []; - this.#commit(); - flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -539,43 +539,43 @@ function flush_queued_effects(effects) { var length = effects.length; if (length === 0) return; - for (var i = 0; i < length; i++) { - var effect = effects[i]; - - if ((effect.f & (DESTROYED | INERT)) === 0) { - if (is_dirty(effect)) { - var wv = write_version; - - update_effect(effect); - - // Effects with no dependencies or teardown do not get added to the effect tree. - // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we - // don't know if we need to keep them until they are executed. Doing the check - // here (rather than in `update_effect`) allows us to skip the work for - // immediate effects. - if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - // if there's no teardown or abort controller we completely unlink - // the effect from the graph - if (effect.teardown === null && effect.ac === null) { - // remove this effect from the graph - unlink_effect(effect); - } else { - // keep the effect in the graph, but free up some memory - effect.fn = null; - } - } + var i = 0; - // if state is written in a user effect, abort and re-schedule, lest we run - // effects that should be removed as a result of the state change - if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { - break; + while (i < length) { + var effect = effects[i++]; + + if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { + var wv = write_version; + + update_effect(effect); + + // Effects with no dependencies or teardown do not get added to the effect tree. + // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we + // don't know if we need to keep them until they are executed. Doing the check + // here (rather than in `update_effect`) allows us to skip the work for + // immediate effects. + if (effect.deps === null && effect.first === null && effect.nodes_start === null) { + // if there's no teardown or abort controller we completely unlink + // the effect from the graph + if (effect.teardown === null && effect.ac === null) { + // remove this effect from the graph + unlink_effect(effect); + } else { + // keep the effect in the graph, but free up some memory + effect.fn = null; } } + + // if state is written in a user effect, abort and re-schedule, lest we run + // effects that should be removed as a result of the state change + if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + break; + } } } - for (; i < length; i += 1) { - schedule_effect(effects[i]); + while (i < length) { + schedule_effect(effects[i++]); } } From 8e2f4b51c50a2e10bc482fac6d900be921dd8b74 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 23 Jul 2025 07:55:51 -0400 Subject: [PATCH 109/367] fix: unset batch before flushing queued effects (#16482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - add state changes resulting from an $effect to a separate new batch - schedule rerunning effects based on the sources that are dirty, not just rerunning them all blindly (excempting async effects which will have run by that time already) * test * better fix * tests * this fixes the last test somehow * fix #16477 * typo * copy over changeset from #16477 * copy over changeset from #16464 * changeset * dedupe * move flushing_sync check inside Batch.ensure * unused * flushing_sync -> is_flushing_sync * remove flush_effects method * dedupe declaration * tweak * tweak * update comment — it _does_ feel slightly wrong, but no wronger than the rest of this cursed function --------- Co-authored-by: Simon Holthausen --- .changeset/grumpy-boats-beg.md | 5 + .changeset/shiny-walls-fix.md | 5 + .changeset/thick-mice-kick.md | 5 + .../src/internal/client/reactivity/batch.js | 190 ++++++++++-------- .../src/internal/client/reactivity/sources.js | 11 +- .../Component.svelte | 7 + .../1000-reading-derived-effects/_config.js | 5 + .../1000-reading-derived-effects/main.svelte | 8 + .../_config.js | 26 +++ .../main.svelte | 27 +++ .../async-effect-triggers-await/_config.js | 32 +++ .../async-effect-triggers-await/main.svelte | 24 +++ .../binding-update-while-focused-2/_config.js | 24 +++ .../main.svelte | 21 ++ 14 files changed, 298 insertions(+), 92 deletions(-) create mode 100644 .changeset/grumpy-boats-beg.md create mode 100644 .changeset/shiny-walls-fix.md create mode 100644 .changeset/thick-mice-kick.md create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte diff --git a/.changeset/grumpy-boats-beg.md b/.changeset/grumpy-boats-beg.md new file mode 100644 index 0000000000..f677743def --- /dev/null +++ b/.changeset/grumpy-boats-beg.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep input in sync when binding updated via effect diff --git a/.changeset/shiny-walls-fix.md b/.changeset/shiny-walls-fix.md new file mode 100644 index 0000000000..91ed548728 --- /dev/null +++ b/.changeset/shiny-walls-fix.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite async loop diff --git a/.changeset/thick-mice-kick.md b/.changeset/thick-mice-kick.md new file mode 100644 index 0000000000..eec55b77ee --- /dev/null +++ b/.changeset/thick-mice-kick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: exclude derived writes from effect abort and rescheduling diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c452211894..ce413fa1e1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -21,14 +21,13 @@ import { is_updating_effect, set_is_updating_effect, set_signal_status, - update_effect, - write_version + update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { old_values } from './sources.js'; +import { mark_reactions, old_values } from './sources.js'; import { unlink_effect } from './effects.js'; import { unset_context } from './async.js'; @@ -70,13 +69,15 @@ let last_scheduled_effect = null; let is_flushing = false; +let is_flushing_sync = false; + export class Batch { /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` * @type {Map} */ - #current = new Map(); + current = new Map(); /** * The values of any sources that are updated in this batch _before_ those updates took place. @@ -156,7 +157,7 @@ export class Batch { * * @param {Effect[]} root_effects */ - #process(root_effects) { + process(root_effects) { queued_root_effects = []; /** @type {Map | null} */ @@ -169,7 +170,7 @@ export class Batch { current_values = new Map(); batch_deriveds = new Map(); - for (const [source, current] of this.#current) { + for (const [source, current] of this.current) { current_values.set(source, { v: source.v, wv: source.wv }); source.v = current; } @@ -202,9 +203,22 @@ export class Batch { this.#effects = []; this.#block_effects = []; + // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with + // newly updated sources, which could lead to infinite loops when effects run over and over again. + current_batch = null; + flush_queued_effects(render_effects); flush_queued_effects(effects); + // Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`. + // That method expects `current_batch` to be set, and could run the loop again if effects result in new effects + // being scheduled but without writes happening in which case no new batch is created. + if (current_batch === null) { + current_batch = this; + } else { + batches.delete(this); + } + this.#deferred?.resolve(); } else { // otherwise mark effects clean so they get scheduled on the next run @@ -300,7 +314,7 @@ export class Batch { this.#previous.set(source, value); } - this.#current.set(source, source.v); + this.current.set(source, source.v); } activate() { @@ -327,13 +341,13 @@ export class Batch { flush() { if (queued_root_effects.length > 0) { - this.flush_effects(); + flush_effects(); } else { this.#commit(); } if (current_batch !== this) { - // this can happen if a `flushSync` occurred during `this.flush_effects()`, + // this can happen if a `flushSync` occurred during `flush_effects()`, // which is permitted in legacy mode despite being a terrible idea return; } @@ -345,52 +359,6 @@ export class Batch { this.deactivate(); } - flush_effects() { - var was_updating_effect = is_updating_effect; - is_flushing = true; - - try { - var flush_count = 0; - set_is_updating_effect(true); - - while (queued_root_effects.length > 0) { - if (flush_count++ > 1000) { - if (DEV) { - var updates = new Map(); - - for (const source of this.#current.keys()) { - for (const [stack, update] of source.updated ?? []) { - var entry = updates.get(stack); - - if (!entry) { - entry = { error: update.error, count: 0 }; - updates.set(stack, entry); - } - - entry.count += update.count; - } - } - - for (const update of updates.values()) { - // eslint-disable-next-line no-console - console.error(update.error); - } - } - - infinite_loop_guard(); - } - - this.#process(queued_root_effects); - old_values.clear(); - } - } finally { - is_flushing = false; - set_is_updating_effect(was_updating_effect); - - last_scheduled_effect = null; - } - } - /** * Append and remove branches to/from the DOM */ @@ -412,19 +380,8 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const e of this.#render_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#block_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); + for (const source of this.current.keys()) { + mark_reactions(source, DIRTY, false); } this.#render_effects = []; @@ -445,12 +402,12 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } - static ensure(autoflush = true) { + static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); batches.add(current_batch); - if (autoflush) { + if (!is_flushing_sync) { Batch.enqueue(() => { if (current_batch !== batch) { // a flushSync happened in the meantime @@ -487,32 +444,85 @@ export function flushSync(fn) { e.flush_sync_in_effect(); } - var result; + var was_flushing_sync = is_flushing_sync; + is_flushing_sync = true; - const batch = Batch.ensure(false); + try { + var result; - if (fn) { - batch.flush_effects(); + if (fn) { + flush_effects(); + result = fn(); + } - result = fn(); - } + while (true) { + flush_tasks(); - while (true) { - flush_tasks(); + if (queued_root_effects.length === 0) { + current_batch?.flush(); - if (queued_root_effects.length === 0) { - if (batch === current_batch) { - batch.flush(); + // we need to check again, in case we just updated an `$effect.pending()` + if (queued_root_effects.length === 0) { + // this would be reset in `flush_effects()` but since we are early returning here, + // we need to reset it here as well in case the first time there's 0 queued root effects + last_scheduled_effect = null; + + return /** @type {T} */ (result); + } } - // this would be reset in `batch.flush_effects()` but since we are early returning here, - // we need to reset it here as well in case the first time there's 0 queued root effects - last_scheduled_effect = null; + flush_effects(); + } + } finally { + is_flushing_sync = was_flushing_sync; + } +} + +function flush_effects() { + var was_updating_effect = is_updating_effect; + is_flushing = true; + + try { + var flush_count = 0; + set_is_updating_effect(true); + + while (queued_root_effects.length > 0) { + var batch = Batch.ensure(); + + if (flush_count++ > 1000) { + if (DEV) { + var updates = new Map(); + + for (const source of batch.current.keys()) { + for (const [stack, update] of source.updated ?? []) { + var entry = updates.get(stack); + + if (!entry) { + entry = { error: update.error, count: 0 }; + updates.set(stack, entry); + } + + entry.count += update.count; + } + } + + for (const update of updates.values()) { + // eslint-disable-next-line no-console + console.error(update.error); + } + } + + infinite_loop_guard(); + } - return /** @type {T} */ (result); + batch.process(queued_root_effects); + old_values.clear(); } + } finally { + is_flushing = false; + set_is_updating_effect(was_updating_effect); - batch.flush_effects(); + last_scheduled_effect = null; } } @@ -545,7 +555,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - var wv = write_version; + var n = current_batch ? current_batch.current.size : 0; update_effect(effect); @@ -568,7 +578,11 @@ function flush_queued_effects(effects) { // if state is written in a user effect, abort and re-schedule, lest we run // effects that should be removed as a result of the state change - if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + if ( + current_batch !== null && + current_batch.current.size > n && + (effect.f & USER_EFFECT) !== 0 + ) { break; } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f6b14f3360..3b28c8fdce 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -179,7 +179,7 @@ export function internal_set(source, value) { source.v = value; - const batch = Batch.ensure(); + var batch = Batch.ensure(); batch.capture(source, old_value); if (DEV) { @@ -301,9 +301,10 @@ export function increment(source) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {boolean} schedule_async * @returns {void} */ -function mark_reactions(signal, status) { +export function mark_reactions(signal, status, schedule_async = true) { var reactions = signal.reactions; if (reactions === null) return; @@ -323,14 +324,16 @@ function mark_reactions(signal, status) { continue; } + var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0); + // don't set a DIRTY reaction to MAYBE_DIRTY - if ((flags & DIRTY) === 0) { + if (should_schedule) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else if ((flags & DIRTY) === 0) { + } else if (should_schedule) { schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte new file mode 100644 index 0000000000..7a54323cb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte new file mode 100644 index 0000000000..bd326edfb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte @@ -0,0 +1,8 @@ + + +{#each arr} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js new file mode 100644 index 0000000000..782ae945f9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '3'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(input.value, '3'); + assert.htmlEqual(target.innerHTML, `

3

`); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(input.value, '2'); + assert.htmlEqual(target.innerHTML, `

2

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

{await value}

+ + + {#snippet pending()} +

loading...

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

1

+

1

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

2

+

2

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

{JSON.stringify((await data), null, 2)}

+ {#if true} + +

{unrelated}

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

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js new file mode 100644 index 0000000000..7a56c79d71 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js @@ -0,0 +1,24 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '3'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.equal(input.value, '3'); + assert.htmlEqual(target.innerHTML, `

3

`); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.equal(input.value, '2'); + assert.htmlEqual(target.innerHTML, `

2

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

{value}

+ From c26365bdda1a13e98a2e75f17da655e08ff4f8bc Mon Sep 17 00:00:00 2001 From: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:02:28 +0900 Subject: [PATCH 110/367] chore: rename form `accept-charset` attribute (#16478) * chore: rename form accept-charset attribute * chore: utf-8 to lowercase Co-authored-by: Rich Harris * Update .changeset/healthy-carpets-deny.md --------- Co-authored-by: Rich Harris --- .changeset/healthy-carpets-deny.md | 5 +++++ packages/svelte/elements.d.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/healthy-carpets-deny.md diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md new file mode 100644 index 0000000000..8f8db7fa9c --- /dev/null +++ b/.changeset/healthy-carpets-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rename form accept-charset attribute diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 1492f77792..604241592a 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -996,7 +996,7 @@ export interface HTMLFieldsetAttributes extends HTMLAttributes { - acceptcharset?: string | undefined | null; + 'accept-charset'?: 'utf-8' | (string & {}) | undefined | null; action?: string | undefined | null; autocomplete?: AutoFillBase | undefined | null; enctype?: From f8820956d2591288de53927c5e173242a13f125a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:23:52 -0400 Subject: [PATCH 111/367] Version Packages (#16484) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/grumpy-boats-beg.md | 5 ----- .changeset/healthy-carpets-deny.md | 5 ----- .changeset/shiny-walls-fix.md | 5 ----- .changeset/thick-mice-kick.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/grumpy-boats-beg.md delete mode 100644 .changeset/healthy-carpets-deny.md delete mode 100644 .changeset/shiny-walls-fix.md delete mode 100644 .changeset/thick-mice-kick.md diff --git a/.changeset/grumpy-boats-beg.md b/.changeset/grumpy-boats-beg.md deleted file mode 100644 index f677743def..0000000000 --- a/.changeset/grumpy-boats-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: keep input in sync when binding updated via effect diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md deleted file mode 100644 index 8f8db7fa9c..0000000000 --- a/.changeset/healthy-carpets-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rename form accept-charset attribute diff --git a/.changeset/shiny-walls-fix.md b/.changeset/shiny-walls-fix.md deleted file mode 100644 index 91ed548728..0000000000 --- a/.changeset/shiny-walls-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent infinite async loop diff --git a/.changeset/thick-mice-kick.md b/.changeset/thick-mice-kick.md deleted file mode 100644 index eec55b77ee..0000000000 --- a/.changeset/thick-mice-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: exclude derived writes from effect abort and rescheduling diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5a5e532a08..8cd3850460 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.36.14 + +### Patch Changes + +- fix: keep input in sync when binding updated via effect ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: rename form accept-charset attribute ([#16478](https://github.com/sveltejs/svelte/pull/16478)) + +- fix: prevent infinite async loop ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: exclude derived writes from effect abort and rescheduling ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + ## 5.36.13 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4bf9a5df22..7fe1d161f2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.13", + "version": "5.36.14", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7d47fbc5f1..cd9d8b459c 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.13'; +export const VERSION = '5.36.14'; export const PUBLIC_VERSION = '5'; From 53417ea8f785f2ec884b095ac1bacd08aa30da86 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 01:48:32 -0400 Subject: [PATCH 112/367] fix: preserve dirty status of deferred effects (#16487) * don't mark_reactions inside decrement, it can cause infinite loops * revert mark_reactions changes * preserve DIRTY/MAYBE_DIRTY status of deferred effects * changeset * tweak --- .changeset/cool-insects-argue.md | 5 ++ .../src/internal/client/reactivity/batch.js | 63 ++++++++++++++----- .../src/internal/client/reactivity/sources.js | 9 +-- .../async-effect-conservative/_config.js | 28 +++++++++ .../async-effect-conservative/main.svelte | 17 +++++ 5 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 .changeset/cool-insects-argue.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte diff --git a/.changeset/cool-insects-argue.md b/.changeset/cool-insects-argue.md new file mode 100644 index 0000000000..ff1c520b7b --- /dev/null +++ b/.changeset/cool-insects-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve dirty status of deferred effects diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ce413fa1e1..89bad947c7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,8 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT + USER_EFFECT, + MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -27,7 +28,7 @@ import * as e from '../errors.js'; import { flush_tasks } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { mark_reactions, old_values } from './sources.js'; +import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; import { unset_context } from './async.js'; @@ -146,6 +147,18 @@ export class Batch { */ #block_effects = []; + /** + * Deferred effects (which run after async work has completed) that are DIRTY + * @type {Effect[]} + */ + #dirty_effects = []; + + /** + * Deferred effects that are MAYBE_DIRTY + * @type {Effect[]} + */ + #maybe_dirty_effects = []; + /** * A set of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process` @@ -221,10 +234,9 @@ export class Batch { this.#deferred?.resolve(); } else { - // otherwise mark effects clean so they get scheduled on the next run - for (const e of this.#render_effects) set_signal_status(e, CLEAN); - for (const e of this.#effects) set_signal_status(e, CLEAN); - for (const e of this.#block_effects) set_signal_status(e, CLEAN); + this.#defer_effects(this.#render_effects); + this.#defer_effects(this.#effects); + this.#defer_effects(this.#block_effects); } if (current_values) { @@ -271,15 +283,15 @@ export class Batch { if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; - } else if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); - } else if (is_dirty(effect)) { - if ((flags & ASYNC) !== 0) { + } else if ((flags & CLEAN) === 0) { + if ((flags & EFFECT) !== 0) { + this.#effects.push(effect); + } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + this.#render_effects.push(effect); + } else if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); - } else { + } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); update_effect(effect); } @@ -303,6 +315,21 @@ export class Batch { } } + /** + * @param {Effect[]} effects + */ + #defer_effects(effects) { + for (const e of effects) { + const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; + target.push(e); + + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(e, CLEAN); + } + + effects.length = 0; + } + /** * Associate a change to a given source with the current * batch, noting its previous and current values @@ -380,8 +407,14 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const source of this.current.keys()) { - mark_reactions(source, DIRTY, false); + for (const e of this.#dirty_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); } this.#render_effects = []; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3b28c8fdce..7b5198542a 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -301,10 +301,9 @@ export function increment(source) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY - * @param {boolean} schedule_async * @returns {void} */ -export function mark_reactions(signal, status, schedule_async = true) { +function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -324,16 +323,14 @@ export function mark_reactions(signal, status, schedule_async = true) { continue; } - var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0); - // don't set a DIRTY reaction to MAYBE_DIRTY - if (should_schedule) { + if ((flags & DIRTY) === 0) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else if (should_schedule) { + } else { schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js new file mode 100644 index 0000000000..bab06a203d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

0

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

1

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

2

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

3

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

{await count}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 46d3261ed9a7d6acb9908f52e989013795555a56 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:39:54 +0200 Subject: [PATCH 113/367] chore: don't do effect scheduling unnecessarily (#16489) mini-cleanup post #16487 - we don't need to do the work of scheduling an effect that's already dirty which means it already scheduled its root effect to run --- packages/svelte/src/internal/client/reactivity/sources.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 7b5198542a..3b2087d56b 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -323,14 +323,16 @@ function mark_reactions(signal, status) { continue; } + var not_dirty = (flags & DIRTY) === 0; + // don't set a DIRTY reaction to MAYBE_DIRTY - if ((flags & DIRTY) === 0) { + if (not_dirty) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { + } else if (not_dirty) { schedule_effect(/** @type {Effect} */ (reaction)); } } From 4e74cd35fedf55e65b60c957041f79ca94dfc197 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:43:28 +0200 Subject: [PATCH 114/367] Version Packages (#16488) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cool-insects-argue.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/cool-insects-argue.md diff --git a/.changeset/cool-insects-argue.md b/.changeset/cool-insects-argue.md deleted file mode 100644 index ff1c520b7b..0000000000 --- a/.changeset/cool-insects-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve dirty status of deferred effects diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8cd3850460..5bffa5f70e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.15 + +### Patch Changes + +- fix: preserve dirty status of deferred effects ([#16487](https://github.com/sveltejs/svelte/pull/16487)) + ## 5.36.14 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7fe1d161f2..051f82ec3a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.14", + "version": "5.36.15", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cd9d8b459c..1d469f29b0 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.14'; +export const VERSION = '5.36.15'; export const PUBLIC_VERSION = '5'; From 7eb11e0e247d9da4edb88dc5652101ceb55fc530 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:00:43 +0200 Subject: [PATCH 115/367] fix: don't destroy effect roots created inside of deriveds (#16492) We were wrongfully adding effect roots to `derived.effects`, too, which meant those were destroyed when the derived reran. --- .changeset/lemon-weeks-call.md | 5 ++ .../src/internal/client/reactivity/effects.js | 6 ++- .../src/internal/client/reactivity/types.d.ts | 2 +- packages/svelte/tests/signals/test.ts | 51 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md new file mode 100644 index 0000000000..ae62305630 --- /dev/null +++ b/.changeset/lemon-weeks-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d..f44efa32f1 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -148,7 +148,11 @@ function create_effect(type, fn, sync, push = true) { } // if we're in a derived, add the effect there too - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { var derived = /** @type {Derived} */ (active_reaction); (derived.effects ??= []).push(effect); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 72187e84a7..81f7197b80 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -54,7 +54,7 @@ export interface Reaction extends Signal { export interface Derived extends Value, Reaction { /** The derived function */ fn: () => V; - /** Effects created inside this signal */ + /** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */ effects: null | Effect[]; /** Parent effect or derived */ parent: Effect | Derived | null; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b..eff6d6166a 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1390,4 +1390,55 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); }); From dc043fb2d3c87619e1c26315f600acd4fe19dfa4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 10:16:15 -0400 Subject: [PATCH 116/367] fix: don't update a focused input with values from its own past (#16491) * fix: don't update a focused input with values from its own past * remove * fix --- .changeset/fast-mails-fail.md | 5 +++ .../client/dom/elements/bindings/input.js | 11 ++++-- .../src/internal/client/reactivity/batch.js | 12 +++++- .../_config.js | 37 +++++++++++++++++++ .../main.svelte | 25 +++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .changeset/fast-mails-fail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md new file mode 100644 index 0000000000..027cb01548 --- /dev/null +++ b/.changeset/fast-mails-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't update a focused input with values from its own past diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 7c1fccea0f..7c73280dd6 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -8,7 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; -import { current_batch } from '../../../reactivity/batch.js'; +import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { + if (input === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // // //

{await find(query)}

- return; + if (batches.has(batch)) { + return; + } } if (is_numberlike_input(input) && value === to_number(input.value)) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 89bad947c7..123bc95d16 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,13 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; +/** + * This is needed to avoid overwriting inputs in non-async mode + * TODO 6.0 remove this, as non-async mode will go away + * @type {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 @@ -71,7 +78,6 @@ let last_scheduled_effect = null; let is_flushing = false; let is_flushing_sync = false; - export class Batch { /** * The current values of any sources that are updated in this batch @@ -173,6 +179,8 @@ export class Batch { process(root_effects) { queued_root_effects = []; + previous_batch = null; + /** @type {Map | null} */ var current_values = null; @@ -218,6 +226,7 @@ export class Batch { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. + previous_batch = current_batch; current_batch = null; flush_queued_effects(render_effects); @@ -350,6 +359,7 @@ export class Batch { deactivate() { current_batch = null; + previous_batch = null; for (const update of effect_pending_updates) { effect_pending_updates.delete(update); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js new file mode 100644 index 0000000000..b0772ad3c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, instance }) { + instance.shift(); + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '1'); + + input.focus(); + input.value = '2'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

2

`); + assert.equal(input.value, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte new file mode 100644 index 0000000000..2fc898e654 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -0,0 +1,25 @@ + + + + +

{await push(count)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 8ad02e4a8c920823034051e5e2a13df58f25ebd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:22:30 +0200 Subject: [PATCH 117/367] Version Packages (#16493) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-mails-fail.md | 5 ----- .changeset/lemon-weeks-call.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/fast-mails-fail.md delete mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md deleted file mode 100644 index 027cb01548..0000000000 --- a/.changeset/fast-mails-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't update a focused input with values from its own past diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md deleted file mode 100644 index ae62305630..0000000000 --- a/.changeset/lemon-weeks-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5bffa5f70e..450ecde53b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.16 + +### Patch Changes + +- fix: don't update a focused input with values from its own past ([#16491](https://github.com/sveltejs/svelte/pull/16491)) + +- fix: don't destroy effect roots created inside of deriveds ([#16492](https://github.com/sveltejs/svelte/pull/16492)) + ## 5.36.15 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 051f82ec3a..07954026b5 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.15", + "version": "5.36.16", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1d469f29b0..5d76fc3f29 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.15'; +export const VERSION = '5.36.16'; export const PUBLIC_VERSION = '5'; From 0bba84cc203c7cab945527cc3928597d3612a37d Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Fri, 25 Jul 2025 14:43:41 +0200 Subject: [PATCH 118/367] fix: add types for `part` attribute to svg attributes (#16499) --- .changeset/seven-colts-obey.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/seven-colts-obey.md diff --git a/.changeset/seven-colts-obey.md b/.changeset/seven-colts-obey.md new file mode 100644 index 0000000000..b41216f649 --- /dev/null +++ b/.changeset/seven-colts-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add types for `part` attribute to svg attributes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 604241592a..2e1042dfd6 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1553,6 +1553,7 @@ export interface SVGAttributes extends AriaAttributes, DO height?: number | string | undefined | null; id?: string | undefined | null; lang?: string | undefined | null; + part?: string | undefined | null; max?: number | string | undefined | null; media?: string | undefined | null; // On the `textPath` element From b0f9ea3ae62cfb9eb4437898bf8a6b90e0531657 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:57:32 -0700 Subject: [PATCH 119/367] fix: throw on duplicate class field declarations (#16502) * fix: throw on duplicate class field declarations * doh * handle assignment in constructor * fix * apply suggestion from review * fix * fix failing test --- .changeset/odd-phones-taste.md | 5 ++ .../98-reference/.generated/compile-errors.md | 6 +++ .../svelte/messages/compile-errors/script.md | 4 ++ packages/svelte/src/compiler/errors.js | 10 ++++ .../phases/2-analyze/visitors/ClassBody.js | 53 ++++++++++++++++++- .../class-state-constructor-9/errors.json | 12 ++--- 6 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 .changeset/odd-phones-taste.md diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md new file mode 100644 index 0000000000..ec9534b741 --- /dev/null +++ b/.changeset/odd-phones-taste.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw on duplicate class field declarations diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 20f57770d1..957a9f67c7 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -364,6 +364,12 @@ The $ name is reserved, and cannot be used for variables and imports The $ prefix is reserved, and cannot be used for variables and imports ``` +### duplicate_class_field + +``` +`%name%` has already been declared +``` + ### each_item_invalid_assignment ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 2b0c5eafdf..5c1080aced 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -30,6 +30,10 @@ > The $ prefix is reserved, and cannot be used for variables and imports +## duplicate_class_field + +> `%name%` has already been declared + ## each_item_invalid_assignment > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 599d3e8248..e763a6e073 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) { e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`); } +/** + * `%name%` has already been declared + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function duplicate_class_field(node, name) { + e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`); +} + /** * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index ffc39ac00d..2bfc1dbce3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -33,6 +33,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const state_fields = new Map(); + /** @type {Map>} */ + const fields = new Map(); + context.state.analysis.classes.set(node, state_fields); /** @type {MethodDefinition | null} */ @@ -54,6 +57,14 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } + const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name; + const field = fields.get(_key); + + // if there's already a method or assigned field, error + if (field && !(field.length === 1 && field[0] === 'prop')) { + e.duplicate_class_field(node, _key); + } + state_fields.set(name, { node, type: rune, @@ -67,10 +78,48 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { handle(child, child.key, child.value); + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.value ? 'assigned_prop' : 'prop']); + continue; + } + e.duplicate_class_field(child, key); } - if (child.type === 'MethodDefinition' && child.kind === 'constructor') { - constructor = child; + if (child.type === 'MethodDefinition') { + if (child.kind === 'constructor') { + constructor = child; + } else if (!child.computed) { + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.kind]); + continue; + } + if ( + field.includes(child.kind) || + field.includes('prop') || + field.includes('assigned_prop') + ) { + e.duplicate_class_field(child, key); + } + if (child.kind === 'get') { + if (field.length === 1 && field[0] === 'set') { + field.push('get'); + continue; + } + } else if (child.kind === 'set') { + if (field.length === 1 && field[0] === 'get') { + field.push('set'); + continue; + } + } else { + field.push(child.kind); + continue; + } + e.duplicate_class_field(child, key); + } } } diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json index b7dd4c8ed4..94b5f191c2 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -1,14 +1,14 @@ [ { - "code": "state_field_invalid_assignment", - "message": "Cannot assign to a state field before its declaration", + "code": "duplicate_class_field", + "message": "`count` has already been declared", "start": { - "line": 2, - "column": 1 + "line": 5, + "column": 2 }, "end": { - "line": 2, - "column": 12 + "line": 5, + "column": 24 } } ] From d0ebd42986c4d3e4db0420f1aaebd337afdb230f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:46:12 -0700 Subject: [PATCH 120/367] Version Packages (#16500) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/odd-phones-taste.md | 5 ----- .changeset/seven-colts-obey.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/odd-phones-taste.md delete mode 100644 .changeset/seven-colts-obey.md diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md deleted file mode 100644 index ec9534b741..0000000000 --- a/.changeset/odd-phones-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: throw on duplicate class field declarations diff --git a/.changeset/seven-colts-obey.md b/.changeset/seven-colts-obey.md deleted file mode 100644 index b41216f649..0000000000 --- a/.changeset/seven-colts-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add types for `part` attribute to svg attributes diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 450ecde53b..766c83763a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.17 + +### Patch Changes + +- fix: throw on duplicate class field declarations ([#16502](https://github.com/sveltejs/svelte/pull/16502)) + +- fix: add types for `part` attribute to svg attributes ([#16499](https://github.com/sveltejs/svelte/pull/16499)) + ## 5.36.16 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 07954026b5..22bc8cc8fa 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.16", + "version": "5.36.17", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 5d76fc3f29..d2992a3dcd 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.16'; +export const VERSION = '5.36.17'; export const PUBLIC_VERSION = '5'; From c7851789d71cc3d0b5d89ce9c759a39be6caa51a Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 26 Jul 2025 21:27:22 +0300 Subject: [PATCH 121/367] fix: always mark props as stateful (#16504) --- .changeset/gorgeous-jeans-begin.md | 5 +++++ .../phases/2-analyze/visitors/Identifier.js | 5 ++++- .../props-default-value-function/_config.js | 15 +++++++++++++++ .../props-default-value-function/inner.svelte | 4 ++++ .../props-default-value-function/main.svelte | 14 ++++++++++++++ .../props-default-value-function/wrapper.svelte | 7 +++++++ .../props-default-value-function/wrapper2.svelte | 7 +++++++ 7 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/gorgeous-jeans-begin.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte diff --git a/.changeset/gorgeous-jeans-begin.md b/.changeset/gorgeous-jeans-begin.md new file mode 100644 index 0000000000..586c4d8a57 --- /dev/null +++ b/.changeset/gorgeous-jeans-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always mark props as stateful diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index cced326f9b..4dfdfe5af1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -93,7 +93,10 @@ export function Identifier(node, context) { context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && - !binding.is_function() && + (binding.kind === 'prop' || + binding.kind === 'bindable_prop' || + binding.kind === 'rest_prop' || + !binding.is_function()) && !context.state.scope.evaluate(node).is_known; } diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js new file mode 100644 index 0000000000..6b281f04f0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, ` Inner: 0 Inner: 0`); + btn?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ` Inner: 1 Inner: 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte new file mode 100644 index 0000000000..6bde0a15a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte @@ -0,0 +1,4 @@ + +Inner: {getter()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte new file mode 100644 index 0000000000..2cb2f67b82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte new file mode 100644 index 0000000000..525494ddfb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte new file mode 100644 index 0000000000..9498f432d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte @@ -0,0 +1,7 @@ + + + From 7cc4d56263f724c4d76ab6140af4d117cbb41f1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Jul 2025 22:16:40 -0400 Subject: [PATCH 122/367] feat: ignore component options in `compileModule` (#16362) --- .changeset/violet-ways-sleep.md | 5 + .../svelte/src/compiler/validate-options.js | 172 +++++++++--------- 2 files changed, 96 insertions(+), 81 deletions(-) create mode 100644 .changeset/violet-ways-sleep.md diff --git a/.changeset/violet-ways-sleep.md b/.changeset/violet-ways-sleep.md new file mode 100644 index 0000000000..749ecb1719 --- /dev/null +++ b/.changeset/violet-ways-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: ignore component options in `compileModule` diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index ed83375d22..2b727ad093 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -8,7 +8,7 @@ import * as w from './warnings.js'; * @typedef {(input: Input, keypath: string) => Required} Validator */ -const common = { +const common_options = { filename: string('(unknown)'), // default to process.cwd() where it exists to replicate svelte4 behavior (and make Deno work with this as well) @@ -48,110 +48,120 @@ const common = { }) }; -export const validate_module_options = - /** @type {Validator} */ ( - object({ - ...common - }) - ); +const component_options = { + accessors: deprecate(w.options_deprecated_accessors, boolean(false)), -export const validate_component_options = - /** @type {Validator} */ ( - object({ - ...common, + css: validator('external', (input) => { + if (input === true || input === false) { + throw_error( + 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' + ); + } + if (input === 'none') { + throw_error( + 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' + ); + } - accessors: deprecate(w.options_deprecated_accessors, boolean(false)), + if (input !== 'external' && input !== 'injected') { + throw_error(`css should be either "external" (default, recommended) or "injected"`); + } - css: validator('external', (input) => { - if (input === true || input === false) { - throw_error( - 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' - ); - } - if (input === 'none') { - throw_error( - 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' - ); - } + return input; + }), - if (input !== 'external' && input !== 'injected') { - throw_error(`css should be either "external" (default, recommended) or "injected"`); - } + cssHash: fun(({ css, hash }) => { + return `svelte-${hash(css)}`; + }), + + // TODO this is a sourcemap option, would be good to put under a sourcemap namespace + cssOutputFilename: string(undefined), + + customElement: boolean(false), + + discloseVersion: boolean(true), - return input; - }), + immutable: deprecate(w.options_deprecated_immutable, boolean(false)), - cssHash: fun(({ css, hash }) => { - return `svelte-${hash(css)}`; - }), + legacy: removed( + 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' + ), + + compatibility: object({ + componentApi: list([4, 5], 5) + }), + + loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + + name: string(undefined), - // TODO this is a sourcemap option, would be good to put under a sourcemap namespace - cssOutputFilename: string(undefined), + namespace: list(['html', 'mathml', 'svg']), - customElement: boolean(false), + modernAst: boolean(false), - discloseVersion: boolean(true), + outputFilename: string(undefined), - immutable: deprecate(w.options_deprecated_immutable, boolean(false)), + preserveComments: boolean(false), - legacy: removed( - 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' - ), + fragments: list(['html', 'tree']), - compatibility: object({ - componentApi: list([4, 5], 5) - }), + preserveWhitespace: boolean(false), - loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + runes: boolean(undefined), - name: string(undefined), + hmr: boolean(false), - namespace: list(['html', 'mathml', 'svg']), + sourcemap: validator(undefined, (input) => { + // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, + // so there's no good way to check type validity here + return input; + }), - modernAst: boolean(false), + enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - outputFilename: string(undefined), + hydratable: warn_removed(w.options_removed_hydratable), - preserveComments: boolean(false), + format: removed( + 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + + 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' + ), - fragments: list(['html', 'tree']), + tag: removed( + 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + + 'If that does not solve your use case, please open an issue on GitHub with details.' + ), - preserveWhitespace: boolean(false), + sveltePath: removed( + 'The sveltePath option has been removed in Svelte 5. ' + + 'If this option was crucial for you, please open an issue on GitHub with your use case.' + ), - runes: boolean(undefined), + // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), + // but with new TypeScript compilation modes strictly separating types it's not necessary anymore + errorMode: removed( + 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ), - hmr: boolean(false), + varsReport: removed( + 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ) +}; - sourcemap: validator(undefined, (input) => { - // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, - // so there's no good way to check type validity here - return input; - }), +export const validate_module_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...Object.fromEntries(Object.keys(component_options).map((key) => [key, () => {}])) + }) + ); - enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - hydratable: warn_removed(w.options_removed_hydratable), - format: removed( - 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + - 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' - ), - tag: removed( - 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + - 'If that does not solve your use case, please open an issue on GitHub with details.' - ), - sveltePath: removed( - 'The sveltePath option has been removed in Svelte 5. ' + - 'If this option was crucial for you, please open an issue on GitHub with your use case.' - ), - // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), - // but with new TypeScript compilation modes strictly separating types it's not necessary anymore - errorMode: removed( - 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ), - varsReport: removed( - 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ) +export const validate_component_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...component_options }) ); From 39ee7cf4c247965ebacdd16fc365a31f00506026 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 22:22:14 -0700 Subject: [PATCH 123/367] Version Packages (#16505) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/gorgeous-jeans-begin.md | 5 ----- .changeset/violet-ways-sleep.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/gorgeous-jeans-begin.md delete mode 100644 .changeset/violet-ways-sleep.md diff --git a/.changeset/gorgeous-jeans-begin.md b/.changeset/gorgeous-jeans-begin.md deleted file mode 100644 index 586c4d8a57..0000000000 --- a/.changeset/gorgeous-jeans-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always mark props as stateful diff --git a/.changeset/violet-ways-sleep.md b/.changeset/violet-ways-sleep.md deleted file mode 100644 index 749ecb1719..0000000000 --- a/.changeset/violet-ways-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: ignore component options in `compileModule` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 766c83763a..f939f69d28 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.37.0 + +### Minor Changes + +- feat: ignore component options in `compileModule` ([#16362](https://github.com/sveltejs/svelte/pull/16362)) + +### Patch Changes + +- fix: always mark props as stateful ([#16504](https://github.com/sveltejs/svelte/pull/16504)) + ## 5.36.17 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 22bc8cc8fa..c781eda8c8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.36.17", + "version": "5.37.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d2992a3dcd..e83ba6fb30 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.36.17'; +export const VERSION = '5.37.0'; export const PUBLIC_VERSION = '5'; From 1773df94845bc53d55a1c9e9eb033d08056c845a Mon Sep 17 00:00:00 2001 From: fkobi Date: Sun, 27 Jul 2025 14:17:50 +0000 Subject: [PATCH 124/367] docs: use sh instead of bash in source blocks (#16506) * use sh instead of bash in source blocks no bash-specific functionality is used * regenerate --------- Co-authored-by: Rich Harris --- CONTRIBUTING.md | 4 ++-- documentation/docs/01-introduction/02-getting-started.md | 2 +- documentation/docs/07-misc/02-testing.md | 4 ++-- .../docs/98-reference/.generated/compile-warnings.md | 2 +- packages/svelte/README.md | 2 +- packages/svelte/messages/compile-warnings/template.md | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2d3e45049..0653b08b76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,13 +101,13 @@ Test samples are kept in `/test/xxx/samples` folder. 1. To run test, run `pnpm test`. 1. To run a particular test suite, use `pnpm test `, for example: - ```bash + ```sh pnpm test validator ``` 1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: - ```bash + ```sh pnpm test validator -t a11y-alt-text ``` diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index c7351729ff..e97a46ad34 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -4,7 +4,7 @@ title: Getting started We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with: -```bash +```sh npx sv create myapp cd myapp npm install diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index db99b70770..bcec4db0a3 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -10,7 +10,7 @@ Unit tests allow you to test small isolated parts of your code. Integration test To setup Vitest manually, first install it: -```bash +```sh npm install -D vitest ``` @@ -166,7 +166,7 @@ It is possible to test your components in isolation using Vitest. To get started, install jsdom (a library that shims DOM APIs): -```bash +```sh npm install -D jsdom ``` diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 2af9021a6a..01003f30c5 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -683,7 +683,7 @@ Some templating languages (including Svelte) will 'fix' HTML by turning ` Date: Sun, 27 Jul 2025 19:50:22 +0200 Subject: [PATCH 125/367] fix: `append_styles` in an effect to make them available on mount (#16509) Co-authored-by: Rich Harris --- .changeset/shaggy-comics-fail.md | 5 +++++ .changeset/wise-hairs-pay.md | 5 +++++ .../src/compiler/phases/2-analyze/index.js | 16 +++++++------- .../3-transform/client/transform-client.js | 5 +++-- .../svelte/src/internal/client/dom/css.js | 4 ++-- .../host-rune-access-injected-css/_config.js | 21 +++++++++++++++++++ .../host-rune-access-injected-css/main.svelte | 16 ++++++++++++++ .../Thing.svelte | 9 ++++++++ .../custom-element-injected-styles/_config.js | 15 +++++++++++++ .../main.svelte | 5 +++++ 10 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 .changeset/shaggy-comics-fail.md create mode 100644 .changeset/wise-hairs-pay.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte diff --git a/.changeset/shaggy-comics-fail.md b/.changeset/shaggy-comics-fail.md new file mode 100644 index 0000000000..981a25c978 --- /dev/null +++ b/.changeset/shaggy-comics-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `append_styles` in an effect to make them available on mount diff --git a/.changeset/wise-hairs-pay.md b/.changeset/wise-hairs-pay.md new file mode 100644 index 0000000000..7d96c1daab --- /dev/null +++ b/.changeset/wise-hairs-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always inject styles when compiling as a custom element diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d407b44556..cd44fd998a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -451,6 +451,8 @@ export function analyze_component(root, source, options) { } } + const is_custom_element = !!options.customElementOptions || options.customElement; + // TODO remove all the ?? stuff, we don't need it now that we're validating the config /** @type {ComponentAnalysis} */ const analysis = { @@ -500,13 +502,13 @@ export function analyze_component(root, source, options) { needs_props: false, event_directive_node: null, uses_event_attributes: false, - custom_element: options.customElementOptions ?? options.customElement, - inject_styles: options.css === 'injected' || options.customElement, - accessors: options.customElement - ? true - : (runes ? false : !!options.accessors) || - // because $set method needs accessors - options.compatibility?.componentApi === 4, + custom_element: is_custom_element, + inject_styles: options.css === 'injected' || is_custom_element, + accessors: + is_custom_element || + (runes ? false : !!options.accessors) || + // because $set method needs accessors + options.compatibility?.componentApi === 4, reactive_statements: new Map(), binding_groups: new Map(), slot_names: new Map(), 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 124438a9da..a56aca9c5f 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 @@ -621,8 +621,9 @@ export function client_component(analysis, options) { ); } - if (analysis.custom_element) { - const ce = analysis.custom_element; + const ce = options.customElementOptions ?? options.customElement; + + if (ce) { const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; /** @type {ESTree.Property[]} */ diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 52be36aa1f..8e6faa0e32 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,6 +1,6 @@ import { DEV } from 'esm-env'; -import { queue_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; +import { effect } from '../reactivity/effects.js'; /** * @param {Node} anchor @@ -8,7 +8,7 @@ import { register_style } from '../dev/css.js'; */ export function append_styles(anchor, css) { // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_micro_task(() => { + effect(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js new file mode 100644 index 0000000000..99a223492b --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + /** @type {any} */ + const el = target.querySelector('custom-element'); + + /** @type {string} */ + let html = ''; + const handle_evt = (e) => (html = e.detail); + el.addEventListener('html', handle_evt); + + await tick(); + await tick(); + await tick(); + + assert.ok(html.includes(' + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte new file mode 100644 index 0000000000..0a2b139274 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte @@ -0,0 +1,9 @@ + + +

hello

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js new file mode 100644 index 0000000000..74597504bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const thing = /** @type HTMLElement & { object: { test: true }; } */ ( + target.querySelector('my-thing') + ); + + await tick(); + + assert.include(thing.shadowRoot?.innerHTML, 'red'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte new file mode 100644 index 0000000000..ba5b788da9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte @@ -0,0 +1,5 @@ + + + From 51771447c6cf941da7a07733435a5b18a91ddd28 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 27 Jul 2025 16:59:29 -0400 Subject: [PATCH 126/367] chore: remove `parser.template_untrimmed` (#16511) --- .changeset/shiny-berries-call.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/index.js | 7 ------- .../svelte/src/compiler/phases/1-parse/state/element.js | 8 -------- 3 files changed, 5 insertions(+), 15 deletions(-) create mode 100644 .changeset/shiny-berries-call.md diff --git a/.changeset/shiny-berries-call.md b/.changeset/shiny-berries-call.md new file mode 100644 index 0000000000..adc62b3cd0 --- /dev/null +++ b/.changeset/shiny-berries-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove `parser.template_untrimmed` diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 77cc2bf3fa..f5e0693b31 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -23,12 +23,6 @@ export class Parser { */ template; - /** - * @readonly - * @type {string} - */ - template_untrimmed; - /** * Whether or not we're in loose parsing mode, in which * case we try to continue parsing as much as possible @@ -67,7 +61,6 @@ export class Parser { } this.loose = loose; - this.template_untrimmed = template; this.template = template.trimEnd(); let match_lang; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 87332f647d..ed1b047d55 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -370,14 +370,6 @@ export default function element(parser) { // ... or we're followed by whitespace, for example near the end of the template, // which we want to take in so that language tools has more room to work with parser.allow_whitespace(); - if (parser.index === parser.template.length) { - while ( - parser.index < parser.template_untrimmed.length && - regex_whitespace.test(parser.template_untrimmed[parser.index]) - ) { - parser.index++; - } - } } } } From 03f2e44757925d0ca53f4e563965c3135ef81f16 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:56:41 -0700 Subject: [PATCH 127/367] chore: remove some todos (#16515) * chore: remove some todos * fix * fix * doh * convert `TemplateComment` back to `Comment` * `Evaluation.has_unknown` --- .changeset/happy-countries-dance.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 4 ++-- packages/svelte/src/compiler/phases/1-parse/index.js | 1 - .../3-transform/client/visitors/CallExpression.js | 4 +++- .../3-transform/client/visitors/OnDirective.js | 4 ++-- packages/svelte/src/compiler/phases/scope.js | 11 +++++++++++ packages/svelte/src/compiler/types/template.d.ts | 12 +++++++++++- packages/svelte/types/index.d.ts | 12 +++++++++++- 8 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 .changeset/happy-countries-dance.md diff --git a/.changeset/happy-countries-dance.md b/.changeset/happy-countries-dance.md new file mode 100644 index 0000000000..641cf21fd8 --- /dev/null +++ b/.changeset/happy-countries-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove some todos diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 6b2e6cda70..eb0e4eff8c 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1707,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) { } // Ensure modifiers are applied in the same order as Svelte 4 -const modifier_order = [ +const modifier_order = /** @type {const} */ ([ 'preventDefault', 'stopPropagation', 'stopImmediatePropagation', 'self', 'trusted', 'once' -]; +]); /** * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index f5e0693b31..8f7ef76be5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -1,5 +1,4 @@ /** @import { AST } from '#compiler' */ -/** @import { Comment } from 'estree' */ // @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from 'acorn'; import fragment from './state/fragment.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3e2f1414e6..c126742d3c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -82,7 +82,9 @@ export function CallExpression(node, context) { ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( node.callee.property.name ) && - node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases? + node.arguments.some( + (arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown + ) ) { return b.call( node.callee, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js index 7a66a8ecbb..0ee3b0fb10 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js @@ -3,14 +3,14 @@ import * as b from '#compiler/builders'; import { build_event, build_event_handler } from './shared/events.js'; -const modifiers = [ +const modifiers = /** @type {const} */ ([ 'stopPropagation', 'stopImmediatePropagation', 'preventDefault', 'self', 'trusted', 'once' -]; +]); /** * @param {AST.OnDirective} node diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 700e098e45..eaacf5dda9 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -180,6 +180,13 @@ class Evaluation { */ is_known = true; + /** + * True if the possible values contains `UNKNOWN` + * @readonly + * @type {boolean} + */ + has_unknown = false; + /** * True if the value is known to not be null/undefined * @readonly @@ -540,6 +547,10 @@ class Evaluation { if (value == null || value === UNKNOWN) { this.is_defined = false; } + + if (value === UNKNOWN) { + this.has_unknown = true; + } } if (this.values.size > 1 || typeof this.value === 'symbol') { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index de06a41469..058a1a8e66 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -247,7 +247,17 @@ export namespace AST { name: string; /** The 'y' in `on:x={y}` */ expression: null | Expression; - modifiers: string[]; // TODO specify + modifiers: Array< + | 'capture' + | 'nonpassive' + | 'once' + | 'passive' + | 'preventDefault' + | 'self' + | 'stopImmediatePropagation' + | 'stopPropagation' + | 'trusted' + >; /** @internal */ metadata: { expression: ExpressionMetadata; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9ea45af7e6..64aa9e23ba 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1296,7 +1296,17 @@ declare module 'svelte/compiler' { name: string; /** The 'y' in `on:x={y}` */ expression: null | Expression; - modifiers: string[]; + modifiers: Array< + | 'capture' + | 'nonpassive' + | 'once' + | 'passive' + | 'preventDefault' + | 'self' + | 'stopImmediatePropagation' + | 'stopPropagation' + | 'trusted' + >; } /** A `style:` directive */ From 48f2fa22c0c941ac6e550151212845adc628b3de Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:04:06 -0700 Subject: [PATCH 128/367] fix: allow await expressions inside `{#await ...}` argument (#16514) * fix: allow await expressions inside `{#await ...}` argument * add test * apply suggestion from review --------- Co-authored-by: 7nik --- .changeset/long-roses-train.md | 5 +++ .../3-transform/client/visitors/AwaitBlock.js | 7 ++- .../samples/async-await/_config.js | 43 +++++++++++++++++++ .../samples/async-await/main.svelte | 22 ++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .changeset/long-roses-train.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await/main.svelte diff --git a/.changeset/long-roses-train.md b/.changeset/long-roses-train.md new file mode 100644 index 0000000000..10920ac5c4 --- /dev/null +++ b/.changeset/long-roses-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow await expressions inside `{#await ...}` argument diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index c550c8e17b..4246091bcf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,7 +1,7 @@ /** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -import { extract_identifiers } from '../../../../utils/ast.js'; +import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; @@ -15,7 +15,10 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); + const expression = b.thunk( + build_expression(context, node.expression, node.metadata.expression), + node.metadata.expression.has_await + ); let then_block; let catch_block; diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js new file mode 100644 index 0000000000..dda6a7a895 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [reset, one, two, reject] = target.querySelectorAll('button'); + + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + one.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' one_res' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + two.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' two_res' + ); + + reset.click(); + reject.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' reject_catch' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte new file mode 100644 index 0000000000..8673e45414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + + {#await await deferred.promise + "_res"} + waiting + {:then res} + {res} + {:catch err} + {err}_catch + {/await} + + {#snippet pending()} +

pending

+ {/snippet} +
From d82edf6b1dfaac8af70462b1009f3b9da47a701a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:07:57 -0700 Subject: [PATCH 129/367] Version Packages (#16513) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/happy-countries-dance.md | 5 ----- .changeset/long-roses-train.md | 5 ----- .changeset/shaggy-comics-fail.md | 5 ----- .changeset/shiny-berries-call.md | 5 ----- .changeset/wise-hairs-pay.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/happy-countries-dance.md delete mode 100644 .changeset/long-roses-train.md delete mode 100644 .changeset/shaggy-comics-fail.md delete mode 100644 .changeset/shiny-berries-call.md delete mode 100644 .changeset/wise-hairs-pay.md diff --git a/.changeset/happy-countries-dance.md b/.changeset/happy-countries-dance.md deleted file mode 100644 index 641cf21fd8..0000000000 --- a/.changeset/happy-countries-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove some todos diff --git a/.changeset/long-roses-train.md b/.changeset/long-roses-train.md deleted file mode 100644 index 10920ac5c4..0000000000 --- a/.changeset/long-roses-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow await expressions inside `{#await ...}` argument diff --git a/.changeset/shaggy-comics-fail.md b/.changeset/shaggy-comics-fail.md deleted file mode 100644 index 981a25c978..0000000000 --- a/.changeset/shaggy-comics-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: `append_styles` in an effect to make them available on mount diff --git a/.changeset/shiny-berries-call.md b/.changeset/shiny-berries-call.md deleted file mode 100644 index adc62b3cd0..0000000000 --- a/.changeset/shiny-berries-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove `parser.template_untrimmed` diff --git a/.changeset/wise-hairs-pay.md b/.changeset/wise-hairs-pay.md deleted file mode 100644 index 7d96c1daab..0000000000 --- a/.changeset/wise-hairs-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always inject styles when compiling as a custom element diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f939f69d28..59c44dd4f9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.37.1 + +### Patch Changes + +- chore: remove some todos ([#16515](https://github.com/sveltejs/svelte/pull/16515)) + +- fix: allow await expressions inside `{#await ...}` argument ([#16514](https://github.com/sveltejs/svelte/pull/16514)) + +- fix: `append_styles` in an effect to make them available on mount ([#16509](https://github.com/sveltejs/svelte/pull/16509)) + +- chore: remove `parser.template_untrimmed` ([#16511](https://github.com/sveltejs/svelte/pull/16511)) + +- fix: always inject styles when compiling as a custom element ([#16509](https://github.com/sveltejs/svelte/pull/16509)) + ## 5.37.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c781eda8c8..826850ea60 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.37.0", + "version": "5.37.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e83ba6fb30..2d5083a2a5 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.37.0'; +export const VERSION = '5.37.1'; export const PUBLIC_VERSION = '5'; From a91e0154d4bc4b0a3209956647f868e518d0acba Mon Sep 17 00:00:00 2001 From: Laszlo Korte Date: Thu, 31 Jul 2025 09:45:52 +0200 Subject: [PATCH 130/367] fix: double event processing in Firefox (#16522) (#16527) * Fix double event processing (#16522) Firefox seems to garbage collect event objects during event propagation if no global reference to the event object is kept. That discards the __root marker set on the event object to early, leading to duplicate processing. * minor tweaks --------- Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> --- .changeset/chilly-bananas-train.md | 5 +++++ .../svelte/src/internal/client/dom/elements/events.js | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/chilly-bananas-train.md diff --git a/.changeset/chilly-bananas-train.md b/.changeset/chilly-bananas-train.md new file mode 100644 index 0000000000..b0305f9e61 --- /dev/null +++ b/.changeset/chilly-bananas-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: double event processing in firefox due to event object being garbage collected diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index fa3bf0b021..19bd1cfa18 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -141,6 +141,13 @@ export function delegate(events) { } } +// used to store the reference to the currently propagated event +// to prevent garbage collection between microtasks in Firefox +// If the event object is GCed too early, the expando __root property +// set on the event object is lost, causing the event delegation +// to process the event twice +let last_propagated_event = null; + /** * @this {EventTarget} * @param {Event} event @@ -153,6 +160,8 @@ export function handle_event_propagation(event) { var path = event.composedPath?.() || []; var current_target = /** @type {null | Element} */ (path[0] || event.target); + last_propagated_event = event; + // composedPath contains list of nodes the event has propagated through. // We check __root to skip all nodes below it in case this is a // parent of the __root node, which indicates that there's nested From f5950f866c51d9325a432ebfd5362bcf2647763a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 31 Jul 2025 02:11:52 -0700 Subject: [PATCH 131/367] fix: correctly differentiate static fields before emitting `duplicate_class_field` (#16526) * fix: correctly differentiate static fields before emitting `duplicate_class_field` * remove unnecessary `#` concatenation * add test --- .changeset/nine-cups-film.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/ClassBody.js | 6 +++--- .../samples/class-state-constructor-9/input.svelte.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/nine-cups-film.md diff --git a/.changeset/nine-cups-film.md b/.changeset/nine-cups-film.md new file mode 100644 index 0000000000..ba72337dac --- /dev/null +++ b/.changeset/nine-cups-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly differentiate static fields before emitting `duplicate_class_field` diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index 2bfc1dbce3..dd21637174 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -57,7 +57,7 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } - const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name; + const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name; const field = fields.get(_key); // if there's already a method or assigned field, error @@ -78,7 +78,7 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { handle(child, child.key, child.value); - const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const key = /** @type {string} */ (get_name(child.key)); const field = fields.get(key); if (!field) { fields.set(key, [child.value ? 'assigned_prop' : 'prop']); @@ -91,7 +91,7 @@ export function ClassBody(node, context) { if (child.kind === 'constructor') { constructor = child; } else if (!child.computed) { - const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const key = (child.static ? '@' : '') + get_name(child.key); const field = fields.get(key); if (!field) { fields.set(key, [child.kind]); diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js index a8469e13af..3806046f3f 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js @@ -1,6 +1,6 @@ export class Counter { count = -1; - + static count() {} constructor() { this.count = $state(0); } From 72e46d31c075eeea232a1cc320efc272f5ef87d7 Mon Sep 17 00:00:00 2001 From: Elliot Bentley Date: Thu, 31 Jul 2025 11:34:28 +0100 Subject: [PATCH 132/367] fix: add types for SVG bindable attributes (#16525) * fix: types for SVG bind: attributes * add changeset and tweak --------- Co-authored-by: 7nik --- .changeset/cuddly-feet-doubt.md | 5 +++++ packages/svelte/elements.d.ts | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/cuddly-feet-doubt.md diff --git a/.changeset/cuddly-feet-doubt.md b/.changeset/cuddly-feet-doubt.md new file mode 100644 index 0000000000..8d7d955daa --- /dev/null +++ b/.changeset/cuddly-feet-doubt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add bindable dimension attributes types to SVG and MathML elements diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 2e1042dfd6..b3d44d9ed6 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -464,6 +464,14 @@ export interface DOMAttributes { onfullscreenerror?: EventHandler | undefined | null; onfullscreenerrorcapture?: EventHandler | undefined | null; + // Dimensions + readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null; + readonly 'bind:contentBoxSize'?: Array | undefined | null; + readonly 'bind:borderBoxSize'?: Array | undefined | null; + readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null; + readonly 'bind:clientWidth'?: number | undefined | null; + readonly 'bind:clientHeight'?: number | undefined | null; + xmlns?: string | undefined | null; } @@ -839,13 +847,7 @@ export interface HTMLAttributes extends AriaAttributes, D */ 'bind:innerText'?: string | undefined | null; - readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null; - readonly 'bind:contentBoxSize'?: Array | undefined | null; - readonly 'bind:borderBoxSize'?: Array | undefined | null; - readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null; readonly 'bind:focused'?: boolean | undefined | null; - readonly 'bind:clientWidth'?: number | undefined | null; - readonly 'bind:clientHeight'?: number | undefined | null; readonly 'bind:offsetWidth'?: number | undefined | null; readonly 'bind:offsetHeight'?: number | undefined | null; From c04975d3db74e59f8612ffd0f64d3b3bb3133ef1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 31 Jul 2025 18:28:01 -0400 Subject: [PATCH 133/367] fix: prevent last_propagated_event from being DCE'd (#16538) * fix: prevent last_propagated_event from being DCE'd * changeset --- .changeset/serious-cars-hear.md | 5 +++++ packages/svelte/src/internal/client/dom/elements/events.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/serious-cars-hear.md diff --git a/.changeset/serious-cars-hear.md b/.changeset/serious-cars-hear.md new file mode 100644 index 0000000000..4647e89ed9 --- /dev/null +++ b/.changeset/serious-cars-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent last_propagated_event from being DCE'd diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 19bd1cfa18..15544d7426 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -168,8 +168,11 @@ export function handle_event_propagation(event) { // mounted apps. In this case we don't want to trigger events multiple times. var path_idx = 0; + // the `last_propagated_event === event` check is redundant, but + // without it the variable will be DCE'd and things will + // fail mysteriously in Firefox // @ts-expect-error is added below - var handled_at = event.__root; + var handled_at = last_propagated_event === event && event.__root; if (handled_at) { var at_idx = path.indexOf(handled_at); From 9134856fece750608ba81da45c4a330b3d8ebb1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:15:45 -0400 Subject: [PATCH 134/367] Version Packages (#16529) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/chilly-bananas-train.md | 5 ----- .changeset/cuddly-feet-doubt.md | 5 ----- .changeset/nine-cups-film.md | 5 ----- .changeset/serious-cars-hear.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/chilly-bananas-train.md delete mode 100644 .changeset/cuddly-feet-doubt.md delete mode 100644 .changeset/nine-cups-film.md delete mode 100644 .changeset/serious-cars-hear.md diff --git a/.changeset/chilly-bananas-train.md b/.changeset/chilly-bananas-train.md deleted file mode 100644 index b0305f9e61..0000000000 --- a/.changeset/chilly-bananas-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: double event processing in firefox due to event object being garbage collected diff --git a/.changeset/cuddly-feet-doubt.md b/.changeset/cuddly-feet-doubt.md deleted file mode 100644 index 8d7d955daa..0000000000 --- a/.changeset/cuddly-feet-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add bindable dimension attributes types to SVG and MathML elements diff --git a/.changeset/nine-cups-film.md b/.changeset/nine-cups-film.md deleted file mode 100644 index ba72337dac..0000000000 --- a/.changeset/nine-cups-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly differentiate static fields before emitting `duplicate_class_field` diff --git a/.changeset/serious-cars-hear.md b/.changeset/serious-cars-hear.md deleted file mode 100644 index 4647e89ed9..0000000000 --- a/.changeset/serious-cars-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent last_propagated_event from being DCE'd diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 59c44dd4f9..6e0a199ddc 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.37.2 + +### Patch Changes + +- fix: double event processing in firefox due to event object being garbage collected ([#16527](https://github.com/sveltejs/svelte/pull/16527)) + +- fix: add bindable dimension attributes types to SVG and MathML elements ([#16525](https://github.com/sveltejs/svelte/pull/16525)) + +- fix: correctly differentiate static fields before emitting `duplicate_class_field` ([#16526](https://github.com/sveltejs/svelte/pull/16526)) + +- fix: prevent last_propagated_event from being DCE'd ([#16538](https://github.com/sveltejs/svelte/pull/16538)) + ## 5.37.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 826850ea60..8536135ca8 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.37.1", + "version": "5.37.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2d5083a2a5..b8f3bcfdd5 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.37.1'; +export const VERSION = '5.37.2'; export const PUBLIC_VERSION = '5'; From 80678411706e99c0224b8bf995959c2d4e7fd4db Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:16:47 -0700 Subject: [PATCH 135/367] docs: add note about attachments to `svelte/action` docs (#16537) --- documentation/docs/98-reference/21-svelte-action.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/98-reference/21-svelte-action.md b/documentation/docs/98-reference/21-svelte-action.md index 53423ec409..ef3ebfbf70 100644 --- a/documentation/docs/98-reference/21-svelte-action.md +++ b/documentation/docs/98-reference/21-svelte-action.md @@ -2,4 +2,6 @@ title: svelte/action --- +This module provides types for [actions](use), which have been superseded by [attachments](@attach). + > MODULE: svelte/action From 540f1b19ec56a69dde85f24471f25d9d465937e4 Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 2 Aug 2025 22:29:10 +0300 Subject: [PATCH 136/367] fix: reset attribute cache after setting corresponding property (#16543) --- .changeset/afraid-carrots-study.md | 5 +++++ .../client/dom/elements/attributes.js | 4 +++- .../attribute-after-property/_config.js | 19 +++++++++++++++++++ .../attribute-after-property/main.svelte | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-carrots-study.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte diff --git a/.changeset/afraid-carrots-study.md b/.changeset/afraid-carrots-study.md new file mode 100644 index 0000000000..71f4232edb --- /dev/null +++ b/.changeset/afraid-carrots-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reset attribute cache after setting corresponding property diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 22e532f5e4..2fa5d4541c 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -19,7 +19,7 @@ import { attach } from './attachments.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; -import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; +import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js'; import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js'; import { init_select, select_option } from './bindings/select.js'; import { flatten } from '../../reactivity/async.js'; @@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal ) { // @ts-ignore element[name] = value; + // remove it from attributes's cache + if (name in attributes) attributes[name] = UNINITIALIZED; } else if (typeof value !== 'function') { set_attribute(element, name, value, skip_warning); } diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js new file mode 100644 index 0000000000..f6a98b1797 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ target, assert }) { + const input = target.querySelector('input'); + const button = target.querySelector('button'); + + assert.equal(input?.step, 'any'); + + button?.click(); + flushSync(); + assert.equal(input?.step, '10'); + + button?.click(); + flushSync(); + assert.equal(input?.step, 'any'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte new file mode 100644 index 0000000000..2921e4e241 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From 0cbb5becf6d62ae3334b78606a76d21a91eda32b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 Aug 2025 21:17:58 -0700 Subject: [PATCH 137/367] Version Packages (#16544) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/afraid-carrots-study.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/afraid-carrots-study.md diff --git a/.changeset/afraid-carrots-study.md b/.changeset/afraid-carrots-study.md deleted file mode 100644 index 71f4232edb..0000000000 --- a/.changeset/afraid-carrots-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reset attribute cache after setting corresponding property diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6e0a199ddc..9766e3f06b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.37.3 + +### Patch Changes + +- fix: reset attribute cache after setting corresponding property ([#16543](https://github.com/sveltejs/svelte/pull/16543)) + ## 5.37.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8536135ca8..e8927fcf56 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.37.2", + "version": "5.37.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b8f3bcfdd5..127b43e205 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.37.2'; +export const VERSION = '5.37.3'; export const PUBLIC_VERSION = '5'; From a3f18351af4f70df4b9029aaa7198ac5dfeac6dc Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 5 Aug 2025 05:38:30 +0200 Subject: [PATCH 138/367] docs: corrections (#16550) * Add missing hyphen in "server-side rendering" * Fix incorrect use of "as such" * regenerate types --------- Co-authored-by: Chew Tee Ming --- documentation/docs/02-runes/04-$effect.md | 2 +- documentation/docs/03-template-syntax/06-snippet.md | 2 +- documentation/docs/03-template-syntax/08-@html.md | 2 +- .../docs/05-special-elements/07-svelte-options.md | 2 +- documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +- documentation/docs/07-misc/07-v5-migration-guide.md | 12 ++++++------ .../docs/98-reference/.generated/compile-errors.md | 2 +- .../docs/98-reference/.generated/compile-warnings.md | 2 +- packages/svelte/messages/compile-errors/script.md | 2 +- .../svelte/messages/compile-warnings/template.md | 2 +- packages/svelte/src/ambient.d.ts | 4 ++-- packages/svelte/types/index.d.ts | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 5820e178a0..6c42f55795 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code. -For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA). +For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA). Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes. diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md index ab536c6e5c..02f58e0f6c 100644 --- a/documentation/docs/03-template-syntax/06-snippet.md +++ b/documentation/docs/03-template-syntax/06-snippet.md @@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr ## Snippets and slots -In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. +In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/documentation/docs/03-template-syntax/08-@html.md b/documentation/docs/03-template-syntax/08-@html.md index 30456fa666..a92accd093 100644 --- a/documentation/docs/03-template-syntax/08-@html.md +++ b/documentation/docs/03-template-syntax/08-@html.md @@ -22,7 +22,7 @@ It also will not compile Svelte code. ## Styling -Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused: +Content rendered this way is 'invisible' to Svelte and thus will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused: ```svelte diff --git a/documentation/docs/05-special-elements/07-svelte-options.md b/documentation/docs/05-special-elements/07-svelte-options.md index d29042e278..a2e47aa04f 100644 --- a/documentation/docs/05-special-elements/07-svelte-options.md +++ b/documentation/docs/05-special-elements/07-svelte-options.md @@ -12,7 +12,7 @@ The `` element provides a place to specify per-component compile - `runes={false}` — forces a component into _legacy mode_ - `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml" - `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option -- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a ` \ No newline at end of file + diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css index bddefdd00c..81c1111967 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css @@ -1,4 +1,4 @@ -.foo.svelte-sg04hs { +.foo.svelte-1nvcr6w { color: red; } diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html index 1f0b2b95fe..a6eba00942 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html @@ -1 +1 @@ -
bar
foo
\ No newline at end of file +
bar
foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html index 6d795670ff..516a9576b3 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css index 8882c6ec7e..eb1f3e7a9b 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css @@ -1,4 +1,4 @@ - .foo.svelte-sg04hs { + .foo.svelte-okauro { color: red; } diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html index 8ebe1ad73e..01ebd79914 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html @@ -1 +1 @@ -
foo
\ No newline at end of file +
foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html index 9c4f8a8538..1a1511f55e 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css index 8882c6ec7e..ec86890478 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css/_expected.css +++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.css @@ -1,4 +1,4 @@ - .foo.svelte-sg04hs { + .foo.svelte-e9omc { color: red; } diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html index 8ebe1ad73e..dc9409fa03 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css/_expected.html +++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected.html @@ -1 +1 @@ -
foo
\ No newline at end of file +
foo
diff --git a/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html index 9c4f8a8538..941c1f13b4 100644 --- a/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html +++ b/packages/svelte/tests/server-side-rendering/samples/css/_expected_head.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js index ee9a3d92c4..3a292ff428 100644 --- a/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/attached-sourcemap/_config.js @@ -57,7 +57,7 @@ export default test({ { str: 'replace_me_script', strGenerated: 'done_replace_script_2' }, { str: 'done_replace_script_2', idxGenerated: 1 } ], - css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-o6vre' }], + css: [{ str: '.replace_me_style', strGenerated: '.done_replace_style_2.svelte-1vsrjd4' }], test({ assert, code_preprocessed, code_css }) { assert.equal( code_preprocessed.includes('\n/*# sourceMappingURL=data:application/json;base64,'), diff --git a/packages/svelte/tests/sourcemaps/samples/css/_config.js b/packages/svelte/tests/sourcemaps/samples/css/_config.js index df3c83c703..fce38d401c 100644 --- a/packages/svelte/tests/sourcemaps/samples/css/_config.js +++ b/packages/svelte/tests/sourcemaps/samples/css/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - css: [{ str: '.foo', strGenerated: '.foo.svelte-sg04hs' }] + css: [{ str: '.foo', strGenerated: '.foo.svelte-1eyw86p' }] }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 97e6f0f5a3..9888de59b2 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -998,7 +998,7 @@ declare module 'svelte/compiler' { css?: 'injected' | 'external'; /** * A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. - * It defaults to returning `svelte-${hash(css)}`. + * It defaults to returning `svelte-${hash(filename ?? css)}`. * * @default undefined */ @@ -2933,7 +2933,7 @@ declare module 'svelte/types/compiler/interfaces' { css?: 'injected' | 'external'; /** * A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. - * It defaults to returning `svelte-${hash(css)}`. + * It defaults to returning `svelte-${hash(filename ?? css)}`. * * @default undefined */ From f09f25e6a3ba7aa3562f62713e7cdc00b267fac7 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 10 Sep 2025 16:29:31 -0600 Subject: [PATCH 192/367] fix: Don't destroy boundary contents on error unless the boundary is an error boundary (#16746) * fix: Don't destroy contents of boundaries on errors if they're not error boundaries * changeset * test * prettier * prettier * simplify test * oops --------- Co-authored-by: Rich Harris --- .changeset/tasty-trainers-sell.md | 5 +++++ .../internal/client/dom/blocks/boundary.js | 12 +++++------ .../_config.js | 20 +++++++++++++++++++ .../main.svelte | 19 ++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 .changeset/tasty-trainers-sell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/main.svelte diff --git a/.changeset/tasty-trainers-sell.md b/.changeset/tasty-trainers-sell.md new file mode 100644 index 0000000000..4a2dd09fa6 --- /dev/null +++ b/.changeset/tasty-trainers-sell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't destroy contents of `svelte:boundary` unless the boundary is an error boundary diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b7f1803782..d4582024f7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -285,6 +285,12 @@ export class Boundary { var onerror = this.#props.onerror; let failed = this.#props.failed; + // If we have nothing to capture the error, or if we hit an error while + // rendering the fallback, re-throw for another boundary to handle + if (this.#is_creating_fallback || (!onerror && !failed)) { + throw error; + } + if (this.#main_effect) { destroy_effect(this.#main_effect); this.#main_effect = null; @@ -346,12 +352,6 @@ export class Boundary { } }; - // If we have nothing to capture the error, or if we hit an error while - // rendering the fallback, re-throw for another boundary to handle - if (this.#is_creating_fallback || (!onerror && !failed)) { - throw error; - } - var previous_reaction = active_reaction; try { diff --git a/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/_config.js b/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/_config.js new file mode 100644 index 0000000000..7654cf1360 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + assert.throws(() => { + flushSync(() => button.click()); + }, /oops/); + + assert.htmlEqual( + target.innerHTML, + ` + +

some content

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/main.svelte new file mode 100644 index 0000000000..1c3f062b43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-error-boundary-preserve-on-error/main.svelte @@ -0,0 +1,19 @@ + + + + + +

some content

+ + {#if should_throw} + {throw_error()} + {/if} +
From 2743cd0fe5a305586e87b65301dc3466b7c265f9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:20:04 -0700 Subject: [PATCH 193/367] fix: correctly analyze `` components (#16711) * fix: correctly analyze `` components * add test --------- Co-authored-by: Rich Harris --- .changeset/khaki-flies-remember.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 2 +- .../samples/snippet-hoisting-4/Component.svelte | 1 + .../samples/snippet-hoisting-4/_config.js | 5 +++++ .../samples/snippet-hoisting-4/main.svelte | 13 +++++++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/khaki-flies-remember.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/main.svelte diff --git a/.changeset/khaki-flies-remember.md b/.changeset/khaki-flies-remember.md new file mode 100644 index 0000000000..16d0f79e0f --- /dev/null +++ b/.changeset/khaki-flies-remember.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly analyze `` components diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 76157d406f..887bc47c56 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1032,7 +1032,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }, Component: (node, context) => { - context.state.scope.reference(b.id(node.name), context.path); + context.state.scope.reference(b.id(node.name.split('.')[0]), context.path); Component(node, context); }, SvelteSelf: Component, diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/Component.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/Component.svelte new file mode 100644 index 0000000000..597ecf5fc4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/Component.svelte @@ -0,0 +1 @@ +

Hello world!

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

Hello world!

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/main.svelte new file mode 100644 index 0000000000..d3130a99bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-hoisting-4/main.svelte @@ -0,0 +1,13 @@ + + +{#snippet not_hoisted()} + +{/snippet} + +{@render not_hoisted()} From 558a3c963bca4423cdd7b93d9588d05546dfa629 Mon Sep 17 00:00:00 2001 From: dai Date: Thu, 11 Sep 2025 03:10:45 +0200 Subject: [PATCH 194/367] fix: transform input defaults from spread (#16481) * fix: transform input defaults from spread * chore: add changeset * fix: prevent duplicates * do not remove defaults if they are in spreads * fix spreading twice * tweak * tweak * drive-by: remove unused export * tweak * undo comment change, to minimise diff * oops * tweak --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .changeset/quiet-planes-doubt.md | 5 ++++ .../client/visitors/RegularElement.js | 18 +++++++++-- .../client/visitors/shared/element.js | 5 +++- .../server/visitors/shared/element.js | 3 ++ packages/svelte/src/constants.js | 1 + .../client/dom/elements/attributes.js | 30 +++++++++++++++++-- packages/svelte/src/internal/client/index.js | 1 - packages/svelte/src/internal/server/index.js | 11 ++++++- .../form-default-value-from-spread/_config.js | 10 +++++++ .../main.svelte | 8 +++++ 10 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 .changeset/quiet-planes-doubt.md create mode 100644 packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte diff --git a/.changeset/quiet-planes-doubt.md b/.changeset/quiet-planes-doubt.md new file mode 100644 index 0000000000..bd895f00ef --- /dev/null +++ b/.changeset/quiet-planes-doubt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform input defaults from spread diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 4296aa959e..e906061650 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -72,6 +72,7 @@ export function RegularElement(node, context) { let has_spread = node.metadata.has_spread; let has_use = false; + let should_remove_defaults = false; for (const attribute of node.attributes) { switch (attribute.type) { @@ -172,7 +173,12 @@ export function RegularElement(node, context) { bindings.has('group') || (!bindings.has('group') && has_value_attribute)) ) { - context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); + if (has_spread) { + // remove_input_defaults will be called inside set_attributes + should_remove_defaults = true; + } else { + context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); + } } } @@ -202,7 +208,15 @@ export function RegularElement(node, context) { bindings.has('checked'); if (has_spread) { - build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id); + build_attribute_effect( + attributes, + class_directives, + style_directives, + context, + node, + node_id, + should_remove_defaults + ); } else { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (is_event_attribute(attribute)) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 9143a57025..4b32dab82a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -16,6 +16,7 @@ import { build_expression, build_template_chunk, Memoizer } from './utils.js'; * @param {ComponentContext} context * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id + * @param {boolean} [should_remove_defaults] */ export function build_attribute_effect( attributes, @@ -23,7 +24,8 @@ export function build_attribute_effect( style_directives, context, element, - element_id + element_id, + should_remove_defaults = false ) { /** @type {ObjectExpression['properties']} */ const values = []; @@ -91,6 +93,7 @@ export function build_attribute_effect( element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), + should_remove_defaults && b.true, is_ignored(element, 'hydration_attribute_changed') && b.true ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 7207564ef9..84692fca9c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -11,6 +11,7 @@ import { import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '#compiler/builders'; import { + ELEMENT_IS_INPUT, ELEMENT_IS_NAMESPACED, ELEMENT_PRESERVE_ATTRIBUTE_CASE } from '../../../../../../constants.js'; @@ -401,6 +402,8 @@ function build_element_spread_attributes( flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; } else if (is_custom_element_node(element)) { flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; + } else if (element.type === 'RegularElement' && element.name === 'input') { + flags |= ELEMENT_IS_INPUT; } const object = build_spread_object(element, attributes, context); diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 69cd213940..63324c860f 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -28,6 +28,7 @@ export const HYDRATION_ERROR = {}; export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1; +export const ELEMENT_IS_INPUT = 1 << 2; export const UNINITIALIZED = Symbol(); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5b7140f25..fb6a92cc82 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -268,10 +268,27 @@ export function set_custom_element_data(node, prop, value) { * @param {Record | undefined} prev * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] + * @param {boolean} [should_remove_defaults] * @param {boolean} [skip_warning] * @returns {Record} */ -export function set_attributes(element, prev, next, css_hash, skip_warning = false) { +function set_attributes( + element, + prev, + next, + css_hash, + should_remove_defaults = false, + skip_warning = false +) { + if (hydrating && should_remove_defaults && element.tagName === 'INPUT') { + var input = /** @type {HTMLInputElement} */ (element); + var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue'; + + if (!(attribute in next)) { + remove_input_defaults(input); + } + } + var attributes = get_attributes(element); var is_custom_element = attributes[IS_CUSTOM_ELEMENT]; @@ -467,6 +484,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {string} [css_hash] + * @param {boolean} [should_remove_defaults] * @param {boolean} [skip_warning] */ export function attribute_effect( @@ -475,6 +493,7 @@ export function attribute_effect( sync = [], async = [], css_hash, + should_remove_defaults = false, skip_warning = false ) { flatten(sync, async, (values) => { @@ -490,7 +509,14 @@ export function attribute_effect( block(() => { var next = fn(...values.map(get)); /** @type {Record} */ - var current = set_attributes(element, prev, next, css_hash, skip_warning); + var current = set_attributes( + element, + prev, + next, + css_hash, + should_remove_defaults, + skip_warning + ); if (inited && is_select && 'value' in next) { select_option(/** @type {HTMLSelectElement} */ (element), next.value); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c5b7bb845c..dbff5c4599 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -28,7 +28,6 @@ export { attach } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, - set_attributes, attribute_effect, set_custom_element_data, set_xlink_attribute, diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fc..3aa44f2daa 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -8,7 +8,8 @@ import { subscribe_to_store } from '../../store/utils.js'; import { UNINITIALIZED, ELEMENT_PRESERVE_ATTRIBUTE_CASE, - ELEMENT_IS_NAMESPACED + ELEMENT_IS_NAMESPACED, + ELEMENT_IS_INPUT } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; @@ -187,6 +188,7 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0; const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0; + const is_input = (flags & ELEMENT_IS_INPUT) !== 0; for (name in attrs) { // omit functions, internal svelte properties and invalid attribute names @@ -200,6 +202,13 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { name = name.toLowerCase(); } + if (is_input) { + if (name === 'defaultvalue' || name === 'defaultchecked') { + name = name === 'defaultvalue' ? 'value' : 'checked'; + if (attrs[name]) continue; + } + } + attr_str += attr(name, value, is_html && is_boolean_attribute(name)); } diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js new file mode 100644 index 0000000000..3808ae6530 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + mode: ['server'], + html: ` + + + +` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte new file mode 100644 index 0000000000..7c0ce9cbe3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file From 96a4b16842e62a2e4224b084425cac6a191e6c2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:22:12 -0400 Subject: [PATCH 195/367] Version Packages (#16744) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-boxes-sort.md | 5 ----- .changeset/khaki-flies-remember.md | 5 ----- .changeset/old-taxis-relate.md | 5 ----- .changeset/quiet-planes-doubt.md | 5 ----- .changeset/tasty-trainers-sell.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/fast-boxes-sort.md delete mode 100644 .changeset/khaki-flies-remember.md delete mode 100644 .changeset/old-taxis-relate.md delete mode 100644 .changeset/quiet-planes-doubt.md delete mode 100644 .changeset/tasty-trainers-sell.md diff --git a/.changeset/fast-boxes-sort.md b/.changeset/fast-boxes-sort.md deleted file mode 100644 index 1edabf0582..0000000000 --- a/.changeset/fast-boxes-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: generate CSS hash using the filename diff --git a/.changeset/khaki-flies-remember.md b/.changeset/khaki-flies-remember.md deleted file mode 100644 index 16d0f79e0f..0000000000 --- a/.changeset/khaki-flies-remember.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly analyze `` components diff --git a/.changeset/old-taxis-relate.md b/.changeset/old-taxis-relate.md deleted file mode 100644 index f38dc03f5e..0000000000 --- a/.changeset/old-taxis-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: clean up scheduling system diff --git a/.changeset/quiet-planes-doubt.md b/.changeset/quiet-planes-doubt.md deleted file mode 100644 index bd895f00ef..0000000000 --- a/.changeset/quiet-planes-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform input defaults from spread diff --git a/.changeset/tasty-trainers-sell.md b/.changeset/tasty-trainers-sell.md deleted file mode 100644 index 4a2dd09fa6..0000000000 --- a/.changeset/tasty-trainers-sell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't destroy contents of `svelte:boundary` unless the boundary is an error boundary diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 20ae98a500..4e384d5697 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.38.9 + +### Patch Changes + +- chore: generate CSS hash using the filename ([#16740](https://github.com/sveltejs/svelte/pull/16740)) + +- fix: correctly analyze `` components ([#16711](https://github.com/sveltejs/svelte/pull/16711)) + +- fix: clean up scheduling system ([#16741](https://github.com/sveltejs/svelte/pull/16741)) + +- fix: transform input defaults from spread ([#16481](https://github.com/sveltejs/svelte/pull/16481)) + +- fix: don't destroy contents of `svelte:boundary` unless the boundary is an error boundary ([#16746](https://github.com/sveltejs/svelte/pull/16746)) + ## 5.38.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 02fa445b0e..d445e0acf8 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.38.8", + "version": "5.38.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8cb69aac5b..edb787c00c 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.38.8'; +export const VERSION = '5.38.9'; export const PUBLIC_VERSION = '5'; From 0b5bcc891e1e4582131b3e5eca1c48229f1f57d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:29:26 -0400 Subject: [PATCH 196/367] chore(deps-dev): bump vite from 5.4.19 to 5.4.20 (#16749) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.19 to 5.4.20. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.20 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- playgrounds/sandbox/package.json | 2 +- pnpm-lock.yaml | 420 ++++++++++++++++++------------- 2 files changed, 252 insertions(+), 170 deletions(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index f11da983f6..8100084832 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -19,7 +19,7 @@ "polka": "^1.0.0-next.25", "svelte": "workspace:*", "tinyglobby": "^0.2.12", - "vite": "^5.4.19", + "vite": "^5.4.20", "vite-plugin-inspect": "^0.8.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cad5fefdf8..1f6bff9bd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,7 +155,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^4.0.0-next.6 - version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) polka: specifier: ^1.0.0-next.25 version: 1.0.0-next.25 @@ -166,11 +166,11 @@ importers: specifier: ^0.2.12 version: 0.2.12 vite: - specifier: ^5.4.19 - version: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^5.4.20 + version: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-plugin-inspect: specifier: ^0.8.4 - version: 0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 0.8.4(rollup@4.50.1)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages: @@ -417,6 +417,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.1': resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -557,8 +563,8 @@ packages: cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.40.2': - resolution: {integrity: sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==} + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] os: [android] @@ -567,8 +573,8 @@ packages: cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.40.2': - resolution: {integrity: sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==} + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} cpu: [arm64] os: [android] @@ -577,8 +583,8 @@ packages: cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-arm64@4.40.2': - resolution: {integrity: sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==} + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} cpu: [arm64] os: [darwin] @@ -587,18 +593,18 @@ packages: cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.40.2': - resolution: {integrity: sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==} + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.40.2': - resolution: {integrity: sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==} + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.40.2': - resolution: {integrity: sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==} + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} cpu: [x64] os: [freebsd] @@ -607,8 +613,8 @@ packages: cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': - resolution: {integrity: sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] @@ -617,8 +623,8 @@ packages: cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.40.2': - resolution: {integrity: sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==} + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] @@ -627,8 +633,8 @@ packages: cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.40.2': - resolution: {integrity: sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==} + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] @@ -637,13 +643,13 @@ packages: cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.40.2': - resolution: {integrity: sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==} + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': - resolution: {integrity: sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] @@ -652,8 +658,8 @@ packages: cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': - resolution: {integrity: sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==} + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] @@ -662,13 +668,13 @@ packages: cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.40.2': - resolution: {integrity: sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==} + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.40.2': - resolution: {integrity: sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==} + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] @@ -677,8 +683,8 @@ packages: cpu: [s390x] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.40.2': - resolution: {integrity: sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==} + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] @@ -687,8 +693,8 @@ packages: cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.40.2': - resolution: {integrity: sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==} + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] @@ -697,18 +703,23 @@ packages: cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.40.2': - resolution: {integrity: sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==} + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.22.4': resolution: {integrity: sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-arm64-msvc@4.40.2': - resolution: {integrity: sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==} + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} cpu: [arm64] os: [win32] @@ -717,8 +728,8 @@ packages: cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.40.2': - resolution: {integrity: sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==} + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} cpu: [ia32] os: [win32] @@ -727,8 +738,8 @@ packages: cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.40.2': - resolution: {integrity: sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==} + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} cpu: [x64] os: [win32] @@ -785,8 +796,8 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -818,13 +829,25 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + '@typescript-eslint/project-service@8.43.0': + resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/scope-manager@8.26.0': resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.32.1': - resolution: {integrity: sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==} + '@typescript-eslint/scope-manager@8.43.0': + resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.43.0': + resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/type-utils@8.26.0': resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} @@ -837,8 +860,8 @@ packages: resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.32.1': - resolution: {integrity: sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==} + '@typescript-eslint/types@8.43.0': + resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.26.0': @@ -847,11 +870,11 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.32.1': - resolution: {integrity: sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==} + '@typescript-eslint/typescript-estree@8.43.0': + resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/utils@8.26.0': resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} @@ -860,19 +883,19 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.32.1': - resolution: {integrity: sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==} + '@typescript-eslint/utils@8.43.0': + resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: '>=4.8.4 <6.0.0' '@typescript-eslint/visitor-keys@8.26.0': resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.32.1': - resolution: {integrity: sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==} + '@typescript-eslint/visitor-keys@8.43.0': + resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.1.9': @@ -923,8 +946,8 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true @@ -1099,6 +1122,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -1161,8 +1193,8 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - enhanced-resolve@5.18.1: - resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -1241,6 +1273,10 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@9.9.1: resolution: {integrity: sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1387,8 +1423,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-tsconfig@4.10.0: - resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1862,6 +1898,10 @@ packages: resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} engines: {node: '>=12'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -1912,6 +1952,10 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1984,8 +2028,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rollup@4.40.2: - resolution: {integrity: sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==} + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2139,8 +2183,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tapable@2.2.1: - resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} engines: {node: '>=6'} term-size@2.2.1: @@ -2325,8 +2369,8 @@ packages: terser: optional: true - vite@5.4.19: - resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -2731,6 +2775,11 @@ snapshots: eslint: 9.9.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.9.1)': + dependencies: + eslint: 9.9.1 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.18.0': @@ -2880,126 +2929,129 @@ snapshots: optionalDependencies: rollup: 4.22.4 - '@rollup/pluginutils@5.1.0(rollup@4.40.2)': + '@rollup/pluginutils@5.1.0(rollup@4.50.1)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: - rollup: 4.40.2 + rollup: 4.50.1 '@rollup/rollup-android-arm-eabi@4.22.4': optional: true - '@rollup/rollup-android-arm-eabi@4.40.2': + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true '@rollup/rollup-android-arm64@4.22.4': optional: true - '@rollup/rollup-android-arm64@4.40.2': + '@rollup/rollup-android-arm64@4.50.1': optional: true '@rollup/rollup-darwin-arm64@4.22.4': optional: true - '@rollup/rollup-darwin-arm64@4.40.2': + '@rollup/rollup-darwin-arm64@4.50.1': optional: true '@rollup/rollup-darwin-x64@4.22.4': optional: true - '@rollup/rollup-darwin-x64@4.40.2': + '@rollup/rollup-darwin-x64@4.50.1': optional: true - '@rollup/rollup-freebsd-arm64@4.40.2': + '@rollup/rollup-freebsd-arm64@4.50.1': optional: true - '@rollup/rollup-freebsd-x64@4.40.2': + '@rollup/rollup-freebsd-x64@4.50.1': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.22.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.40.2': + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': optional: true '@rollup/rollup-linux-arm-musleabihf@4.22.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.40.2': + '@rollup/rollup-linux-arm-musleabihf@4.50.1': optional: true '@rollup/rollup-linux-arm64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.40.2': + '@rollup/rollup-linux-arm64-gnu@4.50.1': optional: true '@rollup/rollup-linux-arm64-musl@4.22.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.40.2': + '@rollup/rollup-linux-arm64-musl@4.50.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.40.2': + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': optional: true '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.40.2': + '@rollup/rollup-linux-ppc64-gnu@4.50.1': optional: true '@rollup/rollup-linux-riscv64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.40.2': + '@rollup/rollup-linux-riscv64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.40.2': + '@rollup/rollup-linux-riscv64-musl@4.50.1': optional: true '@rollup/rollup-linux-s390x-gnu@4.22.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.40.2': + '@rollup/rollup-linux-s390x-gnu@4.50.1': optional: true '@rollup/rollup-linux-x64-gnu@4.22.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.40.2': + '@rollup/rollup-linux-x64-gnu@4.50.1': optional: true '@rollup/rollup-linux-x64-musl@4.22.4': optional: true - '@rollup/rollup-linux-x64-musl@4.40.2': + '@rollup/rollup-linux-x64-musl@4.50.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.1': optional: true '@rollup/rollup-win32-arm64-msvc@4.22.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.40.2': + '@rollup/rollup-win32-arm64-msvc@4.50.1': optional: true '@rollup/rollup-win32-ia32-msvc@4.22.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.40.2': + '@rollup/rollup-win32-ia32-msvc@4.50.1': optional: true '@rollup/rollup-win32-x64-msvc@4.22.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.40.2': + '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true '@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)': dependencies: '@types/eslint': 8.56.12 - acorn: 8.14.1 + acorn: 8.15.0 escape-string-regexp: 4.0.0 eslint: 9.9.1 eslint-visitor-keys: 3.4.3 @@ -3020,25 +3072,25 @@ snapshots: typescript: 5.5.4 typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) - '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte': 4.0.0-next.6(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 svelte: link:packages/svelte - vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.0-next.2(@sveltejs/vite-plugin-svelte@4.0.0-next.6(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: link:packages/svelte - vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitefu: 0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vitefu: 0.2.5(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -3053,14 +3105,14 @@ snapshots: '@types/eslint@8.56.12': dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 '@types/json-schema': 7.0.15 '@types/estree@1.0.5': {} '@types/estree@1.0.6': {} - '@types/estree@1.0.7': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3103,15 +3155,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.43.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.5.4) + '@typescript-eslint/types': 8.43.0 + debug: 4.4.1 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.26.0': dependencies: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/scope-manager@8.32.1': + '@typescript-eslint/scope-manager@8.43.0': dependencies: - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/visitor-keys': 8.32.1 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + + '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.5.4)': + dependencies: + typescript: 5.5.4 '@typescript-eslint/type-utils@8.26.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: @@ -3126,7 +3191,7 @@ snapshots: '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/types@8.32.1': {} + '@typescript-eslint/types@8.43.0': {} '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: @@ -3142,11 +3207,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.32.1(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.43.0(typescript@5.5.4)': dependencies: - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/visitor-keys': 8.32.1 - debug: 4.4.0 + '@typescript-eslint/project-service': 8.43.0(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.5.4) + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/visitor-keys': 8.43.0 + debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -3167,12 +3234,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.32.1(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.43.0(eslint@9.9.1)(typescript@5.5.4)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.32.1 - '@typescript-eslint/types': 8.32.1 - '@typescript-eslint/typescript-estree': 8.32.1(typescript@5.5.4) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) + '@typescript-eslint/scope-manager': 8.43.0 + '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.5.4) eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: @@ -3183,10 +3250,10 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@typescript-eslint/visitor-keys@8.32.1': + '@typescript-eslint/visitor-keys@8.43.0': dependencies: - '@typescript-eslint/types': 8.32.1 - eslint-visitor-keys: 4.2.0 + '@typescript-eslint/types': 8.43.0 + eslint-visitor-keys: 4.2.1 '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: @@ -3250,13 +3317,13 @@ snapshots: dependencies: acorn: 8.14.0 - acorn-jsx@5.3.2(acorn@8.14.1): + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: - acorn: 8.14.1 + acorn: 8.15.0 acorn@8.14.0: {} - acorn@8.14.1: {} + acorn@8.15.0: {} agent-base@7.1.1: dependencies: @@ -3416,6 +3483,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decimal.js@10.4.3: {} deep-eql@5.0.2: {} @@ -3464,10 +3535,10 @@ snapshots: emoji-regex@9.2.2: {} - enhanced-resolve@5.18.1: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.2.3 enquirer@2.4.1: dependencies: @@ -3519,7 +3590,7 @@ snapshots: eslint-plugin-es-x@7.8.0(eslint@9.9.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) '@eslint-community/regexpp': 4.12.1 eslint: 9.9.1 eslint-compat-utils: 0.5.1(eslint@9.9.1) @@ -3528,12 +3599,12 @@ snapshots: eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.9.1) - '@typescript-eslint/utils': 8.32.1(eslint@9.9.1)(typescript@5.5.4) - enhanced-resolve: 5.18.1 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) + '@typescript-eslint/utils': 8.43.0(eslint@9.9.1)(typescript@5.5.4) + enhanced-resolve: 5.18.3 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) - get-tsconfig: 4.10.0 + get-tsconfig: 4.10.1 globals: 15.15.0 ignore: 5.3.2 minimatch: 9.0.5 @@ -3575,6 +3646,8 @@ snapshots: eslint-visitor-keys@4.2.0: {} + eslint-visitor-keys@4.2.1: {} + eslint@9.9.1: dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.9.1) @@ -3624,8 +3697,8 @@ snapshots: espree@9.6.1: dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 esprima@4.0.1: {} @@ -3754,7 +3827,7 @@ snapshots: function-bind@1.1.2: {} - get-tsconfig@4.10.0: + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -4195,6 +4268,8 @@ snapshots: picomatch@4.0.2: {} + picomatch@4.0.3: {} + pify@4.0.1: {} playwright-core@1.46.1: {} @@ -4221,9 +4296,9 @@ snapshots: dependencies: postcss: 8.5.3 - postcss-scss@4.0.9(postcss@8.5.3): + postcss-scss@4.0.9(postcss@8.5.6): dependencies: - postcss: 8.5.3 + postcss: 8.5.6 postcss-selector-parser@7.1.0: dependencies: @@ -4236,6 +4311,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier-plugin-svelte@3.4.0(prettier@3.2.4)(svelte@packages+svelte): @@ -4309,30 +4390,31 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.22.4 fsevents: 2.3.3 - rollup@4.40.2: + rollup@4.50.1: dependencies: - '@types/estree': 1.0.7 + '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.40.2 - '@rollup/rollup-android-arm64': 4.40.2 - '@rollup/rollup-darwin-arm64': 4.40.2 - '@rollup/rollup-darwin-x64': 4.40.2 - '@rollup/rollup-freebsd-arm64': 4.40.2 - '@rollup/rollup-freebsd-x64': 4.40.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.40.2 - '@rollup/rollup-linux-arm-musleabihf': 4.40.2 - '@rollup/rollup-linux-arm64-gnu': 4.40.2 - '@rollup/rollup-linux-arm64-musl': 4.40.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.40.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-gnu': 4.40.2 - '@rollup/rollup-linux-riscv64-musl': 4.40.2 - '@rollup/rollup-linux-s390x-gnu': 4.40.2 - '@rollup/rollup-linux-x64-gnu': 4.40.2 - '@rollup/rollup-linux-x64-musl': 4.40.2 - '@rollup/rollup-win32-arm64-msvc': 4.40.2 - '@rollup/rollup-win32-ia32-msvc': 4.40.2 - '@rollup/rollup-win32-x64-msvc': 4.40.2 + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 fsevents: 2.3.3 rrweb-cssom@0.7.1: {} @@ -4455,15 +4537,15 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.0 espree: 10.1.0 - postcss: 8.5.3 - postcss-scss: 4.0.9(postcss@8.5.3) + postcss: 8.5.6 + postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 optionalDependencies: svelte: link:packages/svelte symbol-tree@3.2.4: {} - tapable@2.2.1: {} + tapable@2.2.3: {} term-size@2.2.1: {} @@ -4543,7 +4625,7 @@ snapshots: ts-declaration-location@1.0.7(typescript@5.5.4): dependencies: - picomatch: 4.0.2 + picomatch: 4.0.3 typescript: 5.5.4 type-check@0.4.0: @@ -4582,7 +4664,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 1.1.2 - vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - '@types/node' - less @@ -4594,10 +4676,10 @@ snapshots: - supports-color - terser - vite-plugin-inspect@0.8.4(rollup@4.40.2)(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-inspect@0.8.4(rollup@4.50.1)(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: '@antfu/utils': 0.7.8 - '@rollup/pluginutils': 5.1.0(rollup@4.40.2) + '@rollup/pluginutils': 5.1.0(rollup@4.50.1) debug: 4.4.0 error-stack-parser-es: 0.1.1 fs-extra: 11.2.0 @@ -4605,7 +4687,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 2.0.4 - vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - rollup - supports-color @@ -4613,7 +4695,7 @@ snapshots: vite@5.4.14(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.3 + postcss: 8.5.6 rollup: 4.22.4 optionalDependencies: '@types/node': 20.12.7 @@ -4622,11 +4704,11 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: esbuild: 0.21.5 - postcss: 8.5.3 - rollup: 4.40.2 + postcss: 8.5.6 + rollup: 4.50.1 optionalDependencies: '@types/node': 20.12.7 fsevents: 2.3.3 @@ -4634,9 +4716,9 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vitefu@0.2.5(vite@5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitefu@0.2.5(vite@5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): optionalDependencies: - vite: 5.4.19(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 5.4.20(@types/node@20.12.7)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitest@2.1.9(@types/node@20.12.7)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: From 72ce753278a3e1f0046d1d13171d3b86445a241a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Sep 2025 10:58:56 -0400 Subject: [PATCH 197/367] fix: flush effects scheduled during boundary's pending phase (#16738) Alternative to #16721, partial alternative to #16709. Closes #16691, closes #16627, closes #16582 and #16651 as well. --- .changeset/ninety-pandas-move.md | 5 +++++ .../src/internal/client/dom/blocks/boundary.js | 2 +- .../src/internal/client/reactivity/batch.js | 2 +- .../samples/async-attachment/Inner.svelte | 10 ++++++++++ .../samples/async-attachment/_config.js | 18 ++++++++++++++++++ .../samples/async-attachment/main.svelte | 16 ++++++++++++++++ .../async-effect-after-await/Child.svelte | 7 +++++++ .../async-effect-after-await/_config.js | 9 +++++++++ .../async-effect-after-await/main.svelte | 9 +++++++++ 9 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 .changeset/ninety-pandas-move.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte diff --git a/.changeset/ninety-pandas-move.md b/.changeset/ninety-pandas-move.md new file mode 100644 index 0000000000..65f57ddbbf --- /dev/null +++ b/.changeset/ninety-pandas-move.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush effects scheduled during boundary's pending phase diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index d4582024f7..2a9736553c 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -23,7 +23,7 @@ import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; import { DEV } from 'esm-env'; -import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; +import { Batch, current_batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c28617608e..e504ae2e3f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -668,7 +668,7 @@ export function suspend() { batch.activate(); batch.decrement(); } else { - batch.deactivate(); + batch.flush(); } unset_context(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte new file mode 100644 index 0000000000..b9b9d7a3d0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment/Inner.svelte @@ -0,0 +1,10 @@ + + +

{test}

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

foo

foo
'); + + const [toggle] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ''); + + toggle.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

foo

foo
'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte new file mode 100644 index 0000000000..6cef6e8f5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-attachment/main.svelte @@ -0,0 +1,16 @@ + + + + + {#if show} + + {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte new file mode 100644 index 0000000000..682f7a0631 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js new file mode 100644 index 0000000000..81548a25ea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js @@ -0,0 +1,9 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + await tick(); + assert.deepEqual(logs, ['hello']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte new file mode 100644 index 0000000000..d4b67f8803 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/main.svelte @@ -0,0 +1,9 @@ + + + + + + {#snippet pending()}{/snippet} + From df13be8727ab16319a4f01ac39ff2ab195e953ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:30:50 -0400 Subject: [PATCH 198/367] Version Packages (#16754) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/ninety-pandas-move.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/ninety-pandas-move.md diff --git a/.changeset/ninety-pandas-move.md b/.changeset/ninety-pandas-move.md deleted file mode 100644 index 65f57ddbbf..0000000000 --- a/.changeset/ninety-pandas-move.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush effects scheduled during boundary's pending phase diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4e384d5697..62f109c82f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.38.10 + +### Patch Changes + +- fix: flush effects scheduled during boundary's pending phase ([#16738](https://github.com/sveltejs/svelte/pull/16738)) + ## 5.38.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d445e0acf8..6c91ec6bbb 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.38.9", + "version": "5.38.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index edb787c00c..9bfa7a5421 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.38.9'; +export const VERSION = '5.38.10'; export const PUBLIC_VERSION = '5'; From 808fbb4989d0e1687577cb4534e4d8350fb8971d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 11 Sep 2025 11:49:14 -0400 Subject: [PATCH 199/367] chore: inline `suspend` (#16755) * chore: inline `suspend` * invert condition --- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/async.js | 21 +++++++++++++++--- .../src/internal/client/reactivity/batch.js | 22 ------------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index dbff5c4599..3c5409bcfe 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -103,7 +103,7 @@ export { save, track_reactivity_loss } from './reactivity/async.js'; -export { flushSync as flush, suspend } from './reactivity/batch.js'; +export { flushSync as flush } from './reactivity/batch.js'; export { async_derived, user_derived as derived, diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index b7a5d5cdb7..a109a1f4d8 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch, suspend } from './batch.js'; +import { Batch, current_batch } from './batch.js'; import { async_derived, current_async_effect, @@ -178,7 +178,13 @@ export function unset_context() { * @param {() => Promise} fn */ export async function async_body(fn) { - var unsuspend = suspend(); + var boundary = get_boundary(); + var batch = /** @type {Batch} */ (current_batch); + var pending = boundary.is_pending(); + + boundary.update_pending_count(1); + if (!pending) batch.increment(); + var active = /** @type {Effect} */ (active_effect); try { @@ -188,6 +194,15 @@ export async function async_body(fn) { invoke_error_boundary(error, active); } } finally { - unsuspend(); + boundary.update_pending_count(-1); + + if (pending) { + batch.flush(); + } else { + batch.activate(); + batch.decrement(); + } + + unset_context(); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index e504ae2e3f..3d234f5bba 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -653,28 +653,6 @@ export function schedule_effect(signal) { queued_root_effects.push(effect); } -export function suspend() { - var boundary = get_boundary(); - var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); - - boundary.update_pending_count(1); - if (!pending) batch.increment(); - - return function unsuspend() { - boundary.update_pending_count(-1); - - if (!pending) { - batch.activate(); - batch.decrement(); - } else { - batch.flush(); - } - - unset_context(); - }; -} - /** * Forcibly remove all current batches, to prevent cross-talk between tests */ From a0598014d2b634566d8a62acca6b0c7601054e18 Mon Sep 17 00:00:00 2001 From: Aaron Ajose Date: Sun, 14 Sep 2025 22:09:40 +0300 Subject: [PATCH 200/367] docs: Fix some inaccuracies (#16759) * docs: Fix some inaccuracies * Apply suggestions from code review --------- Co-authored-by: Rich Harris --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0653b08b76..e940252892 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ The [Open Source Guides](https://opensource.guide/) website has a collection of ## Get involved -There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here's a few ideas to get started: +There are many ways to contribute to Svelte, and many of them do not involve writing any code. Here are a few ideas to get started: - Simply start using Svelte. Go through the [Getting Started](https://svelte.dev/docs#getting-started) guide. Does everything work as expected? If not, we're always looking for improvements. Let us know by [opening an issue](#reporting-new-issues). - Look through the [open issues](https://github.com/sveltejs/svelte/issues). A good starting point would be issues tagged [good first issue](https://github.com/sveltejs/svelte/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Provide workarounds, ask for clarification, or suggest labels. Help [triage issues](#triaging-issues-and-pull-requests). @@ -90,9 +90,9 @@ A good test plan has the exact commands you ran and their output, provides scree #### Writing tests -All tests are located in `/test` folder. +All tests are located in the `/tests` folder. -Test samples are kept in `/test/xxx/samples` folder. +Test samples are kept in `/tests/xxx/samples` folders. #### Running tests From 8b4e1fcb7aa321c15382652b60d430bcd0bda960 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 15 Sep 2025 10:00:45 -0400 Subject: [PATCH 201/367] chore: extract a couple of drive-by fixes from other branch (#16772) * chore: extract a couple of drive-by fixes from other branch * more --- packages/svelte/src/internal/client/dom/operations.js | 6 +++--- packages/svelte/src/internal/client/dom/template.js | 3 +++ packages/svelte/src/internal/client/reactivity/batch.js | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index abc29a7670..c527ca23e3 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -130,11 +130,11 @@ export function child(node, is_text) { /** * Don't mark this as side-effect-free, hydration needs to walk all nodes - * @param {DocumentFragment | TemplateNode[]} fragment - * @param {boolean} is_text + * @param {DocumentFragment | TemplateNode | TemplateNode[]} fragment + * @param {boolean} [is_text] * @returns {Node | null} */ -export function first_child(fragment, is_text) { +export function first_child(fragment, is_text = false) { if (!hydrating) { // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) var first = /** @type {DocumentFragment} */ (get_first_child(/** @type {Node} */ (fragment))); diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 265a52262f..135ca86610 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -316,6 +316,9 @@ export function text(value = '') { return node; } +/** + * @returns {TemplateNode | DocumentFragment} + */ export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 3d234f5bba..35aff7d4c9 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,12 +10,10 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT, MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; -import { get_boundary } from '../dom/blocks/boundary.js'; import { active_effect, is_dirty, @@ -30,7 +28,6 @@ import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; -import { unset_context } from './async.js'; /** @type {Set} */ const batches = new Set(); From 8b106b94f41a87ebfff251f70d602dda70265dc9 Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 15 Sep 2025 20:24:31 +0300 Subject: [PATCH 202/367] fix: correctly SSR hidden="until-found" (#16773) --- .changeset/pink-gifts-sell.md | 5 +++++ packages/svelte/src/internal/shared/attributes.js | 4 ++++ packages/svelte/src/utils.js | 1 - .../samples/attribute-spread-hidden-2/_expected.html | 1 + .../samples/attribute-spread-hidden-2/main.svelte | 3 +++ 5 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .changeset/pink-gifts-sell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte diff --git a/.changeset/pink-gifts-sell.md b/.changeset/pink-gifts-sell.md new file mode 100644 index 0000000000..f3f91ff7d9 --- /dev/null +++ b/.changeset/pink-gifts-sell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly SSR hidden="until-found" diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index c8758c1d4d..a96e71ff6f 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -23,6 +23,10 @@ const replacements = { */ export function attr(name, value, is_boolean = false) { if (value == null || (!value && is_boolean)) return ''; + // attribute hidden for values other than "until-found" behaves like a boolean attribute + if (name === 'hidden' && value !== 'until-found') { + is_boolean = true; + } const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index f8c39253ac..f8a7e8d46d 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -154,7 +154,6 @@ const DOM_BOOLEAN_ATTRIBUTES = [ 'default', 'disabled', 'formnovalidate', - 'hidden', 'indeterminate', 'inert', 'ismap', diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html new file mode 100644 index 0000000000..80937efaee --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html @@ -0,0 +1 @@ +
A
diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte new file mode 100644 index 0000000000..6738b34054 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte @@ -0,0 +1,3 @@ +
A
+
B
+
C
\ No newline at end of file From 8c982f61013a517bec2edf68a82ce1a1188fcd8f Mon Sep 17 00:00:00 2001 From: 7nik Date: Tue, 16 Sep 2025 11:45:18 +0300 Subject: [PATCH 203/367] fix: correct wrong fix, get test to actually do something (#16779) #16773 added a test with _expected.html to a wrong suit, so it was ignored, and this allowed to slip in a new bug of printing the falsy hidden attribute as hidden (shortened enabled form). That fix wasn't released yet, so no changeset. --- packages/svelte/src/internal/shared/attributes.js | 2 +- .../samples/attribute-spread-hidden}/_expected.html | 0 .../samples/attribute-spread-hidden}/main.svelte | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename packages/svelte/tests/{runtime-runes/samples/attribute-spread-hidden-2 => server-side-rendering/samples/attribute-spread-hidden}/_expected.html (100%) rename packages/svelte/tests/{runtime-runes/samples/attribute-spread-hidden-2 => server-side-rendering/samples/attribute-spread-hidden}/main.svelte (100%) diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index a96e71ff6f..4ad550e8d6 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -22,11 +22,11 @@ const replacements = { * @returns {string} */ export function attr(name, value, is_boolean = false) { - if (value == null || (!value && is_boolean)) return ''; // attribute hidden for values other than "until-found" behaves like a boolean attribute if (name === 'hidden' && value !== 'until-found') { is_boolean = true; } + if (value == null || (!value && is_boolean)) return ''; const normalized = (name in replacements && replacements[name].get(value)) || value; const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`; return ` ${name}${assignment}`; diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/_expected.html rename to packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/_expected.html diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte b/packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/attribute-spread-hidden-2/main.svelte rename to packages/svelte/tests/server-side-rendering/samples/attribute-spread-hidden/main.svelte From b8fd326d96c3a57feff3a4fafcd2d1651001b109 Mon Sep 17 00:00:00 2001 From: Elliott Johnson Date: Wed, 17 Sep 2025 15:49:57 -0600 Subject: [PATCH 204/367] feat: async SSR (#16748) * feat: First pass at payload * first crack * snapshots * checkpoint * fix: cloning * add test option * big dumb * today's hard work; few tests left to fix * improve * tests passing no wayyyyy yo * lots of progress, couple of failing tests around selects * meh * solve async tree stuff * fix select/option stuff * whoop, tests * simplify * feat: hoisting * fix: `$effect.pending` sends updates to incorrect boundary * changeset * stuff from upstream * feat: first hydrationgaa * remove docs * snapshots * silly fix * checkpoint * meh * ALKASJDFALSKDFJ the test passes * chore: Update a bunch of tests for hydration markers * chore: remove snippet and is_async * naming * better errors for sync-in-async * test improvements * idk man * merge local branches (#16757) * use fragment as async hoist boundary * remove async_hoist_boundary * only dewaterfall when necessary * unused * simplify/fix * de-waterfall awaits in separate elements * update snapshots * remove unnecessary wrapper * fix * fix * remove suspends_without_fallback --------- Co-authored-by: Rich Harris * Update payload.js Co-authored-by: Rich Harris * checkpoint * got the extra children to go away * just gonna go ahead and merge this as the review comments take up too much space * chore: remove hoisted_promises (#16766) * chore: remove hoisted_promises * WIP optimise promises * WIP * fix with await in prop * tweak * fix type error * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js * chore: fix hydration treeshaking (#16767) * chore: fix hydration treeshaking * fix * remove await_outside_boundary error (#16762) * chore: remove unused analysis.boundary (#16763) * chore: simplify slots (#16765) * chore: simplify slots * unused * Apply suggestions from code review * chore: remove metadata.pending (#16764) * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js * put this back where it was, keep the diff small * Update packages/svelte/src/compiler/phases/types.d.ts Co-authored-by: Rich Harris * chore: remove analysis.state.title (#16771) * chore: remove analysis.state.title * unused * chore: remove is_async (#16769) * chore: remove is_async * unused * Apply suggestions from code review Co-authored-by: Rich Harris * cleanup * lint * clean up payload a bit * compiler work * run ssr on sync and async * prettier * inline capture * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/EachBlock.js * chore: simplify build_template (#16780) * small tweak to aid greppability * chore: fix SSR context (#16781) * at least passing * cleanup * fix * remove push/pop from exports, not needed with payload * I think this is better but tbh not sure * async SSR * qualification * errors: * I have lost the plot * finally * ugh * tweak error codes to better align with existing conventions, such as they are * tweak messages * remove unused args * DRY out a bit * unused * unused * unused * simplify - we can enforce readonly at a type level * unused * simplify * avoid magical accessors * simplify algorithm * unused * unused * reduce indirection * TreeState -> SSRState * mark deprecated methods * grab this.local from parent directly * rename render -> fn per conventions (fn indicates 'arbitrary code') * reduce indirection * Revert "reduce indirection" This reverts commit 3ec461baad451654db6d518734aeeb7b366f7c2e. * tweak * okay works this time * no way chat, it works * fix context stuff * tweak * make it chainable * lint * clean up * lint * Update packages/svelte/src/internal/server/types.d.ts Co-authored-by: Rich Harris * sunset html for async * types * we use 'deprecated' in other messages * oops --------- Co-authored-by: Rich Harris --- .changeset/forty-insects-cheat.md | 5 + .prettierignore | 1 + .../98-reference/.generated/server-errors.md | 14 + .../.generated/server-warnings.md | 9 + .../98-reference/.generated/shared-errors.md | 20 - eslint.config.js | 1 + .../svelte/messages/server-errors/errors.md | 15 + .../messages/server-errors/lifecycle.md | 5 - .../messages/server-warnings/warnings.md | 5 + .../svelte/messages/shared-errors/errors.md | 18 - .../svelte/scripts/process-messages/index.js | 1 + .../templates/server-warnings.js | 20 + .../compiler/phases/1-parse/state/element.js | 3 +- .../src/compiler/phases/2-analyze/index.js | 15 +- .../src/compiler/phases/2-analyze/types.d.ts | 5 + .../2-analyze/visitors/AwaitExpression.js | 122 +++- .../2-analyze/visitors/CallExpression.js | 1 + .../phases/2-analyze/visitors/ConstTag.js | 6 +- .../2-analyze/visitors/RegularElement.js | 2 +- .../2-analyze/visitors/VariableDeclarator.js | 6 + .../3-transform/client/transform-client.js | 2 - .../phases/3-transform/client/types.d.ts | 8 +- .../client/visitors/AwaitExpression.js | 106 +-- .../client/visitors/CallExpression.js | 4 +- .../3-transform/client/visitors/ConstTag.js | 9 +- .../client/visitors/RegularElement.js | 41 +- .../client/visitors/VariableDeclaration.js | 14 +- .../3-transform/server/transform-server.js | 26 +- .../3-transform/server/visitors/AwaitBlock.js | 38 +- .../server/visitors/AwaitExpression.js | 22 +- .../3-transform/server/visitors/EachBlock.js | 27 +- .../3-transform/server/visitors/IfBlock.js | 22 +- .../server/visitors/RegularElement.js | 76 ++- .../server/visitors/SlotElement.js | 27 +- .../server/visitors/SvelteBoundary.js | 26 +- .../server/visitors/TitleElement.js | 4 +- .../server/visitors/shared/component.js | 60 +- .../server/visitors/shared/utils.js | 149 +++-- .../svelte/src/compiler/phases/types.d.ts | 4 +- .../svelte/src/compiler/types/template.d.ts | 2 + packages/svelte/src/compiler/utils/ast.js | 16 + .../svelte/src/compiler/utils/builders.js | 12 +- packages/svelte/src/index-server.js | 8 +- .../internal/client/dom/blocks/boundary.js | 106 +-- .../src/internal/client/reactivity/async.js | 3 +- packages/svelte/src/internal/client/render.js | 55 +- .../src/internal/server/blocks/snippet.js | 2 +- .../svelte/src/internal/server/context.js | 58 +- packages/svelte/src/internal/server/dev.js | 46 +- packages/svelte/src/internal/server/errors.js | 24 + packages/svelte/src/internal/server/index.js | 232 ++++--- .../svelte/src/internal/server/payload.js | 625 ++++++++++++++++-- .../src/internal/server/payload.test.ts | 364 ++++++++++ .../svelte/src/internal/server/types.d.ts | 23 +- .../svelte/src/internal/server/warnings.js | 17 + packages/svelte/src/internal/shared/errors.js | 16 - packages/svelte/src/legacy/legacy-server.js | 1 + .../samples/binding-input/_expected.html | 2 +- .../_override.html | 2 +- .../dynamic-text-changed/_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../_expected.html | 2 +- .../element-attribute-added/_expected.html | 2 +- .../element-attribute-changed/_expected.html | 2 +- .../element-attribute-removed/_expected.html | 2 +- .../if-block-mismatch-2/_expected.html | 2 +- .../samples/if-block-mismatch/_expected.html | 2 +- .../_expected.html | 2 +- .../input-value-changed/_expected.html | 2 +- .../hydration/samples/noscript/_expected.html | 2 +- .../pre-first-node-newline/_expected.html | 4 +- .../_expected.html | 2 +- .../repair-mismatched-a-href/_expected.html | 2 +- .../samples/safari-borking/_override.html | 2 +- .../hydration/samples/script/_expected.html | 2 +- .../snippet-raw-hydrate/_expected.html | 2 +- .../standalone-component/_expected.html | 2 +- .../samples/standalone-snippet/_expected.html | 2 +- .../surrounding-whitespace/_expected.html | 2 +- .../surrounding-whitespace/_override.html | 2 +- .../samples/text-empty-2/_expected.html | 2 +- .../samples/text-empty/_expected.html | 2 +- .../whitespace-at-block-start/_override.html | 4 +- .../tests/runtime-browser/driver-ssr.js | 6 +- .../after-render-prevents-loop/_config.js | 2 +- .../after-render-triggers-update/_config.js | 2 +- .../before-render-prevents-loop/_config.js | 2 +- .../_config.js | 18 +- .../_config.js | 6 +- .../samples/binding-select-late-2/_config.js | 6 +- .../samples/binding-select-late-3/_config.js | 6 +- .../samples/binding-select-late/_config.js | 6 +- .../binding-select-unmatched-2/_config.js | 30 +- .../binding-select-unmatched-3/_config.js | 4 +- .../_config.js | 2 +- .../samples/textarea-content/_config.js | 4 +- .../svelte/tests/runtime-legacy/shared.ts | 53 +- .../async-no-pending-throws-sync/_config.js | 7 + .../async-no-pending-throws-sync/main.svelte | 1 + .../samples/async-no-pending/_config.js | 17 + .../samples/async-no-pending/main.svelte | 1 + .../samples/async-ondestroy-ordering/A.svelte | 9 + .../samples/async-ondestroy-ordering/B.svelte | 9 + .../samples/async-ondestroy-ordering/C.svelte | 5 + .../async-ondestroy-ordering/_config.js | 14 + .../async-ondestroy-ordering/destroyed.js | 4 + .../async-ondestroy-ordering/main.svelte | 13 + .../samples/snippet-slot-let-error/_config.js | 1 + .../_config.js | 6 + .../main.svelte | 7 + .../_expected.html | 1 + .../async-each-fallback-hoisting/main.svelte | 5 + .../async-each-hoisting/_expected.html | 1 + .../samples/async-each-hoisting/main.svelte | 9 + .../A.svelte | 9 + .../B.svelte | 7 + .../_config.js | 3 + .../_expected.html | 0 .../_expected_head.html | 1 + .../main.svelte | 27 + .../_expected.html | 1 + .../async-if-alternate-hoisting/main.svelte | 5 + .../samples/async-if-hoisting/_expected.html | 1 + .../samples/async-if-hoisting/main.svelte | 5 + .../_expected.html | 3 + .../main.svelte | 3 + .../_expected.html | 3 + .../main.svelte | 3 + .../Option.svelte | 5 + .../_expected.html | 1 + .../async-select-value-component/main.svelte | 8 + .../_expected.html | 1 + .../main.svelte | 13 + .../_expected.html | 1 + .../main.svelte | 5 + .../samples/comment-preserve/_expected.html | 2 +- .../samples/context/Child.svelte | 8 + .../samples/context/_config.js | 5 + .../samples/context/_expected.html | 3 + .../samples/context/main.svelte | 11 + .../_expected.html | 2 +- .../_expected.html | 2 + .../invalid-nested-svelte-element/_config.js | 4 +- .../tests/server-side-rendering/test.ts | 146 ++-- .../async-each-fallback-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 35 + .../_expected/server/index.svelte.js | 25 + .../async-each-fallback-hoisting/index.svelte | 5 + .../samples/async-each-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 24 + .../_expected/server/index.svelte.js | 23 + .../samples/async-each-hoisting/index.svelte | 9 + .../async-if-alternate-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 30 + .../_expected/server/index.svelte.js | 16 + .../async-if-alternate-hoisting/index.svelte | 5 + .../samples/async-if-hoisting/_config.js | 3 + .../_expected/client/index.svelte.js | 30 + .../_expected/server/index.svelte.js | 16 + .../samples/async-if-hoisting/index.svelte | 5 + .../_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 64 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/main.svelte.js | 2 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 8 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../hmr/_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 21 +- .../purity/_expected/server/index.svelte.js | 4 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- packages/svelte/types/index.d.ts | 4 +- playgrounds/sandbox/package.json | 3 +- 181 files changed, 2638 insertions(+), 901 deletions(-) create mode 100644 .changeset/forty-insects-cheat.md create mode 100644 documentation/docs/98-reference/.generated/server-warnings.md create mode 100644 packages/svelte/messages/server-errors/errors.md delete mode 100644 packages/svelte/messages/server-errors/lifecycle.md create mode 100644 packages/svelte/messages/server-warnings/warnings.md create mode 100644 packages/svelte/scripts/process-messages/templates/server-warnings.js create mode 100644 packages/svelte/src/internal/server/payload.test.ts create mode 100644 packages/svelte/src/internal/server/warnings.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending-throws-sync/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-no-pending/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/C.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/destroyed.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ondestroy-ordering/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-context-throws-after-await/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-fallback-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-each-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/A.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/B.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-head-multiple-title-order-preserved/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-alternate-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-if-hoisting/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-complex-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-option-implicit-simple-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/Option.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-component/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value-complex/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/async-select-value-implicit-value/main.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/Child.svelte create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/context/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-each-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/index.svelte create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-if-hoisting/index.svelte diff --git a/.changeset/forty-insects-cheat.md b/.changeset/forty-insects-cheat.md new file mode 100644 index 0000000000..993a8fdb24 --- /dev/null +++ b/.changeset/forty-insects-cheat.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: experimental async SSR diff --git a/.prettierignore b/.prettierignore index 5e1d9b1aa7..9cf9a4bfe1 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,6 +15,7 @@ packages/svelte/src/internal/client/warnings.js packages/svelte/src/internal/shared/errors.js packages/svelte/src/internal/shared/warnings.js packages/svelte/src/internal/server/errors.js +packages/svelte/src/internal/server/warnings.js packages/svelte/tests/migrate/samples/*/output.svelte packages/svelte/tests/**/*.svelte packages/svelte/tests/**/_expected* diff --git a/documentation/docs/98-reference/.generated/server-errors.md b/documentation/docs/98-reference/.generated/server-errors.md index c3e8b53c31..6263032212 100644 --- a/documentation/docs/98-reference/.generated/server-errors.md +++ b/documentation/docs/98-reference/.generated/server-errors.md @@ -1,5 +1,19 @@ +### await_invalid + +``` +Encountered asynchronous work while rendering synchronously. +``` + +You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [``](svelte-boundary) with a `pending` snippet. + +### html_deprecated + +``` +The `html` property of server render results has been deprecated. Use `body` instead. +``` + ### lifecycle_function_unavailable ``` diff --git a/documentation/docs/98-reference/.generated/server-warnings.md b/documentation/docs/98-reference/.generated/server-warnings.md new file mode 100644 index 0000000000..26b3628be9 --- /dev/null +++ b/documentation/docs/98-reference/.generated/server-warnings.md @@ -0,0 +1,9 @@ + + +### 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/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index de34b3f5da..6c31aaafd0 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -1,25 +1,5 @@ -### await_outside_boundary - -``` -Cannot await outside a `` with a `pending` snippet -``` - -The `await` keyword can only appear in a `$derived(...)` or template expression, or at the top level of a component's ``); -} - -export function reset_elements() { - let old_parent = parent; - parent = null; - return () => { - parent = old_parent; - }; + payload.child( + (payload) => payload.push(``), + 'head' + ); } /** @@ -58,10 +52,12 @@ export function reset_elements() { * @param {number} column */ export function push_element(payload, tag, line, column) { - var filename = /** @type {Component} */ (current_component).function[FILENAME]; - var child = { tag, parent, filename, line, column }; + var context = /** @type {SSRContext} */ (ssr_context); + var filename = context.function[FILENAME]; + var parent = context.element; + var element = { tag, parent, filename, line, column }; - if (parent !== null) { + if (parent !== undefined) { var ancestor = parent.parent; var ancestors = [parent.tag]; @@ -86,11 +82,11 @@ export function push_element(payload, tag, line, column) { } } - parent = child; + set_ssr_context({ ...context, p: context, element }); } export function pop_element() { - parent = /** @type {Element} */ (parent).parent; + set_ssr_context(/** @type {SSRContext} */ (ssr_context).p); } /** @@ -100,7 +96,7 @@ export function validate_snippet_args(payload) { if ( typeof payload !== 'object' || // for some reason typescript consider the type of payload as never after the first instanceof - !(payload instanceof Payload || /** @type {any} */ (payload) instanceof HeadPayload) + !(payload instanceof Payload) ) { e.invalid_snippet_arguments(); } diff --git a/packages/svelte/src/internal/server/errors.js b/packages/svelte/src/internal/server/errors.js index 458937218f..bde49fe935 100644 --- a/packages/svelte/src/internal/server/errors.js +++ b/packages/svelte/src/internal/server/errors.js @@ -2,6 +2,30 @@ export * from '../shared/errors.js'; +/** + * Encountered asynchronous work while rendering synchronously. + * @returns {never} + */ +export function await_invalid() { + const error = new Error(`await_invalid\nEncountered asynchronous work while rendering synchronously.\nhttps://svelte.dev/e/await_invalid`); + + error.name = 'Svelte error'; + + throw error; +} + +/** + * The `html` property of server render results has been deprecated. Use `body` instead. + * @returns {never} + */ +export function html_deprecated() { + const error = new Error(`html_deprecated\nThe \`html\` property of server render results has been deprecated. Use \`body\` instead.\nhttps://svelte.dev/e/html_deprecated`); + + error.name = 'Svelte error'; + + throw error; +} + /** * `%name%(...)` is not available on the server * @param {string} name diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3aa44f2daa..a2cf222da6 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,6 +1,7 @@ -/** @import { ComponentType, SvelteComponent } from 'svelte' */ -/** @import { Component, RenderOutput } from '#server' */ +/** @import { ComponentType, SvelteComponent, Component } from 'svelte' */ +/** @import { RenderOutput, SSRContext } from '#server' */ /** @import { Store } from '#shared' */ +/** @import { AccumulatedContent } from './payload.js' */ export { FILENAME, HMR } from '../../constants.js'; import { attr, clsx, to_class, to_style } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; @@ -13,13 +14,14 @@ import { } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; -import { current_component, pop, push } from './context.js'; +import { ssr_context, pop, push, set_ssr_context } from './context.js'; import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN, BLOCK_OPEN_ELSE } from './hydration.js'; import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; -import { reset_elements } from './dev.js'; -import { Payload } from './payload.js'; +import { Payload, SSRState } from './payload.js'; import { abort } from './abort-signal.js'; +import { async_mode_flag } from '../flags/index.js'; +import * as e from './errors.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -34,102 +36,48 @@ const INVALID_ATTR_NAME_CHAR_REGEX = * @returns {void} */ export function element(payload, tag, attributes_fn = noop, children_fn = noop) { - payload.out.push(''); + payload.push(''); if (tag) { - payload.out.push(`<${tag}`); + payload.push(`<${tag}`); attributes_fn(); - payload.out.push(`>`); + payload.push(`>`); if (!is_void(tag)) { children_fn(); if (!is_raw_text_element(tag)) { - payload.out.push(EMPTY_COMMENT); + payload.push(EMPTY_COMMENT); } - payload.out.push(``); + payload.push(``); } } - payload.out.push(''); + payload.push(''); } -/** - * Array of `onDestroy` callbacks that should be called at the end of the server render function - * @type {Function[]} - */ -export let on_destroy = []; - /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props - * @param {import('svelte').Component | ComponentType>} component + * @param {Component | ComponentType>} component * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { - try { - const payload = new Payload(options.idPrefix ? options.idPrefix + '-' : ''); - - const prev_on_destroy = on_destroy; - on_destroy = []; - payload.out.push(BLOCK_OPEN); - - let reset_reset_element; - - if (DEV) { - // prevent parent/child element state being corrupted by a bad render - reset_reset_element = reset_elements(); - } - - if (options.context) { - push(); - /** @type {Component} */ (current_component).c = options.context; - } - - // @ts-expect-error - component(payload, options.props ?? {}, {}, {}); - - if (options.context) { - pop(); - } - - if (reset_reset_element) { - reset_reset_element(); - } - - payload.out.push(BLOCK_CLOSE); - for (const cleanup of on_destroy) cleanup(); - on_destroy = prev_on_destroy; - - let head = payload.head.out.join('') + payload.head.title; - - for (const { hash, code } of payload.css) { - head += ``; - } - - const body = payload.out.join(''); - - return { - head, - html: body, - body: body - }; - } finally { - abort(); - } + return Payload.render(/** @type {Component} */ (component), options); } /** * @param {Payload} payload - * @param {(head_payload: Payload['head']) => void} fn + * @param {(payload: Payload) => Promise | void} fn * @returns {void} */ export function head(payload, fn) { - const head_payload = payload.head; - head_payload.out.push(BLOCK_OPEN); - fn(head_payload); - head_payload.out.push(BLOCK_CLOSE); + payload.child((payload) => { + payload.push(BLOCK_OPEN); + payload.child(fn); + payload.push(BLOCK_CLOSE); + }, 'head'); } /** @@ -144,21 +92,21 @@ export function css_props(payload, is_html, props, component, dynamic = false) { const styles = style_object_to_string(props); if (is_html) { - payload.out.push(``); + payload.push(``); } else { - payload.out.push(``); + payload.push(``); } if (dynamic) { - payload.out.push(''); + payload.push(''); } component(); if (is_html) { - payload.out.push(``); + payload.push(``); } else { - payload.out.push(``); + payload.push(``); } } @@ -451,13 +399,13 @@ export function bind_props(props_parent, props_now) { */ function await_block(payload, promise, pending_fn, then_fn) { if (is_promise(promise)) { - payload.out.push(BLOCK_OPEN); + payload.push(BLOCK_OPEN); promise.then(null, noop); if (pending_fn !== null) { pending_fn(); } } else if (then_fn !== null) { - payload.out.push(BLOCK_OPEN_ELSE); + payload.push(BLOCK_OPEN_ELSE); then_fn(promise); } } @@ -503,8 +451,8 @@ export function once(get_value) { * @returns {string} */ export function props_id(payload) { - const uid = payload.uid(); - payload.out.push(''); + const uid = payload.global.uid(); + payload.push(''); return uid; } @@ -512,12 +460,10 @@ export { attr, clsx }; export { html } from './blocks/html.js'; -export { push, pop } from './context.js'; +export { save } from './context.js'; export { push_element, pop_element, validate_snippet_args } from './dev.js'; -export { assign_payload, copy_payload } from './payload.js'; - export { snapshot } from '../shared/clone.js'; export { fallback, to_array } from '../shared/utils.js'; @@ -531,8 +477,6 @@ export { export { escape_html as escape }; -export { await_outside_boundary } from '../shared/errors.js'; - /** * @template T * @param {()=>T} fn @@ -557,29 +501,117 @@ export function derived(fn) { /** * * @param {Payload} payload - * @param {*} value + * @param {unknown} value */ export function maybe_selected(payload, value) { - return value === payload.select_value ? ' selected' : ''; + return value === payload.local.select_value ? ' selected' : ''; } /** + * When an `option` element has no `value` attribute, we need to treat the child + * content as its `value` to determine whether we should apply the `selected` attribute. + * This has to be done at runtime, for hopefully obvious reasons. It is also complicated, + * for sad reasons. * @param {Payload} payload - * @param {() => void} children + * @param {((payload: Payload) => void | Promise)} children * @returns {void} */ export function valueless_option(payload, children) { - var i = payload.out.length; + const i = payload.length; + + // prior to children, `payload` has some combination of string/unresolved payload that ends in `