From 2afb895ffabe918d69f8463e06637b060b7a4dce Mon Sep 17 00:00:00 2001 From: hjaber <41503068+hjaber@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:36:18 -0700 Subject: [PATCH 1/8] fix: parse declaration tags with a division operator in the initializer (#18353) Mimic what `parseExpressionAt` does --------- Co-authored-by: hjaber Co-authored-by: Simon Holthausen --- .changeset/declaration-tag-division.md | 5 +++ .../src/compiler/phases/1-parse/acorn.js | 45 +++++++------------ .../declaration-tag-division/input.svelte | 20 +++++++++ .../declaration-tag-division/output.svelte | 20 +++++++++ 4 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 .changeset/declaration-tag-division.md create mode 100644 packages/svelte/tests/print/samples/declaration-tag-division/input.svelte create mode 100644 packages/svelte/tests/print/samples/declaration-tag-division/output.svelte diff --git a/.changeset/declaration-tag-division.md b/.changeset/declaration-tag-division.md new file mode 100644 index 0000000000..de265f0ab1 --- /dev/null +++ b/.changeset/declaration-tag-division.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: parse declaration tag contents more robustly diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index d338f9ffb8..fb60c228c5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -5,7 +5,6 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from '@sveltejs/acorn-typescript'; import * as e from '../../errors.js'; -import { find_matching_bracket } from './utils/bracket.js'; const JSParser = acorn.Parser; const TSParser = JSParser.extend(tsPlugin()); @@ -106,38 +105,26 @@ export function parse_expression_at(parser, source, index) { * @returns {Statement} */ export function parse_statement_at(parser, source, index) { - const acorn = parser.ts ? TSParser : JSParser; - let end = find_matching_bracket(source, index, '{'); - if (end === undefined) e.unexpected_eof(source.length); - - while (source[end - 1] === ';') { - end -= 1; - } - - const padded_source = `${' '.repeat(index)}${source.slice(index, end)}`; - const { onComment, add_comments } = get_comment_handlers( - padded_source, - parser.root.comments, - index - ); + // cast to `any`: acorn's Parser constructor and parseStatement/nextToken aren't in its public types + const acorn = /** @type {any} */ (parser.ts ? TSParser : JSParser); + const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index); try { - const ast = acorn.parse(padded_source, { - onComment, - sourceType: 'module', - ecmaVersion: 16, - locations: true - }); - - add_comments(ast); - - const statement = /** @type {Statement} */ ( - /** @type {unknown} */ (/** @type {Program} */ (ast).body[0]) + // This is like parseExpressionAt but for statements + const p = new acorn( + { onComment, sourceType: 'module', ecmaVersion: 16, locations: true }, + source, + index ); - statement.end = Math.min(/** @type {number} */ (statement.end), end); + p.nextToken(); + const statement = /** @type {Statement} */ (p.parseStatement(null, true, Object.create(null))); + add_comments(/** @type {acorn.Node} */ (statement)); return statement; - } catch (e) { - handle_parse_error(e); + } catch (err) { + // A statement that runs to the end of the source (e.g. an unterminated declaration tag) + // is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic. + if (/** @type {any} */ (err).pos === source.length) e.unexpected_eof(source.length); + handle_parse_error(err); } } diff --git a/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte b/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte new file mode 100644 index 0000000000..0e20727748 --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag-division/input.svelte @@ -0,0 +1,20 @@ + + +{#if visible} + {const half = total / 2} + {let derived = $derived(total / 4)} + {const member = width / height} + {const call = Math.max(total, 1) / 2} + {const string_then_division = 'ab' / divisor} + {const typed: number = total / 2} + {const { fallback = total / 2 } = options} + {const regex = /[}]/} +

{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}

+{/if} diff --git a/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte b/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte new file mode 100644 index 0000000000..0e20727748 --- /dev/null +++ b/packages/svelte/tests/print/samples/declaration-tag-division/output.svelte @@ -0,0 +1,20 @@ + + +{#if visible} + {const half = total / 2} + {let derived = $derived(total / 4)} + {const member = width / height} + {const call = Math.max(total, 1) / 2} + {const string_then_division = 'ab' / divisor} + {const typed: number = total / 2} + {const { fallback = total / 2 } = options} + {const regex = /[}]/} +

{half} {derived} {member} {call} {string_then_division} {typed} {fallback} {regex}

+{/if} From 56013a2c01454388ea5f58642377038f29cdec4a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:39:50 +0200 Subject: [PATCH 2/8] fix: check references for blockers on server, too (#18352) We forgot the server in #18309 Fixes #18344 --- .changeset/strong-cups-prove.md | 5 +++++ .../3-transform/server/visitors/shared/utils.js | 2 +- .../Child.svelte | 5 +++++ .../async-derived-prop-closure-hydration/_config.js | 11 +++++++++++ .../async-derived-prop-closure-hydration/main.svelte | 12 ++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .changeset/strong-cups-prove.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/main.svelte diff --git a/.changeset/strong-cups-prove.md b/.changeset/strong-cups-prove.md new file mode 100644 index 0000000000..ad85a1feba --- /dev/null +++ b/.changeset/strong-cups-prove.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: check references for blockers on server, too 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 62e1c44094..3da6fcf321 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 @@ -343,7 +343,7 @@ export class PromiseOptimiser { * @param {ExpressionMetadata} metadata */ check_blockers(metadata) { - for (const binding of metadata.dependencies) { + for (const binding of metadata.references) { if (binding.blocker) { this.#blockers.add(binding.blocker); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/Child.svelte new file mode 100644 index 0000000000..6e5197b2fe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/Child.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/_config.js new file mode 100644 index 0000000000..37a3f0dae1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, 'foo '); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/main.svelte new file mode 100644 index 0000000000..c87d3f8a08 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-prop-closure-hydration/main.svelte @@ -0,0 +1,12 @@ + + +{foo} + + + + ` + ); + assert.deepEqual(logs, [0]); + + real.click(); + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + 1 + + + + ` + ); + assert.deepEqual(logs, [0, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte new file mode 100644 index 0000000000..85056e02b8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-obsolete-ignore/main.svelte @@ -0,0 +1,23 @@ + + +{await push(count)} + + + From 85dcb91f7c890a4d6d6c23dcbcf1df23cc624272 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:45:58 +0200 Subject: [PATCH 6/8] chore: upgrade to vitest v4 (#18265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Bumps `vitest` and `@vitest/coverage-v8` from `^2.1.9` to `^4.1.7` (two major versions). Three small test-harness updates compensate for vitest 4 behavior changes; all 7569 tests still pass. - **`packages/svelte/tests/runtime-browser/test.ts`** — vitest 4 removed the deprecated `describe(name, fn, opts)` signature. Pass options as the second argument. - **`packages/svelte/tests/runtime-legacy/shared.ts`** — vitest 4's jsdom env binds `virtualConsole` to the original `globalThis.console` reference *before* vitest wraps the console, so inline-` + +
+ {#each [] as n (n)} + {const a = n} +
+ {/each} +
From 94bd27953e0a5ffef80e9451b27e122d185d093f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20L=C3=AA?= <115204145+DucMinhNe@users.noreply.github.com> Date: Wed, 3 Jun 2026 09:41:15 +0700 Subject: [PATCH 8/8] docs: fix invalid `rows` CSS property in {#each} chess-board example (#18367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem The `{#each}` docs page has a runnable "chess board" example whose `